/*
 * Copyright (C) 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 "common.h"
#include "git2/config.h"
#include "git2/types.h"
#include "git2/repository.h"
#include "git2/index.h"
#include "git2/submodule.h"
#include "buffer.h"
#include "vector.h"
#include "posix.h"
#include "config_file.h"
#include "config.h"
#include "repository.h"

static git_cvar_map _sm_update_map[] = {
	{GIT_CVAR_STRING, "checkout", GIT_SUBMODULE_UPDATE_CHECKOUT},
	{GIT_CVAR_STRING, "rebase", GIT_SUBMODULE_UPDATE_REBASE},
	{GIT_CVAR_STRING, "merge", GIT_SUBMODULE_UPDATE_MERGE}
};

static git_cvar_map _sm_ignore_map[] = {
	{GIT_CVAR_STRING, "all", GIT_SUBMODULE_IGNORE_ALL},
	{GIT_CVAR_STRING, "dirty", GIT_SUBMODULE_IGNORE_DIRTY},
	{GIT_CVAR_STRING, "untracked", GIT_SUBMODULE_IGNORE_UNTRACKED},
	{GIT_CVAR_STRING, "none", GIT_SUBMODULE_IGNORE_NONE}
};

static kh_inline khint_t str_hash_no_trailing_slash(const char *s)
{
	khint_t h;

	for (h = 0; *s; ++s)
		if (s[1] || *s != '/')
			h = (h << 5) - h + *s;

	return h;
}

static kh_inline int str_equal_no_trailing_slash(const char *a, const char *b)
{
	size_t alen = a ? strlen(a) : 0;
	size_t blen = b ? strlen(b) : 0;

	if (alen && a[alen] == '/')
		alen--;
	if (blen && b[blen] == '/')
		blen--;

	return (alen == blen && strncmp(a, b, alen) == 0);
}

__KHASH_IMPL(str, static kh_inline, const char *, void *, 1, str_hash_no_trailing_slash, str_equal_no_trailing_slash);

static git_submodule *submodule_alloc(const char *name)
{
	git_submodule *sm = git__calloc(1, sizeof(git_submodule));
	if (sm == NULL)
		return sm;

	sm->path = sm->name = git__strdup(name);
	if (!sm->name) {
		git__free(sm);
		return NULL;
	}

	return sm;
}

static void submodule_release(git_submodule *sm, int decr)
{
	if (!sm)
		return;

	sm->refcount -= decr;

	if (sm->refcount == 0) {
		if (sm->name != sm->path)
			git__free(sm->path);
		git__free(sm->name);
		git__free(sm->url);
		git__free(sm);
	}
}

static int submodule_from_entry(
	git_strmap *smcfg, git_index_entry *entry)
{
	git_submodule *sm;
	void *old_sm;
	khiter_t pos;
	int error;

	pos = git_strmap_lookup_index(smcfg, entry->path);

	if (git_strmap_valid_index(smcfg, pos))
		sm = git_strmap_value_at(smcfg, pos);
	else
		sm = submodule_alloc(entry->path);

	git_oid_cpy(&sm->oid, &entry->oid);

	if (strcmp(sm->path, entry->path) != 0) {
		if (sm->path != sm->name) {
			git__free(sm->path);
			sm->path = sm->name;
		}
		sm->path = git__strdup(entry->path);
		if (!sm->path)
			goto fail;
	}

	git_strmap_insert2(smcfg, sm->path, sm, old_sm, error);
	if (error < 0)
		goto fail;
	sm->refcount++;

	if (old_sm && ((git_submodule *)old_sm) != sm) {
		/* TODO: log warning about multiple entrys for same submodule path */
		submodule_release(old_sm, 1);
	}

	return 0;

fail:
	submodule_release(sm, 0);
	return -1;
}

static int submodule_from_config(
	const char *key, const char *value, void *data)
{
	git_strmap *smcfg = data;
	const char *namestart;
	const char *property;
	git_buf name = GIT_BUF_INIT;
	git_submodule *sm;
	void *old_sm = NULL;
	bool is_path;
	khiter_t pos;
	int error;

	if (git__prefixcmp(key, "submodule.") != 0)
		return 0;

	namestart = key + strlen("submodule.");
	property  = strrchr(namestart, '.');
	if (property == NULL)
		return 0;
	property++;
	is_path = (strcmp(property, "path") == 0);

	if (git_buf_set(&name, namestart, property - namestart - 1) < 0)
		return -1;

	pos = git_strmap_lookup_index(smcfg, name.ptr);
	if (!git_strmap_valid_index(smcfg, pos) && is_path)
		pos = git_strmap_lookup_index(smcfg, value);
	if (!git_strmap_valid_index(smcfg, pos))
		sm = submodule_alloc(name.ptr);
	else
		sm = git_strmap_value_at(smcfg, pos);
	if (!sm)
		goto fail;

	if (strcmp(sm->name, name.ptr) != 0) {
		assert(sm->path == sm->name);
		sm->name = git_buf_detach(&name);

		git_strmap_insert2(smcfg, sm->name, sm, old_sm, error);
		if (error < 0)
			goto fail;
		sm->refcount++;
	}
	else if (is_path && strcmp(sm->path, value) != 0) {
		assert(sm->path == sm->name);
		sm->path = git__strdup(value);
		if (sm->path == NULL)
			goto fail;

		git_strmap_insert2(smcfg, sm->path, sm, old_sm, error);
		if (error < 0)
			goto fail;
		sm->refcount++;
	}
	git_buf_free(&name);

	if (old_sm && ((git_submodule *)old_sm) != sm) {
		/* TODO: log warning about multiple submodules with same path */
		submodule_release(old_sm, 1);
	}

	if (is_path)
		return 0;

	/* copy other properties into submodule entry */
	if (strcmp(property, "url") == 0) {
		if (sm->url) {
			git__free(sm->url);
			sm->url = NULL;
		}
		if ((sm->url = git__strdup(value)) == NULL)
			goto fail;
	}
	else if (strcmp(property, "update") == 0) {
		int val;
		if (git_config_lookup_map_value(
			_sm_update_map, ARRAY_SIZE(_sm_update_map), value, &val) < 0) {
			giterr_set(GITERR_INVALID,
				"Invalid value for submodule update property: '%s'", value);
			goto fail;
		}
		sm->update = (git_submodule_update_t)val;
	}
	else if (strcmp(property, "fetchRecurseSubmodules") == 0) {
		if (git__parse_bool(&sm->fetch_recurse, value) < 0) {
			giterr_set(GITERR_INVALID,
				"Invalid value for submodule 'fetchRecurseSubmodules' property: '%s'", value);
			goto fail;
		}
	}
	else if (strcmp(property, "ignore") == 0) {
		int val;
		if (git_config_lookup_map_value(
			_sm_ignore_map, ARRAY_SIZE(_sm_ignore_map), value, &val) < 0) {
			giterr_set(GITERR_INVALID,
				"Invalid value for submodule ignore property: '%s'", value);
			goto fail;
		}
		sm->ignore = (git_submodule_ignore_t)val;
	}
	/* ignore other unknown submodule properties */

	return 0;

fail:
	submodule_release(sm, 0);
	git_buf_free(&name);
	return -1;
}

