#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif

#include <errno.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#ifdef FALLIBLE
#include <fallible.h>
#include <fallible/alloc.h>
#include <fallible/string.h>
#endif

#ifdef TEST
#include <assert.h>

static void testing(const char *message) {
	fprintf(stderr, "testing: %s...", message);
}

static void test_ok() {
	fprintf(stderr, " OK.\n");
}

static FILE *testfile() {
	char filename[] = "remembering-test.XXXXXX";

	errno = 0;
	int fd = mkstemp(filename);
	if (fd == -1) {
		perror("tmpfile mkstemp");
		return NULL;
	}

	errno = 0;
	FILE *f = fdopen(fd, "w");
	if (!f) {
		perror("tmpfile fdopen");
		close(fd);
		return NULL;
	}

	return f;
}

static char *testdir() {
	char dirname[] = "remembering-test.XXXXXX";

	errno = 0;
	char *dircpy = malloc(strlen(dirname) + 1);
	if (!dircpy) {
		perror("malloc");
		return NULL;
	}

	errno = 0;
	char *dir = mkdtemp(dirname);
	if (!dir) {
		perror("mkdtemp");
		free(dircpy);
		return NULL;
	}

	strcpy(dircpy, dir);

	return dircpy;
}

static void assert_file_contents(const char *const filename, const char *const contents) {

}
#endif

static int print_usage(FILE *stream, char *name) {
	errno = 0;
	fprintf(stream, "Usage:	 %s -p PROFILE -c 'COMMAND'\n",
	        name ? name : "remembering");
	if (errno) {
		perror("fprintf");
		return -1;
	}
	return 0;
}

#ifdef TEST
static void print_usage_test() {
	{
		testing("print_usage a-name");
		FILE *f = testfile();
		if (!f) {
			exit(EXIT_FAILURE);
		}

		int ret = print_usage(f, "a-name");
		fclose(f);
		if (ret) {
			exit(EXIT_FAILURE);
		}

		assert_file_contents(f, "Usage: a-name -p PROFILE -c 'COMMAND'\n");

		test_ok();
	}
	{
		testing("print_usage NULL");
		FILE *f = testfile();
		if (!f) {
			exit(EXIT_FAILURE);
		}

		int ret = print_usage(f, NULL);
		fclose(f);
		if (ret) {
			exit(EXIT_FAILURE);
		}

		assert_file_contents(f, "Usage: remembering -p PROFILE -c 'COMMAND'\n");

		test_ok();
	}
}
#endif

static void print_help(FILE *stream) {
	const char *help_text =
			"\n"
			"Options:\n"
			" -p PROFILE			profile to be used for gathering and storing data\n"
			" -c 'COMMAND'		command to be run, reading from STDIN, writing to "
			"STDOUT\n"
			" -h							show this help\n"
			" -V							print program version\n"
			"See remembering(1) manpages for more information.\n";
	fprintf(stream, "%s", help_text);
}

static void print_missing(FILE *stream, char *text) {
	fprintf(stream, "Missing option: %s\n", text);
}

static void print_version(FILE *stream) {
	fprintf(stream, "remembering-%s %s\n", VERSION, DATE);
}

static int get_options(FILE *out, FILE *err, int argc, char *argv[],
											 char **command, char **profile) {
	for (int i = 0; i < argc; i++) {
		if (strcmp(argv[i], "--help") == 0) {
			print_usage(out, argv[0]);
			print_help(out);
			return 1;
		} else if (strcmp(argv[i], "--version")) {
			print_version(out);
			return 1;
		}
	}

	char *profilearg = NULL;
	char *commandarg = NULL;
	int option;
	while ((option = getopt(argc, argv, "c:p:hV")) != -1) {
		switch (option) {
		case 'c':
			commandarg = optarg;
			break;
		case 'p':
			profilearg = optarg;
			break;
		case 'h':
			print_usage(out, argv[0]);
			print_help(out);
			return 1;
		case 'V':
			print_version(out);
			return 1;
		}
	}

	if (!commandarg) {
		print_missing(err, "-c 'COMMAND'");
		print_usage(err, argv[0]);
		return -1;
	}

	if (!profilearg) {
		print_missing(err, "-p 'PROFILE'");
		print_usage(err, argv[0]);
		return 1;
	}

	*command = strdup(commandarg);
	if (!*command) {
		perror("strdup");
		return -1;
	}
	*profile = strdup(profilearg);
	if (!*profile) {
		perror("strdup");
		free(*command);
		return -1;
	}

	return 0;
}

