#define USE_THE_REPOSITORY_VARIABLE

#include "git-compat-util.h"
#include "environment.h"
#include "hex.h"
#include "merge-ort.h"
#include "object-name.h"
#include "refs.h"
#include "replay.h"
#include "revision.h"
#include "sequencer.h"
#include "strmap.h"
#include "tree.h"

/*
 * We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but
 * do not want to use the_repository.
 */
#define the_repository DO_NOT_USE_THE_REPOSITORY

enum replay_mode {
	REPLAY_MODE_PICK,
	REPLAY_MODE_REVERT,
};

static const char *short_commit_name(struct repository *repo,
				     struct commit *commit)
{
	return repo_find_unique_abbrev(repo, &commit->object.oid,
				       DEFAULT_ABBREV);
}

static struct commit *peel_committish(struct repository *repo,
				      const char *name,
				      const char *mode)
{
	struct object *obj;
	struct object_id oid;

	if (repo_get_oid(repo, name, &oid))
		die(_("'%s' is not a valid commit-ish for %s"), name, mode);
	obj = parse_object_or_die(repo, &oid, name);
	return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
						  OBJ_COMMIT);
}

static char *get_author(const char *message)
{
	size_t len;
	const char *a;

	a = find_commit_header(message, "author", &len);
	if (a)
		return xmemdupz(a, len);

	return NULL;
}

static void generate_revert_message(struct strbuf *msg,
				    struct commit *commit,
				    struct repository *repo)
{
	const char *out_enc = get_commit_output_encoding();
	const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);
	const char *subject_start;
	int subject_len;
	char *subject;

	subject_len = find_commit_subject(message, &subject_start);
	subject = xmemdupz(subject_start, subject_len);

	sequencer_format_revert_message(repo, subject, commit,
					commit->parents ? commit->parents->item : NULL,
					false, msg);

	free(subject);
	repo_unuse_commit_buffer(repo, commit, message);
}

static struct commit *create_commit(struct repository *repo,
				    struct tree *tree,
				    struct commit *based_on,
				    struct commit *parent,
				    enum replay_mode mode)
{
	struct object_id ret;
	struct object *obj = NULL;
	struct commit_list *parents = NULL;
	char *author = NULL;
	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
	struct commit_extra_header *extra = NULL;
	struct strbuf msg = STRBUF_INIT;
	const char *out_enc = get_commit_output_encoding();
	const char *message = repo_logmsg_reencode(repo, based_on,
						   NULL, out_enc);
	const char *orig_message = NULL;
	const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };

	commit_list_insert(parent, &parents);
	extra = read_commit_extra_headers(based_on, exclude_gpgsig);
	if (mode == REPLAY_MODE_REVERT) {
		generate_revert_message(&msg, based_on, repo);
		/* For revert, use current user as author (NULL = use default) */
	} else if (mode == REPLAY_MODE_PICK) {
		find_commit_subject(message, &orig_message);
		strbuf_addstr(&msg, orig_message);
		author = get_author(message);
	} else {
		BUG("unexpected replay mode %d", mode);
	}
	reset_ident_date();
	if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
				 &ret, author, NULL, sign_commit, extra)) {
		error(_("failed to write commit object"));
		goto out;
	}

	obj = parse_object(repo, &ret);

out:
	repo_unuse_commit_buffer(repo, based_on, message);
	free_commit_extra_headers(extra);
	commit_list_free(parents);
	strbuf_release(&msg);
	free(author);
	return (struct commit *)obj;
}

struct ref_info {
	struct commit *onto;
	struct strset positive_refs;
	struct strset negative_refs;
	size_t positive_refexprs;
	size_t negative_refexprs;
};

static void get_ref_information(struct repository *repo,
				struct rev_cmdline_info *cmd_info,
				struct ref_info *ref_info)
{
	ref_info->onto = NULL;
	strset_init(&ref_info->positive_refs);
	strset_init(&ref_info->negative_refs);
	ref_info->positive_refexprs = 0;
	ref_info->negative_refexprs = 0;

