/*
 * The MIT License
 *
 * Copyright (c) 2014 GitHub, Inc
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "rugged.h"

VALUE rb_cRuggedIndex;
extern VALUE rb_mRugged;
extern VALUE rb_cRuggedCommit;
extern VALUE rb_cRuggedDiff;
extern VALUE rb_cRuggedTree;

static void rb_git_indexentry_toC(git_index_entry *entry, VALUE rb_entry);
static VALUE rb_git_indexentry_fromC(const git_index_entry *entry);

/*
 * Index
 */

static void rb_git_index__free(git_index *index)
{
	git_index_free(index);
}

VALUE rugged_index_new(VALUE klass, VALUE owner, git_index *index)
{
	VALUE rb_index = Data_Wrap_Struct(klass, NULL, &rb_git_index__free, index);
	rugged_set_owner(rb_index, owner);
	return rb_index;
}

/*
 *  call-seq:
 *    Index.new([path])
 *
 *  Create a bare index object based on the index file at +path+.
 *
 *  Any index methods that rely on the ODB or a working directory (e.g. #add)
 *  will raise a Rugged::IndexError.
 */
static VALUE rb_git_index_new(int argc, VALUE *argv, VALUE klass)
{
	git_index *index;
	int error;

	VALUE rb_path;
	const char *path = NULL;

	if (rb_scan_args(argc, argv, "01", &rb_path) == 1) {
		Check_Type(rb_path, T_STRING);
		path = StringValueCStr(rb_path);
	}

	error = git_index_open(&index, path);
	rugged_exception_check(error);

	return rugged_index_new(klass, Qnil, index);
}

/*
 *  call-seq:
 *    index.clear -> nil
 *
 *  Clear the contents (remove all entries) of the index object. Changes are in-memory only
 *  and can be saved by calling #write.
 */
static VALUE rb_git_index_clear(VALUE self)
{
	git_index *index;
	Data_Get_Struct(self, git_index, index);
	git_index_clear(index);
	return Qnil;
}

/*
 *  call-seq:
 *    index.reload -> nil
 *
 *  Reloads the index contents from the disk, discarding any changes that
 *  have not been saved through #write.
 */