#ifdef FALLIBLE
static int fallible_get_options(FILE *out, FILE *err, int argc, char *argv[],
																char **command, char **profile,
																const char *const filename, int lineno) {
	if (fallible_should_fail(filename, lineno)) {
		return -1;
	}
	return get_options(out, err, argc, argv, command, profile);
}

#define get_options(out, err, argc, argv, command, profile)										 \
	fallible_get_options(out, err, argc, argv, command, profile, __FILE__,			 \
											 __LINE__)
#endif

#ifdef TEST
int arrlen(char **argv) {
	int count = 0;
	while (argv[count] != NULL) {
		count++;
	}
	return count;
}

void get_options_test() {
	{
		testing("get_options when given -h, -V and --help");
		FILE *f = tmpfile();
		if (!f) {
			exit(EXIT_FAILURE);
		}

		char *argvs[13][6] = {
				{"--help", NULL},
				{"other", "arguments before", "--help", NULL},
				{"some", "--help", "arguments after", NULL},
				{"--help", "all", "arguments after", NULL},
				{"-h", NULL},
				{"-V", NULL},
				{"-hc", "1", NULL},
				{"-hp", "2", NULL},
				{"-hV", NULL},
				{"-Vh", NULL},
				{"-p", "p", "-h", NULL},
				{"-p", "p", "-c", "c", "-h", NULL},
				{NULL},
		};
		int rows = 0;
		while (argvs[rows][0] != NULL) {
			rows++;
		}

#ifdef DISABLE
		int ret;
		char *command = NULL;
		char *profile = NULL;
		for (int i = 0; i < rows; i++) {
			char **argv = argvs[i];
			ret = get_options(f, f, arrlen(argvs[i]), argv, &command, &profile);
			if (ret == -1) {
				fclose(f);
				exit(EXIT_FAILURE);
			}

			assert(command == NULL);
			assert(profile == NULL);
			assert(ret == 1);
		}
#endif

		fclose(f);
		test_ok();
	}
	// { "" },
}
#endif

static int mkdir_p(char *path, mode_t mode) {
	struct stat s;
	if (stat(path, &s) == 0 && S_ISDIR(s.st_mode)) {
		return 0;
	}

	char *parent = dirname(path);
	int ret = mkdir_p(parent, mode);
	if (ret) {
		return ret;
	}
	return mkdir(path, mode);
}

#ifdef FALLIBLE
static int fallible_mkdir_p(char *path, mode_t mode, const char *const filename,
														int lineno) {
	if (fallible_should_fail(filename, lineno)) {
		return -1;
	}
	return mkdir_p(path, mode);
}

#define mkdir_p(path, mode) fallible_mkdir_p(path, mode, __FILE__, __LINE__)
#endif

#ifdef TEST
static void mkdir_p_test() {
	/*
{
testing("mkdir_p a single directory");

char *dir = tmpdir();
if (!dir) {
exit(EXIT_FAILURE);
}

int ret = mkdir_p(dir, 0755);
if (ret) {
free(dir);
exit(EXIT_FAILURE);
}

free(dir);
test_ok();
}
	*/
	/*
{
testing("mkdir_p nested directories");

const char *const dir_suffix = "/a/b/c/d/e";
char *dir_prefix = tmpdir();
if (!dir_prefix) {
exit(EXIT_FAILURE);
}

					char *dir = malloc(strlen(dir_prefix) + strlen(dir_suffix) + 1);
					if (!dir) {
									free(dir_prefix);
									exit(EXIT_FAILURE);
					}

					strcpy(dir, dir_prefix);
					strcat(dir, dir_suffix);

int ret = mkdir_p(dir, 0755);
if (ret) {
									free(dir);
									free(dir_prefix);
exit(EXIT_FAILURE);
}

					free(dir);
					free(dir_prefix);
test_ok();
}
	*/
}
#endif

static char *expand_profile_name(const char *const profile_name) {
	char *prefix = NULL;
	char *env_prefix = getenv("XDG_DATA_HOME");
	if (env_prefix) {
		prefix = strdup(env_prefix);
		if (!prefix) {
			return NULL;
		}
	} else {
		char *home = getenv("HOME");
		char *path = "/.local/share/remembering";
		prefix = malloc(strlen(home) + strlen(path) + 1);
		if (!prefix) {
			return NULL;
		}
		strcpy(prefix, home);
		strcat(prefix, path);
	}
	char *separator = "/";
	char *expanded_profile =
			malloc(strlen(prefix) + strlen(separator) + strlen(profile_name) + 1);
	if (!expanded_profile) {
		free(prefix);
		return NULL;
	}
	strcpy(expanded_profile, prefix);
	strcat(expanded_profile, separator);
	strcat(expanded_profile, profile_name);

	free(prefix);

	errno = 0;
	int ret = mkdir_p(dirname(expanded_profile), 0755);
	if (ret) {
		perror("mkdir_p");
		return NULL;
	}
	return expanded_profile;
}