	/*
	 * When the user specifies e.g.
	 *   git replay origin/main..mybranch
	 *   git replay ^origin/next mybranch1 mybranch2
	 * we want to be able to determine where to replay the commits.  In
	 * these examples, the branches are probably based on an old version
	 * of either origin/main or origin/next, so we want to replay on the
	 * newest version of that branch.  In contrast we would want to error
	 * out if they ran
	 *   git replay ^origin/master ^origin/next mybranch
	 *   git replay mybranch~2..mybranch
	 * the first of those because there's no unique base to choose, and
	 * the second because they'd likely just be replaying commits on top
	 * of the same commit and not making any difference.
	 */
	for (size_t i = 0; i < cmd_info->nr; i++) {
		struct rev_cmdline_entry *e = cmd_info->rev + i;
		struct object_id oid;
		const char *refexpr = e->name;
		char *fullname = NULL;
		int can_uniquely_dwim = 1;

		if (*refexpr == '^')
			refexpr++;
		if (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
			can_uniquely_dwim = 0;

		if (e->flags & BOTTOM) {
			if (can_uniquely_dwim)
				strset_add(&ref_info->negative_refs, fullname);
			if (!ref_info->negative_refexprs)
				ref_info->onto = lookup_commit_reference_gently(repo,
										&e->item->oid, 1);
			ref_info->negative_refexprs++;
		} else {
			if (can_uniquely_dwim)
				strset_add(&ref_info->positive_refs, fullname);
			ref_info->positive_refexprs++;
		}

		free(fullname);
	}
}

static void set_up_branch_mode(struct repository *repo,
			       char **branch_name,
			       const char *option_name,
			       struct ref_info *rinfo,
			       struct commit **onto)
{
	struct object_id oid;
	char *fullname = NULL;

	if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
			  &oid, &fullname, 0) == 1) {
		free(*branch_name);
		*branch_name = fullname;
	} else {
		die(_("argument to %s must be a reference"), option_name);
	}
	*onto = peel_committish(repo, *branch_name, option_name);
	if (rinfo->positive_refexprs > 1)
		die(_("'%s' cannot be used with multiple revision ranges "
		      "because the ordering would be ill-defined"),
		    option_name);
}

static void set_up_replay_mode(struct repository *repo,
			       struct rev_cmdline_info *cmd_info,
			       const char *onto_name,
			       bool *detached_head,
			       char **advance_name,
			       char **revert_name,
			       struct commit **onto,
			       struct strset **update_refs)
{
	struct ref_info rinfo;
	int head_flags = 0;

	refs_read_ref_full(get_main_ref_store(repo), "HEAD",
			   RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
	*detached_head = !(head_flags & REF_ISSYMREF);

	get_ref_information(repo, cmd_info, &rinfo);
	if (!rinfo.positive_refexprs)
		die(_("need some commits to replay"));

	if (onto_name) {
		*onto = peel_committish(repo, onto_name, "--onto");
		if (rinfo.positive_refexprs <
		    strset_get_size(&rinfo.positive_refs))
			die(_("all positive revisions given must be references"));
		*update_refs = xcalloc(1, sizeof(**update_refs));
		**update_refs = rinfo.positive_refs;
		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
	} else if (*advance_name) {
		set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto);
	} else if (*revert_name) {
		set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto);
	} else {
		BUG("expected one of onto_name, *advance_name, or *revert_name");
	}
	strset_clear(&rinfo.negative_refs);
	strset_clear(&rinfo.positive_refs);
}

static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
				    struct commit *commit,
				    struct commit *fallback)
{
	khint_t pos;
	if (!commit)
		return fallback;
	pos = kh_get_oid_map(replayed_commits, commit->object.oid);
	if (pos == kh_end(replayed_commits))
		return fallback;
	return kh_value(replayed_commits, pos);
}

static struct commit *pick_regular_commit(struct repository *repo,
					  struct commit *pickme,
					  kh_oid_map_t *replayed_commits,
					  struct commit *onto,
					  struct merge_options *merge_opt,
					  struct merge_result *result,
					  enum replay_mode mode,
					  enum replay_empty_commit_action empty)
{
	struct commit *base, *replayed_base;
	struct tree *pickme_tree, *base_tree, *replayed_base_tree;

	if (pickme->parents) {
		base = pickme->parents->item;
		base_tree = repo_get_commit_tree(repo, base);
	} else {
		base = NULL;
		base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
	}

	replayed_base = mapped_commit(replayed_commits, base, onto);
	replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
	pickme_tree = repo_get_commit_tree(repo, pickme);

	if (mode == REPLAY_MODE_PICK) {
		/* Cherry-pick: normal order */
		merge_opt->branch1 = short_commit_name(repo, replayed_base);
		merge_opt->branch2 = short_commit_name(repo, pickme);
		if (pickme->parents)
			merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
		else
			merge_opt->ancestor = xstrdup("empty tree");

		merge_incore_nonrecursive(merge_opt,
					  base_tree,
					  replayed_base_tree,
					  pickme_tree,
					  result);

		free((char *)merge_opt->ancestor);
	} else if (mode == REPLAY_MODE_REVERT) {
		/* Revert: swap base and pickme to reverse the diff */
		const char *pickme_name = short_commit_name(repo, pickme);
		merge_opt->branch1 = short_commit_name(repo, replayed_base);
		merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
		merge_opt->ancestor = pickme_name;

		merge_incore_nonrecursive(merge_opt,
					  pickme_tree,
					  replayed_base_tree,
					  base_tree,
					  result);

		free((char *)merge_opt->branch2);
	} else {
		BUG("unexpected replay mode %d", mode);
	}
	merge_opt->ancestor = NULL;
	merge_opt->branch2 = NULL;

