/*
 * Copyright (C) 2009-2012 the libgit2 contributors
 *
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
 */

#include <assert.h>

#include "git2/checkout.h"
#include "git2/repository.h"
#include "git2/refs.h"
#include "git2/tree.h"
#include "git2/blob.h"
#include "git2/config.h"
#include "git2/diff.h"

#include "common.h"
#include "refs.h"
#include "buffer.h"
#include "repository.h"
#include "filter.h"
#include "blob.h"
#include "diff.h"

struct checkout_diff_data
{
	git_buf *path;
	size_t workdir_len;
	git_checkout_opts *checkout_opts;
	git_repository *owner;
	bool can_symlink;
	bool found_submodules;
	bool create_submodules;
	int error;
	size_t total_steps;
	size_t completed_steps;
};

static int buffer_to_file(
	git_buf *buffer,
	const char *path,
	mode_t dir_mode,
	int file_open_flags,
	mode_t file_mode)
{
	int fd, error;

	if ((error = git_futils_mkpath2file(path, dir_mode)) < 0)
		return error;

	if ((fd = p_open(path, file_open_flags, file_mode)) < 0) {
		giterr_set(GITERR_OS, "Could not open '%s' for writing", path);
		return fd;
	}

	if ((error = p_write(fd, git_buf_cstr(buffer), git_buf_len(buffer))) < 0) {
		giterr_set(GITERR_OS, "Could not write to '%s'", path);
		(void)p_close(fd);
	} else {
		if ((error = p_close(fd)) < 0)
			giterr_set(GITERR_OS, "Error while closing '%s'", path);
	}

	if (!error &&
		(file_mode & 0100) != 0 &&
		(error = p_chmod(path, file_mode)) < 0)
		giterr_set(GITERR_OS, "Failed to set permissions on '%s'", path);

	return error;
}

static int blob_content_to_file(
	git_blob *blob,
	const char *path,
	mode_t entry_filemode,
	git_checkout_opts *opts)
{
	int error = -1, nb_filters = 0;
	mode_t file_mode = opts->file_mode;
	bool dont_free_filtered = false;
	git_buf unfiltered = GIT_BUF_INIT, filtered = GIT_BUF_INIT;
	git_vector filters = GIT_VECTOR_INIT;

	if (opts->disable_filters ||
		(nb_filters = git_filters_load(
			&filters,
			git_object_owner((git_object *)blob),
			path,
			GIT_FILTER_TO_WORKTREE)) == 0) {

		/* Create a fake git_buf from the blob raw data... */
		filtered.ptr = blob->odb_object->raw.data;
		filtered.size = blob->odb_object->raw.len;

		/* ... and make sure it doesn't get unexpectedly freed */
		dont_free_filtered = true;
	}

	if (nb_filters < 0)
		return nb_filters;

	if (nb_filters > 0)	 {
		if ((error = git_blob__getbuf(&unfiltered, blob)) < 0)
			goto cleanup;

		if ((error = git_filters_apply(&filtered, &unfiltered, &filters)) < 0)
			goto cleanup;
	}

	/* Allow overriding of file mode */
	if (!file_mode)
		file_mode = entry_filemode;

	error = buffer_to_file(&filtered, path, opts->dir_mode, opts->file_open_flags, file_mode);

cleanup:
	git_filters_free(&filters);
	git_buf_free(&unfiltered);
	if (!dont_free_filtered)
		git_buf_free(&filtered);

	return error;
}

static int blob_content_to_link(git_blob *blob, const char *path, bool can_symlink)
{
	git_buf linktarget = GIT_BUF_INIT;
	int error;

	if ((error = git_blob__getbuf(&linktarget, blob)) < 0)
		return error;

	if (can_symlink)
		error = p_symlink(git_buf_cstr(&linktarget), path);
	else
		error = git_futils_fake_symlink(git_buf_cstr(&linktarget), path);

	git_buf_free(&linktarget);

	return error;
}

static int checkout_submodule(
	struct checkout_diff_data *data,
	const git_diff_file *file)
{
	if (git_futils_mkdir(
			file->path, git_repository_workdir(data->owner),
			data->checkout_opts->dir_mode, GIT_MKDIR_PATH) < 0)
		return -1;

	/* TODO: two cases:
	 * 1 - submodule already checked out, but we need to move the HEAD
	 *     to the new OID, or
	 * 2 - submodule not checked out and we should recursively check it out
	 *
	 * Checkout will not execute a pull request on the submodule, but a
	 * clone command should probably be able to.  Do we need a submodule
	 * callback option?
	 */

	return 0;
}

