#include "Kyty/Core/Compression.h"

#include "Kyty/Core/Common.h"
#include "Kyty/Core/DbgAssert.h"
#include "Kyty/Core/File.h"
#include "Kyty/Core/MemoryAlloc.h"
#include "Kyty/Core/SafeDelete.h"
#include "Kyty/Sys/SysTimer.h"

extern "C" {
#include "LzmaDec.h"
#include "LzmaEnc.h"
}

// IWYU pragma: no_include "7zTypes.h"

#include "miniz.h"

//#if KYTY_COMPILER == KYTY_COMPILER_MSVC
//#define MINIZ_NO_STDIO
//#define MINIZ_NO_MALLOC
//#define MZ_ASSERT(x) ASSERT(x)
////#pragma GCC diagnostic push
////#pragma GCC diagnostic ignored "-Wunused-value"
//#include "miniz.c.h"
////#pragma GCC diagnostic pop
//#else
//#define MINIZ_NO_STDIO
//#define MINIZ_NO_MALLOC
//#define MZ_ASSERT(x) ASSERT(x)
//#pragma GCC diagnostic push
//#pragma GCC diagnostic ignored "-Wunused-value"
//#if KYTY_COMPILER == KYTY_COMPILER_MINGW
//#pragma GCC diagnostic ignored "-Wmisleading-indentation"
//#endif
////#pragma GCC diagnostic ignored "-Wenum-compare"
//#include "miniz.c.h"
//#pragma GCC diagnostic pop
//#endif

#include "zstd.h"

namespace Kyty::Core {

namespace LzmaImpl {

struct InStream
{
	ISeqInStream t {};
	Core::File   mem_file;

	void Init(const uint8_t* buf, uint32_t size);
	void Close();
};

struct OutStream
{
	ISeqOutStream    t {};
	Core::ByteBuffer buf;