static int load_submodule_config(git_repository *repo)
{
	int error;
	git_index *index;
	unsigned int i, max_i;
	git_oid gitmodules_oid;
	git_strmap *smcfg;
	struct git_config_file *mods = NULL;

	if (repo->submodules)
		return 0;

	/* submodule data is kept in a hashtable with each submodule stored
	 * under both its name and its path.  These are usually the same, but
	 * that is not guaranteed.
	 */
	smcfg = git_strmap_alloc();
	GITERR_CHECK_ALLOC(smcfg);

	/* scan index for gitmodules (and .gitmodules entry) */
	if ((error = git_repository_index__weakptr(&index, repo)) < 0)
		goto cleanup;
	memset(&gitmodules_oid, 0, sizeof(gitmodules_oid));
	max_i = git_index_entrycount(index);

	for (i = 0; i < max_i; i++) {
		git_index_entry *entry = git_index_get(index, i);
		if (S_ISGITLINK(entry->mode)) {
			if ((error = submodule_from_entry(smcfg, entry)) < 0)
				goto cleanup;
		}
		else if (strcmp(entry->path, ".gitmodules") == 0)
			git_oid_cpy(&gitmodules_oid, &entry->oid);
	}

	/* load .gitmodules from workdir if it exists */
	if (git_repository_workdir(repo) != NULL) {
		/* look in workdir for .gitmodules */
		git_buf path = GIT_BUF_INIT;
		if (!git_buf_joinpath(
				&path, git_repository_workdir(repo), ".gitmodules") &&
			git_path_isfile(path.ptr))
		{
			if (!(error = git_config_file__ondisk(&mods, path.ptr)))
				error = git_config_file_open(mods);
		}
		git_buf_free(&path);
	}

	/* load .gitmodules from object cache if not in workdir */
	if (!error && mods == NULL && !git_oid_iszero(&gitmodules_oid)) {
		/* TODO: is it worth loading gitmodules from object cache? */
	}

	/* process .gitmodules info */
	if (!error && mods != NULL)
		error = git_config_file_foreach(mods, submodule_from_config, smcfg);

	/* store submodule config in repo */
	if (!error)
		repo->submodules = smcfg;

cleanup:
	if (mods != NULL)
		git_config_file_free(mods);
	if (error)
		git_strmap_free(smcfg);
	return error;
}

void git_submodule_config_free(git_repository *repo)
{
	git_strmap *smcfg = repo->submodules;
	git_submodule *sm;

	repo->submodules = NULL;

	if (smcfg == NULL)
		return;

	git_strmap_foreach_value(smcfg, sm, {
		submodule_release(sm,1);
	});
	git_strmap_free(smcfg);
}

static int submodule_cmp(const void *a, const void *b)
{
	return strcmp(((git_submodule *)a)->name, ((git_submodule *)b)->name);
}

int git_submodule_foreach(
	git_repository *repo,
	int (*callback)(const char *name, void *payload),
	void *payload)
{
	int error;
	git_submodule *sm;
	git_vector seen = GIT_VECTOR_INIT;
	seen._cmp = submodule_cmp;

	if ((error = load_submodule_config(repo)) < 0)
		return error;

	git_strmap_foreach_value(repo->submodules, sm, {
		/* usually the following will not come into play */
		if (sm->refcount > 1) {
			if (git_vector_bsearch(&seen, sm) != GIT_ENOTFOUND)
				continue;
			if ((error = git_vector_insert(&seen, sm)) < 0)
				break;
		}

		if ((error = callback(sm->name, payload)) < 0)
			break;
	});

	git_vector_free(&seen);

	return error;
}

int git_submodule_lookup(
	git_submodule **sm_ptr, /* NULL allowed if user only wants to test */
	git_repository *repo,
	const char *name)       /* trailing slash is allowed */
{
	khiter_t pos;

	if (load_submodule_config(repo) < 0)
		return -1;

	pos = git_strmap_lookup_index(repo->submodules, name);
	if (!git_strmap_valid_index(repo->submodules, pos))
		return GIT_ENOTFOUND;

	if (sm_ptr)
		*sm_ptr = git_strmap_value_at(repo->submodules, pos);

	return 0;
}
