#include "compat.h"

#ifdef HAVE_STDIO_H
#include <stdio.h>
#endif

#ifdef HAVE_ASM_PAGE_H
#include <asm/page.h>
#endif

#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif

#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif

#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif

#ifdef HAVE_SYS_SWAP_H
#include <sys/swap.h>
#endif


#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif

#ifdef HAVE_DIRENT_H
#include <dirent.h>
#endif

#ifdef HAVE_CTYPE_H
#include <ctype.h>
#endif

#ifndef SWAPD_DEF_CONFIG
#define SWAPD_DEF_CONFIG "swapd.conf"
#endif

#ifndef SWAPD_SWAP_PERMS
#define SWAPD_SWAP_PERMS 0600
#endif

extern char *optarg;

/* Reasonable defaults */
int64_t minfree = (10 * 1024 *1024), maxfree = 0;
size_t swapsize = (32 * 1024 * 1024);
time_t checkdelay = 30;
char *swapdir = "/var/tmp";
int max_inactive_swaps = 5;
int include_cache_mem = 1;

typedef struct {
	char *pathname;
	size_t swapsize;
	int active;
} swap_t;

int daemonize(void) {
#ifndef DEBUG
	pid_t pid;

	close(STDIN_FILENO);
	close(STDOUT_FILENO);
	close(STDERR_FILENO);

	setsid();

	pid = fork();

	if (pid > 0) {
		exit(EXIT_SUCCESS);
	}

	if (pid < 0) {
		exit(EXIT_FAILURE);
	}
#endif
	return(0);
}

int swapd_init_stats(void) {
#ifdef HAVE_LIBSTATGRAB
	/* Initialize the libstatgrab interface. */
	if (sg_init() != 0) {
		/* Failure. */
		PRINTERR("Unable to initialize statistics interface (libstatgrab).");
		return(0);
	}
#endif
	/* No initialization is needed for Linux proc interface. */

	/* Success! */
	return(1);
}

int64_t swapd_get_free_mem(void) {
	int64_t retval = -1;
#ifdef HAVE_LIBSTATGRAB
	sg_mem_stats *meminfo = NULL;
	sg_swap_stats *swapinfo = NULL;

	meminfo = sg_get_mem_stats();
	if (meminfo == NULL) {
		return(-1);
	}

	swapinfo = sg_get_swap_stats();
	if (swapinfo == NULL) {
		free(meminfo);
		return(-1);
	}

	retval = meminfo->free + swapinfo->free;
	if (include_cache_mem) {
		retval += meminfo->cache;
	}

	return(retval);
#else
	FILE *meminfofd = NULL;
	char buf[128] = {0};
	char *buf_part = NULL, *buf_whole = NULL;
	uint64_t meminfo_part = 0, meminfo = 0;

	meminfofd = fopen("/proc/meminfo", "r");

	if (meminfofd == NULL) {
		return(-1);
	}

	while (1) {
		fgets(buf, sizeof(buf) - 1, meminfofd);
		if (feof(meminfofd)) {
			break;
		}

		buf_whole = buf;
		buf_part = strsep(&buf_whole, ":");
		if (buf_part == NULL || buf_whole == NULL) {
			continue;
		}
		meminfo_part = strtoull(buf_whole, NULL, 10) * 1024;
		if (strcmp(buf_part, "MemFree") == 0 ||
		    strcmp(buf_part, "Buffers") == 0 ||
		    (strcmp(buf_part, "Cached") == 0 && include_cache_mem) ||
		    strcmp(buf_part, "SwapFree") == 0) {
			meminfo += meminfo_part;
		}
	}

	fclose(meminfofd);

	if (meminfo != 0) {
		retval = meminfo;
	}

	return(retval);
#endif
}

int swapd_call_mkswap(const char *swapfile) {
#ifdef SWAPD_NEED_MKSWAP
	char cmdline[8192] = {0};
	int fd;

	snprintf(cmdline, sizeof(cmdline) - 1, "mkswap \"%s\"", swapfile);
	SPOTVAR_S(cmdline);
	system(cmdline);

	fd = open(swapfile, O_RDONLY);
	fsync(fd);
	close(fd);
#endif

	return(0);
}

int swapd_swapon(const char *swapfile) {
	int swaponret = -1;
#ifdef HAVE_SWAPON
#  ifdef SWAPON_TAKES_2_ARGS
	/* Linux */
	swaponret = swapon(swapfile, 0);
#  else
	/* BSD */
	swaponret = swapon(swapfile);
#  endif
#else
# ifdef SWAPD_SWAPON_CMDLINE
	char cmdline[1024] = {0};
	snprintf(cmdline, sizeof(cmdline) - 1, SWAPD_SWAPON_CMDLINE, swapfile);
	swaponret = system(cmdline);
#  else
#    error Dont know how to swapon() on this platform!
#  endif
#endif

	return(swaponret);
}

