/*
 * Licensed under a two-clause BSD-style license.
 * See LICENSE for details.
 */

#include "git-compat-util.h"

#include "string_pool.h"
#include "repo_tree.h"
#include "obj_pool.h"
#include "fast_export.h"

#include "trp.h"

struct repo_dirent {
	uint32_t name_offset;
	struct trp_node children;
	uint32_t mode;
	uint32_t content_offset;
};

struct repo_dir {
	struct trp_root entries;
};

struct repo_commit {
	uint32_t root_dir_offset;
};

/* Memory pools for commit, dir and dirent */
obj_pool_gen(commit, struct repo_commit, 4096)
obj_pool_gen(dir, struct repo_dir, 4096)
obj_pool_gen(dent, struct repo_dirent, 4096)

static uint32_t active_commit;
static uint32_t mark;

static int repo_dirent_name_cmp(const void *a, const void *b);

/* Treap for directory entries */
trp_gen(static, dent_, struct repo_dirent, children, dent, repo_dirent_name_cmp)

uint32_t next_blob_mark(void)
{
	return mark++;
}

static struct repo_dir *repo_commit_root_dir(struct repo_commit *commit)
{
	return dir_pointer(commit->root_dir_offset);
}

static struct repo_dirent *repo_first_dirent(struct repo_dir *dir)
{
	return dent_first(&dir->entries);
}

static int repo_dirent_name_cmp(const void *a, const void *b)
{
	const struct repo_dirent *dent1 = a, *dent2 = b;
	uint32_t a_offset = dent1->name_offset;
	uint32_t b_offset = dent2->name_offset;
	return (a_offset > b_offset) - (a_offset < b_offset);
}

static int repo_dirent_is_dir(struct repo_dirent *dent)
{
	return dent != NULL && dent->mode == REPO_MODE_DIR;
}

static struct repo_dir *repo_dir_from_dirent(struct repo_dirent *dent)
{
	if (!repo_dirent_is_dir(dent))
		return NULL;
	return dir_pointer(dent->content_offset);
}

static struct repo_dir *repo_clone_dir(struct repo_dir *orig_dir)
{
	uint32_t orig_o, new_o;
	orig_o = dir_offset(orig_dir);
	if (orig_o >= dir_pool.committed)
		return orig_dir;
	new_o = dir_alloc(1);
	orig_dir = dir_pointer(orig_o);
	*dir_pointer(new_o) = *orig_dir;
	return dir_pointer(new_o);
}

static struct repo_dirent *repo_read_dirent(uint32_t revision,
					    const uint32_t *path)
{
	uint32_t name = 0;
	struct repo_dirent *key = dent_pointer(dent_alloc(1));
	struct repo_dir *dir = NULL;
	struct repo_dirent *dent = NULL;
	dir = repo_commit_root_dir(commit_pointer(revision));
	while (~(name = *path++)) {
		key->name_offset = name;
		dent = dent_search(&dir->entries, key);
		if (dent == NULL || !repo_dirent_is_dir(dent))
			break;
		dir = repo_dir_from_dirent(dent);
	}
	dent_free(1);
	return dent;
}

static void repo_write_dirent(const uint32_t *path, uint32_t mode,
			      uint32_t content_offset, uint32_t del)
{
	uint32_t name, revision, dir_o = ~0, parent_dir_o = ~0;
	struct repo_dir *dir;
	struct repo_dirent *key;
	struct repo_dirent *dent = NULL;
	revision = active_commit;
	dir = repo_commit_root_dir(commit_pointer(revision));
	dir = repo_clone_dir(dir);
	commit_pointer(revision)->root_dir_offset = dir_offset(dir);
	while (~(name = *path++)) {
		parent_dir_o = dir_offset(dir);

		key = dent_pointer(dent_alloc(1));
		key->name_offset = name;

		dent = dent_search(&dir->entries, key);
		if (dent == NULL)
			dent = key;
		else
			dent_free(1);

		if (dent == key) {
			dent->mode = REPO_MODE_DIR;
			dent->content_offset = 0;
			dent = dent_insert(&dir->entries, dent);
		}

		if (dent_offset(dent) < dent_pool.committed) {
			dir_o = repo_dirent_is_dir(dent) ?
					dent->content_offset : ~0;
			dent_remove(&dir->entries, dent);
			dent = dent_pointer(dent_alloc(1));
			dent->name_offset = name;
			dent->mode = REPO_MODE_DIR;
			dent->content_offset = dir_o;
			dent = dent_insert(&dir->entries, dent);
		}

		dir = repo_dir_from_dirent(dent);
		dir = repo_clone_dir(dir);
		dent->content_offset = dir_offset(dir);
	}
	if (dent == NULL)
		return;
	dent->mode = mode;
	dent->content_offset = content_offset;
	if (del && ~parent_dir_o)
		dent_remove(&dir_pointer(parent_dir_o)->entries, dent);
}

uint32_t repo_read_path(const uint32_t *path)
{
	uint32_t content_offset = 0;
	struct repo_dirent *dent = repo_read_dirent(active_commit, path);
	if (dent != NULL)
		content_offset = dent->content_offset;
	return content_offset;
}

