/*
 * Copyright (C) the Rugged contributors.  All rights reserved.
 *
 * This file is part of Rugged, distributed under the MIT license.
 * For full terms see the included LICENSE file.
 */

#include "rugged.h"

extern VALUE rb_mRugged;
VALUE rb_cRuggedBlame;

static VALUE rb_git_blame_hunk_fromC(const git_blame_hunk *hunk)
{
	VALUE rb_hunk;
	if (!hunk)  {
		return Qnil;
	}

	rb_hunk = rb_hash_new();
	rb_hash_aset(rb_hunk, CSTR2SYM("lines_in_hunk"), UINT2NUM(hunk->lines_in_hunk));

	rb_hash_aset(rb_hunk, CSTR2SYM("final_commit_id"), rugged_create_oid(&(hunk->final_commit_id)));
	rb_hash_aset(rb_hunk, CSTR2SYM("final_start_line_number"), UINT2NUM(hunk->final_start_line_number));
	rb_hash_aset(rb_hunk, CSTR2SYM("final_signature"), hunk->final_signature ? rugged_signature_new(hunk->final_signature, NULL) : Qnil);

	rb_hash_aset(rb_hunk, CSTR2SYM("orig_commit_id"), rugged_create_oid(&(hunk->orig_commit_id)));
	rb_hash_aset(rb_hunk, CSTR2SYM("orig_path"), hunk->orig_path ? rb_str_new2(hunk->orig_path) : Qnil);
	rb_hash_aset(rb_hunk, CSTR2SYM("orig_start_line_number"), UINT2NUM(hunk->orig_start_line_number));
	rb_hash_aset(rb_hunk, CSTR2SYM("orig_signature"), hunk->orig_signature ? rugged_signature_new(hunk->orig_signature, NULL) : Qnil);

	rb_hash_aset(rb_hunk, CSTR2SYM("boundary"), hunk->boundary ? Qtrue : Qfalse);

	return rb_hunk;
}

static void rugged_parse_blame_options(git_blame_options *opts, git_repository *repo, 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("min_line"));
		if (!NIL_P(rb_value)) {
			Check_Type(rb_value, T_FIXNUM);
			opts->min_line = FIX2UINT(rb_value);
		}

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

		rb_value = rb_hash_aref(rb_options, CSTR2SYM("newest_commit"));
		if (!NIL_P(rb_value)) {
			int error = rugged_oid_get(&opts->newest_commit, repo, rb_value);
			rugged_exception_check(error);
		}

		rb_value = rb_hash_aref(rb_options, CSTR2SYM("oldest_commit"));
		if (!NIL_P(rb_value)) {
			int error = rugged_oid_get(&opts->oldest_commit, repo, rb_value);
			rugged_exception_check(error);
		}

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

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

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

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

/*
 *  call-seq:
 *    Blame.new(repo, path, options = {}) -> blame
 *
 *  Get blame data for the file at +path+ in +repo+.
 *
 *  The following options can be passed in the +options+ Hash:
 *
 *  :newest_commit ::
 *    The ID of the newest commit to consider in the blame. Defaults to +HEAD+.
 *    This can either be a Rugged::Object instance, or a full or abbreviated
 *    SHA1 id.
 *
 *  :oldest_commit ::
 *    The id of the oldest commit to consider. Defaults to the first commit
 *    encountered with a NULL parent. This can either be a Rugged::Object
 *    instance, or a full or abbreviated SHA1 id.
 *
 *  :min_line ::
 *    The first line in the file to blame. Line numbers start with 1.
 *    Defaults to +1+.
 *
 *  :max_line ::
 *    The last line in the file to blame. Defaults to the last line in
 *    the file.
 *
 *  :track_copies_same_file ::
 *    If this value is +true+, lines that have moved within a file will be
 *    tracked (like `git blame -M`).
 *
 *  :track_copies_same_commit_moves ::
 *    If this value is +true+, lines that have moved across files in the same
 *    commit will be tracked (like `git blame -C`).
 *
 *  :track_copies_same_commit_copies ::
 *    If this value is +true+, lines that have been copied from another file
 *    that exists in the same commit will be tracked (like `git blame -CC`).
 *
 *  :track_copies_any_commit_copies ::
 *    If this value is +true+, lines that have been copied from another file
 *    that exists in *any* commit will be tracked (like `git blame -CCC`).
 *
 */