static void report_progress(
	struct checkout_diff_data *data,
	const char *path)
{
	if (data->checkout_opts->progress_cb)
		data->checkout_opts->progress_cb(
				path,
				data->completed_steps,
				data->total_steps,
				data->checkout_opts->progress_payload);
}

static int checkout_blob(
	struct checkout_diff_data *data,
	const git_diff_file *file)
{
	git_blob *blob;
	int error = 0;

	git_buf_truncate(data->path, data->workdir_len);
	if (git_buf_joinpath(data->path, git_buf_cstr(data->path), file->path) < 0)
		return -1;

	/* If there is a directory where this blob should go, then there is an
	 * existing tree that conflicts with this file, but REMOVE_UNTRACKED must
	 * not have been passed in.  Signal to caller with GIT_ENOTFOUND.
	 */
	if (git_path_isdir(git_buf_cstr(data->path)))
		return GIT_ENOTFOUND;

	if ((error = git_blob_lookup(&blob, data->owner, &file->oid)) < 0)
		return error;

	if (S_ISLNK(file->mode))
		error = blob_content_to_link(
			blob, git_buf_cstr(data->path), data->can_symlink);
	else
		error = blob_content_to_file(
			blob, git_buf_cstr(data->path), file->mode, data->checkout_opts);

	git_blob_free(blob);

	return error;
}

static int retrieve_symlink_capabilities(git_repository *repo, bool *can_symlink)
{
	git_config *cfg;
	int error;

	if (git_repository_config__weakptr(&cfg, repo) < 0)
		return -1;

	error = git_config_get_bool((int *)can_symlink, cfg, "core.symlinks");

	/*
	 * When no "core.symlinks" entry is found in any of the configuration
	 * store (local, global or system), default value is "true".
	 */
	if (error == GIT_ENOTFOUND) {
		*can_symlink = true;
		error = 0;
	}

	return error;
}

static void normalize_options(git_checkout_opts *normalized, git_checkout_opts *proposed)
{
	assert(normalized);

	if (!proposed)
		memset(normalized, 0, sizeof(git_checkout_opts));
	else
		memmove(normalized, proposed, sizeof(git_checkout_opts));

	/* Default options */
	if (!normalized->checkout_strategy)
		normalized->checkout_strategy = GIT_CHECKOUT_DEFAULT;

	/* opts->disable_filters is false by default */
	if (!normalized->dir_mode)
		normalized->dir_mode = GIT_DIR_MODE;

	if (!normalized->file_open_flags)
		normalized->file_open_flags = O_CREAT | O_TRUNC | O_WRONLY;
}

enum {
	CHECKOUT_ACTION__NONE = 0,
	CHECKOUT_ACTION__REMOVE = 1,
	CHECKOUT_ACTION__CREATE_BLOB = 2,
	CHECKOUT_ACTION__CREATE_SUBMODULE = 4,
	CHECKOUT_ACTION__NOTIFY = 8
};

