#include <stdio.h>
#include <git2.h>
#include <stdlib.h>
#include <string.h>

static void check(int error, const char *message, const char *arg)
{
	if (!error)
		return;
	if (arg)
		fprintf(stderr, "%s '%s' (%d)\n", message, arg, error);
	else
		fprintf(stderr, "%s (%d)\n", message, error);
	exit(1);
}

static void usage(const char *message, const char *arg)
{
	if (message && arg)
		fprintf(stderr, "%s: %s\n", message, arg);
	else if (message)
		fprintf(stderr, "%s\n", message);
	fprintf(stderr, "usage: log [<options>]\n");
	exit(1);
}

struct log_state {
	git_repository *repo;
	const char *repodir;
	git_revwalk *walker;
	int hide;
	int sorting;
};

static void set_sorting(struct log_state *s, unsigned int sort_mode)
{
	if (!s->repo) {
		if (!s->repodir) s->repodir = ".";
		check(git_repository_open_ext(&s->repo, s->repodir, 0, NULL),
			"Could not open repository", s->repodir);
	}

	if (!s->walker)
		check(git_revwalk_new(&s->walker, s->repo),
			"Could not create revision walker", NULL);

	if (sort_mode == GIT_SORT_REVERSE)
		s->sorting = s->sorting ^ GIT_SORT_REVERSE;
	else
		s->sorting = sort_mode | (s->sorting & GIT_SORT_REVERSE);

	git_revwalk_sorting(s->walker, s->sorting);
}

static void push_rev(struct log_state *s, git_object *obj, int hide)
{
	hide = s->hide ^ hide;

	if (!s->walker) {
		check(git_revwalk_new(&s->walker, s->repo),
			"Could not create revision walker", NULL);
		git_revwalk_sorting(s->walker, s->sorting);
	}

	if (!obj)
		check(git_revwalk_push_head(s->walker),
			"Could not find repository HEAD", NULL);
	else if (hide)
		check(git_revwalk_hide(s->walker, git_object_id(obj)),
			"Reference does not refer to a commit", NULL);
	else
		check(git_revwalk_push(s->walker, git_object_id(obj)),
			"Reference does not refer to a commit", NULL);

	git_object_free(obj);
}

static int add_revision(struct log_state *s, const char *revstr)
{
	git_revspec revs;
	int hide = 0;

	if (!s->repo) {
		if (!s->repodir) s->repodir = ".";
		check(git_repository_open_ext(&s->repo, s->repodir, 0, NULL),
			"Could not open repository", s->repodir);
	}

	if (!revstr) {
		push_rev(s, NULL, hide);
		return 0;
	}

	if (*revstr == '^') {
		revs.flags = GIT_REVPARSE_SINGLE;
		hide = !hide;

		if (git_revparse_single(&revs.from, s->repo, revstr + 1) < 0)
			return -1;
	} else if (git_revparse(&revs, s->repo, revstr) < 0)
		return -1;

	if ((revs.flags & GIT_REVPARSE_SINGLE) != 0)
		push_rev(s, revs.from, hide);
	else {
		push_rev(s, revs.to, hide);

		if ((revs.flags & GIT_REVPARSE_MERGE_BASE) != 0) {
			git_oid base;
			check(git_merge_base(&base, s->repo,
				git_object_id(revs.from), git_object_id(revs.to)),
				"Could not find merge base", revstr);
			check(git_object_lookup(&revs.to, s->repo, &base, GIT_OBJ_COMMIT),
				"Could not find merge base commit", NULL);

			push_rev(s, revs.to, hide);
		}

		push_rev(s, revs.from, !hide);
	}

	return 0;
}

static void print_time(const git_time *intime, const char *prefix)
{
	char sign, out[32];
	struct tm intm;
	int offset, hours, minutes;
	time_t t;

	offset = intime->offset;
	if (offset < 0) {
		sign = '-';
		offset = -offset;
	} else {
		sign = '+';
	}

	hours   = offset / 60;
	minutes = offset % 60;

	t = (time_t)intime->time + (intime->offset * 60);

	gmtime_r(&t, &intm);
	strftime(out, sizeof(out), "%a %b %e %T %Y", &intm);

	printf("%s%s %c%02d%02d\n", prefix, out, sign, hours, minutes);
}

