#include "clar_libgit2.h"
#include "posix.h"
#include "blob.h"
#include "filter.h"
#include "buf_text.h"
#include "git2/sys/filter.h"
#include "git2/sys/repository.h"

static git_repository *g_repo = NULL;

static git_filter *create_compress_filter(void);
static git_filter *compress_filter;

void test_filter_stream__initialize(void)
{
	compress_filter = create_compress_filter();

	cl_git_pass(git_filter_register("compress", compress_filter, 50));
	g_repo = cl_git_sandbox_init("empty_standard_repo");
}

void test_filter_stream__cleanup(void)
{
	cl_git_sandbox_cleanup();
	g_repo = NULL;

	git_filter_unregister("compress");
	git__free(compress_filter);
}

#define CHUNKSIZE 10240

struct compress_stream {
	git_writestream parent;
	git_writestream *next;
	git_filter_mode_t mode;
	char current;
	size_t current_chunk;
};

static int compress_stream_write__deflated(struct compress_stream *stream, const char *buffer, size_t len)
{
	size_t idx = 0;

	while (len > 0) {
		size_t chunkremain, chunksize;

		if (stream->current_chunk == 0)
			stream->current = buffer[idx];

		chunkremain = CHUNKSIZE - stream->current_chunk;
		chunksize = min(chunkremain, len);

		stream->current_chunk += chunksize;
		len -= chunksize;
		idx += chunksize;

		if (stream->current_chunk == CHUNKSIZE) {
			cl_git_pass(stream->next->write(stream->next, &stream->current, 1));
			stream->current_chunk = 0;
		}
	}

	return 0;
}

static int compress_stream_write__inflated(struct compress_stream *stream, const char *buffer, size_t len)
{
	char inflated[CHUNKSIZE];
	size_t i, j;

	for (i = 0; i < len; i++) {
		for (j = 0; j < CHUNKSIZE; j++)
			inflated[j] = buffer[i];

		cl_git_pass(stream->next->write(stream->next, inflated, CHUNKSIZE));
	}

	return 0;
}

static int compress_stream_write(git_writestream *s, const char *buffer, size_t len)
{
	struct compress_stream *stream = (struct compress_stream *)s;

	return (stream->mode == GIT_FILTER_TO_ODB) ?
		compress_stream_write__deflated(stream, buffer, len) :
		compress_stream_write__inflated(stream, buffer, len);
}

static int compress_stream_close(git_writestream *s)
{
	struct compress_stream *stream = (struct compress_stream *)s;
	cl_assert_equal_i(0, stream->current_chunk);
	stream->next->close(stream->next);
	return 0;
}

static void compress_stream_free(git_writestream *stream)
{
	git__free(stream);
}

static int compress_filter_stream_init(
	git_writestream **out,
	git_filter *self,
	void **payload,
	const git_filter_source *src,
	git_writestream *next)
{
	struct compress_stream *stream = git__calloc(1, sizeof(struct compress_stream));
	cl_assert(stream);

	GIT_UNUSED(self);
	GIT_UNUSED(payload);

	stream->parent.write = compress_stream_write;
	stream->parent.close = compress_stream_close;
	stream->parent.free = compress_stream_free;
	stream->next = next;
	stream->mode = git_filter_source_mode(src);

	*out = (git_writestream *)stream;
	return 0;
}

git_filter *create_compress_filter(void)
{
	git_filter *filter = git__calloc(1, sizeof(git_filter));
	cl_assert(filter);

	filter->version = GIT_FILTER_VERSION;
	filter->attributes = "+compress";
	filter->stream = compress_filter_stream_init;

	return filter;
}

static void writefile(const char *filename, size_t numchunks)
{
	git_buf path = GIT_BUF_INIT;
	char buf[CHUNKSIZE];
	size_t i = 0, j = 0;
	int fd;

	cl_git_pass(git_buf_joinpath(&path, "empty_standard_repo", filename));

	fd = p_open(path.ptr, O_RDWR|O_CREAT, 0666);
	cl_assert(fd >= 0);

	for (i = 0; i < numchunks; i++) {
		for (j = 0; j < CHUNKSIZE; j++) {
			buf[j] = i % 256;
		}

		cl_git_pass(p_write(fd, buf, CHUNKSIZE));
	}
	p_close(fd);

	git_buf_free(&path);
}

static void test_stream(size_t numchunks)
{
	git_index *index;
	const git_index_entry *entry;
	git_blob *blob;
	struct stat st;
	git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;

	checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE;

	cl_git_mkfile(
		"empty_standard_repo/.gitattributes",
		"* compress\n");

	/* write a file to disk */
	writefile("streamed_file", numchunks);

	/* place it in the index */
	cl_git_pass(git_repository_index(&index, g_repo));
	cl_git_pass(git_index_add_bypath(index, "streamed_file"));
	cl_git_pass(git_index_write(index));

	/* ensure it was appropriately compressed */
	cl_assert(entry = git_index_get_bypath(index, "streamed_file", 0));

	cl_git_pass(git_blob_lookup(&blob, g_repo, &entry->id));
	cl_assert_equal_i(numchunks, git_blob_rawsize(blob));

	/* check the file back out */
	cl_must_pass(p_unlink("empty_standard_repo/streamed_file"));
	cl_git_pass(git_checkout_index(g_repo, index, &checkout_opts));

	/* ensure it was decompressed */
	cl_must_pass(p_stat("empty_standard_repo/streamed_file", &st));
	cl_assert_equal_sz((numchunks * CHUNKSIZE), st.st_size);

	git_index_free(index);
	git_blob_free(blob);
}

/* write a 50KB file through the "compression" stream */
void test_filter_stream__smallfile(void)
{
	test_stream(5);
}

/* optionally write a 500 MB file through the compression stream */
void test_filter_stream__bigfile(void)
{
	if (!cl_getenv("GITTEST_INVASIVE_FS_SIZE"))
		cl_skip();

	test_stream(51200);
}
