#include "StdAfx.h"

#include "fs/zip/ZipWriter.h"

extern "C"
{
#include "zip.h"
}

#include "fs/CRCStream.h"
#include "fs/ISource.h"
#include "fs/ISink.h"
#include "tl/narrow_cast.h"
#include <tl/finally.h>

namespace fs
{
//////////////////////////////////////////////////////////////////////////

namespace
{
void my_zip_fclose(zip_file* file)
{
	fs_zip_fclose(file);
}
void my_zip_close(zip* file)
{
	fs_zip_close(file);
}

zip_int64_t sourceCallback(void* userdata, void* data, zip_uint64_t len, zip_source_cmd_t cmd)
{
	ISource& source = *(ISource*)userdata;

	switch (cmd)
	{
	case ZIP_SOURCE_SUPPORTS:
		return ZIP_SOURCE_SUPPORTS_SEEKABLE;
	case ZIP_SOURCE_OPEN:
		return 0;
	case ZIP_SOURCE_CLOSE:
		return 0;
	case ZIP_SOURCE_READ:
		return source.read({ (uint8_t*)data, len });
	case ZIP_SOURCE_SEEK:
	{
		zip_error error;
		fs_zip_error_init(&error);
		tl::finally finally([&] { fs_zip_error_fini(&error); });
		const zip_source_args_seek* args = ZIP_SOURCE_GET_ARGS(zip_source_args_seek, data, len, &error);
		if (!args)
			return -1;
		if (args->whence == SEEK_SET)
			source.seekBeg(args->offset);
		else if (args->whence == SEEK_CUR)
			source.seekRel(args->offset);
		else
			return -1;
		return source.getLastError().has_value() ? -1 : 0;
	}
	case ZIP_SOURCE_TELL:
		return source.tell();
	case ZIP_SOURCE_STAT:
	{
		zip_stat_t* stat = (zip_stat_t*)data;
		fs_zip_stat_init(stat);
		stat->valid = ZIP_STAT_SIZE;
		stat->size = source.getSize();
		return sizeof(zip_stat_t);
	}
	}

	return 0;
}

zip_int64_t sinkCallback(void* userdata, void* data, zip_uint64_t len, zip_source_cmd_t cmd)
{
	ISink& sink = *(ISink*)userdata;

	switch (cmd)
	{
	case ZIP_SOURCE_SUPPORTS:
		return ZIP_SOURCE_SUPPORTS_WRITABLE;
	case ZIP_SOURCE_OPEN:
		return 0;
	case ZIP_SOURCE_BEGIN_WRITE:
	case ZIP_SOURCE_COMMIT_WRITE:
		return 0;
	case ZIP_SOURCE_WRITE:
	{
		return sink.write({ (uint8_t*)data, len });
	}
	case ZIP_SOURCE_SEEK:
	case ZIP_SOURCE_SEEK_WRITE:
	{
		zip_error error;
		fs_zip_error_init(&error);
		tl::finally finally([&] { fs_zip_error_fini(&error); });
		const zip_source_args_seek* args = ZIP_SOURCE_GET_ARGS(zip_source_args_seek, data, len, &error);
		if (!args)
		{
			return -1;
		}
		if (args->whence == SEEK_SET)
			sink.seekBeg(args->offset);
		else if (args->whence == SEEK_CUR)
			sink.seekRel(args->offset);
		else
			return -1;
		return sink.getLastError().has_value() ? -1 : 0;
	}
	case ZIP_SOURCE_TELL:
	case ZIP_SOURCE_TELL_WRITE:
		return sink.tell();
	case ZIP_SOURCE_STAT:
	{
		zip_stat_t* stat = (zip_stat_t*)data;
		fs_zip_stat_init(stat);
		stat->valid = ZIP_STAT_SIZE;
		stat->size = sink.getSize();
		return sizeof(zip_stat_t);
	}
	}

	return 0;
}
}

ZipWriter::ZipWriter(ISink& sink)
	: m_sink(sink)
{
	zip_error error;
	fs_zip_error_init(&error);
	tl::finally finally([&] { fs_zip_error_fini(&error); });
	zip_source* zipSink = fs_zip_source_function_create(sinkCallback, &m_sink, &error);

	zip* _zipArchive = fs_zip_open_from_source(zipSink, ZIP_CREATE | ZIP_TRUNCATE, &error);
	if (!_zipArchive)
	{
		const int ze = fs_zip_error_code_zip(&error);
		const int se = fs_zip_error_code_system(&error);
		zip_source_free(zipSink);
		return;
	}
	m_zipArchive = tl::unique_ref<zip>(_zipArchive, &my_zip_close);
}

ZipWriter::~ZipWriter()
{
	finish();
}


//////////////////////////////////////////////////////////////////////////

tl::result<void> ZipWriter::addBuffer(tl::string name, Compression compression, tl::span<const uint8_t> buffer)
{
	return addBuffer(std::move(name),
		{},
		{},
		0,
		compression, buffer);
}

tl::result<void> ZipWriter::addBuffer(tl::string name, time_t lastModificationTime, Compression compression, tl::span<const uint8_t> buffer)
{
	return addBuffer(std::move(name),
		{},
		{},
		lastModificationTime,
		compression, buffer);
}

//////////////////////////////////////////////////////////////////////////

tl::result<void> ZipWriter::addBuffer(tl::string name,
	tl::span<const uint8_t> comment,
	tl::span<const uint8_t> extraField,
	time_t lastModificationTime,
	Compression compression,
	tl::span<const uint8_t> buffer)
{
	if (!m_zipArchive)
		return tl::make_generic_error("Cannot create archive");

	if (name.size() > tl::numeric_limits<uint16_t>::max())
		return tl::make_generic_error("Name too long");

	if (comment.size() > tl::numeric_limits<uint16_t>::max())
		return tl::make_generic_error("Comment too long");

	uint8_t* newBuffer = new uint8_t[buffer.size()];
	memcpy(newBuffer, buffer.data(), buffer.size());

	zip_source* zipSource = fs_zip_source_buffer(m_zipArchive.get(), newBuffer, buffer.size(), 1);

	auto result = _addSource(name, comment, extraField, lastModificationTime, compression, zipSource);
	if (result.has_error())
	{
		zip_source_free(zipSource);
		return tl::make_generic_error("{}", result.error());
	}

	fs_zip_set_file_compression(m_zipArchive.get(), result.value(), compression == 0 ? ZIP_CM_STORE : ZIP_CM_DEFLATE, compression);
	fs_zip_file_set_mtime(m_zipArchive.get(), result.value(), lastModificationTime, 0);
	return tl::success();
}

tl::result<void> ZipWriter::addFile(tl::string name, Compression compression, ISource& source)
{
	return addFile(std::move(name),
		{},
		{},
		0,
		compression, source);
}

tl::result<void> ZipWriter::addFile(tl::string name, time_t lastModificationTime, Compression compression, ISource& source)
{
	return addFile(std::move(name),
		{},
		{},
		lastModificationTime,
		compression, source);
}

//////////////////////////////////////////////////////////////////////////

tl::result<void> ZipWriter::addFile(tl::string name,
	tl::span<const uint8_t> comment,
	tl::span<const uint8_t> extraField,
	time_t lastModificationTime,
	Compression compression,
	ISource& source)
{
	if (!m_zipArchive)
		return tl::make_generic_error("Cannot create archive");

	if (name.size() > tl::numeric_limits<uint16_t>::max())
		return tl::make_generic_error("Name too long");

	if (comment.size() > tl::numeric_limits<uint16_t>::max())
		return tl::make_generic_error("Comment too long");

	zip_source* zipSource = fs_zip_source_function(m_zipArchive.get(), sourceCallback, &source);

	auto result = _addSource(name, comment, extraField, lastModificationTime, compression, zipSource);
	if (result.has_error())
	{
		zip_source_free(zipSource);
		return tl::make_generic_error("{}", result.error());
	}

	fs_zip_set_file_compression(m_zipArchive.get(), result.value(), compression == 0 ? ZIP_CM_STORE : ZIP_CM_DEFLATE, compression);
	fs_zip_file_set_mtime(m_zipArchive.get(), result.value(), lastModificationTime, 0);

	return tl::success();
}


tl::result<uint64_t> ZipWriter::_addSource(tl::string name,
	tl::span<const uint8_t> comment,
	tl::span<const uint8_t> extraField,
	time_t lastModificationTime,
	Compression compression,
	zip_source* source)
{
	const RelPath path = RelPath(name);
	//if (!path.empty())
	//{
	//	for (size_t i = 0; i < path.size() - 1; i++)
	//	{
	//		RelPath subPath = path.subpath(0, i);
	//		if (m_folders.find(subPath) == m_folders.end())
	//		{
	//			if (fs_zip_dir_add(m_zipArchive.get(), subPath.str<PosixSystem>().c_str(), ZIP_FL_ENC_UTF_8) < 0)
	//				return tl::make_generic_error("Error adding zip file: {}", fs_zip_strerror(m_zipArchive.get()));

	//			m_folders.emplace(subPath);
	//		}
	//	}
	//}

	const int64_t res = fs_zip_file_add(m_zipArchive.get(), name.c_str(), source, ZIP_FL_OVERWRITE | ZIP_FL_ENC_UTF_8);
	if (res < 0)
		return tl::make_generic_error("Error adding zip file: {}", fs_zip_strerror(m_zipArchive.get()));

	return static_cast<uint64_t>(res);
}

//////////////////////////////////////////////////////////////////////////

void ZipWriter::finish()
{
	m_zipArchive = {};
}


//////////////////////////////////////////////////////////////////////////
}