swap_t *swapd_mkswap(const char *swapdir, size_t swapsize, const char *pathname) {
	swap_t *retval = NULL;
#ifdef PATH_MAX
	char swapfilepath[PATH_MAX + 1] = {0};
#else
	char swapfilepath[1024] = {0};
#endif
#ifdef PAGE_SIZE
	char databuf[(PAGE_SIZE * 2)] = {0};
#else
	char databuf[4096] = {0};
#endif
	int64_t numbufs = 0, bufcnt = 0;
	ssize_t writeret = 0;
	size_t realswapsize = 0;
	int swapfd = -1;
	int swaponret = -1;

	if (pathname == NULL) {
		/* Create a temporary file. */
		snprintf(swapfilepath, sizeof(swapfilepath) - 1, "%s/.swapXXXXXX", swapdir);

		swapfd = mkstemp(swapfilepath);
		if (swapfd < 0) {
			return(NULL);
		}

		/* Fill the file with empty data. */
		numbufs = (swapsize / sizeof(databuf)) + 1;	
		realswapsize = numbufs * sizeof(databuf);

		for (bufcnt = 0; bufcnt < numbufs; bufcnt++) {
			writeret = write(swapfd, databuf, sizeof(databuf));
			if (writeret != sizeof(databuf)) {
				break;
			}
		}
		close(swapfd);

		/* If we couldn't make the file, give up. */
		if (writeret != sizeof(databuf)) {
			unlink(swapfilepath);
			return(NULL);
		}

		/* Set permissions */
		chmod(swapfilepath, SWAPD_SWAP_PERMS);

		/* Initialize the swap signature. */
		swapd_call_mkswap(swapfilepath);
	} else {
		strncpy(swapfilepath, pathname, sizeof(swapfilepath) - 1);
	}

	/* Start swaping. */
	swaponret = swapd_swapon(swapfilepath);
	if (swaponret < 0) {
		unlink(swapfilepath);
		return(NULL);
	}

	retval = malloc(sizeof(*retval));
	if (retval == NULL) {
		return(NULL);
	}

	/* Create the return value. */
	retval->pathname = strdup(swapfilepath);
	retval->swapsize = realswapsize;
	retval->active = 1;

	/* Don't return an invalid structure. */
	if (retval->pathname == NULL) {
		free(retval);
		return(NULL);
	}

	return(retval);
}

int swapd_swapoff(swap_t *swapfile) {
	int swapoffret = -1;
	char cmdline[1024] = {0};

	if (swapfile == NULL) {
		return(-1);
	}

	if (swapfile->pathname == NULL) {
		return(-1);
	}

/* Prefer the swapoff() system call ... */
#ifdef HAVE_SWAPOFF
	swapoffret = swapoff(swapfile->pathname);
#else
/* ... if that's not available, try some command.. */
#  ifdef SWAPD_SWAPOFF_CMDLINE
	snprintf(cmdline, sizeof(cmdline) - 1, SWAPD_SWAPOFF_CMDLINE, swapfile->pathname);
	swapoffret = system(cmdline);
#  else
/*  ... otherwise, issue a warning since we don't know what to do. */
#    warning Dont know how to swapoff on this platform
#  endif
#endif

	if (swapoffret >= 0) {
		swapfile->active = 0;
		return(0);
	}

	return(-1);
}

