/*
 * 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"

extern VALUE rb_mRugged;
VALUE rb_cRuggedConfig;

void rb_git_config__free(git_config *config)
{
	git_config_free(config);
}

VALUE rugged_config_new(VALUE klass, VALUE owner, git_config *cfg)
{
	VALUE rb_config = Data_Wrap_Struct(klass, NULL, &rb_git_config__free, cfg);
	rugged_set_owner(rb_config, owner);
	return rb_config;
}

/*
 *  call-seq:
 *    Config.new(path) -> new_config
 *
 *  Open the file specified in +path+ as a +Rugged::Config+ file.
 *  If +path+ cannot be found, or the file is an invalid Git config,
 *  an exception will be raised.
 */
static VALUE rb_git_config_new(VALUE klass, VALUE rb_path)
{
	git_config *config = NULL;

	if (TYPE(rb_path) == T_ARRAY) {
		int error, i;

		error = git_config_new(&config);
		rugged_exception_check(error);

		for (i = 0; i < RARRAY_LEN(rb_path) && !error; ++i) {
			VALUE f = rb_ary_entry(rb_path, i);
			Check_Type(f, T_STRING);
			error = git_config_add_file_ondisk(config, StringValueCStr(f), i + 1, 1);
		}

		if (error) {
			git_config_free(config);
			rugged_exception_check(error);
		}
	} else if (TYPE(rb_path) == T_STRING) {
		rugged_exception_check(
			git_config_open_ondisk(&config, StringValueCStr(rb_path))
		);
	} else {
		rb_raise(rb_eTypeError, "Expecting a filename or an array of filenames");
	}

	return rugged_config_new(klass, Qnil, config);
}

/*
 *  call-seq:
 *    cfg.get(key) -> value
 *    cfg[key] -> value
 *
 *  Get the value for the given config +key+. Values are always
 *  returned as +String+, or +nil+ if the given key doesn't exist
 *  in the Config file.
 *
 *    cfg['apply.whitespace'] #=> 'fix'
 *    cfg['diff.renames'] #=> 'true'
 */
static VALUE rb_git_config_get(VALUE self, VALUE rb_key)
{
	git_config *config;
	git_buf buf = { NULL };
	int error;
	VALUE rb_result;

	Data_Get_Struct(self, git_config, config);
	Check_Type(rb_key, T_STRING);

	error = git_config_get_string_buf(&buf, config, StringValueCStr(rb_key));
	if (error == GIT_ENOTFOUND)
		return Qnil;

	rugged_exception_check(error);
	rb_result = rb_str_new_utf8(buf.ptr);
	git_buf_free(&buf);

	return rb_result;
}

/*
 *  call-seq:
 *    cfg.store(key, value)
 *    cfg[key] = value
 *
 *  Store the given +value+ in the Config file, under the section
 *  and name specified by +key+. Value can be any of the following
 *  Ruby types: +String+, +true+, +false+ and +Fixnum+.
 *
 *  The config file will be automatically stored to disk.
 *
 *    cfg['apply.whitespace'] = 'fix'
 *    cfg['diff.renames'] = true
 *    cfg['gc.reflogexpre'] = 90
 */
static VALUE rb_git_config_store(VALUE self, VALUE rb_key, VALUE rb_val)
{
	git_config *config;
	const char *key;
	int error;

	Data_Get_Struct(self, git_config, config);
	Check_Type(rb_key, T_STRING);

	key = StringValueCStr(rb_key);

	switch (TYPE(rb_val)) {
	case T_STRING:
		error = git_config_set_string(config, key, StringValueCStr(rb_val));
		break;

	case T_TRUE:
	case T_FALSE:
		error = git_config_set_bool(config, key, (rb_val == Qtrue));
		break;

	case T_FIXNUM:
		error = git_config_set_int32(config, key, FIX2INT(rb_val));
		break;

	default:
		rb_raise(rb_eTypeError,
			"Invalid value; config files can only store string, bool or int keys");
	}

	rugged_exception_check(error);
	return Qnil;
}

/*
 *  call-seq:
 *    cfg.delete(key) -> true or false
 *
 *  Delete the given +key+ from the config file. Return +true+ if
 *  the deletion was successful, or +false+ if the key was not
 *  found in the Config file.
 *
 *  The config file is immediately updated on disk.
 */
static VALUE rb_git_config_delete(VALUE self, VALUE rb_key)
{
	git_config *config;
	int error;

	Data_Get_Struct(self, git_config, config);
	Check_Type(rb_key, T_STRING);

	error = git_config_delete_entry(config, StringValueCStr(rb_key));
	if (error == GIT_ENOTFOUND)
		return Qfalse;

	rugged_exception_check(error);
	return Qtrue;
}

static int cb_config__each_key(const git_config_entry *entry, void *opaque)
{
	rb_funcall((VALUE)opaque, rb_intern("call"), 1, rb_str_new_utf8(entry->name));
	return GIT_OK;
}

static int cb_config__each_pair(const git_config_entry *entry, void *opaque)
{
	rb_funcall((VALUE)opaque, rb_intern("call"), 2,
		rb_str_new_utf8(entry->name),
		rb_str_new_utf8(entry->value)
	);

	return GIT_OK;
}

static int cb_config__to_hash(const git_config_entry *entry, void *opaque)
{
	rb_hash_aset((VALUE)opaque,
		rb_str_new_utf8(entry->name),
		rb_str_new_utf8(entry->value)
	);

	return GIT_OK;
}

/*
 *  call-seq:
 *    cfg.each_key { |key| block }
 *    cfg.each_key -> enumarator
 *
 *  Call the given block once for each key in the config file. If no block
 *  is given, an enumerator is returned.
 *
 *    cfg.each_key do |key|
 *      puts key
 *    end
 */