#ifdef FALLIBLE
static char *fallible_expand_profile_name(const char *const profile_name,
																					const char *const filename,
																					int lineno) {
	if (fallible_should_fail(filename, lineno)) {
		return NULL;
	}
	return expand_profile_name(profile_name);
}

#define expand_profile_name(profile_name)																			 \
	fallible_expand_profile_name(profile_name, __FILE__, __LINE__)
#endif

#ifdef TEST
static void expand_profile_name_test() {
	{
		testing("expand_profile_name");
		if (0) {
			expand_profile_name("oij");
		}
		test_ok();
	}
}
#endif

static const size_t RANKINGS_INITIAL_SIZE = 100;
static const int RANKINGS_GROWTH_MULTIPLIER = 2;

struct Rankings {
	size_t count;
	size_t size;
	char **values;
	int *ranks;
};

static struct Rankings *rankings_new() {
	struct Rankings *r = malloc(sizeof(struct Rankings));
	if (!r) {
		return NULL;
	}
	r->count = 0;
	r->size = RANKINGS_INITIAL_SIZE;
	r->values = malloc(r->size * sizeof(char *));
	if (!r->values) {
		free(r);
		return NULL;
	}
	r->ranks = malloc(r->size * sizeof(int));
	if (!r->ranks) {
		free(r->values);
		free(r);
		return NULL;
	}
	return r;
}

#ifdef FALLIBLE
static struct Rankings *fallible_rankings_new(const char *const filename,
																							int lineno) {
	if (fallible_should_fail(filename, lineno)) {
		return NULL;
	}
	return rankings_new();
}

#define rankings_new() fallible_rankings_new(__FILE__, __LINE__)
#endif

static void rankings_free(struct Rankings *r) {
	if (!r) {
		return;
	}
	for (size_t i = 0; i < r->count; i++) {
		free(r->values[i]);
	}
	free(r->values);
	free(r->ranks);
	free(r);
}

static int rankings_insert(struct Rankings *r, char *value, int rank) {
	if (r->count == r->size) {
		r->size *= RANKINGS_GROWTH_MULTIPLIER;
		char **new_values = realloc(r->values, r->size * sizeof(char *));
		if (!new_values) {
			return -1;
		}
		r->values = new_values;
		int *new_ranks = realloc(r->ranks, r->size * sizeof(int));
		if (!new_ranks) {
			return -1;
		}
		r->ranks = new_ranks;
	}
	r->values[r->count] = value;
	r->ranks[r->count] = rank;
	r->count++;
	return 0;
}

#ifdef FALLIBLE
static int fallible_rankings_insert(struct Rankings *r, char *value, int rank,
																		const char *const filename, int lineno) {
	if (fallible_should_fail(filename, lineno)) {
		return -1;
	}
	return rankings_insert(r, value, rank);
}

#define rankings_insert(r, value, rank)																				 \
	fallible_rankings_insert(r, value, rank, __FILE__, __LINE__)
#endif

#ifdef TEST
static void rankings_test() {
	{
		testing("struct Rankings expands its size when needed");
		struct Rankings *r = rankings_new();
		if (!r) {
			exit(EXIT_FAILURE);
		}
		size_t some_limit = RANKINGS_INITIAL_SIZE + 1;
		int ret;
		for (size_t i = 0; i < some_limit; i++) {
			char *s = strdup("some string");
			ret = rankings_insert(r, s, i);
			if (ret) {
				free(s);
				rankings_free(r);
				exit(EXIT_FAILURE);
			}
		}

		assert(r->size == RANKINGS_INITIAL_SIZE * RANKINGS_GROWTH_MULTIPLIER);
		assert(r->count == some_limit);

		rankings_free(r);
		test_ok();
	}

	{
		testing("an empty Rankings doesn't leak");
		struct Rankings *r = rankings_new();
		if (!r) {
			exit(EXIT_FAILURE);
		}

		assert(r->size == RANKINGS_INITIAL_SIZE);
		assert(r->count == 0);

		rankings_free(r);
		test_ok();
	}
}
#endif