static VALUE rb_git_index_read(VALUE self)
{
	git_index *index;
	int error;

	Data_Get_Struct(self, git_index, index);

	error = git_index_read(index, 0);
	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    index.write -> nil
 *
 *  Writes the index object from memory back to the disk, persisting all changes.
 */
static VALUE rb_git_index_write(VALUE self)
{
	git_index *index;
	int error;

	Data_Get_Struct(self, git_index, index);

	error = git_index_write(index);
	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    index.count -> int
 *
 *  Returns the number of entries currently in the index.
 */
static VALUE rb_git_index_count(VALUE self)
{
	git_index *index;
	Data_Get_Struct(self, git_index, index);
	return INT2FIX(git_index_entrycount(index));
}

/*
 *  call-seq:
 *    index[path[, stage = 0]]     -> entry or nil
 *    index[position]              -> entry or nil
 *    index.get(path[, stage = 0]) -> entry or nil
 *    index.get(position)          -> entry or nil
 *
 *  Return a specific entry in the index.
 *
 *  The first two forms returns entries based on their +path+ in the index and an optional +stage+,
 *  while the last two forms return entries based on their position in the index.
 */
static VALUE rb_git_index_get(int argc, VALUE *argv, VALUE self)
{
	git_index *index;
	const git_index_entry *entry = NULL;

	VALUE rb_entry, rb_stage;

	Data_Get_Struct(self, git_index, index);

	rb_scan_args(argc, argv, "11", &rb_entry, &rb_stage);

	if (TYPE(rb_entry) == T_STRING) {
		int stage = 0;

		if (!NIL_P(rb_stage)) {
			Check_Type(rb_stage, T_FIXNUM);
			stage = FIX2INT(rb_stage);
		}

		entry = git_index_get_bypath(index, StringValueCStr(rb_entry), stage);
	}

	else if (TYPE(rb_entry) == T_FIXNUM) {
		if (argc > 1) {
			rb_raise(rb_eArgError,
				"Too many arguments when trying to lookup entry by index");
		}

		entry = git_index_get_byindex(index, FIX2INT(rb_entry));
	} else {
		rb_raise(rb_eArgError,
			"Invalid type for `entry`: expected String or Fixnum");
	}

	return entry ? rb_git_indexentry_fromC(entry) : Qnil;
}

/*
 *  call-seq:
 *    index.each { |entry| } -> nil
 *    index.each -> Enumerator
 *
 *  Passes each entry of the index to the given block.
 *
 *  If no block is given, an enumerator is returned instead.
 */
static VALUE rb_git_index_each(VALUE self)
{
	git_index *index;
	unsigned int i, count;

	Data_Get_Struct(self, git_index, index);

	if (!rb_block_given_p())
		return rb_funcall(self, rb_intern("to_enum"), 0);

	count = (unsigned int)git_index_entrycount(index);
	for (i = 0; i < count; ++i) {
		const git_index_entry *entry = git_index_get_byindex(index, i);
		if (entry)
			rb_yield(rb_git_indexentry_fromC(entry));
	}

	return Qnil;
}

/*
 *  call-seq:
 *    index.remove(path[, stage = 0]) -> nil
 *
 *  Removes the entry at the given +path+ with the given +stage+
 *  from the index.
 */
static VALUE rb_git_index_remove(int argc, VALUE *argv, VALUE self)
{
	git_index *index;
	int error, stage = 0;

	VALUE rb_entry, rb_stage;

	Data_Get_Struct(self, git_index, index);

	if (rb_scan_args(argc, argv, "11", &rb_entry, &rb_stage) > 1) {
		Check_Type(rb_stage, T_FIXNUM);
		stage = FIX2INT(rb_stage);
	}

	Check_Type(rb_entry, T_STRING);

	error = git_index_remove(index, StringValueCStr(rb_entry), stage);
	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    index.remove_dir(dir[, stage = 0]) -> nil
 *
 *  Removes all entries under the given +dir+ with the given +stage+
 *  from the index.
 */
static VALUE rb_git_index_remove_directory(int argc, VALUE *argv, VALUE self)
{
	git_index *index;
	int error, stage = 0;

	VALUE rb_dir, rb_stage;

	Data_Get_Struct(self, git_index, index);

	if (rb_scan_args(argc, argv, "11", &rb_dir, &rb_stage) > 1) {
		Check_Type(rb_stage, T_FIXNUM);
		stage = FIX2INT(rb_stage);
	}

	Check_Type(rb_dir, T_STRING);

	error = git_index_remove_directory(index, StringValueCStr(rb_dir), stage);
	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    index << entry -> nil
 *    index << path -> nil
 *    index.add(entry) -> nil
 *    index.add(path) -> nil
 *    index.update(entry) -> nil
 *    index.update(path) -> nil
 *
 *  Add a new entry to the index or update an existing entry in the index.
 *
 *  If passed a +path+ to an existing, readable file relative to the workdir,
 *  creates a new index entry based on this file.
 *
 *  Alternatively, a new index entry can be created by passing a Hash containing
 *  all key/value pairs of an index entry.
 *
 *  Any gitignore rules that might match +path+ (or the +:path+ value of the
 *  entry hash) are ignored.
 *
 *  If the index entry at +path+ (or +:path+) currently contains a merge conflict,
 *  it will no longer be marked as conflicting and the data about the conflict
 *  will be moved into the "resolve undo" (REUC) section of the index.
 */
static VALUE rb_git_index_add(VALUE self, VALUE rb_entry)
{
	git_index *index;
	int error = 0;

	Data_Get_Struct(self, git_index, index);

	if (TYPE(rb_entry) == T_HASH) {
		git_index_entry entry;

		rb_git_indexentry_toC(&entry, rb_entry);
		error = git_index_add(index, &entry);
	}

	else if (TYPE(rb_entry) == T_STRING) {
		error = git_index_add_bypath(index, StringValueCStr(rb_entry));
	}

	else {
		rb_raise(rb_eTypeError,
			"Expecting a hash defining an Index Entry or a path to a file in the repository");
	}

	rugged_exception_check(error);
	return Qnil;
}

int rugged__index_matched_path_cb(const char *path, const char *matched_pathspec, void *payload)
{
	int *exception = (int *)payload;

	VALUE rb_result, rb_args = rb_ary_new2(2);
	rb_ary_push(rb_args, rb_str_new2(path));
	rb_ary_push(rb_args, matched_pathspec == NULL ? Qnil : rb_str_new2(matched_pathspec));

	rb_result = rb_protect(rb_yield_splat, rb_args, exception);

	if (*exception)
		return GIT_ERROR;

	return RTEST(rb_result) ? 0 : 1;
}

/*
 *  call-seq:
 *    index.add_all(pathspec = [][, options])                            -> nil
 *    index.add_all(pathspec = [][, options]) { |path, pathspec| block } -> nil
 *
 *  Add or update index entries matching files in the working directory.
 *
 *  Searches the working directory for files that +pathspec+ and adds them
 *  to +index+ (by updating an existing entry or adding a new entry).
 *
 *  +pathspec+ can either be a String, or an Array of Strings.
 *  If +pathspec+ is empty, all entries in the index will be matched.
 *
 *  Files that are ignored due to +.gitignore+ rules will be skipped,
 *  unless they're already have an entry in +index+.
 *
 *  Files that are marked as the result of a merge request, will have this
 *  marking removed and the merge conflict information will be moved into the
 *  "resolve undo" (REUC) section of +index+.
 *
 *  If a block is given, each matched +path+ and the +pathspec+ that matched
 *  it will be passed to the block. If the return value of +block+ is
 *  falsy, the matching item will not be added to the index.
 *
 *  This method will fail in bare index instances.
 *
 *  The following options can be passed in the +options+ Hash:
 *
 *  :force ::
 *    If +true+, any +.gitignore+ rules will be ignored.
 *
 *  :disable_pathspec_match ::
 *    If +true+, glob expansion will be disabled and exact matching will be forced.
 *
 *  :check_pathspec ::
 *    If +true+, and the +:force+ options is +false+ or not given, exact matches
 *    of ignored files or files that are not already in +index+ will raise a
 *    Rugged::InvalidError. This emulates <code>git add -A</code>.
 */
static VALUE rb_git_index_add_all(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_pathspecs, rb_options;

	git_index *index;
	git_strarray pathspecs;
	int error, exception = 0;
	unsigned int flags = GIT_INDEX_ADD_DEFAULT;

	Data_Get_Struct(self, git_index, index);

	if (rb_scan_args(argc, argv, "02", &rb_pathspecs, &rb_options) > 1) {
		Check_Type(rb_options, T_HASH);

		if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("force"))))
			flags |= GIT_INDEX_ADD_FORCE;

		if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("disable_pathspec_match"))))
			flags |= GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH;

		if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("check_pathspec"))))
			flags |= GIT_INDEX_ADD_CHECK_PATHSPEC;
	}

	rugged_rb_ary_to_strarray(rb_pathspecs, &pathspecs);

	error = git_index_add_all(index, &pathspecs, flags,
		rb_block_given_p() ? rugged__index_matched_path_cb : NULL, &exception);

	xfree(pathspecs.strings);

	if (exception)
		rb_jump_tag(exception);

	rugged_exception_check(error);
	return Qnil;
}