static uint32_t checkout_action_for_delta(
	git_checkout_opts *opts,
	const git_diff_delta *delta)
{
	uint32_t action = CHECKOUT_ACTION__NONE;

	switch (delta->status) {
	case GIT_DELTA_UNTRACKED:
		if ((opts->checkout_strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0)
			action |= CHECKOUT_ACTION__REMOVE;
		break;

	case GIT_DELTA_TYPECHANGE:
		if ((opts->checkout_strategy & GIT_CHECKOUT_OVERWRITE_MODIFIED) != 0)
			action |= CHECKOUT_ACTION__REMOVE | CHECKOUT_ACTION__CREATE_BLOB;
		else if (opts->skipped_notify_cb != NULL)
			action = CHECKOUT_ACTION__NOTIFY;
		break;

	case GIT_DELTA_DELETED:
		if ((opts->checkout_strategy & GIT_CHECKOUT_CREATE_MISSING) != 0)
			action |= CHECKOUT_ACTION__CREATE_BLOB;
		break;

	case GIT_DELTA_MODIFIED:
		if ((opts->checkout_strategy & GIT_CHECKOUT_OVERWRITE_MODIFIED) != 0)
			action |= CHECKOUT_ACTION__CREATE_BLOB;
		else if (opts->skipped_notify_cb != NULL)
			action = CHECKOUT_ACTION__NOTIFY;
		break;

	default:
		break;
	}

	if ((action & CHECKOUT_ACTION__CREATE_BLOB) != 0 &&
		S_ISGITLINK(delta->old_file.mode))
		action = (action & ~CHECKOUT_ACTION__CREATE_BLOB) |
			CHECKOUT_ACTION__CREATE_SUBMODULE;

	return action;
}

static int checkout_get_actions(
	uint32_t **actions_ptr,
	size_t **counts_ptr,
	git_diff_list *diff,
	git_checkout_opts *opts)
{
	git_diff_delta *delta;
	size_t i, *counts;
	uint32_t *actions;

	*counts_ptr = counts =
		git__calloc(CHECKOUT_ACTION__NOTIFY, sizeof(size_t));
	GITERR_CHECK_ALLOC(counts);

	*actions_ptr = actions =
		git__calloc(diff->deltas.length, sizeof(uint32_t));
	GITERR_CHECK_ALLOC(actions);

	git_vector_foreach(&diff->deltas, i, delta) {
		actions[i] = checkout_action_for_delta(opts, delta);

		if (actions[i] & CHECKOUT_ACTION__REMOVE)
			counts[CHECKOUT_ACTION__REMOVE]++;

		if (actions[i] & CHECKOUT_ACTION__CREATE_BLOB)
			counts[CHECKOUT_ACTION__CREATE_BLOB]++;

		if (actions[i] & CHECKOUT_ACTION__CREATE_SUBMODULE)
			counts[CHECKOUT_ACTION__CREATE_SUBMODULE]++;

		if (actions[i] & CHECKOUT_ACTION__NOTIFY)
			counts[CHECKOUT_ACTION__NOTIFY]++;
	}

	return 0;
}

static int checkout_remove_the_old(
	git_diff_list *diff,
	unsigned int *actions,
	struct checkout_diff_data *data)
{
	git_diff_delta *delta;
	size_t i;

	git_vector_foreach(&diff->deltas, i, delta) {
		if (actions[i] & CHECKOUT_ACTION__REMOVE) {
			int error = git_futils_rmdir_r(
				delta->new_file.path, git_buf_cstr(data->path),
				GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_EMPTY_PARENTS);
			if (error < 0)
				return error;

			data->completed_steps++;
			report_progress(data, delta->new_file.path);
		}
	}

	return 0;
}

static int checkout_create_the_new(
	git_diff_list *diff,
	unsigned int *actions,
	struct checkout_diff_data *data)
{
	git_diff_delta *delta;
	size_t i;

	git_vector_foreach(&diff->deltas, i, delta) {
		if (actions[i] & CHECKOUT_ACTION__CREATE_BLOB) {
			int error = checkout_blob(data, &delta->old_file);

			/* ENOTFOUND means unable to create the file because of
			 * an existing parent dir.  Probably flags were given
			 * asking to create new blobs without allowing removal
			 * of a conflicting tree (or vice versa).
			 */
			if (error == GIT_ENOTFOUND)
				giterr_clear();
			else if (error < 0)
				return error;

			data->completed_steps++;
			report_progress(data, delta->old_file.path);
		}

		if (actions[i] & CHECKOUT_ACTION__NOTIFY) {
			if (data->checkout_opts->skipped_notify_cb(
					delta->old_file.path, &delta->old_file.oid,
					delta->old_file.mode, data->checkout_opts->notify_payload))
				return GIT_EUSER;
		}
	}

	return 0;
}

static int checkout_create_submodules(
	git_diff_list *diff,
	unsigned int *actions,
	struct checkout_diff_data *data)
{
	git_diff_delta *delta;
	size_t i;

	git_vector_foreach(&diff->deltas, i, delta) {
		if (actions[i] & CHECKOUT_ACTION__CREATE_SUBMODULE) {
			int error = checkout_submodule(data, &delta->old_file);
			if (error < 0)
				return error;

			data->completed_steps++;
			report_progress(data, delta->old_file.path);
		}
	}

	return 0;
}

int git_checkout_index(
	git_repository *repo,
	git_checkout_opts *opts)
{
	git_diff_list *diff = NULL;

	git_diff_options diff_opts = {0};
	git_checkout_opts checkout_opts;

	struct checkout_diff_data data;
	git_buf workdir = GIT_BUF_INIT;

	uint32_t *actions = NULL;
	size_t *counts = NULL;

	int error;

	assert(repo);

	if ((error = git_repository__ensure_not_bare(repo, "checkout")) < 0)
		return error;

	diff_opts.flags =
		GIT_DIFF_INCLUDE_UNTRACKED |
		GIT_DIFF_INCLUDE_TYPECHANGE |
		GIT_DIFF_SKIP_BINARY_CHECK;

	if (opts && opts->paths.count > 0)
		diff_opts.pathspec = opts->paths;

	if ((error = git_diff_workdir_to_index(repo, &diff_opts, &diff)) < 0)
		goto cleanup;

	if ((error = git_buf_puts(&workdir, git_repository_workdir(repo))) < 0)
		goto cleanup;

	normalize_options(&checkout_opts, opts);

	/* Checkout is best performed with up to four passes through the diff.
	 *
	 * 0. Figure out what actions should be taken and record for later.
	 * 1. Next do removes, because we iterate in alphabetical order, thus
	 *    a new untracked directory will end up sorted *after* a blob that
	 *    should be checked out with the same name.
	 * 2. Then checkout all blobs.
	 * 3. Then checkout all submodules in case a new .gitmodules blob was
	 *    checked out during pass #2.
	 */
	if ((error = checkout_get_actions(&actions, &counts, diff, &checkout_opts)) < 0)
		goto cleanup;

	memset(&data, 0, sizeof(data));
	data.path = &workdir;
	data.workdir_len = git_buf_len(&workdir);
	data.checkout_opts = &checkout_opts;
	data.owner = repo;
	data.total_steps = counts[CHECKOUT_ACTION__REMOVE] +
		counts[CHECKOUT_ACTION__CREATE_BLOB] +
		counts[CHECKOUT_ACTION__CREATE_SUBMODULE];

	if ((error = retrieve_symlink_capabilities(repo, &data.can_symlink)) < 0)
		goto cleanup;

	report_progress(&data, NULL);

	if (counts[CHECKOUT_ACTION__REMOVE] > 0 &&
		(error = checkout_remove_the_old(diff, actions, &data)) < 0)
		goto cleanup;

	if ((counts[CHECKOUT_ACTION__CREATE_BLOB] +
		 counts[CHECKOUT_ACTION__NOTIFY]) > 0 &&
		(error = checkout_create_the_new(diff, actions, &data)) < 0)
		goto cleanup;

	if (counts[CHECKOUT_ACTION__CREATE_SUBMODULE] > 0 &&
		(error = checkout_create_submodules(diff, actions, &data)) < 0)
		goto cleanup;

	assert(data.completed_steps == data.total_steps);

cleanup:
	if (error == GIT_EUSER)
		giterr_clear();

	git__free(actions);
	git__free(counts);
	git_diff_list_free(diff);
	git_buf_free(&workdir);

	return error;
}

int git_checkout_tree(
	git_repository *repo,
	git_object *treeish,
	git_checkout_opts *opts)
{
	git_index *index = NULL;
	git_tree *tree = NULL;

	int error;

	assert(repo && treeish);

	if (git_object_peel((git_object **)&tree, treeish, GIT_OBJ_TREE) < 0) {
		giterr_set(GITERR_INVALID, "Provided treeish cannot be peeled into a tree.");
		return GIT_ERROR;
	}

	if ((error = git_repository_index(&index, repo)) < 0)
		goto cleanup;

	if ((error = git_index_read_tree(index, tree)) < 0)
		goto cleanup;

	if ((error = git_index_write(index)) < 0)
		goto cleanup;

	error = git_checkout_index(repo, opts);

cleanup:
	git_index_free(index);
	git_tree_free(tree);
	return error;
}

int git_checkout_head(
	git_repository *repo,
	git_checkout_opts *opts)
{
	git_reference *head;
	int error;
	git_object *tree = NULL;

	assert(repo);

	if ((error = git_repository_head(&head, repo)) < 0)
		return error;

	if ((error = git_reference_peel(&tree, head, GIT_OBJ_TREE)) < 0)
		goto cleanup;

	error = git_checkout_tree(repo, tree, opts);

cleanup:
	git_reference_free(head);
	git_object_free(tree);

	return error;
}
