/* git2.c
 *
 * Simple binding to the parts of libgit2 in use by Gall.
 *
 * Copyright 2014 Daniel Silverstone <dsilvers@digital-scurf.org>
 */

/**
 * Low-Level C binding for Gall's use of libgit2.
 *
 * @module gall.ll.git2
 */

#include <lua.h>
#include <lauxlib.h>
#include "git2.h"

typedef struct {
	git_repository *repo;
	git_odb *odb;
} repo_et_al_t;

static inline git_repository *to_repo(lua_State *L, int idx)
{
	repo_et_al_t *real = lua_touserdata(L, idx);
	if (real == NULL)
		return NULL;
	return real->repo;
}

static inline git_odb *to_odb(lua_State *L, int idx)
{
	repo_et_al_t *real = lua_touserdata(L, idx);
	if (real == NULL)
		return NULL;
	return real->odb;
}

/* Push the most recent error from libgit2 */
static int push_git2_error(lua_State *L, int retcode)
{
	const git_error *err = giterr_last();
	lua_pushnil(L);
	if (err == NULL) {
		lua_pushfstring(L, "Unknown error: %d", retcode);
	} else {
		lua_pushstring(L, err->message);
	}
	return 2;
}



static int L_gc_repo(lua_State *L)
{
	repo_et_al_t *real = lua_touserdata(L, 1);
	if (real->odb != NULL)
		git_odb_free(real->odb);
	if (real->repo != NULL)
		git_repository_free(real->repo);
	real->repo = NULL;
	real->odb = NULL;
	return 0;
}

/**
 * Open a repository from disk.
 *
 * This function opens a repository on disk using libgit2.  This low level
 * repository type can be used in routines such as @{lookup_symbolic_ref} or
 * @{lookup_sha_from_ref}.
 *
 * @function open_repo
 * @tparam string repopath The path to the repository
 * @treturn llrepo The repository (or nil on error)
 * @treturn string An error message (if repo is nil)
 */

/* Trivial Repository binding, GC will free the repo */
static int L_open_repo(lua_State *L)
{
	const char *repopath = luaL_checkstring(L, 1);
	int ret;
	repo_et_al_t *real = lua_newuserdata(L, sizeof(*real));
	real->repo = NULL; real->odb = NULL;
	
	ret = git_repository_open(&real->repo, repopath);
	if (ret != 0) {
		return push_git2_error(L, ret);
	}
	
	ret = git_repository_odb(&real->odb, real->repo);
	if (ret != 0) {
		push_git2_error(L, ret);
		git_repository_free(real->repo);
		return 2;
	}
	
	lua_pushvalue(L, lua_upvalueindex(1));
	lua_setmetatable(L, -2);
	return 1;
}

/**
 * Dereference a symbolic ref.
 *
 * @function lookup_symbolic_ref
 * @tparam llrepo repository The repository to look up the ref in.
 * @tparam string ref The symbolic ref name to dereference.
 * @treturn string The symbolic referent (or nil on error)
 * @treturn string The error message if the referent is nil
 */

static int L_lookup_symbolic_ref(lua_State *L)
{
	git_repository *repo = to_repo(L, 1);
	const char *refname = luaL_checkstring(L, 2);
	git_reference *ref = NULL;
	int ret;
	if ((ret = git_reference_lookup(&ref, repo, refname)) != 0) {
		return push_git2_error(L, ret);
	}
	if (git_reference_type(ref) != GIT_REF_SYMBOLIC) {
		git_reference_free(ref);
		lua_pushnil(L);
		lua_pushfstring(L, "%s is not symbolic", refname);
		return 2;
	}
	lua_pushstring(L, git_reference_symbolic_target(ref));
	git_reference_free(ref);
	return 1;
}

static int format_oid(lua_State *L, const git_oid *oid)
{
	char oidstr[40];
	git_oid_fmt(oidstr, oid);
	lua_pushlstring(L, oidstr, 40);
	return 1;
}

/**
 * Lookup the SHA1 pointed at by a reference.
 *
 * @function lookup_sha_from_ref
 * @tparam llrepo repository The repository to look up the reference in.
 * @tparam string ref The reference to look up.
 * @treturn string The OID (SHA1) of the referent (or nil on error).
 * @treturn string The error message if the OID is nil.
 */

static int L_lookup_sha_from_ref(lua_State *L)
{
	git_repository *repo = to_repo(L, 1);
	const char *refname = luaL_checkstring(L, 2);
	git_oid ref;
	int ret;
	if ((ret = git_reference_name_to_id(&ref, repo, refname)) != 0) {
		return push_git2_error(L, ret);
	}
	return format_oid(L, &ref);
}

static int parse_oid(lua_State *L, int spos, git_oid *oid)
{
	const char *oid_s = luaL_checkstring(L, spos);
	int ret = git_oid_fromstr(oid, oid_s);
	if (ret != 0)
		return push_git2_error(L, ret);
	return 0;
}