	void Init(uint32_t initial_size);
};

SRes Read(void* p, void* buf, size_t* size)
{
	SRes  result = SZ_OK;
	auto* s      = static_cast<InStream*>(p);

	EXIT_IF(!p);
	EXIT_IF(!buf);
	EXIT_IF(!size);

	uint64_t s64 = *size;

	EXIT_IF((s64 >> 32u) != 0);

	uint32_t br = 0;
	s->mem_file.Read(buf, s64, &br);

	//	if (br != s64)
	//	{
	//		result = SZ_ERROR_READ;
	//	}

	*size = br;

	return result;
}

size_t Write(void* p, const void* buf, size_t size)
{
	auto* s = static_cast<OutStream*>(p);

	EXIT_IF(!p);
	EXIT_IF(!buf);
	EXIT_IF(size == 0);

	uint64_t s64 = size;

	EXIT_IF((s64 >> 32u) != 0);

	s->buf.Add(static_cast<const Byte*>(buf), s64);

	return s64;
}

SRes Progress(void* /*p*/, UInt64 /*inSize*/, UInt64 /*outSize*/)
{
	return SZ_OK;
}

void* Alloc(void* /*p*/, size_t size)
{
	return Core::mem_alloc(size);
}

void Free(void* /*p*/, void* address)
{
	Core::mem_free(address);
}

void InStream::Init(const uint8_t* buf, uint32_t size)
{
	this->t.Read = Read;
	// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
	this->mem_file.OpenInMem(const_cast<uint8_t*>(buf), size);
}

void InStream::Close()
{
	this->mem_file.Close();
}

void OutStream::Init(uint32_t initial_size)
{
	this->t.Write = Write;
	this->buf.Expand(initial_size);
}

static ICompressProgress g_progress_callback = {&LzmaImpl::Progress};
static ISzAlloc          g_alloc_lzma        = {&LzmaImpl::Alloc, &LzmaImpl::Free};

} // namespace LzmaImpl

namespace ZipImpl {
void* Alloc(void* /*opaque*/, size_t items, size_t size)
{
	return Core::mem_alloc(items * size);
}

void Free(void* /*opaque*/, void* address)
{
	Core::mem_free(address);
}

void* Realloc(void* /*opaque*/, void* address, size_t items, size_t size)
{
	return Core::mem_realloc(address, items * size);
}

size_t Read(void* opaque, mz_uint64 file_ofs, void* buf, size_t n)
{
	File* f = static_cast<File*>(opaque);
	f->Seek(file_ofs);
	uint32_t b = 0;
	EXIT_IF(sizeof(size_t) > 4 && (uint64_t(n) >> 32u) > 0);
	auto nn = static_cast<uint32_t>(n);
	f->Read(buf, nn, &b);
	return b;
}

size_t Write(void* opaque, mz_uint64 file_ofs, const void* buf, size_t n)
{
	File* f = static_cast<File*>(opaque);
	f->Seek(file_ofs);
	uint32_t b = 0;
	EXIT_IF(sizeof(size_t) > 4 && (uint64_t(n) >> 32u) > 0);
	auto nn = static_cast<uint32_t>(n);
	f->Write(buf, nn, &b);
	return b;
}

} // namespace ZipImpl

constexpr uint32_t HASH_LOG  = 12u;
constexpr uint32_t HASH_SIZE = (1u << HASH_LOG);
constexpr uint32_t HASH_MASK = (HASH_SIZE - 1u);

static void UPDATE_HASH(uint32_t* v, const uint8_t* p)
{
	(*v) = *(reinterpret_cast<const uint16_t*>(p));
	(*v) ^= *(reinterpret_cast<const uint16_t*>((p) + 1u)) ^ ((*v) >> (16u - HASH_LOG));
}

constexpr int32_t MAX_COPY     = 32;
constexpr int32_t MAX_LEN      = 264; /* 256 + 8 */
constexpr int32_t MAX_DISTANCE = 8192;

// NOLINTNEXTLINE(readability-function-cognitive-complexity)
static uint32_t lzf_calc_compressed_size(const void* input, uint32_t length)
{
	const auto*    ip       = static_cast<const uint8_t*>(input);
	const uint8_t* ip_limit = ip + length - MAX_COPY - 4;
	uint32_t       opl      = 0;

	const uint8_t*  htab[HASH_SIZE];
	const uint8_t** hslot = nullptr;
	uint32_t        hval  = 0;

	const uint8_t* ref  = nullptr;
	int32_t        copy = 0;
	int32_t        len  = 0;
	// int32_t distance;
	const uint8_t* anchor = nullptr;

	for (hslot = htab; hslot < htab + HASH_SIZE; hslot++)
	{
		*hslot = ip;
	}

	copy = 0;
	opl++;

	while (ip < ip_limit)
	{
		UPDATE_HASH(&hval, ip);
		hslot = htab + (hval & HASH_MASK);
		ref   = /*(uint8_t*)*/ *hslot;

		*hslot = ip;

		if ((ip == ref) || (*(reinterpret_cast<const uint16_t*>(ref)) != *(reinterpret_cast<const uint16_t*>(ip))) || (ref[2] != ip[2]) ||
		    ((ip - ref) >= MAX_DISTANCE))
		{
			ip++;
			opl++;
			copy++;
			if (copy >= MAX_COPY)
			{
				copy = 0;
				opl++;
			}
			continue;
		}

		anchor = /*(uint8_t*)*/ ip;
		len    = 3;
		ref += 3;
		ip += 3;

		if (ip < ip_limit - MAX_LEN)
		{
			while (len < MAX_LEN - 8)
			{
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				len += 8;
			}
			ip--;
		}
		len = static_cast<int32_t>(ip - anchor);

		ip = anchor + len;

		if (copy != 0)
		{
			// anchor = anchor - copy - 1;
			copy = 0;
		} else
		{
			opl--;
		}

		len -= 2;

		// distance--;

		if (len < 7)
		{
			opl++;
		} else
		{
			opl += 2;
		}

		opl += 2;

		--ip;
		UPDATE_HASH(&hval, ip);
		htab[hval & HASH_MASK] = ip;
		ip++;
	}

	ip_limit = static_cast<const uint8_t*>(input) + length;
	while (ip < ip_limit)
	{
		ip++;
		opl++;
		copy++;
		if (copy == MAX_COPY)
		{
			copy = 0;
			opl++;
		}
	}

	if (copy == 0)
	{
		opl--;
	}

	return opl;
}

// NOLINTNEXTLINE(readability-function-cognitive-complexity)
static uint32_t lzf_compress(const void* input, uint32_t length, void* output)
{
	const auto*    ip       = static_cast<const uint8_t*>(input);
	const uint8_t* ip_limit = ip + length - MAX_COPY - 4;
	auto*          op       = static_cast<uint8_t*>(output);

	const uint8_t*  htab[HASH_SIZE];
	const uint8_t** hslot = nullptr;
	uint32_t        hval  = 0;

	const uint8_t* ref      = nullptr;
	int32_t        copy     = 0;
	int32_t        len      = 0;
	int32_t        distance = 0;
	const uint8_t* anchor   = nullptr;

	for (hslot = htab; hslot < htab + HASH_SIZE; hslot++)
	{
		*hslot = ip;
	}

	copy  = 0;
	*op++ = MAX_COPY - 1;

	while (ip < ip_limit)
	{
		UPDATE_HASH(&hval, ip);
		hslot = htab + (hval & HASH_MASK);
		ref   = /*(uint8_t*)*/ *hslot;

		*hslot = ip;

		distance = static_cast<int32_t>(ip - ref);

		if ((ip == ref) || (*(reinterpret_cast<const uint16_t*>(ref)) != *(reinterpret_cast<const uint16_t*>(ip))) || (ref[2] != ip[2]) ||
		    (distance >= MAX_DISTANCE))
		{
			*op++ = *ip++;
			copy++;
			if (copy >= MAX_COPY)
			{
				copy  = 0;
				*op++ = MAX_COPY - 1;
			}
			continue;
		}

		anchor = /*(uint8_t*)*/ ip;
		len    = 3;
		ref += 3;
		ip += 3;

		if (ip < ip_limit - MAX_LEN)
		{
			while (len < MAX_LEN - 8)
			{
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				if (*ref++ != *ip++)
				{
					break;
				}
				len += 8;
			}
			ip--;
		}
		len = static_cast<int32_t>(ip - anchor);

		ip = anchor + len;

		if (copy != 0)
		{
			// anchor = anchor - copy - 1;
			*(op - copy - 1) = copy - 1;
			copy             = 0;
		} else
		{
			op--;
		}

		len -= 2;

		distance--;

		if (len < 7)
		{
			*op++ = (static_cast<uint32_t>(len) << 5u) + (static_cast<uint32_t>(distance) >> 8u);
		} else
		{
			*op++ = (7u << 5u) + (static_cast<uint32_t>(distance) >> 8u);
			*op++ = len - 7;
		}

		*op++ = (static_cast<uint32_t>(distance) & 255u);

		*op++ = MAX_COPY - 1;

		--ip;
		UPDATE_HASH(&hval, ip);
		htab[hval & HASH_MASK] = ip;
		ip++;
	}

	ip_limit = static_cast<const uint8_t*>(input) + length;
	while (ip < ip_limit)
	{
		*op++ = *ip++;
		copy++;
		if (copy == MAX_COPY)
		{
			copy  = 0;
			*op++ = MAX_COPY - 1;
		}
	}

	if (copy != 0)
	{
		*(op - copy - 1) = copy - 1;
	} else
	{
		op--;
	}

	return op - static_cast<uint8_t*>(output);
}

static uint32_t lzf_calc_decompressed_size(const void* input, uint32_t length)
{
	const auto*    ip       = static_cast<const uint8_t*>(input);
	const uint8_t* ip_limit = ip + length - 1;
	uint32_t       opl      = 0;

	while (ip < ip_limit)
	{
		uint32_t ctrl = (*ip) + 1;
		uint32_t len  = (*ip++) >> 5u;

		if (ctrl < 33)
		{
			if (ctrl != 0u)
			{
				ip++;
				opl++;
				ctrl--;

				if (ctrl != 0u)
				{
					ip++;
					opl++;
					ctrl--;

					if (ctrl != 0u)
					{
						ip++;
						opl++;
						ctrl--;

						for (; ctrl != 0u; ctrl--)
						{
							ip++;
							opl++;
						}
					}
				}
			}
		} else
		{
			len--;

			if (len == 7 - 1)
			{
				len += *ip++;
			}

			ip++;

			opl += 3;
			if (len != 0u)
			{
				for (; len != 0u; --len)
				{
					opl++;
				}
			}
		}
	}

	return opl;
}

static uint32_t lzf_decompress(const void* input, uint32_t length, void* output, uint32_t maxout)
{
	const auto*    ip       = static_cast<const uint8_t*>(input);
	const uint8_t* ip_limit = ip + length - 1;
	auto*          op       = static_cast<uint8_t*>(output);
	uint8_t*       op_limit = op + maxout;
	uint8_t*       ref      = nullptr;

	while (ip < ip_limit)
	{
		uint32_t ctrl = (*ip) + 1;
		uint32_t ofs  = ((*ip) & 31u) << 8u;
		uint32_t len  = (*ip++) >> 5u;

		if (ctrl < 33)
		{
			if (op + ctrl > op_limit)
			{
				return 0;
			}

			if (ctrl != 0u)
			{
				*op++ = *ip++;
				ctrl--;

				if (ctrl != 0u)
				{
					*op++ = *ip++;
					ctrl--;

					if (ctrl != 0u)
					{
						*op++ = *ip++;
						ctrl--;

						for (; ctrl != 0u; ctrl--)
						{
							*op++ = *ip++;
						}
					}
				}
			}
		} else
		{
			len--;
			ref = op - ofs;
			ref--;

			if (len == 7 - 1)
			{
				len += *ip++;
			}

			ref -= *ip++;

			if (op + len + 3 > op_limit)
			{
				return 0;
			}

			if (ref < static_cast<uint8_t*>(output))
			{
				return 0;
			}

			*op++ = *ref++;
			*op++ = *ref++;
			*op++ = *ref++;
			if (len != 0u)
			{
				for (; len != 0u; --len)
				{
					*op++ = *ref++;
				}
			}
		}
	}

	return op - static_cast<uint8_t*>(output);
}

ByteBuffer CompressLzma(const uint8_t* buf, uint32_t length)
{
	CLzmaEncHandle enc = LzmaEnc_Create(&LzmaImpl::g_alloc_lzma);

	EXIT_IF(!enc);
	EXIT_IF(!buf);
	EXIT_IF(length == 0);

	CLzmaEncProps props;
	LzmaEncProps_Init(&props);

	[[maybe_unused]] SRes res = LzmaEnc_SetProps(enc, &props);

	EXIT_IF(res != SZ_OK);

	LzmaImpl::InStream  in_stream {};
	LzmaImpl::OutStream out_stream {};

	EXIT_IF(reinterpret_cast<ISeqOutStream*>(&out_stream) != &out_stream.t);
	EXIT_IF(reinterpret_cast<ISeqInStream*>(&in_stream) != &in_stream.t);

	uint8_t header[LZMA_PROPS_SIZE + 8];

	SizeT size = LZMA_PROPS_SIZE;
	res        = LzmaEnc_WriteProperties(enc, header, &size);

	EXIT_IF(res != SZ_OK || size != LZMA_PROPS_SIZE);

	uint64_t length64 = length;

	std::memcpy(header + LZMA_PROPS_SIZE, &length64, 8);

	in_stream.Init(buf, length);
	out_stream.Init(0);

	out_stream.t.Write(&out_stream.t, header, LZMA_PROPS_SIZE + 8);

	res =
	    LzmaEnc_Encode(enc, &out_stream.t, &in_stream.t, &LzmaImpl::g_progress_callback, &LzmaImpl::g_alloc_lzma, &LzmaImpl::g_alloc_lzma);

	EXIT_IF(res != SZ_OK);

	LzmaEnc_Destroy(enc, &LzmaImpl::g_alloc_lzma, &LzmaImpl::g_alloc_lzma);

	in_stream.Close();

	return out_stream.buf;
}

ByteBuffer CompressLzma(const ByteBuffer& buf)
{
	return CompressLzma(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size());
}

ByteBuffer CompressLzma(const String& str)
{
	String::Utf8 utf8 = str.utf8_str();
	return CompressLzma(reinterpret_cast<const uint8_t*>(utf8.GetDataConst()), utf8.Size());
}

constexpr size_t LZMA_IN_BUF_SIZE  = (1u << 16u);
constexpr size_t LZMA_OUT_BUF_SIZE = (1u << 16u);

static SRes Decode2(CLzmaDec* state, ISeqOutStream* out_stream, ISeqInStream* in_stream, UInt64 unpack_size)
{
	uint8_t in_buf[LZMA_IN_BUF_SIZE];
	uint8_t out_buf[LZMA_OUT_BUF_SIZE];
	size_t  in_pos  = 0;
	size_t  in_size = 0;
	size_t  out_pos = 0;
	LzmaDec_Init(state);
	for (;;)
	{
		if (in_pos == in_size)
		{
			in_size = LZMA_IN_BUF_SIZE;
			RINOK(in_stream->Read(in_stream, in_buf, &in_size));
			in_pos = 0;
		}
		{
			SRes            res           = 0;
			SizeT           in_processed  = in_size - in_pos;
			SizeT           out_processed = LZMA_OUT_BUF_SIZE - out_pos;
			ELzmaFinishMode finish_mode   = LZMA_FINISH_ANY;
			ELzmaStatus     status        = LZMA_STATUS_NOT_SPECIFIED;
			if (out_processed > unpack_size)
			{
				out_processed = static_cast<SizeT>(unpack_size);
				finish_mode   = LZMA_FINISH_END;
			}

			res = LzmaDec_DecodeToBuf(state, out_buf + out_pos, &out_processed, in_buf + in_pos, &in_processed, finish_mode, &status);
			in_pos += in_processed;
			out_pos += out_processed;
			unpack_size -= out_processed;

			if (out_stream != nullptr)
			{
				if (out_stream->Write(out_stream, out_buf, out_pos) != out_pos)
				{
					return SZ_ERROR_WRITE;
				}
			}

			out_pos = 0;

			if (res != SZ_OK || (unpack_size == 0))
			{
				return res;
			}

			if (in_processed == 0 && out_processed == 0)
			{
				return SZ_ERROR_DATA;
			}
		}
	}

	return SZ_OK;
}

ByteBuffer DecompressLzma(const uint8_t* buf, uint32_t length)
{
	EXIT_IF(!buf);
	EXIT_IF(length == 0);

	CLzmaDec dec;
	LzmaDec_Construct(&dec);

	LzmaImpl::InStream  in_stream {};
	LzmaImpl::OutStream out_stream {};

	in_stream.Init(buf, length);

	EXIT_IF(reinterpret_cast<ISeqOutStream*>(&out_stream) != &out_stream.t);
	EXIT_IF(reinterpret_cast<ISeqInStream*>(&in_stream) != &in_stream.t);

	uint8_t header[LZMA_PROPS_SIZE + 8];

	size_t size = LZMA_PROPS_SIZE + 8;
	in_stream.t.Read(&in_stream.t, header, &size);

	EXIT_IF(size != LZMA_PROPS_SIZE + 8);

	uint64_t length64 = 0;

	std::memcpy(&length64, header + LZMA_PROPS_SIZE, 8);

	EXIT_IF((length64 >> 32u) != 0);

	out_stream.Init(length64);

	[[maybe_unused]] SRes res = LzmaDec_Allocate(&dec, header, LZMA_PROPS_SIZE, &LzmaImpl::g_alloc_lzma);

	EXIT_IF(res != SZ_OK);

	res = Decode2(&dec, &out_stream.t, &in_stream.t, length64);

	EXIT_IF(res != SZ_OK);

	LzmaDec_Free(&dec, &LzmaImpl::g_alloc_lzma);

	in_stream.Close();

	return out_stream.buf;
}

ByteBuffer DecompressLzma(const ByteBuffer& buf)
{
	return DecompressLzma(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size());
}

String DecompressLzmaStr(const uint8_t* buf, uint32_t length)
{
	ByteBuffer utf8 = DecompressLzma(buf, length);
	EXIT_IF(utf8.At(utf8.Size() - 1) != (Byte)0);
	return String::FromUtf8(reinterpret_cast<const char*>(utf8.GetDataConst()));
}

String DecompressLzmaStr(const ByteBuffer& buf)
{
	return DecompressLzmaStr(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size());
}

constexpr uint32_t ZIP_OUT_BUF_SIZE = (16 * 1024);

ByteBuffer CompressZip(const uint8_t* buf, uint32_t length, ZipCompressLevel level)
{
	int       status = 0;
	mz_stream stream;
	memset(&stream, 0, sizeof(stream));

	ByteBuffer out;
	uint8_t    temp_buf[ZIP_OUT_BUF_SIZE];

	stream.next_in  = buf;
	stream.avail_in = length;
	stream.zalloc   = ZipImpl::Alloc;
	stream.zfree    = ZipImpl::Free;

	status = mz_deflateInit(&stream, level);

	EXIT_IF(status != MZ_OK);

	if (status == MZ_OK)
	{
		for (;;)
		{
			stream.next_out  = temp_buf;
			stream.avail_out = ZIP_OUT_BUF_SIZE;

			status = mz_deflate(&stream, MZ_FINISH);

			EXIT_IF(status != MZ_OK && status != MZ_STREAM_END);

			out.Add(reinterpret_cast<Byte*>(temp_buf), ZIP_OUT_BUF_SIZE - stream.avail_out);

			if (status != MZ_OK)
			{
				break;
			}
		}
	}

	EXIT_IF(stream.total_out != out.Size());

	mz_deflateEnd(&stream);

	return out;
}

ByteBuffer CompressZip(const ByteBuffer& buf, ZipCompressLevel level)
{
	return CompressZip(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size(), level);
}

ByteBuffer CompressZip(const String& str, ZipCompressLevel level)
{
	String::Utf8 utf8 = str.utf8_str();
	return CompressZip(reinterpret_cast<const uint8_t*>(utf8.GetDataConst()), utf8.Size(), level);
}

ByteBuffer DecompressZip(const uint8_t* buf, uint32_t length)
{
	int       status = 0;
	mz_stream stream;
	memset(&stream, 0, sizeof(stream));

	ByteBuffer out;
	uint8_t    temp_buf[ZIP_OUT_BUF_SIZE];

	stream.next_in  = buf;
	stream.avail_in = length;
	stream.zalloc   = ZipImpl::Alloc;
	stream.zfree    = ZipImpl::Free;

	status = mz_inflateInit(&stream);

	EXIT_IF(status != MZ_OK);

	if (status == MZ_OK)
	{
		for (;;)
		{
			stream.next_out  = temp_buf;
			stream.avail_out = ZIP_OUT_BUF_SIZE;

			status = mz_inflate(&stream, MZ_NO_FLUSH);

			EXIT_IF(status != MZ_OK && status != MZ_STREAM_END);

			out.Add(reinterpret_cast<Byte*>(temp_buf), ZIP_OUT_BUF_SIZE - stream.avail_out);

			if (status != MZ_OK)
			{
				break;
			}
		}
	}

	EXIT_IF(stream.total_out != out.Size());

	mz_deflateEnd(&stream);

	return out;
}

ByteBuffer DecompressZip(const ByteBuffer& buf)
{
	return DecompressZip(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size());
}

String DecompressZipStr(const uint8_t* buf, uint32_t length)
{
	ByteBuffer utf8 = DecompressZip(buf, length);
	EXIT_IF(utf8.At(utf8.Size() - 1) != Byte(0));
	return String::FromUtf8(reinterpret_cast<const char*>(utf8.GetDataConst()));
}

String DecompressZipStr(const ByteBuffer& buf)
{
	return DecompressZipStr(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size());
}

ByteBuffer CompressLzf(const uint8_t* buf, uint32_t length)
{
	uint32_t   size = lzf_calc_compressed_size(buf, length);
	ByteBuffer b(size * 2);
	size = lzf_compress(buf, length, b.GetData());

	KYTY_MEM_CHECK(b.GetDataConst());

	EXIT_IF(size > b.Size());

	if (size != b.Size())
	{
		b.RemoveAt(size, b.Size() - size);
	}

	return b;
}

ByteBuffer CompressLzf(const ByteBuffer& buf)
{
	return CompressLzf(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size());
}

ByteBuffer CompressLzf(const String& str)
{
	String::Utf8 utf8 = str.utf8_str();
	return CompressLzf(reinterpret_cast<const uint8_t*>(utf8.GetDataConst()), utf8.Size());
}

ByteBuffer DecompressLzf(const uint8_t* buf, uint32_t length)
{
	[[maybe_unused]] uint32_t size = lzf_calc_decompressed_size(buf, length);
	ByteBuffer                b(size);
	size = lzf_decompress(buf, length, b.GetData(), size);

	EXIT_IF(size != b.Size());

	return b;
}

ByteBuffer DecompressLzf(const ByteBuffer& buf)
{
	return DecompressLzf(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size());
}

String DecompressLzfStr(const uint8_t* buf, uint32_t length)
{
	ByteBuffer utf8 = DecompressLzf(buf, length);
	EXIT_IF(utf8.At(utf8.Size() - 1) != (Byte)0);
	return String::FromUtf8(reinterpret_cast<const char*>(utf8.GetDataConst()));
}

String DecompressLzfStr(const ByteBuffer& buf)
{
	return DecompressLzfStr(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size());
}

ByteBuffer CompressZstd(const uint8_t* buf, uint32_t length, int level)
{
	size_t dst_size = ZSTD_compressBound(length) * 2;
	auto*  dst      = new uint8_t[dst_size];
	dst_size        = ZSTD_compress(dst, dst_size, buf, length, level);
	if (ZSTD_isError(dst_size) != 0u)
	{
		EXIT("ZSTD: %s\n", ZSTD_getErrorName(dst_size));
	}
	ByteBuffer ret(dst, static_cast<uint32_t>(dst_size));
	DeleteArray(dst);
	return ret;
}

ByteBuffer CompressZstd(const ByteBuffer& buf, int level)
{
	return CompressZstd(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size(), level);
}

ByteBuffer CompressZstd(const String& str, int level)
{
	String::Utf8 utf8 = str.utf8_str();
	return CompressZstd(reinterpret_cast<const uint8_t*>(utf8.GetDataConst()), utf8.Size(), level);
}

ByteBuffer DecompressZstd(const uint8_t* buf, uint32_t length)
{
	ByteBuffer       r;
	size_t           buff_out_size = ZSTD_DStreamOutSize();
	Byte*            buff_out      = new Byte[buff_out_size];
	ZSTD_DCtx* const dctx          = ZSTD_createDCtx();
	ZSTD_inBuffer    input         = {buf, length, 0};
	while (input.pos < input.size)
	{
		ZSTD_outBuffer output = {buff_out, buff_out_size, 0};
		auto           ret    = ZSTD_decompressStream(dctx, &output, &input);
		if (ZSTD_isError(ret) != 0u)
		{
			EXIT("ZSTD: %s\n", ZSTD_getErrorName(ret));
		}
		r.Add(buff_out, static_cast<uint32_t>(output.pos));
	}
	ZSTD_freeDCtx(dctx);
	DeleteArray(buff_out);
	return r;
}

ByteBuffer DecompressZstd(const ByteBuffer& buf)
{
	return DecompressZstd(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size());
}

String DecompressZstdStr(const uint8_t* buf, uint32_t length)
{
	ByteBuffer utf8 = DecompressZstd(buf, length);
	EXIT_IF(utf8.At(utf8.Size() - 1) != (Byte)0);
	return String::FromUtf8(reinterpret_cast<const char*>(utf8.GetDataConst()));
}

String DecompressZstdStr(const ByteBuffer& buf)
{
	return DecompressZstdStr(reinterpret_cast<const uint8_t*>(buf.GetDataConst()), buf.Size());
}

struct ZipPrivate
{
	mz_zip_archive zip;
};

ZipReader::~ZipReader()
{
	Close();
}

bool ZipReader::Open(const String& file_name)
{
	Close();

	m_p = new ZipPrivate;
	memset(&m_p->zip, 0, sizeof(m_p->zip));
	m_p->zip.m_pRead    = ZipImpl::Read;
	m_p->zip.m_pWrite   = ZipImpl::Write;
	m_p->zip.m_pAlloc   = ZipImpl::Alloc;
	m_p->zip.m_pFree    = ZipImpl::Free;
	m_p->zip.m_pRealloc = ZipImpl::Realloc;

	File* f = new File;
	f->Open(file_name, File::Mode::Read);

	m_p->zip.m_pIO_opaque = f;

	if (f->IsInvalid() || (mz_zip_reader_init(&m_p->zip, f->Size(), 0) == 0))
	{
		Close();

		return false;
	}

	return true;
}

bool ZipReader::Open(const ByteBuffer& buf)
{
	Close();

	m_p = new ZipPrivate;
	memset(&m_p->zip, 0, sizeof(m_p->zip));
	m_p->zip.m_pRead    = ZipImpl::Read;
	m_p->zip.m_pWrite   = ZipImpl::Write;
	m_p->zip.m_pAlloc   = ZipImpl::Alloc;
	m_p->zip.m_pFree    = ZipImpl::Free;
	m_p->zip.m_pRealloc = ZipImpl::Realloc;

	File* f = new File;
	f->OpenInMem(const_cast<ByteBuffer&>(buf)); // NOLINT(cppcoreguidelines-pro-type-const-cast)

	m_p->zip.m_pIO_opaque = f;

	if (mz_zip_reader_init(&m_p->zip, f->Size(), 0) == 0)
	{
		Close();

		return false;
	}

	return true;
}

bool ZipReader::Open(uint8_t* mem, uint32_t size)
{
	Close();

	m_p = new ZipPrivate;
	memset(&m_p->zip, 0, sizeof(m_p->zip));
	m_p->zip.m_pRead    = ZipImpl::Read;
	m_p->zip.m_pWrite   = ZipImpl::Write;
	m_p->zip.m_pAlloc   = ZipImpl::Alloc;
	m_p->zip.m_pFree    = ZipImpl::Free;
	m_p->zip.m_pRealloc = ZipImpl::Realloc;

	File* f = new File;
	f->OpenInMem(mem, size);

	m_p->zip.m_pIO_opaque = f;

	if (mz_zip_reader_init(&m_p->zip, f->Size(), 0) == 0)
	{
		Close();

		return false;
	}

	return true;
}

void ZipReader::Close()
{
	if (m_p != nullptr)
	{
		mz_zip_reader_end(&m_p->zip);
		File* f = static_cast<File*>(m_p->zip.m_pIO_opaque);
		f->Close();
		Delete(f);
		Delete(m_p);
		m_p = nullptr;
	}
}

int ZipReader::GetNumFiles()
{
	EXIT_IF(!m_p);

	return static_cast<int>(mz_zip_reader_get_num_files(&m_p->zip));
}

bool ZipReader::GetFileStat(int file_index, ZipFileStat* o)
{
	EXIT_IF(!m_p);

	mz_zip_archive_file_stat s;

	if (mz_zip_reader_file_stat(&m_p->zip, file_index, &s) != 0)
	{
		o->m_file_index = s.m_file_index;

		SysTimeStruct at = {0};
		sys_time_t_to_system(s.m_time, at);
		o->m_time = DateTime(Date(at.Year, at.Month, at.Day), Time(at.Hour, at.Minute, at.Second, at.Milliseconds));

		o->m_crc32       = s.m_crc32;
		o->m_comp_size   = s.m_comp_size;
		o->m_uncomp_size = s.m_uncomp_size;
		o->m_filename    = String::FromUtf8(s.m_filename);
		o->m_comment     = String::FromUtf8(s.m_comment);

		return true;
	}

	return false;
}

bool ZipReader::IsFileDirectory(int file_index)
{
	EXIT_IF(!m_p);

	return mz_zip_reader_is_file_a_directory(&m_p->zip, file_index) != 0;
}

bool ZipReader::IsFileEncrypted(int file_index)
{
	EXIT_IF(!m_p);

	return mz_zip_reader_is_file_encrypted(&m_p->zip, file_index) != 0;
}

String ZipReader::GetFileName(int file_index)
{
	uint32_t   len = mz_zip_reader_get_filename(&m_p->zip, file_index, nullptr, 0);
	ByteBuffer buf(len);
	mz_zip_reader_get_filename(&m_p->zip, file_index, reinterpret_cast<char*>(buf.GetData()), len);
	EXIT_IF(buf.At(buf.Size() - 1) != (Byte)0);
	return String::FromUtf8(reinterpret_cast<const char*>(buf.GetDataConst()));
}

int ZipReader::FindFile(const String& name, const String& comment)
{
	return mz_zip_reader_locate_file(&m_p->zip, name.C_Str(), comment.C_Str(), 0);
}

ByteBuffer ZipReader::ExtractFile(int file_index)
{
	size_t size = 0;

	if (file_index < 0 || IsFileDirectory(file_index))
	{
		return {};
	}

	ZipFileStat s {};
	GetFileStat(file_index, &s);

	if (s.m_uncomp_size == 0)
	{
		return {};
	}

	void* ptr = mz_zip_reader_extract_to_heap(&m_p->zip, file_index, &size, 0);

	EXIT_IF(!ptr || !size);

	EXIT_IF(sizeof(size_t) > 4 && (uint64_t(size) >> 32u) > 0);
	auto nn = static_cast<uint32_t>(size);

	ByteBuffer buf(nn);
	std::memcpy(buf.GetData(), ptr, size);

	m_p->zip.m_pFree(nullptr, ptr);

	return buf;
}

ByteBuffer ZipReader::ExtractFile(const String& name)
{
	return ExtractFile(FindFile(name));
}

// ZipWriter::ZipWriter()
//{
//	m_p = nullptr;
//}

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

bool ZipWriter::Create(const String& file_name)
{
	Close();

	m_p = new ZipPrivate;
	memset(&m_p->zip, 0, sizeof(m_p->zip));
	m_p->zip.m_pRead    = ZipImpl::Read;
	m_p->zip.m_pWrite   = ZipImpl::Write;
	m_p->zip.m_pAlloc   = ZipImpl::Alloc;
	m_p->zip.m_pFree    = ZipImpl::Free;
	m_p->zip.m_pRealloc = ZipImpl::Realloc;

	File* f = new File;
	f->Create(file_name);

	m_p->zip.m_pIO_opaque = f;

	if (f->IsInvalid() || (mz_zip_writer_init(&m_p->zip, 0) == 0))
	{
		Close();

		return false;
	}

	return true;
}

void ZipWriter::Close()
{
	if (m_p != nullptr)
	{
		mz_zip_writer_finalize_archive(&m_p->zip);
		mz_zip_writer_end(&m_p->zip);
		File* f = static_cast<File*>(m_p->zip.m_pIO_opaque);
		f->Close();
		Delete(f);
		Delete(m_p);
		m_p = nullptr;
	}
}

bool ZipWriter::AddFile(const String& file_name, const ByteBuffer& buf, ZipCompressLevel level)
{
	EXIT_IF(!m_p);

	if (m_p != nullptr)
	{
		return AddFile(file_name, reinterpret_cast<const uint8_t*>(buf.GetData()), buf.Size(), level);
	}
	return false;
}

bool ZipWriter::AddFile(const String& file_name, const uint8_t* buf, uint32_t size, ZipCompressLevel level)
{
	EXIT_IF(!m_p);
	EXIT_IF(!buf && size > 0);

	if (m_p != nullptr)
	{
		return mz_zip_writer_add_mem(&m_p->zip, file_name.C_Str(), buf, size, level) != 0;
	}
	return false;
}

bool ZipWriter::AddFileFromFile(const String& file_name, const String& from_file, ZipCompressLevel level)
{
	EXIT_IF(!m_p);

	if (m_p != nullptr)
	{
		File f;
		f.Open(from_file, File::Mode::Read);
		if (f.IsInvalid())
		{
			return false;
		}
		ByteBuffer buf = f.ReadWholeBuffer();
		f.Close();
		return AddFile(file_name, buf, level);
	}

	return false;
}

bool ZipWriter::AddFileFromReader(const String& /*file_name*/, ZipReader* from_reader, int file_index)
{
	EXIT_IF(!m_p);
	EXIT_IF(!from_reader);

	return mz_zip_writer_add_from_zip_reader(&m_p->zip, &from_reader->m_p->zip, file_index) != 0;
}

bool ZipWriter::AddDir(const String& dir_name)
{
	EXIT_IF(!m_p);

	String dir = dir_name.EndsWith(U"/") ? dir_name : dir_name + U"/";
	return AddFile(dir, nullptr, 0);
}

} // namespace Kyty::Core