static VALUE rb_git_config_each_key(VALUE self)
{
	git_config *config;
	int error;

	Data_Get_Struct(self, git_config, config);

	if (!rb_block_given_p())
		return rb_funcall(self, rb_intern("to_enum"), 1, CSTR2SYM("each_key"));

	error = git_config_foreach(config, &cb_config__each_key, (void *)rb_block_proc());
	rugged_exception_check(error);
	return Qnil;
}

/*
 *  call-seq:
 *    cfg.each_pair { |key, value| block }
 *    cfg.each_pair -> enumerator
 *    cfg.each { |key, value| block }
 *    cfg.each -> enumerator
 *
 *  Call the given block once for each key/value pair in the config file.
 *  If no block is given, an enumerator is returned.
 *
 *    cfg.each do |key, value|
 *      puts "#{key} => #{value}"
 *    end
 */
static VALUE rb_git_config_each_pair(VALUE self)
{
	git_config *config;
	int error;

	Data_Get_Struct(self, git_config, config);

	if (!rb_block_given_p())
		return rb_funcall(self, rb_intern("to_enum"), 1, CSTR2SYM("each_pair"));

	error = git_config_foreach(config, &cb_config__each_pair, (void *)rb_block_proc());
	rugged_exception_check(error);
	return Qnil;
}

/*
 *  call-seq:
 *    cfg.to_hash -> hash
 *
 *  Returns the config file represented as a Ruby hash, where
 *  each configuration entry appears as a key with its
 *  corresponding value.
 *
 *    cfg.to_hash #=> {"core.autolf" => "true", "core.bare" => "true"}
 */
static VALUE rb_git_config_to_hash(VALUE self)
{
	git_config *config;
	int error;
	VALUE hash;

	Data_Get_Struct(self, git_config, config);
	hash = rb_hash_new();

	error = git_config_foreach(config, &cb_config__to_hash, (void *)hash);
	rugged_exception_check(error);
	return hash;
}

/*
 *  call-seq:
 *    Config.global() -> new_config
 *    Config.open_global() -> new_config
 *
 *  Open the default global config file as a new +Rugged::Config+ object.
 *  An exception will be raised if the global config file doesn't
 *  exist.
 */
static VALUE rb_git_config_open_default(VALUE klass)
{
	git_config *cfg;
	int error;

	error = git_config_open_default(&cfg);
	rugged_exception_check(error);

	return rugged_config_new(klass, Qnil, cfg);
}

/*
 *  call-seq:
 *    config.snapshot -> snapshot
 *
 *  Create a snapshot of the configuration.
 *
 *  Provides a consistent, read-only view of the configuration for
 *  looking up complex values from a configuration.
 */
static VALUE rb_git_config_snapshot(VALUE self)
{
	git_config *config, *snapshot;

	Data_Get_Struct(self, git_config, config);

	rugged_exception_check(
		git_config_snapshot(&snapshot, config)
	);

	return rugged_config_new(rb_obj_class(self), Qnil, snapshot);
}

/*
 *  call-seq:
 *    config.transaction { |config| }
 *
 *  Perform configuration changes in a transaction.
 *
 *  Locks the configuration, executes the given block and stores
 *  any changes that were made to the configuration. If the block
 *  throws an exception, all changes are rolled back automatically.
 *
 *  During the execution of the block, configuration changes don't
 *  get stored to disk immediately, so reading from the configuration
 *  will continue to return the values that were stored in the configuration
 *  when the transaction was started.
 */
static VALUE rb_git_config_transaction(VALUE self)
{
	git_config *config;
	git_transaction *tx;
	VALUE rb_result;
	int error = 0, exception = 0;

	Data_Get_Struct(self, git_config, config);

	git_config_lock(&tx, config);

	rb_result = rb_protect(rb_yield, self, &exception);

	if (!exception)
		error = git_transaction_commit(tx);

	git_transaction_free(tx);

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

	return rb_result;
}

void Init_rugged_config(void)
{
	/*
	 * Config
	 */
	rb_cRuggedConfig = rb_define_class_under(rb_mRugged, "Config", rb_cObject);
	rb_define_singleton_method(rb_cRuggedConfig, "new", rb_git_config_new, 1);

	rb_define_singleton_method(rb_cRuggedConfig, "global", rb_git_config_open_default, 0);
	rb_define_singleton_method(rb_cRuggedConfig, "open_global", rb_git_config_open_default, 0);

	rb_define_method(rb_cRuggedConfig, "delete", rb_git_config_delete, 1);

	rb_define_method(rb_cRuggedConfig, "store", rb_git_config_store, 2);
	rb_define_method(rb_cRuggedConfig, "[]=", rb_git_config_store, 2);

	rb_define_method(rb_cRuggedConfig, "get", rb_git_config_get, 1);
	rb_define_method(rb_cRuggedConfig, "[]", rb_git_config_get, 1);

	rb_define_method(rb_cRuggedConfig, "each_key", rb_git_config_each_key, 0);
	rb_define_method(rb_cRuggedConfig, "each_pair", rb_git_config_each_pair, 0);
	rb_define_method(rb_cRuggedConfig, "each", rb_git_config_each_pair, 0);
	rb_define_method(rb_cRuggedConfig, "to_hash", rb_git_config_to_hash, 0);

	rb_define_method(rb_cRuggedConfig, "snapshot", rb_git_config_snapshot, 0);
	rb_define_method(rb_cRuggedConfig, "transaction", rb_git_config_transaction, 0);
}