/**
 * Calculate the merge-base of a pair of OIDs.
 *
 * @function merge_base
 * @tparam llrepo repository The repository to calculate the merge-base within.
 * @tparam string left The left-side of the merge as an OID.
 * @tparam string right The right-side of the merge as an OID.
 * @treturn string The OID of the merge-base if available, or nil on error.
 * @treturn string The error message if the merge-base is nil.
 */

static int L_merge_base(lua_State *L)
{
	git_repository *repo = to_repo(L, 1);
	git_oid left, right, out;
	int ret;	
	if (parse_oid(L, 2, &left) != 0)
		return 2;
	if (parse_oid(L, 3, &right) != 0)
		return 2;
	ret = git_merge_base(&out, repo, &left, &right);
	if (ret == 0)
		return format_oid(L, &out);
	if (ret == GIT_ENOTFOUND) {
		lua_pushnil(L);
		lua_pushliteral(L, "ENOTFOUND");
		return 2;
	}
	return push_git2_error(L, ret);
}

/**
 * Set a symbolic reference's referent.
 *
 * @function set_symbolic_ref
 * @tparam llrepo repository The repository to set the reference within.
 * @tparam string reference The reference to set.
 * @tparam string referent The reference name to act as referent for reference.
 * @treturn boolean On success, true, otherwise nil.
 * @treturn string On failure, the error message.
 */

static int L_set_symbolic_ref(lua_State *L)
{
	git_repository *repo = to_repo(L, 1);
	git_reference *ref;
	int ret;
	if ((ret = git_reference_symbolic_create(&ref, repo,
						 luaL_checkstring(L, 2),
						 luaL_checkstring(L, 3),
						 1, NULL
#if LIBGIT2_VER_MINOR < 23
						 , NULL
#endif
		     )) != 0) {
		return push_git2_error(L, ret);
	}
	git_reference_free(ref);
	lua_pushboolean(L, 1);
	return 1;
}

static int L_gc_odb_object(lua_State *L)
{
	git_odb_object **obj = lua_touserdata(L, 1);
	if (*obj != NULL)
		git_odb_object_free(*obj);
	*obj = NULL;
	return 0;
}

/**
 * Retrieve a generic object from git.
 *
 * @function get_object
 * @tparam llrepo repository The repository to retrieve the object from.
 * @tparam string oid The object to return (as a SHA1)
 * @treturn gitobject The git object whose OID was provided (or nil)
 * @treturn string The error message if the returned git object was nil
 */

static int L_get_object(lua_State *L)
{
	git_odb *odb = to_odb(L, 1);
	git_odb_object **obj = lua_newuserdata(L, sizeof(*obj));
	git_oid oid;
	int ret;
	if (parse_oid(L, 2, &oid) != 0)
		return 2;
	if ((ret = git_odb_read(obj, odb, &oid)) != 0)
		return push_git2_error(L, ret);
	lua_pushvalue(L, lua_upvalueindex(1));
	lua_setmetatable(L, -2);
	return 1;
}

/**
 * Retrieve the size of a git object.
 *
 * @function get_object_size
 * @tparam gitobject obj The object whose size you wish to query
 * @treturn number The size of the object in bytes
 */

static int L_get_object_size(lua_State *L)
{
	git_odb_object **obj = lua_touserdata(L, 1);
	lua_pushnumber(L, git_odb_object_size(*obj));
	return 1;
}

static void push_object_type_str(lua_State *L, git_otype ty)
{
	switch(ty) {
	case GIT_OBJ_ANY:
		lua_pushliteral(L, "any");
		break;
	case GIT_OBJ_BAD:
		lua_pushliteral(L, "bad");
		break;
	case GIT_OBJ__EXT1:
		lua_pushliteral(L, "reserved");
		break;
	case GIT_OBJ_COMMIT:
		lua_pushliteral(L, "commit");
		break;
	case GIT_OBJ_TREE:
		lua_pushliteral(L, "tree");
		break;
	case GIT_OBJ_BLOB:
		lua_pushliteral(L, "blob");
		break;
	case GIT_OBJ_TAG:
		lua_pushliteral(L, "tag");
		break;
	case GIT_OBJ__EXT2:
		lua_pushliteral(L, "reserved");
		break;
	case GIT_OBJ_OFS_DELTA:
		lua_pushliteral(L, "delta");
		break;
	case GIT_OBJ_REF_DELTA:
		lua_pushliteral(L, "refdelta");
		break;
	default:
		lua_pushliteral(L, "unknown");
		break;
	}
}


/**
 * Retrieve a git object's type
 *
 * @function get_object_type
 * @tparam gitobject obj The object whose type you wish to retrieve.
 * @treturn string The type of the object provided.
 */