uint32_t repo_read_mode(const uint32_t *path)
{
	struct repo_dirent *dent = repo_read_dirent(active_commit, path);
	if (dent == NULL)
		die("invalid dump: path to be modified is missing");
	return dent->mode;
}

void repo_copy(uint32_t revision, const uint32_t *src, const uint32_t *dst)
{
	uint32_t mode = 0, content_offset = 0;
	struct repo_dirent *src_dent;
	src_dent = repo_read_dirent(revision, src);
	if (src_dent != NULL) {
		mode = src_dent->mode;
		content_offset = src_dent->content_offset;
		repo_write_dirent(dst, mode, content_offset, 0);
	}
}

void repo_add(uint32_t *path, uint32_t mode, uint32_t blob_mark)
{
	repo_write_dirent(path, mode, blob_mark, 0);
}

void repo_delete(uint32_t *path)
{
	repo_write_dirent(path, 0, 0, 1);
}

static void repo_git_add_r(uint32_t depth, uint32_t *path, struct repo_dir *dir);

static void repo_git_add(uint32_t depth, uint32_t *path, struct repo_dirent *dent)
{
	if (repo_dirent_is_dir(dent))
		repo_git_add_r(depth, path, repo_dir_from_dirent(dent));
	else
		fast_export_modify(depth, path,
				   dent->mode, dent->content_offset);
}

static void repo_git_add_r(uint32_t depth, uint32_t *path, struct repo_dir *dir)
{
	struct repo_dirent *de = repo_first_dirent(dir);
	while (de) {
		path[depth] = de->name_offset;
		repo_git_add(depth + 1, path, de);
		de = dent_next(&dir->entries, de);
	}
}

static void repo_diff_r(uint32_t depth, uint32_t *path, struct repo_dir *dir1,
			struct repo_dir *dir2)
{
	struct repo_dirent *de1, *de2;
	de1 = repo_first_dirent(dir1);
	de2 = repo_first_dirent(dir2);

	while (de1 && de2) {
		if (de1->name_offset < de2->name_offset) {
			path[depth] = de1->name_offset;
			fast_export_delete(depth + 1, path);
			de1 = dent_next(&dir1->entries, de1);
			continue;
		}
		if (de1->name_offset > de2->name_offset) {
			path[depth] = de2->name_offset;
			repo_git_add(depth + 1, path, de2);
			de2 = dent_next(&dir2->entries, de2);
			continue;
		}
		path[depth] = de1->name_offset;

		if (de1->mode == de2->mode &&
		    de1->content_offset == de2->content_offset) {
			; /* No change. */
		} else if (repo_dirent_is_dir(de1) && repo_dirent_is_dir(de2)) {
			repo_diff_r(depth + 1, path,
				    repo_dir_from_dirent(de1),
				    repo_dir_from_dirent(de2));
		} else if (!repo_dirent_is_dir(de1) && !repo_dirent_is_dir(de2)) {
			repo_git_add(depth + 1, path, de2);
		} else {
			fast_export_delete(depth + 1, path);
			repo_git_add(depth + 1, path, de2);
		}
		de1 = dent_next(&dir1->entries, de1);
		de2 = dent_next(&dir2->entries, de2);
	}
	while (de1) {
		path[depth] = de1->name_offset;
		fast_export_delete(depth + 1, path);
		de1 = dent_next(&dir1->entries, de1);
	}
	while (de2) {
		path[depth] = de2->name_offset;
		repo_git_add(depth + 1, path, de2);
		de2 = dent_next(&dir2->entries, de2);
	}
}

static uint32_t path_stack[REPO_MAX_PATH_DEPTH];

void repo_diff(uint32_t r1, uint32_t r2)
{
	repo_diff_r(0,
		    path_stack,
		    repo_commit_root_dir(commit_pointer(r1)),
		    repo_commit_root_dir(commit_pointer(r2)));
}

void repo_commit(uint32_t revision, const char *author,
		const struct strbuf *log, const char *uuid, const char *url,
		unsigned long timestamp)
{
	fast_export_commit(revision, author, log, uuid, url, timestamp);
	dent_commit();
	dir_commit();
	active_commit = commit_alloc(1);
	commit_pointer(active_commit)->root_dir_offset =
		commit_pointer(active_commit - 1)->root_dir_offset;
}

static void mark_init(void)
{
	uint32_t i;
	mark = 0;
	for (i = 0; i < dent_pool.size; i++)
		if (!repo_dirent_is_dir(dent_pointer(i)) &&
		    dent_pointer(i)->content_offset > mark)
			mark = dent_pointer(i)->content_offset;
	mark++;
}

void repo_init(void)
{
	mark_init();
	if (commit_pool.size == 0) {
		/* Create empty tree for commit 0. */
		commit_alloc(1);
		commit_pointer(0)->root_dir_offset = dir_alloc(1);
		dir_pointer(0)->entries.trp_root = ~0;
		dir_commit();
	}
	/* Preallocate next commit, ready for changes. */
	active_commit = commit_alloc(1);
	commit_pointer(active_commit)->root_dir_offset =
		commit_pointer(active_commit - 1)->root_dir_offset;
}

void repo_reset(void)
{
	pool_reset();
	commit_reset();
	dir_reset();
	dent_reset();
}