static void print_commit(git_commit *commit)
{
	char buf[GIT_OID_HEXSZ + 1];
	int i, count;
	const git_signature *sig;
	const char *scan, *eol;

	git_oid_tostr(buf, sizeof(buf), git_commit_id(commit));
	printf("commit %s\n", buf);

	if ((count = (int)git_commit_parentcount(commit)) > 1) {
		printf("Merge:");
		for (i = 0; i < count; ++i) {
			git_oid_tostr(buf, 8, git_commit_parent_id(commit, i));
			printf(" %s", buf);
		}
		printf("\n");
	}

	if ((sig = git_commit_author(commit)) != NULL) {
		printf("Author: %s <%s>\n", sig->name, sig->email);
		print_time(&sig->when, "Date:   ");
	}
	printf("\n");

	for (scan = git_commit_message(commit); scan && *scan; ) {
		for (eol = scan; *eol && *eol != '\n'; ++eol) /* find eol */;

		printf("    %.*s\n", (int)(eol - scan), scan);
		scan = *eol ? eol + 1 : NULL;
	}
	printf("\n");
}

static int print_diff(
	const git_diff_delta *delta,
	const git_diff_hunk *hunk,
	const git_diff_line *line,
	void *data)
{
	(void)delta; (void)hunk; (void)data;

	if (line->origin == GIT_DIFF_LINE_CONTEXT ||
		line->origin == GIT_DIFF_LINE_ADDITION ||
		line->origin == GIT_DIFF_LINE_DELETION)
		fputc(line->origin, stdout);

	fwrite(line->content, 1, line->content_len, stdout);
	return 0;
}

static int match_int(int *value, const char *arg, int allow_negative)
{
	char *found;
	*value = (int)strtol(arg, &found, 10);
	return (found && *found == '\0' && (allow_negative || *value >= 0));
}

static int match_int_arg(
	int *value, const char *arg, const char *pfx, int allow_negative)
{
	size_t pfxlen = strlen(pfx);
	if (strncmp(arg, pfx, pfxlen) != 0)
		return 0;
	if (!match_int(value, arg + pfxlen, allow_negative))
		usage("Invalid value after argument", arg);
	return 1;
}

static int match_with_parent(
	git_commit *commit, int i, git_diff_options *opts)
{
	git_commit *parent;
	git_tree *a, *b;
	git_diff *diff;
	int ndeltas;

	check(git_commit_parent(&parent, commit, (size_t)i), "Get parent", NULL);
	check(git_commit_tree(&a, parent), "Tree for parent", NULL);
	check(git_commit_tree(&b, commit), "Tree for commit", NULL);
	check(git_diff_tree_to_tree(&diff, git_commit_owner(commit), a, b, opts),
		  "Checking diff between parent and commit", NULL);

	ndeltas = (int)git_diff_num_deltas(diff);

	git_diff_free(diff);
	git_tree_free(a);
	git_tree_free(b);
	git_commit_free(parent);

	return ndeltas > 0;
}

struct log_options {
	int show_diff;
	int skip, limit;
	int min_parents, max_parents;
	git_time_t before;
	git_time_t after;
	char *author;
	char *committer;
};

