/*
 * 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 "hashtable.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 uint32_t strhash_no_trailing_slash(const void *key, int hash_id)
{
	static uint32_t hash_seeds[GIT_HASHTABLE_HASHES] = {
		0x01010101,
		0x12345678,
		0xFEDCBA98
	};

	size_t key_len = key ? strlen((const char *)key) : 0;
	if (key_len > 0 && ((const char *)key)[key_len - 1] == '/')
		key_len--;

	return git__hash(key, (int)key_len, hash_seeds[hash_id]);
}

static int strcmp_no_trailing_slash(const void *a, const void *b)
{
	const char *astr = (const char *)a;
	const char *bstr = (const char *)b;
	size_t alen = a ? strlen(astr) : 0;
	size_t blen = b ? strlen(bstr) : 0;
	int cmp;

	if (alen > 0 && astr[alen - 1] == '/')
		alen--;
	if (blen > 0 && bstr[blen - 1] == '/')
		blen--;

	cmp = strncmp(astr, bstr, min(alen, blen));
	if (cmp == 0)
		cmp = (alen < blen) ? -1 : (alen > blen) ? 1 : 0;

	return cmp;
}

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_hashtable *smcfg, git_index_entry *entry)
{
	git_submodule *sm;
	void *old_sm;

	sm = git_hashtable_lookup(smcfg, entry->path);
	if (!sm)
		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;
	}

	if (git_hashtable_insert2(smcfg, sm->path, sm, &old_sm) < 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_hashtable *smcfg = data;
	const char *namestart;
	const char *property;
	git_buf name = GIT_BUF_INIT;
	git_submodule *sm;
	void *old_sm = NULL;
	bool is_path;

	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;

	sm = git_hashtable_lookup(smcfg, name.ptr);
	if (!sm && is_path)
		sm = git_hashtable_lookup(smcfg, value);
	if (!sm)
		sm = submodule_alloc(name.ptr);
	if (!sm)
		goto fail;

	if (strcmp(sm->name, name.ptr) != 0) {
		assert(sm->path == sm->name);
		sm->name = git_buf_detach(&name);
		if (git_hashtable_insert2(smcfg, sm->name, sm, &old_sm) < 0)
			goto fail;
		sm->refcount++;
	}
	else if (is_path && strcmp(sm->path, value) != 0) {
		assert(sm->path == sm->name);
		if ((sm->path = git__strdup(value)) == NULL ||
			git_hashtable_insert2(smcfg, sm->path, sm, &old_sm) < 0)
			goto fail;
		sm->refcount++;
	}
	git_buf_free(&name);

	if (old_sm && ((git_submodule *)old_sm) != sm) {
		/* TODO: log entry 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_config_parse_bool(&sm->fetch_recurse, value) < 0)
			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_hashtable *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_hashtable_alloc(
		4, strhash_no_trailing_slash, strcmp_no_trailing_slash);
	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_hashtable_free(smcfg);
	return error;
}

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

	repo->submodules = NULL;

	if (smcfg == NULL)
		return;

	GIT_HASHTABLE_FOREACH_VALUE(smcfg, sm, { submodule_release(sm,1); });
	git_hashtable_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_HASHTABLE_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 */
{
	git_submodule *sm;

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

	sm = git_hashtable_lookup(repo->submodules, name);

	if (sm_ptr)
		*sm_ptr = sm;

	return sm ? 0 : GIT_ENOTFOUND;
}