static int L_get_object_type(lua_State *L)
{
	git_odb_object **obj = lua_touserdata(L, 1);
	push_object_type_str(L, git_odb_object_type(*obj));
	return 1;
}

/**
 * Retrieve the raw content of an object
 *
 * @function get_object_raw
 * @tparam gitobject obj The object whose content you wish to retrieve.
 * @treturn string The raw content of the object as a string.
 */

static int L_get_object_raw(lua_State *L)
{
	git_odb_object **obj = lua_touserdata(L, 1);
	lua_pushlstring(L, git_odb_object_data(*obj),
			git_odb_object_size(*obj));
	return 1;
}

/**
 * Retrieve a tree's content as a table
 *
 * The returned table is a numerically indexed table of entries.  Each entry
 * is a table with `name` (string) `sha` (string) and `perms` (number) entries.
 *
 * @function get_tree_table
 * @tparam llrepo repository The repository to query for the tree's content.
 * @tparam string tree The OID of the tree object to retrieve
 * @treturn table The tree as a table, or nil on error
 * @treturn string The error message if the returned tree table is nil
 */

static int L_get_tree_table(lua_State *L)
{
	git_repository *repo = to_repo(L, 1);
	git_oid oid;
	git_tree *tree;
	size_t ent;
	int ret;
	if (parse_oid(L, 2, &oid) != 0)
		return 2;
	if ((ret = git_tree_lookup(&tree, repo, &oid)) != 0)
		return push_git2_error(L, ret);
	lua_newtable(L);
	for (ent = 0; ent < git_tree_entrycount(tree); ++ent) {
		const git_tree_entry *tree_ent =
			git_tree_entry_byindex(tree, ent);
		lua_pushnumber(L, ent+1);
		lua_newtable(L);
		lua_pushliteral(L, "name");
		lua_pushstring(L, git_tree_entry_name(tree_ent));
		lua_settable(L, -3);
		lua_pushliteral(L, "sha");
		format_oid(L, git_tree_entry_id(tree_ent));
		lua_settable(L, -3);
		lua_pushliteral(L, "perms");
		lua_pushnumber(L, git_tree_entry_filemode_raw(tree_ent));
		lua_settable(L, -3);
		lua_settable(L, -3);
	}
	
	git_tree_free(tree);
	return 1;
}

/**
 * Lookup the SHA1 pointed at by a sha1ish.
 *
 * @function revparse_single
 * @tparam llrepo repository The repository to look up the reference in.
 * @tparam string ref The sha1ish to look up.
 * @treturn string The OID (SHA1) of the referent (or nil on error).
 * @treturn string The error message if the OID is nil.
 */

static int L_revparse_single(lua_State *L)
{
	git_repository *repo = to_repo(L, 1);
	const char *sha1ish = luaL_checkstring(L, 2);
	git_object *obj;
	int ret;
	if ((ret = git_revparse_single(&obj, repo, sha1ish)) != 0) {
		return push_git2_error(L, ret);
	}
	ret = format_oid(L, git_object_id(obj));
        git_object_free(obj);
        return ret;
}

/**
 * The version of libgit2 which this instance of gall was built against.
 *
 * When Gall is compiled, the version of libgit2 is baked into the C binding
 * to be checked against when run.
 *
 * @field LIBGIT2_VERSION
 */

int luaopen_gall_ll_git2(lua_State *L)
{
	lua_newtable(L);
	lua_pushliteral(L, "LIBGIT2_VERSION");
	lua_pushliteral(L, LIBGIT2_VERSION);
	lua_settable(L, -3);
	
	lua_pushliteral(L, "open_repo");
	lua_newtable(L);
	lua_pushliteral(L, "__gc");
	lua_pushcclosure(L, L_gc_repo, 0);
	lua_settable(L, -3);
	lua_pushcclosure(L, L_open_repo, 1);
	lua_settable(L, -3);

	lua_pushliteral(L, "get_object");
	lua_newtable(L);
	lua_pushliteral(L, "__gc");
	lua_pushcclosure(L, L_gc_odb_object, 0);
	lua_settable(L, -3);
	lua_pushcclosure(L, L_get_object, 1);
	lua_settable(L, -3);

#define BASIC_FUNC(FN)				\
	do {					\
		lua_pushliteral(L, #FN);	\
		lua_pushcclosure(L, L_##FN, 0);	\
		lua_settable(L, -3);		\
	} while (0)
	BASIC_FUNC(lookup_symbolic_ref);
	BASIC_FUNC(lookup_sha_from_ref);
	BASIC_FUNC(set_symbolic_ref);
	BASIC_FUNC(merge_base);
	BASIC_FUNC(get_object_size);
	BASIC_FUNC(get_object_type);
	BASIC_FUNC(get_object_raw);
	BASIC_FUNC(get_tree_table);
	BASIC_FUNC(revparse_single);

#if LIBGIT2_SOVERSION > 22
	git_libgit2_init();
#endif
	return 1;
}