/*
 *  call-seq:
 *    index.update_all(pathspec = [])                            -> nil
 *    index.update_all(pathspec = []) { |path, pathspec| block } -> nil
 *
 *  Update all index entries to match the working directory.
 *
 *  Searches +index+ for entries that match +pathspec+ and synchronizes
 *  them with the content of the working directory.
 *
 *  +pathspec+ can either be a String, or an Array of Strings.
 *  If +pathspec+ is empty, all entries in the index will be matched.
 *
 *  Entries where the corresponding working directory file no longer exists
 *  get deleted, all other matched entries will get updated to reflect their
 *  working directory state (the latest version of the a file's content will
 *  automatically be added to the ODB).
 *
 *  If a block is given, each matched +path+ and the +pathspec+ that matched
 *  it will be passed to the block. If the return value of +block+ is
 *  falsy, the matching item will not be updated in the index.
 *
 *  This method will fail in bare index instances.
 */
static VALUE rb_git_index_update_all(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_pathspecs = rb_ary_new();

	git_index *index;
	git_strarray pathspecs;
	int error, exception = 0;

	Data_Get_Struct(self, git_index, index);

	rb_scan_args(argc, argv, "01", &rb_pathspecs);

	rugged_rb_ary_to_strarray(rb_pathspecs, &pathspecs);

	error = git_index_update_all(index, &pathspecs,
		rb_block_given_p() ? rugged__index_matched_path_cb : NULL, &exception);

	xfree(pathspecs.strings);

	if (exception)
		rb_jump_tag(exception);
	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    index.remove_all(pathspec = []) -> nil
 *    index.remove_all(pathspec = []) { |path, pathspec| block } -> nil
 *
 *  Remove all matching index entries.
 *
 *  Searches +index+ for entries that match +pathspec+ and removes them
 *  from the index.
 *
 *  +pathspec+ can either be a String, or an Array of Strings.
 *  If +pathspec+ is empty, all entries in the index will be matched.
 *
 *  If a block is given, each matched +path+ and the +pathspec+ that matched
 *  it will be passed to the block. If the return value of +block+ is
 *  falsy, the matching item will not be removed from the index.
 */
static VALUE rb_git_index_remove_all(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_pathspecs = rb_ary_new();

	git_index *index;
	git_strarray pathspecs;
	int error, exception = 0;

	Data_Get_Struct(self, git_index, index);

	rb_scan_args(argc, argv, "01", &rb_pathspecs);

	if (NIL_P(rb_pathspecs))
		rb_pathspecs = rb_ary_new();

	rugged_rb_ary_to_strarray(rb_ary_to_ary(rb_pathspecs), &pathspecs);

	error = git_index_remove_all(index, &pathspecs,
		rb_block_given_p() ? rugged__index_matched_path_cb : NULL, &exception);

	xfree(pathspecs.strings);

	if (exception)
		rb_jump_tag(exception);
	rugged_exception_check(error);

	return Qnil;
}

static VALUE rb_git_indexentry_fromC(const git_index_entry *entry)
{
	VALUE rb_entry, rb_mtime, rb_ctime;
	unsigned int valid, stage;

	if (!entry)
		return Qnil;

	rb_entry = rb_hash_new();

	rb_hash_aset(rb_entry, CSTR2SYM("path"), rb_str_new_utf8(entry->path));
	rb_hash_aset(rb_entry, CSTR2SYM("oid"), rugged_create_oid(&entry->id));

	rb_hash_aset(rb_entry, CSTR2SYM("dev"), INT2FIX(entry->dev));
	rb_hash_aset(rb_entry, CSTR2SYM("ino"), INT2FIX(entry->ino));
	rb_hash_aset(rb_entry, CSTR2SYM("mode"), INT2FIX(entry->mode));
	rb_hash_aset(rb_entry, CSTR2SYM("gid"), INT2FIX(entry->gid));
	rb_hash_aset(rb_entry, CSTR2SYM("uid"), INT2FIX(entry->uid));
	rb_hash_aset(rb_entry, CSTR2SYM("file_size"), INT2FIX(entry->file_size));

	valid = (entry->flags & GIT_IDXENTRY_VALID);
	rb_hash_aset(rb_entry, CSTR2SYM("valid"), valid ? Qtrue : Qfalse);

	stage = (entry->flags & GIT_IDXENTRY_STAGEMASK) >> GIT_IDXENTRY_STAGESHIFT;
	rb_hash_aset(rb_entry, CSTR2SYM("stage"), INT2FIX(stage));

	rb_mtime = rb_time_new(entry->mtime.seconds, entry->mtime.nanoseconds / 1000);
	rb_ctime = rb_time_new(entry->ctime.seconds, entry->ctime.nanoseconds / 1000);

	rb_hash_aset(rb_entry, CSTR2SYM("ctime"), rb_ctime);
	rb_hash_aset(rb_entry, CSTR2SYM("mtime"), rb_mtime);

	return rb_entry;
}

static inline unsigned int
default_entry_value(VALUE rb_entry, const char *key)
{
	VALUE val = rb_hash_aref(rb_entry, CSTR2SYM(key));
	if (NIL_P(val))
		return 0;

	Check_Type(val, T_FIXNUM);
	return FIX2INT(val);
}

static void rb_git_indexentry_toC(git_index_entry *entry, VALUE rb_entry)
{
	VALUE val;

	Check_Type(rb_entry, T_HASH);

	val = rb_hash_aref(rb_entry, CSTR2SYM("path"));
	Check_Type(val, T_STRING);
	entry->path = StringValueCStr(val);

	val = rb_hash_aref(rb_entry, CSTR2SYM("oid"));
	Check_Type(val, T_STRING);
	rugged_exception_check(
		git_oid_fromstr(&entry->id, StringValueCStr(val))
	);

	entry->dev = default_entry_value(rb_entry, "dev");
	entry->ino = default_entry_value(rb_entry, "ino");
	entry->mode = default_entry_value(rb_entry, "mode");
	entry->gid = default_entry_value(rb_entry, "gid");
	entry->uid = default_entry_value(rb_entry, "uid");
	entry->file_size = (git_off_t)default_entry_value(rb_entry, "file_size");

	if ((val = rb_hash_aref(rb_entry, CSTR2SYM("mtime"))) != Qnil) {
		if (!rb_obj_is_kind_of(val, rb_cTime))
			rb_raise(rb_eTypeError, ":mtime must be a Time instance");

		entry->mtime.seconds = NUM2INT(rb_funcall(val, rb_intern("to_i"), 0));
		entry->mtime.nanoseconds = NUM2INT(rb_funcall(val, rb_intern("usec"), 0)) * 1000;
	} else {
		entry->mtime.seconds = entry->mtime.nanoseconds = 0;
	}

	if ((val = rb_hash_aref(rb_entry, CSTR2SYM("ctime"))) != Qnil) {
		if (!rb_obj_is_kind_of(val, rb_cTime))
			rb_raise(rb_eTypeError, ":ctime must be a Time instance");

		entry->ctime.seconds = NUM2INT(rb_funcall(val, rb_intern("to_i"), 0));
		entry->ctime.nanoseconds = NUM2INT(rb_funcall(val, rb_intern("usec"), 0)) * 1000;
	} else {
		entry->ctime.seconds = entry->ctime.nanoseconds = 0;
	}

	entry->flags = 0x0;
	entry->flags_extended = 0x0;

	val = rb_hash_aref(rb_entry, CSTR2SYM("stage"));
	if (!NIL_P(val)) {
		unsigned int stage = NUM2INT(val);
		entry->flags &= ~GIT_IDXENTRY_STAGEMASK;
		entry->flags |= (stage << GIT_IDXENTRY_STAGESHIFT) & GIT_IDXENTRY_STAGEMASK;
	}

	val = rb_hash_aref(rb_entry, CSTR2SYM("valid"));
	if (!NIL_P(val)) {
		entry->flags &= ~GIT_IDXENTRY_VALID;
		if (rugged_parse_bool(val))
			entry->flags |= GIT_IDXENTRY_VALID;
	} else {
		entry->flags |= GIT_IDXENTRY_VALID;
	}
}

/*
 *  call-seq:
 *    index.write_tree([repo]) -> oid
 *
 *  Write the index to a tree, either in the index's repository, or in
 *  the given +repo+.
 *
 *  If the index contains any files in conflict, writing the tree will fail.
 *
 *  Returns the OID string of the written tree object.
 */
static VALUE rb_git_index_writetree(int argc, VALUE *argv, VALUE self)
{
	git_index *index;
	git_oid tree_oid;
	int error;
	VALUE rb_repo;

	Data_Get_Struct(self, git_index, index);

	if (rb_scan_args(argc, argv, "01", &rb_repo) == 1) {
		git_repository *repo = NULL;
		rugged_check_repo(rb_repo);
		Data_Get_Struct(rb_repo, git_repository, repo);
		error = git_index_write_tree_to(&tree_oid, index, repo);
	}
	else {
		error = git_index_write_tree(&tree_oid, index);
	}

	rugged_exception_check(error);
	return rugged_create_oid(&tree_oid);
}

/*
 *  call-seq:
 *    index.read_tree(tree)
 *
 *  Clear the current index and start the index again on top of +tree+
 *
 *  Further index operations (+add+, +update+, +remove+, etc) will
 *  be considered changes on top of +tree+.
 */
static VALUE rb_git_index_readtree(VALUE self, VALUE rb_tree)
{
	git_index *index;
	git_tree *tree;
	int error;

	Data_Get_Struct(self, git_index, index);
	Data_Get_Struct(rb_tree, git_tree, tree);

	error = git_index_read_tree(index, tree);
	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    index.diff([options]) -> diff
 *    index.diff(diffable[, options]) -> diff
 *
 *  The first form returns a diff between the index and the current working
 *  directory.
 *
 *  The second form returns a diff between the index and the given diffable object.
 *  +diffable+ can either be a +Rugged::Commit+ or a +Rugged::Tree+.
 *
 *  The index will be used as the "old file" side of the diff, while the working
 *  directory or the +diffable+ will be used for the "new file" side.
 *
 *  The following options can be passed in the +options+ Hash:
 *
 *  :paths ::
 *    An array of paths / fnmatch patterns to constrain the diff to a specific
 *    set of files. Also see +:disable_pathspec_match+.
 *
 *  :max_size ::
 *    An integer specifying the maximum byte size of a file before a it will
 *    be treated as binary. The default value is 512MB.
 *
 *  :context_lines ::
 *    The number of unchanged lines that define the boundary of a hunk (and
 *    to display before and after the actual changes). The default is 3.
 *
 *  :interhunk_lines ::
 *    The maximum number of unchanged lines between hunk boundaries before the hunks
 *    will be merged into a one. The default is 0.
 *
 *  :reverse ::
 *    If true, the sides of the diff will be reversed.
 *
 *  :force_text ::
 *    If true, all files will be treated as text, disabling binary attributes & detection.
 *
 *  :ignore_whitespace ::
 *    If true, all whitespace will be ignored.
 *
 *  :ignore_whitespace_change ::
 *    If true, changes in amount of whitespace will be ignored.
 *
 *  :ignore_whitespace_eol ::
 *    If true, whitespace at end of line will be ignored.
 *
 *  :ignore_submodules ::
 *    if true, submodules will be excluded from the diff completely.
 *
 *  :patience ::
 *    If true, the "patience diff" algorithm will be used (currenlty unimplemented).
 *
 *  :include_ignored ::
 *    If true, ignored files will be included in the diff.
 *
 *  :include_untracked ::
 *   If true, untracked files will be included in the diff.
 *
 *  :include_unmodified ::
 *    If true, unmodified files will be included in the diff.
 *
 *  :recurse_untracked_dirs ::
 *    Even if +:include_untracked+ is true, untracked directories will only be
 *    marked with a single entry in the diff. If this flag is set to true,
 *    all files under ignored directories will be included in the di ff, too.
 *
 *  :disable_pathspec_match ::
 *    If true, the given +:paths+ will be applied as exact matches, instead of
 *    as fnmatch patterns.
 *
 *  :deltas_are_icase ::
 *    If true, filename comparisons will be made with case-insensitivity.
 *
 *  :include_untracked_content ::
 *    if true, untracked content will be contained in the the diff patch text.
 *
 *  :skip_binary_check ::
 *    If true, diff deltas will be generated without spending time on binary
 *    detection. This is useful to improve performance in cases where the actual
 *    file content difference is not needed.
 *
 *  :include_typechange ::
 *    If true, type changes for files will not be interpreted as deletion of
 *    the "old file" and addition of the "new file", but will generate
 *    typechange records.
 *
 *  :include_typechange_trees ::
 *    Even if +:include_typechange+ is true, blob -> tree changes will still
 *    usually be handled as a deletion of the blob. If this flag is set to true,
 *    blob -> tree changes will be marked as typechanges.
 *
 *  :ignore_filemode ::
 *    If true, file mode changes will be ignored.
 *
 *  :recurse_ignored_dirs ::
 *    Even if +:include_ignored+ is true, ignored directories will only be
 *    marked with a single entry in the diff. If this flag is set to true,
 *    all files under ignored directories will be included in the diff, too.
 */
static VALUE rb_git_index_diff(int argc, VALUE *argv, VALUE self)
{
	git_index *index;
	git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
	git_repository *repo;
	git_diff *diff = NULL;
	VALUE owner, rb_other, rb_options;
	int error;

	rb_scan_args(argc, argv, "01:", &rb_other, &rb_options);
	rugged_parse_diff_options(&opts, rb_options);

	Data_Get_Struct(self, git_index, index);
	owner = rugged_owner(self);
	Data_Get_Struct(owner, git_repository, repo);

	if (NIL_P(rb_other)) {
		error = git_diff_index_to_workdir(&diff, repo, index, &opts);
	} else {
		// Need to flip the reverse option, so that the index is by default
		// the "old file" side of the diff.
		opts.flags ^= GIT_DIFF_REVERSE;

		if (rb_obj_is_kind_of(rb_other, rb_cRuggedCommit)) {
			git_tree *other_tree;
			git_commit *commit;
			Data_Get_Struct(rb_other, git_commit, commit);
			error = git_commit_tree(&other_tree, commit);

			if (!error)
				error = git_diff_tree_to_index(&diff, repo, other_tree, index, &opts);
		} else if (rb_obj_is_kind_of(rb_other, rb_cRuggedTree)) {
			git_tree *other_tree;
			Data_Get_Struct(rb_other, git_tree, other_tree);
			error = git_diff_tree_to_index(&diff, repo, other_tree, index, &opts);
		} else {
			xfree(opts.pathspec.strings);
			rb_raise(rb_eTypeError, "A Rugged::Commit or Rugged::Tree instance is required");
		}
	}

	xfree(opts.pathspec.strings);
	rugged_exception_check(error);

	return rugged_diff_new(rb_cRuggedDiff, self, diff);
}

/*
 *  call-seq:
 *    index.conflicts? -> true or false
 *
 *  Determines if the index contains entries representing conflicts.
 */
static VALUE rb_git_index_conflicts_p(VALUE self)
{
	git_index *index;
	Data_Get_Struct(self, git_index, index);
	return git_index_has_conflicts(index) ? Qtrue : Qfalse;
}

/*
 *  call-seq:
 *    index.conflict_add(conflict) -> nil
 *
 *  Add or update index entries that represent a conflict.
 *
 *  +conflict+ has to be a hash containing +:ancestor+, +:ours+ and
 *  +:theirs+ key/value pairs. Any of those paris can be +nil+ (or left out)
 *  to indicate that the file was not present in the respective tree during
 *  the merge.
 */
static VALUE rb_git_conflict_add(VALUE self, VALUE rb_conflict)
{
	VALUE rb_ancestor, rb_ours, rb_theirs;
	git_index *index;
	git_index_entry ancestor, ours, theirs;
	int error;

	Check_Type(rb_conflict, T_HASH);

	rb_ancestor = rb_hash_aref(rb_conflict, CSTR2SYM("ancestor"));
	rb_ours     = rb_hash_aref(rb_conflict, CSTR2SYM("ours"));
	rb_theirs   = rb_hash_aref(rb_conflict, CSTR2SYM("theirs"));

	if (!NIL_P(rb_ancestor))
		rb_git_indexentry_toC(&ancestor, rb_ancestor);

	if (!NIL_P(rb_ours))
		rb_git_indexentry_toC(&ours, rb_ours);

	if (!NIL_P(rb_theirs))
		rb_git_indexentry_toC(&theirs, rb_theirs);

	Data_Get_Struct(self, git_index, index);

	error = git_index_conflict_add(index,
		NIL_P(rb_ancestor) ? NULL : &ancestor,
		NIL_P(rb_theirs) ? NULL : &ours,
		NIL_P(rb_ours) ? NULL : &theirs);
	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    index.conflict_remove(path) -> nil
 *
 *  Removes the index entries that represent the conflict at +path+.
 */
static VALUE rb_git_conflict_remove(VALUE self, VALUE rb_path)
{
	git_index *index;
	int error;

	Check_Type(rb_path, T_STRING);

	Data_Get_Struct(self, git_index, index);

	error = git_index_conflict_remove(index, StringValueCStr(rb_path));
	rugged_exception_check(error);

	return Qnil;
}

/*
 *  call-seq:
 *    index.conflict_get(path) -> conflict or nil
 *
 *  Return index entries from the ancestor, our side and their side of
 *  the conflict at +path+.
 *
 *  If +:ancestor+, +:ours+ or +:theirs+ is +nil+, that indicates that +path+
 *  did not exist in the respective tree.
 *
 *  Returns nil if no conflict is present at +path+.
 */
static VALUE rb_git_conflict_get(VALUE self, VALUE rb_path)
{
	VALUE rb_result = rb_hash_new();
	git_index *index;
	const git_index_entry *ancestor, *ours, *theirs;
	int error;

	Check_Type(rb_path, T_STRING);

	Data_Get_Struct(self, git_index, index);

	error = git_index_conflict_get(&ancestor, &ours, &theirs, index, StringValueCStr(rb_path));
	if (error == GIT_ENOTFOUND)
		return Qnil;
	else
		rugged_exception_check(error);

	rb_hash_aset(rb_result, CSTR2SYM("ancestor"), rb_git_indexentry_fromC(ancestor));
	rb_hash_aset(rb_result, CSTR2SYM("ours"),     rb_git_indexentry_fromC(ours));
	rb_hash_aset(rb_result, CSTR2SYM("theirs"),   rb_git_indexentry_fromC(theirs));

	return rb_result;
}

void rugged_parse_merge_file_options(git_merge_file_options *opts, VALUE rb_options)
{
	if (!NIL_P(rb_options)) {
		VALUE rb_value;
		Check_Type(rb_options, T_HASH);

		rb_value = rb_hash_aref(rb_options, CSTR2SYM("ancestor_label"));
		if (!NIL_P(rb_value)) {
			Check_Type(rb_value, T_FIXNUM);
			opts->ancestor_label = StringValueCStr(rb_value);
		}

		rb_value = rb_hash_aref(rb_options, CSTR2SYM("our_label"));
		if (!NIL_P(rb_value)) {
			Check_Type(rb_value, T_FIXNUM);
			opts->our_label = StringValueCStr(rb_value);
		}

		rb_value = rb_hash_aref(rb_options, CSTR2SYM("their_label"));
		if (!NIL_P(rb_value)) {
			Check_Type(rb_value, T_FIXNUM);
			opts->their_label = StringValueCStr(rb_value);
		}

		rb_value = rb_hash_aref(rb_options, CSTR2SYM("favor"));
		if (!NIL_P(rb_value)) {
			ID id_favor;

			Check_Type(rb_value, T_SYMBOL);
			id_favor = SYM2ID(rb_value);

			if (id_favor == rb_intern("normal")) {
				opts->favor = GIT_MERGE_FILE_FAVOR_NORMAL;
			} else if (id_favor == rb_intern("ours")) {
				opts->favor = GIT_MERGE_FILE_FAVOR_OURS;
			} else if (id_favor == rb_intern("theirs")) {
				opts->favor = GIT_MERGE_FILE_FAVOR_THEIRS;
			} else if (id_favor == rb_intern("union")) {
				opts->favor = GIT_MERGE_FILE_FAVOR_UNION;
			} else {
				rb_raise(rb_eTypeError,
					"Invalid favor mode. Expected `:normal`, `:ours`, `:theirs` or `:union`");
			}
		}

		rb_value = rb_hash_aref(rb_options, CSTR2SYM("style"));
		if (!NIL_P(rb_value)) {
			ID id_style;

			Check_Type(rb_value, T_SYMBOL);
			id_style = SYM2ID(rb_value);

			if (id_style == rb_intern("standard")) {
				opts->flags |= GIT_MERGE_FILE_STYLE_MERGE;
			} else if (id_style == rb_intern("diff3")) {
				opts->flags |= GIT_MERGE_FILE_STYLE_DIFF3;
			} else {
				rb_raise(rb_eTypeError,
					"Invalid style mode. Expected `:standard`, or `:diff3`");
			}
		} else {
			opts->flags |= GIT_MERGE_FILE_STYLE_MERGE;
		}

		if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("simplify")))) {
			opts->flags |= GIT_MERGE_FILE_SIMPLIFY_ALNUM;
		}
	}
}