int swapd_process_config_line(const char *cmd) {
	char *tmpbuf = NULL, *tmpbuf_s = NULL, *tmppart = NULL, *argbuf[10] = {NULL}, *endbuf = NULL;
	int64_t argbuf_int[10] = {0};
	int argc = 0, i = 0;

	if (cmd == NULL) {
		return(-1);
	}

	tmpbuf_s = tmpbuf = strdup(cmd);

	if (tmpbuf_s == NULL) {
		return(-1);
	}

	for (argc = 0; argc < sizeof(argbuf) / sizeof(argbuf[0]); argc++) {
		tmppart = strsep(&tmpbuf, ": \t");
		if (tmppart == NULL) {
			break;
		}

		if (strcmp(tmppart, "") == 0) {
			argc--;
			continue;
		}

		argbuf[argc] = tmppart;
	}

	if (argc == 0 || argbuf[0] == NULL) {
		return(0);
	}

	if (argbuf[0][0] == '#') {
		return(0);
	}

	for (i = 0; i < argc; i++) {
		argbuf_int[i] = strtoull(argbuf[i], &endbuf, 10);
		if (endbuf != NULL) {
			switch (tolower(endbuf[0])) {
				case 'g':
					argbuf_int[i] *= (1024 * 1024 * 1024);
					break;
				case 'm':
					argbuf_int[i] *= (1024 * 1024);
					break;
				case 'k':
					argbuf_int[i] *= 1024;
					break;
			}
		}
	}

	if (strcasecmp(argbuf[0], "maxfree") == 0 && argc == 2) {
		maxfree = argbuf_int[1];
	} else if (strcasecmp(argbuf[0], "minfree") == 0 && argc == 2) {
		minfree = argbuf_int[1];
	} else if (strcasecmp(argbuf[0], "swapsize") == 0 && argc == 2) {
		swapsize = argbuf_int[1];
	} else if (strcasecmp(argbuf[0], "maxunusedswaps") == 0 && argc == 2) {
		max_inactive_swaps = argbuf_int[1];
	} else if (strcasecmp(argbuf[0], "swapdir") == 0 && argc == 2) {
		swapdir = strdup(argbuf[1]);
	} else if (strcasecmp(argbuf[0], "include_cache") == 0 && argc == 2) {
		include_cache_mem = !strcasecmp(argbuf[1], "yes");
	} else if (strcasecmp(argbuf[0], "delay") == 0 && argc == 2) {
		checkdelay = argbuf_int[1];
	} else {
		PRINTERR("Unknown command: %s/%i", argbuf[0], argc);
	}

	free(tmpbuf_s);

	return(0);
}

int swapd_process_config_file(const char *pathname) {
	FILE *configfp = NULL;
	char buf[1024] = {0};

	if (pathname == NULL) {
		return(-1);
	}

	configfp = fopen(pathname, "r");
	if (configfp == NULL) {
		return(-1);
	}

	while (1) {
		fgets(buf, sizeof(buf) - 1, configfp);
		if (feof(configfp)) {
			break;
		}

		while (strlen(buf) > 0) {
			if (buf[strlen(buf) - 1] < ' ') {
				buf[strlen(buf) - 1] = '\0';
			} else {
				break;
			}
		}

		swapd_process_config_line(buf);
	}

	fclose(configfp);

	return(0);
}