static const char RANKING_DELIMITER = ':';

int parse_ranked_line(FILE *stream, const char *entry, char **value,
											int *rank) {
	char *value_substr = strchr(entry, RANKING_DELIMITER);
	if (value_substr == NULL) {
		fprintf(stream, "WARN: Missing delimiter ('%c') in line: %s\n",
						RANKING_DELIMITER, entry);
		return -1;
	}

	int rank_strlen = value_substr - entry;
	char *rank_str = strndup(entry, rank_strlen);
	if (!rank_str) {
		return -1;
	}
	*rank = atoi(rank_str);
	free(rank_str);

	*value = malloc(strlen(value_substr) + 1);
	if (!*value) {
		return -1;
	}
	strcpy(*value, entry + (rank_strlen + 1 /* RANKING_DELIMITER */));

	return 0;
}

#ifdef FALLIBLE
int fallible_parse_ranked_line(FILE *stream, const char *entry, char **value,
															 int *rank, const char *const filename,
															 int lineno) {
	if (fallible_should_fail(filename, lineno)) {
		return -1;
	}
	return parse_ranked_line(stream, entry, value, rank);
}

#define parse_ranked_line(stream, entry, value, rank)													 \
	fallible_parse_ranked_line(stream, entry, value, rank, __FILE__, __LINE__)
#endif

#ifdef TEST
static void parse_ranked_line_test() {
	{
		testing("parse_ranked_line with an empty string");
		FILE *f = tmpfile();
		if (!f) {
			exit(EXIT_FAILURE);
		}

		char *value;
		int rank;
		int ret = parse_ranked_line(f, "", &value, &rank);
		assert(ret == -1);
		fclose(f);
		test_ok();
	}
	{
		testing("parse_ranked_line when RANKING DELIMITER is missing");
		FILE *f = tmpfile();
		if (!f) {
			exit(EXIT_FAILURE);
		}

		char *value;
		int rank, ret;

		ret = parse_ranked_line(f, "0 command", &value, &rank);
		assert(ret == -1);

		ret = parse_ranked_line(f, "1", &value, &rank);
		assert(ret == -1);

		fclose(f);
		test_ok();
	}
	{
		testing("parse_ranked_line with a happy path examples");
		char *value;
		int rank;
		int ret;

		ret = parse_ranked_line(stderr, "0:command", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "command") == 0);
		assert(rank == 0);
		free(value);

		ret = parse_ranked_line(stderr, "10:another command", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "another command") == 0);
		assert(rank == 10);
		free(value);

		ret = parse_ranked_line(stderr, "123:123", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "123") == 0);
		assert(rank == 123);
		free(value);

		ret = parse_ranked_line(stderr, "-123:command", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "command") == 0);
		assert(rank == -123);
		free(value);

		ret = parse_ranked_line(stderr, "0:0", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "0") == 0);
		assert(rank == 0);
		free(value);

		ret = parse_ranked_line(stderr, "0:command with : in the middle", &value,
														&rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "command with : in the middle") == 0);
		assert(rank == 0);
		free(value);

		ret = parse_ranked_line(
				stderr, "0:::command:with:multiple:::in:the:middle:", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "::command:with:multiple:::in:the:middle:") == 0);
		assert(rank == 0);
		free(value);

		test_ok();
	}
	{
		testing("parse_ranked_line with an empty command");
		char *value;
		int rank;

		int ret = parse_ranked_line(stderr, "0:", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "") == 0);
		assert(rank == 0);
		free(value);

		test_ok();
	}
	{
		testing("parse_ranked_line with a bad rank numbers");
		char *value;
		int rank;
		int ret;

		ret = parse_ranked_line(stderr, ":command", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "command") == 0);
		assert(rank == 0);
		free(value);

		ret = parse_ranked_line(stderr, "1 2 3:command", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "command") == 0);
		assert(rank == 1);
		free(value);

		ret = parse_ranked_line(stderr, ".1:command", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "command") == 0);
		assert(rank == 0);
		free(value);

		ret = parse_ranked_line(stderr, "3.14:command", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "command") == 0);
		assert(rank == 3);
		free(value);

		ret = parse_ranked_line(stderr, ":5:command", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "5:command") == 0);
		assert(rank == 0);
		free(value);

		ret = parse_ranked_line(stderr, "command:command", &value, &rank);
		if (ret) {
			exit(EXIT_FAILURE);
		}
		assert(ret == 0);
		assert(strcmp(value, "command") == 0);
		assert(rank == 0);
		free(value);

		test_ok();
	}
}
#endif