/*
 *  call-seq:
 *    index.merge_file(path[, options]) -> merge_file or nil
 *    index.merge_file(path) -> merge_file or nil
 *
 *  Return merge_file (in memory) from the ancestor, our side and their side of
 *  the conflict at +path+.
 *
 *  If +:ancestor+, +:ours+ or +:theirs+ is +nil+, that indicates that +path+
 *  did not exist in the respective tree.
 *
 *  Returns nil if no conflict is present at +path+.

 *  The following options can be passed in the +options+ Hash:
 *
 *  :ancestor_label ::
 *    The name of the ancestor branch used to decorate conflict markers.
 *
 *  :our_label ::
 *    The name of our branch used to decorate conflict markers.
 *
 *  :their_label ::
 *    The name of their branch used to decorate conflict markers.
 *
 *  :favor ::
 *    Specifies how and if conflicts are auto-resolved by favoring a specific
 *    file output. Can be one of `:normal`, `:ours`, `:theirs` or `:union`.
 *    Defaults to `:normal`.
 *
 *  :style ::
 *    Specifies the type of merge file to produce. Can be one of `:standard`, `:diff3`. Defaults to `:standard`
 *
 *  :simplify ::
 *    If true, the merge file is simplified by condensing non-alphanumeric regions.
 *
 */