static VALUE rb_git_blame_new(int argc, VALUE *argv, VALUE klass)
{
	VALUE rb_repo, rb_path, rb_options;
	git_repository *repo;
	git_blame *blame;
	git_blame_options opts = GIT_BLAME_OPTIONS_INIT;

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

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

	Check_Type(rb_path, T_STRING);

	rugged_parse_blame_options(&opts, repo, rb_options);

	rugged_exception_check(git_blame_file(
		&blame, repo, StringValueCStr(rb_path), &opts
	));

	return Data_Wrap_Struct(klass, NULL, &git_blame_free, blame);
}

/*
 *  call-seq:
 *    blame.for_line(line_no) -> hunk
 *
 *  Returns the blame hunk data for the given +line_no+ in +blame+.
 *  Line number counting starts with +1+.
 */
static VALUE rb_git_blame_for_line(VALUE self, VALUE rb_line_no)
{
	git_blame *blame;
	int line_no;

	Data_Get_Struct(self, git_blame, blame);
	Check_Type(rb_line_no, T_FIXNUM);

	line_no = NUM2INT(rb_line_no);

	if (line_no < 0) {
		rb_raise(rb_eArgError, "line number can't be negative");
	}

	return rb_git_blame_hunk_fromC(
		git_blame_get_hunk_byline(blame, (uint32_t)line_no)
	);
}

/*
 *  call-seq:
 *    blame.count -> count
 *    blame.size -> count
 *
 *  Returns the total +count+ of blame hunks in +blame+.
 */
static VALUE rb_git_blame_count(VALUE self)
{
	git_blame *blame;
	Data_Get_Struct(self, git_blame, blame);
	return UINT2NUM(git_blame_get_hunk_count(blame));
}

static VALUE rugged_blame_enum_size(VALUE rb_blame, VALUE rb_args, VALUE rb_eobj)
{
	return rb_git_blame_count(rb_blame);
}

/*
 *  call-seq:
 *    blame[index] -> hunk
 *
 *  Returns the blame hunk data at the given +index+ in +blame+.
 *
 *  Negative indices count backward from the end of the blame hunks (-1 is the last
 *  element).
 *
 *  Returns +nil+ if no blame hunk exists at the given +index+.
 */
static VALUE rb_git_blame_get_by_index(VALUE self, VALUE rb_index)
{
	git_blame *blame;
	int index;
	uint32_t blame_count;

	Data_Get_Struct(self, git_blame, blame);
	Check_Type(rb_index, T_FIXNUM);

	index = NUM2INT(rb_index);
	blame_count = git_blame_get_hunk_count(blame);

	if (index < 0) {
		if ((uint32_t)(-index) > blame_count) {
			return Qnil;
		}

		return rb_git_blame_hunk_fromC(
			git_blame_get_hunk_byindex(blame, (uint32_t)(blame_count + index))
		);
	}

	if ((uint32_t)index > blame_count)
		return Qnil;

	return rb_git_blame_hunk_fromC(
		git_blame_get_hunk_byindex(blame, (uint32_t)index)
	);
}

/*
 *  call-seq:
 *    blame.each { |hunk| ... } -> blame
 *    blame.each -> enumerator
 *
 *  If given a block, yields each +hunk+ that is part of +blame+.
 *  If no block is given, an enumerator will be returned.
 */
static VALUE rb_git_blame_each(VALUE self)
{
	git_blame *blame;
	uint32_t i, blame_count;

	RETURN_SIZED_ENUMERATOR(self, 0, 0, rugged_blame_enum_size);

	Data_Get_Struct(self, git_blame, blame);

	blame_count = git_blame_get_hunk_count(blame);
	for (i = 0; i < blame_count; ++i) {
		rb_yield(rb_git_blame_hunk_fromC(
			git_blame_get_hunk_byindex(blame, i)
		));
	}

	return self;
}

void Init_rugged_blame(void)
{
	rb_cRuggedBlame = rb_define_class_under(rb_mRugged, "Blame", rb_cObject);
	rb_include_module(rb_cRuggedBlame, rb_mEnumerable);

	rb_define_singleton_method(rb_cRuggedBlame, "new", rb_git_blame_new, -1);

	rb_define_method(rb_cRuggedBlame, "[]", rb_git_blame_get_by_index, 1);
	rb_define_method(rb_cRuggedBlame, "for_line", rb_git_blame_for_line, 1);

	rb_define_method(rb_cRuggedBlame, "count", rb_git_blame_count, 0);
	rb_define_method(rb_cRuggedBlame, "size", rb_git_blame_count, 0);

	rb_define_method(rb_cRuggedBlame, "each", rb_git_blame_each, 0);
}