struct GetlineParams {
	char *line;
	size_t len;
	ssize_t read;
	FILE *stream;
};

int get_stdin(struct GetlineParams *s_params, struct GetlineParams *p_params,
							struct Rankings *p_rankings) {
	s_params->read = getline(&s_params->line, &s_params->len, s_params->stream);
	if (s_params->read == -1) {
		while ((p_params->read = getline(&p_params->line, &p_params->len,
																		 p_params->stream)) != -1) {
			char *value;
			int rank;
			int ret;
			if ((ret = parse_ranked_line(stderr, p_params->line, &value, &rank)) ==
					-1) {
				return ret;
			}
			if ((ret = rankings_insert(p_rankings, value, rank)) == -1) {
				return ret;
			}
		}
		return 1;
	}
	return 0;
}

int get_profile(struct GetlineParams *s_params, struct GetlineParams *p_params,
								struct Rankings *s_rankings, struct Rankings *p_rankings) {
	p_params->read = getline(&p_params->line, &p_params->len, p_params->stream);
	if (p_params->read == -1) {
		while ((s_params->read = getline(&s_params->line, &s_params->len,
																		 s_params->stream)) != -1) {
			char *s_value_copy = strdup(s_params->line);
			if (!s_value_copy) {
				return -1;
			}
			char *p_value_copy = strdup(s_params->line);
			if (!p_value_copy) {
				free(s_value_copy);
				return -1;
			}

			int ret;
			if ((ret = rankings_insert(s_rankings, s_value_copy, 0)) == -1) {
				free(s_value_copy);
				free(p_value_copy);
				return -1;
			}

			// profile grows with stdin entries
			if ((ret = rankings_insert(p_rankings, p_value_copy, 0)) == -1) {
				free(s_value_copy);
				free(p_value_copy);
				return -1;
			}
		}
		return 1;
	}
	return 0;
}

/*
int merge_stdin_with_profile(char *profile_name, struct Rankings *s_rankings,
														 struct Rankings *p_rankings) {
	FILE *profile = fopen(profile_name, "r");
	struct GetlineParams s_params = {
			.line = NULL, .len = 0, .read = 0, .stream = stdin};
	struct GetlineParams p_params = {
			.line = NULL, .len = 0, .read = 0, .stream = profile};

	int stop = get_stdin(&s_params, &p_params, p_rankings) ||
						 get_profile(&s_params, &p_params, s_rankings, p_rankings);
	while (!stop) {
		char *value;
		int rank;
		parse_ranked_line(stderr, p_params.line, &value, &rank);
		int cmp = strcmp(s_params.line, value);

		if (cmp == 0) {
			rankings_insert(s_rankings, value, rank);
			rankings_insert(p_rankings, value, rank); // double free
			stop = get_stdin(&s_params, &p_params, p_rankings) ||
						 get_profile(&s_params, &p_params, s_rankings, p_rankings);
			continue;
		}

		if (cmp < 0) {
			char *value_copy = malloc(strlen(s_params.line) + 1);
			strcpy(value_copy, s_params.line);
			int *rank = malloc(sizeof(int));
			*rank = 0;
			rankings_insert(s_rankings, value_copy, rank);
			rankings_insert(p_rankings, value_copy, rank);
			stop = get_stdin(&s_params, &p_params, p_rankings);
			tuple_free(&t);
			continue;
		}

		if (cmp > 0) {
			rankings_insert(p_rankings, t.first, t.second);
			stop = get_profile(&s_params, &p_params, s_rankings, p_rankings);
			tuple_free(&t);
			continue;
		}
	}

	free(s_params.line);
	free(p_params.line);
	fclose(profile);

	if (stop == -1) {
		return stop;
	}
	return 0;
}
*/

#ifdef TEST
static void unit_tests() {
	print_usage_test();
	get_options_test();
	mkdir_p_test();
	expand_profile_name_test();
	rankings_test();
	parse_ranked_line_test();
}
#endif

int main(int argc, char *argv[]) {
#ifdef TEST
	unit_tests();
	return EXIT_SUCCESS;
#endif

	int ret;

	char *command, *profile;
	ret = get_options(stdout, stderr, argc, argv, &command, &profile);
	if (ret == -1) {
		return EXIT_FAILURE;
	} else if (ret) {
		return EXIT_SUCCESS;
	}
}