static VALUE rb_git_merge_file(int argc, VALUE *argv, VALUE self)
{
	VALUE rb_path, rb_options;
	VALUE rb_result = rb_hash_new();
	VALUE rb_repo = rugged_owner(self);

	git_repository *repo;
	git_index *index;
	const git_index_entry *ancestor, *ours, *theirs;
	git_merge_file_result merge_file_result = {0};
	git_merge_file_options opts = GIT_MERGE_FILE_OPTIONS_INIT;
	int error;

	rb_scan_args(argc, argv, "1:", &rb_path, &rb_options);

	if (!NIL_P(rb_options)) {
		Check_Type(rb_options, T_HASH);
		rugged_parse_merge_file_options(&opts, rb_options);
	}

	Check_Type(rb_path, T_STRING);

	Data_Get_Struct(self, git_index, index);

	rugged_check_repo(rb_repo);
	Data_Get_Struct(rb_repo, git_repository, repo);

	error = git_index_conflict_get(&ancestor, &ours, &theirs, index, StringValueCStr(rb_path));
	if (error == GIT_ENOTFOUND)
		return Qnil;
	else
		rugged_exception_check(error);

	error = git_merge_file_from_index(&merge_file_result, repo, ancestor, ours, theirs, &opts);
	rugged_exception_check(error);

	rb_hash_aset(rb_result, CSTR2SYM("automergeable"), merge_file_result.automergeable ? Qtrue : Qfalse);
	rb_hash_aset(rb_result, CSTR2SYM("path"),         rb_path);
	rb_hash_aset(rb_result, CSTR2SYM("filemode"),     INT2FIX(merge_file_result.mode));
	rb_hash_aset(rb_result, CSTR2SYM("data"),         rb_str_new(merge_file_result.ptr, merge_file_result.len));

	git_merge_file_result_free(&merge_file_result);

	return rb_result;
}