	if (!result->clean)
		return NULL;

	/* Handle commits that become empty */
	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) {
		switch (empty) {
		case REPLAY_EMPTY_COMMIT_DROP:
			return replayed_base;
		case REPLAY_EMPTY_COMMIT_KEEP:
			break;
		case REPLAY_EMPTY_COMMIT_ABORT:
			result->clean = error(_("commit %s became empty after replay"),
					      oid_to_hex(&pickme->object.oid));
			return NULL;
		}
	}

	return create_commit(repo, result->tree, pickme, replayed_base, mode);
}

void replay_result_release(struct replay_result *result)
{
	for (size_t i = 0; i < result->updates_nr; i++)
		free(result->updates[i].refname);
	free(result->updates);
}

static void replay_result_queue_update(struct replay_result *result,
				       const char *refname,
				       const struct object_id *old_oid,
				       const struct object_id *new_oid)
{
	ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);
	result->updates[result->updates_nr].refname = xstrdup(refname);
	result->updates[result->updates_nr].old_oid = *old_oid;
	result->updates[result->updates_nr].new_oid = *new_oid;
	result->updates_nr++;
}

int replay_revisions(struct rev_info *revs,
		     struct replay_revisions_options *opts,
		     struct replay_result *out)
{
	kh_oid_map_t *replayed_commits = NULL;
	struct strset *update_refs = NULL;
	struct commit *last_commit = NULL;
	struct commit *commit;
	struct commit *onto = NULL;
	struct merge_options merge_opt = { 0 };
	struct merge_result result = {
		.clean = 1,
	};
	bool detached_head;
	char *advance;
	char *revert;
	const char *ref;
	struct object_id old_oid;
	enum replay_mode mode = REPLAY_MODE_PICK;
	int ret;

	advance = xstrdup_or_null(opts->advance);
	revert = xstrdup_or_null(opts->revert);
	if (revert)
		mode = REPLAY_MODE_REVERT;
	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
			   &detached_head, &advance, &revert, &onto, &update_refs);

	if (opts->ref) {
		struct object_id oid;

		if (update_refs && strset_get_size(update_refs) > 1) {
			ret = error(_("'--ref' cannot be used with multiple revision ranges"));
			goto out;
		}
		if (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, "refs/")) {
			ret = error(_("'%s' is not a valid refname"), opts->ref);
			goto out;
		}
		ref = opts->ref;
		if (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid))
			oidcpy(&old_oid, &oid);
		else
			oidclr(&old_oid, revs->repo->hash_algo);
	} else {
		ref = advance ? advance : revert;
		oidcpy(&old_oid, &onto->object.oid);
	}

	if (prepare_revision_walk(revs) < 0) {
		ret = error(_("error preparing revisions"));
		goto out;
	}

	init_basic_merge_options(&merge_opt, revs->repo);
	merge_opt.show_rename_progress = 0;
	last_commit = onto;
	replayed_commits = kh_init_oid_map();
	while ((commit = get_revision(revs))) {
		const struct name_decoration *decoration;
		khint_t pos;
		int hr;

		if (commit->parents && commit->parents->next)
			die(_("replaying merge commits is not supported yet!"));

		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
						  mode == REPLAY_MODE_REVERT ? last_commit : onto,
						  &merge_opt, &result, mode, opts->empty);
		if (!last_commit)
			break;

		/* Record commit -> last_commit mapping */
		pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
		if (hr == 0)
			BUG("Duplicate rewritten commit: %s\n",
			    oid_to_hex(&commit->object.oid));
		kh_value(replayed_commits, pos) = last_commit;

		/* Update any necessary branches */
		if (ref)
			continue;

		for (decoration = get_name_decoration(&commit->object);
		     decoration;
		     decoration = decoration->next)
		{
			if (decoration->type != DECORATION_REF_LOCAL &&
			    decoration->type != DECORATION_REF_HEAD)
				continue;

			/*
			 * We only need to update HEAD separately in case it's
			 * detached. If it's not we'd already update the branch
			 * it is pointing to.
			 */
			if (decoration->type == DECORATION_REF_HEAD && !detached_head)
				continue;

			if (!opts->contained &&
			    !strset_contains(update_refs, decoration->name))
				continue;

			replay_result_queue_update(out, decoration->name,
						   &commit->object.oid,
						   &last_commit->object.oid);
		}
	}

	if (result.clean < 0) {
		ret = -1;
		goto out;
	}

	if (!result.clean) {
		ret = 1;
		goto out;
	}

	if (ref)
		replay_result_queue_update(out, ref, &old_oid,
					   &last_commit->object.oid);

	ret = 0;

out:
	if (update_refs) {
		strset_clear(update_refs);
		free(update_refs);
	}
	kh_destroy_oid_map(replayed_commits);
	merge_finalize(&merge_opt, &result);
	free(advance);
	free(revert);
	return ret;
}