int main(int argc, char *argv[])
{
	int i, count = 0, printed = 0, parents;
	char *a;
	struct log_state s;
	struct log_options opt;
	git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT;
	git_oid oid;
	git_commit *commit = NULL;
	git_pathspec *ps = NULL;

	git_threads_init();

	memset(&s, 0, sizeof(s));
	s.sorting = GIT_SORT_TIME;

	memset(&opt, 0, sizeof(opt));
	opt.max_parents = -1;
 	opt.limit = -1;

	for (i = 1; i < argc; ++i) {
		a = argv[i];

		if (a[0] != '-') {
			if (!add_revision(&s, a))
				++count;
			else /* try failed revision parse as filename */
				break;
		} else if (!strcmp(a, "--")) {
			++i;
			break;
		}
		else if (!strcmp(a, "--date-order"))
			set_sorting(&s, GIT_SORT_TIME);
		else if (!strcmp(a, "--topo-order"))
			set_sorting(&s, GIT_SORT_TOPOLOGICAL);
		else if (!strcmp(a, "--reverse"))
			set_sorting(&s, GIT_SORT_REVERSE);
		else if (!strncmp(a, "--git-dir=", strlen("--git-dir=")))
			s.repodir = a + strlen("--git-dir=");
		else if (match_int_arg(&opt.skip, a, "--skip=", 0))
			/* found valid --skip */;
		else if (match_int_arg(&opt.limit, a, "--max-count=", 0))
			/* found valid --max-count */;
		else if (a[1] >= '0' && a[1] <= '9') {
			if (!match_int(&opt.limit, a + 1, 0))
				usage("Invalid limit on number of commits", a);
		} else if (!strcmp(a, "-n")) {
			if (i + 1 == argc || !match_int(&opt.limit, argv[i + 1], 0))
				usage("Argument -n not followed by valid count", argv[i + 1]);
			else
				++i;
		}
		else if (!strcmp(a, "--merges"))
			opt.min_parents = 2;
		else if (!strcmp(a, "--no-merges"))
			opt.max_parents = 1;
		else if (!strcmp(a, "--no-min-parents"))
			opt.min_parents = 0;
		else if (!strcmp(a, "--no-max-parents"))
			opt.max_parents = -1;
		else if (match_int_arg(&opt.max_parents, a, "--max-parents=", 1))
			/* found valid --max-parents */;
		else if (match_int_arg(&opt.min_parents, a, "--min-parents=", 0))
			/* found valid --min_parents */;
		else if (!strcmp(a, "-p") || !strcmp(a, "-u") || !strcmp(a, "--patch"))
			opt.show_diff = 1;
		else
			usage("Unsupported argument", a);
	}

	if (!count)
		add_revision(&s, NULL);

	diffopts.pathspec.strings = &argv[i];
	diffopts.pathspec.count   = argc - i;
	if (diffopts.pathspec.count > 0)
		check(git_pathspec_new(&ps, &diffopts.pathspec),
			"Building pathspec", NULL);

	printed = count = 0;

	for (; !git_revwalk_next(&oid, s.walker); git_commit_free(commit)) {
		check(git_commit_lookup(&commit, s.repo, &oid),
			"Failed to look up commit", NULL);

		parents = (int)git_commit_parentcount(commit);
		if (parents < opt.min_parents)
			continue;
		if (opt.max_parents > 0 && parents > opt.max_parents)
			continue;

		if (diffopts.pathspec.count > 0) {
			int unmatched = parents;

			if (parents == 0) {
				git_tree *tree;
				check(git_commit_tree(&tree, commit), "Get tree", NULL);
				if (git_pathspec_match_tree(
						NULL, tree, GIT_PATHSPEC_NO_MATCH_ERROR, ps) != 0)
					unmatched = 1;
				git_tree_free(tree);
			} else if (parents == 1) {
				unmatched = match_with_parent(commit, 0, &diffopts) ? 0 : 1;
			} else {
				for (i = 0; i < parents; ++i) {
					if (match_with_parent(commit, i, &diffopts))
						unmatched--;
				}
			}

			if (unmatched > 0)
				continue;
		}

		if (count++ < opt.skip)
			continue;
		if (opt.limit != -1 && printed++ >= opt.limit) {
			git_commit_free(commit);
			break;
		}

		print_commit(commit);

		if (opt.show_diff) {
			git_tree *a = NULL, *b = NULL;
			git_diff *diff = NULL;

			if (parents > 1)
				continue;
			check(git_commit_tree(&b, commit), "Get tree", NULL);
			if (parents == 1) {
				git_commit *parent;
				check(git_commit_parent(&parent, commit, 0), "Get parent", NULL);
				check(git_commit_tree(&a, parent), "Tree for parent", NULL);
				git_commit_free(parent);
			}

			check(git_diff_tree_to_tree(
				&diff, git_commit_owner(commit), a, b, &diffopts),
				"Diff commit with parent", NULL);
			check(git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, print_diff, NULL),
				"Displaying diff", NULL);

			git_diff_free(diff);
			git_tree_free(a);
			git_tree_free(b);
		}
	}

	git_pathspec_free(ps);
	git_revwalk_free(s.walker);
	git_repository_free(s.repo);
	git_threads_shutdown();

	return 0;
}