/*
 *  call-seq:
 *    index.conflict_cleanup -> nil
 *
 *  Remove all conflicting entries (entries with a stage greater than 0)
 *  from the index.
 */
static VALUE rb_git_conflict_cleanup(VALUE self)
{
	git_index *index;

	Data_Get_Struct(self, git_index, index);
	git_index_conflict_cleanup(index);

	return Qnil;
}

/*
 *  call-seq:
 *    index.conflicts -> conflicts
 *
 *  Return all conflicts in +index+.
 *
 *  Each conflict is represented as a Hash with +:ancestor+, +:ours+ or
 *  +:theirs+ key-value pairs, each containing index entry data.
 *
 *  If the value of the +:ancestor+, +:ours+ or +:theirs+ key is +nil+,
 *  that indicates that file in conflict did not exists in the respective tree.
 */
static VALUE rb_git_index_conflicts(VALUE self)
{
	VALUE rb_conflicts = rb_ary_new();
	git_index *index;
	git_index_conflict_iterator *iter;
	const git_index_entry *ancestor, *ours, *theirs;
	int error;

	Data_Get_Struct(self, git_index, index);

	error = git_index_conflict_iterator_new(&iter, index);
	rugged_exception_check(error);

	while ((error = git_index_conflict_next(&ancestor, &ours, &theirs, iter)) == GIT_OK) {
		VALUE rb_conflict = rb_hash_new();

		rb_hash_aset(rb_conflict, CSTR2SYM("ancestor"), rb_git_indexentry_fromC(ancestor));
		rb_hash_aset(rb_conflict, CSTR2SYM("ours"),     rb_git_indexentry_fromC(ours));
		rb_hash_aset(rb_conflict, CSTR2SYM("theirs"),   rb_git_indexentry_fromC(theirs));

		rb_ary_push(rb_conflicts, rb_conflict);
	}

	git_index_conflict_iterator_free(iter);

	if (error != GIT_ITEROVER)
		rugged_exception_check(error);

	return rb_conflicts;
}