int main(int argc, char **argv) {
	struct stat swapfilestat;
	struct dirent *dent = NULL;
	swap_t *swaps[128], *swapinfo = NULL;
	int64_t freemem = -1;
	char *swapfile = NULL;
	DIR *dh = NULL;
	int ch = -1, i = 0;
	int active_swaps = 0, inactive_swaps = 0, dbg_cmp_active_swaps;
	int swapoffret = -1;
	int retval = EXIT_SUCCESS;
	int chdirret = 0, statret = 0;
	int gfm_errorcount = 0;

	if (!swapd_init_stats()) {
		return(EXIT_FAILURE);
	}

	for (i = 0; i < (sizeof(swaps) / sizeof(swaps[0])); i++) {
		swaps[i] = NULL;
	}

	/* Process config file.. */
	swapd_process_config_file(SWAPD_DEF_CONFIG);

	/* Process arguments.. */
	while ((ch = getopt(argc, argv, "Vhm:s:d:M:o:c:u:")) != -1) {
		switch (ch) {
			case 'V':
				printf("%s version %s\n", PACKAGE_NAME, PACKAGE_VERSION);
				return(EXIT_SUCCESS);
			case 'M':
				maxfree = strtoull(optarg, NULL, 10);
				break;
			case 'm':
				minfree = strtoull(optarg, NULL, 10);
				break;
			case 's':
				swapsize = strtoull(optarg, NULL, 10);
				break;
			case 'd':
				swapdir = strdup(optarg);
				break;
			case 'o':
				swapd_process_config_line(optarg);
				break;
			case 'c':
				swapd_process_config_file(optarg);
				break;
			case 'u':
				max_inactive_swaps = strtoull(optarg, NULL, 10);
				break;
			case ':':
			case '?':
				retval = EXIT_FAILURE;
			case 'h':
				printf("Usage: swapd [-Vh] [-c config] [-o cmd] [-m min] [-M max] [-s swapsize] [-d dir]\n");
				printf("             [-u maxunusedswaps]\n");
				printf("  -V     Print version and exit.\n");
				printf("  -h     This help.\n");
				printf("  -c     Process an additional configuration file.\n");
				printf("  -o     Process a command as if it were in a configuration file.\n");
				printf("  -m     Specify the lower boundary and where to start making new swap files.\n");
				printf("  -M     Specify the upper boundary and where to start disabling swap files.\n");
				printf("  -s     Specify the size of each swap file.\n");
				printf("  -d     Specify the directory where swapfiles will be created.\n");
				printf("  -u     Specify the number of unused swaps that can exist before swap files\n");
				printf("         start being deleted.\n");
				return(retval);
				break;
		}
	}

	/* Fix illegal values. */
	if (maxfree < (minfree + swapsize + 1) && maxfree != 0) {
		PRINTERR("Illegal max value.");
		maxfree = 0;
	}


	chdirret = chdir(swapdir);
	if (chdirret < 0) {
		PERROR("Couldn't change directories to swap directory.");
		return(EXIT_FAILURE);
	}

	daemonize();

	dh = opendir(".");

	if (dh != NULL) {
		inactive_swaps = 0;
		while (1) {
			dent = readdir(dh);
			if (dent == NULL) break;

			if (strlen(dent->d_name) == 11 && strncmp(dent->d_name, ".swap", 5) == 0) {
				statret = stat(dent->d_name, &swapfilestat);
				if (statret < 0) {
					unlink(dent->d_name);
					continue;
				}

				if (swapfilestat.st_size != swapsize) {
					unlink(dent->d_name);
					continue;
				}

				if (inactive_swaps >= max_inactive_swaps) {
					unlink(dent->d_name);
					continue;
				}

				swaps[inactive_swaps] = malloc(sizeof(*swaps));
				if (swaps[inactive_swaps] == NULL) continue;

				swaps[inactive_swaps]->pathname = strdup(dent->d_name);
				swaps[inactive_swaps]->swapsize = swapsize;
				swaps[inactive_swaps]->active = 0;
				inactive_swaps++;
			}
		}
		closedir(dh);
	}

	SPOTVAR_LL(maxfree);
	SPOTVAR_LL(minfree);

	while (1) {
		sleep(checkdelay);

		freemem = swapd_get_free_mem();
		if (freemem == -1) {
			gfm_errorcount++;
			if (gfm_errorcount == 10) {
				exit(EXIT_FAILURE);
			}
			continue;
		}
		gfm_errorcount = 0;

		SPOTVAR_LL(freemem);

		if (freemem < minfree) {
			swapfile = NULL;
			for (i = 0; i < (sizeof(swaps) / sizeof(swaps[0])); i++) {
				if (swaps[i] != NULL) {
					if (swaps[i]->active == 0 && swaps[i]->pathname != NULL) {
						swapfile = strdup(swaps[i]->pathname);
						free(swaps[i]->pathname);
						free(swaps[i]);
						swaps[i] = NULL;
						break;
					}
				}
			}

			swapinfo = swapd_mkswap(".", swapsize, swapfile);

			if (swapfile != NULL) {
				free(swapfile);
			}

			if (swapinfo != NULL) {
				for (i = 0; i < (sizeof(swaps) / sizeof(swaps[0])); i++) {
					if (swaps[i] == NULL) {
						swaps[i] = swapinfo;
						break;
					}
				}
				active_swaps++;
			}

			continue;
		}

		if (freemem > maxfree && maxfree > 0) {
			SPOTVAR_I(active_swaps);

			/* Deactivate swaps if there is too much free memory. */
			if (active_swaps > 0) {
				for (i = 0; i < (sizeof(swaps) / sizeof(swaps[0])); i++) {
					if (swaps[i] != NULL) {
						if (swaps[i]->active == 0) continue;
						swapoffret = swapd_swapoff(swaps[i]);
						if (swapoffret >= 0) {
							active_swaps--;
							break;
						}
					}
				}
			}

			/* Delete swap files if there are too many of them. */
			inactive_swaps = 0;
			dbg_cmp_active_swaps = 0;
			for (i = 0; i < (sizeof(swaps) / sizeof(swaps[0])); i++) {
				if (swaps[i] != NULL) {
#ifdef DEBUG
					/* DEBUG */
					if (swaps[i]->active == 1) {
						dbg_cmp_active_swaps++;
					}
#endif
					if (swaps[i]->active == 0 && swaps[i]->pathname != NULL) {
						inactive_swaps++;
						if (inactive_swaps > max_inactive_swaps) {
							unlink(swaps[i]->pathname);
							free(swaps[i]->pathname);
							free(swaps[i]);
							swaps[i] = NULL;
						}
					}
				}
			}
#ifdef DEBUG
			if (dbg_cmp_active_swaps != active_swaps) {
				PRINTERR_D("Error!  Swap count mismatch, chk=%i, var=%i", dbg_cmp_active_swaps, active_swaps);
			}
#endif

			continue;
		}

	

	}

	return(EXIT_FAILURE);
}
