/*
 * Copyright (C) the libgit2 contributors. All rights reserved.
 *
 * 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 <git2.h>

#include "common.h"
#include "cmd.h"

#define COMMAND_NAME "config"

typedef enum {
	ACTION_NONE = 0,
	ACTION_GET,
	ACTION_ADD,
	ACTION_REPLACE_ALL,
	ACTION_LIST
} action_t;

static action_t action = ACTION_NONE;
static int show_origin;
static int show_scope;
static int show_help;
static int null_separator;
static int config_level;
static char *config_filename;
static char *name, *value, *value_pattern;

static const cli_opt_spec opts[] = {
	CLI_COMMON_OPT, \

	{ CLI_OPT_TYPE_SWITCH,    "null",       'z', &null_separator, 1,
	  0,                       NULL,        "use NUL as a separator" },

	{ CLI_OPT_TYPE_SWITCH,    "system",      0,  &config_level, GIT_CONFIG_LEVEL_SYSTEM,
	  0,                       NULL,        "read/write to system configuration" },
	{ CLI_OPT_TYPE_SWITCH,    "global",      0,  &config_level, GIT_CONFIG_LEVEL_GLOBAL,
	  CLI_OPT_USAGE_CHOICE,    NULL,        "read/write to global configuration" },
	{ CLI_OPT_TYPE_SWITCH,    "local",       0,  &config_level, GIT_CONFIG_LEVEL_LOCAL,
	  CLI_OPT_USAGE_CHOICE,    NULL,        "read/write to local configuration" },
	{ CLI_OPT_TYPE_VALUE,     "file",        0,  &config_filename, 0,
	  CLI_OPT_USAGE_CHOICE,   "filename",   "read/write to specified configuration file" },

	{ CLI_OPT_TYPE_SWITCH,    "get",         0,  &action,       ACTION_GET,
	  CLI_OPT_USAGE_REQUIRED,  NULL,        "get a configuration value" },
	{ CLI_OPT_TYPE_SWITCH,    "add",         0,  &action,       ACTION_ADD,
	  CLI_OPT_USAGE_CHOICE,    NULL,        "add a configuration value" },
	{ CLI_OPT_TYPE_SWITCH,    "replace-all", 0,  &action,       ACTION_REPLACE_ALL,
	  CLI_OPT_USAGE_CHOICE,    NULL,        "add a configuration value, replacing any old values" },
	{ CLI_OPT_TYPE_SWITCH,    "list",       'l', &action,       ACTION_LIST,
	  CLI_OPT_USAGE_CHOICE | CLI_OPT_USAGE_SHOW_LONG,
	                           NULL,        "list all configuration entries" },
	{ CLI_OPT_TYPE_SWITCH,    "show-origin", 0,  &show_origin,  1,
	  0,                       NULL,        "show origin of configuration" },
	{ CLI_OPT_TYPE_SWITCH,    "show-scope",  0,  &show_scope,   1,
	  0,                       NULL,        "show scope of configuration" },
	{ CLI_OPT_TYPE_ARG,       "name",        0,  &name,         0,
	  0,                      "name",       "name of configuration entry" },
	{ CLI_OPT_TYPE_ARG,       "value",       0,  &value,        0,
	  0,                      "value",      "value of configuration entry" },
	{ CLI_OPT_TYPE_ARG,       "regexp",      0,  &value_pattern, 0,
	  0,                      "regexp",     "regular expression of values to replace" },
	{ 0 },
};

static void print_help(void)
{
	cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts);
	printf("\n");

	printf("Query and set configuration options.\n");
	printf("\n");

	printf("Options:\n");

	cli_opt_help_fprint(stdout, opts);
}

static int get_config(git_config *config)
{
	git_buf value = GIT_BUF_INIT;
	char sep = null_separator ? '\0' : '\n';
	int error;

	error = git_config_get_string_buf(&value, config, name);

	if (error && error != GIT_ENOTFOUND)
		return cli_error_git();

	else if (error == GIT_ENOTFOUND)
		return 1;

	printf("%s%c", value.ptr, sep);
	return 0;
}

static int add_config(git_config *config)
{
	if (git_config_set_multivar(config, name, "$^", value) < 0)
		return cli_error_git();

	return 0;
}

static int replace_all_config(git_config *config)
{
	if (git_config_set_multivar(config, name, value_pattern ? value_pattern : ".*", value) < 0)
		return cli_error_git();

	return 0;
}

static const char *level_name(git_config_level_t level)
{
	switch (level) {
	case GIT_CONFIG_LEVEL_PROGRAMDATA:
		return "programdata";
	case GIT_CONFIG_LEVEL_SYSTEM:
		return "system";
	case GIT_CONFIG_LEVEL_XDG:
		return "global";
	case GIT_CONFIG_LEVEL_GLOBAL:
		return "global";
	case GIT_CONFIG_LEVEL_LOCAL:
		return "local";
	case GIT_CONFIG_LEVEL_APP:
		return "command";
	default:
		return "unknown";
	}
}

static int list_config(git_config *config)
{
	git_config_iterator *iterator;
	git_config_entry *entry;
	char data_separator = null_separator ? '\0' : '\t';
	char kv_separator = null_separator ? '\n' : '=';
	char entry_separator = null_separator ? '\0' : '\n';
	int error;

	if (git_config_iterator_new(&iterator, config) < 0)
		return cli_error_git();

	while ((error = git_config_next(&entry, iterator)) == 0) {
		if (show_scope)
			printf("%s%c",
				level_name(entry->level),
				data_separator);

		if (show_origin)
			printf("%s%s%s%c",
				entry->backend_type ? entry->backend_type : "",
				entry->backend_type && entry->origin_path ? ":" : "",
				entry->origin_path ? entry->origin_path : "",
				data_separator);

		printf("%s%c%s%c", entry->name, kv_separator, entry->value,
			entry_separator);
	}

	if (error != GIT_ITEROVER)
		return cli_error_git();

	git_config_iterator_free(iterator);
	return 0;
}

int cmd_config(int argc, char **argv)
{
	git_repository *repo = NULL;
	git_config *config = NULL;
	cli_repository_open_options open_opts = { argv + 1, argc - 1};
	cli_opt invalid_opt;
	int ret = 0;

	if (cli_opt_parse(&invalid_opt, opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU))
		return cli_opt_usage_error(COMMAND_NAME, opts, &invalid_opt);

	if (show_help) {
		print_help();
		return 0;
	}

	if (config_filename) {
		if (git_config_new(&config) < 0 ||
		    git_config_add_file_ondisk(config, config_filename,
				GIT_CONFIG_LEVEL_APP, NULL, 0) < 0) {
			ret = cli_error_git();
			goto done;
		}
	} else {
		if (cli_repository_open(&repo, &open_opts) < 0 ||
		    git_repository_config(&config, repo) < 0) {
			ret = cli_error_git();
			goto done;
		}

		if (config_level &&
		    git_config_open_level(&config, config, config_level) < 0) {
			ret = cli_error_git();
			goto done;
		}
	}

	switch (action) {
	case ACTION_ADD:
		if (!name || !value || value_pattern)
			ret = cli_error_usage("%s --add requires two arguments", COMMAND_NAME);
		else
			ret = add_config(config);
		break;
	case ACTION_REPLACE_ALL:
		if (!name || !value)
			ret = cli_error_usage("%s --replace-all requires two or three arguments", COMMAND_NAME);
		else
			ret = replace_all_config(config);
		break;
	case ACTION_GET:
		if (!name)
			ret = cli_error_usage("%s --get requires an argument", COMMAND_NAME);
		else
			ret = get_config(config);
		break;
	case ACTION_LIST:
		if (name)
			ret = cli_error_usage("%s --list does not take an argument", COMMAND_NAME);
		else
			ret = list_config(config);
		break;
	default:
		ret = cli_error_usage("unknown action");
	}

done:
	git_config_free(config);
	git_repository_free(repo);
	return ret;
}