/*
 *  Document-class: Rugged::Index
 *
 *  == Index Entries
 *
 *  Index entries are represented as Hash instances with the following key/value pairs:
 *
 *  path: ::
 *    The entry's path in the index.
 *
 *  oid: ::
 *    The oid of the entry's git object (blob / tree).
 *
 *  dev: ::
 *    The device for the index entry.
 *
 *  ino: ::
 *    The inode for the index entry.
 *
 *  mode: ::
 *    The current permissions of the index entry.
 *
 *  gid: ::
 *    Group ID of the index entry's owner.
 *
 *  uid: ::
 *    User ID of the index entry's owner.
 *
 *  file_size: ::
 *    The index entry's size, in bytes.
 *
 *  valid: ::
 *    +true+ if the index entry is valid, +false+ otherwise.
 *
 *  stage: ::
 *    The current stage of the index entry.
 *
 *  mtime: ::
 *    A Time instance representing the index entry's time of last modification.
 *
 *  mtime: ::
 *    A Time instance representing the index entry's time of last status change
 *    (ie. change of owner, group, mode, etc.).
 */
void Init_rugged_index(void)
{
	/*
	 * Index
	 */
	rb_cRuggedIndex = rb_define_class_under(rb_mRugged, "Index", rb_cObject);
	rb_define_singleton_method(rb_cRuggedIndex, "new", rb_git_index_new, -1);

	rb_define_method(rb_cRuggedIndex, "count", rb_git_index_count, 0);
	rb_define_method(rb_cRuggedIndex, "reload", rb_git_index_read, 0);
	rb_define_method(rb_cRuggedIndex, "clear", rb_git_index_clear, 0);
	rb_define_method(rb_cRuggedIndex, "write", rb_git_index_write, 0);
	rb_define_method(rb_cRuggedIndex, "get", rb_git_index_get, -1);
	rb_define_method(rb_cRuggedIndex, "[]", rb_git_index_get, -1);
	rb_define_method(rb_cRuggedIndex, "each", rb_git_index_each, 0);
	rb_define_method(rb_cRuggedIndex, "diff", rb_git_index_diff, -1);

	rb_define_method(rb_cRuggedIndex, "conflicts?", rb_git_index_conflicts_p, 0);
	rb_define_method(rb_cRuggedIndex, "conflicts", rb_git_index_conflicts, 0);
	rb_define_method(rb_cRuggedIndex, "conflict_get", rb_git_conflict_get, 1);
	rb_define_method(rb_cRuggedIndex, "conflict_add", rb_git_conflict_add, 1);
	rb_define_method(rb_cRuggedIndex, "conflict_remove", rb_git_conflict_remove, 1);
	rb_define_method(rb_cRuggedIndex, "conflict_cleanup", rb_git_conflict_cleanup, 0);

	rb_define_method(rb_cRuggedIndex, "merge_file", rb_git_merge_file, -1);

	rb_define_method(rb_cRuggedIndex, "add", rb_git_index_add, 1);
	rb_define_method(rb_cRuggedIndex, "update", rb_git_index_add, 1);
	rb_define_method(rb_cRuggedIndex, "<<", rb_git_index_add, 1);

	rb_define_method(rb_cRuggedIndex, "remove", rb_git_index_remove, -1);
	rb_define_method(rb_cRuggedIndex, "remove_dir", rb_git_index_remove_directory, -1);

	rb_define_method(rb_cRuggedIndex, "add_all", rb_git_index_add_all, -1);
	rb_define_method(rb_cRuggedIndex, "update_all", rb_git_index_update_all, -1);
	rb_define_method(rb_cRuggedIndex, "remove_all", rb_git_index_remove_all, -1);

	rb_define_method(rb_cRuggedIndex, "write_tree", rb_git_index_writetree, -1);
	rb_define_method(rb_cRuggedIndex, "read_tree", rb_git_index_readtree, 1);

	rb_const_set(rb_cRuggedIndex, rb_intern("ENTRY_FLAGS_STAGE"), INT2FIX(GIT_IDXENTRY_STAGEMASK));
	rb_const_set(rb_cRuggedIndex, rb_intern("ENTRY_FLAGS_STAGE_SHIFT"), INT2FIX(GIT_IDXENTRY_STAGESHIFT));
	rb_const_set(rb_cRuggedIndex, rb_intern("ENTRY_FLAGS_VALID"), INT2FIX(GIT_IDXENTRY_VALID));
}
