// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

#include <zip.h>
#include <unzip.h>
#include "logs.h"

#ifdef small
#undef small
#endif // small

namespace zlib {
namespace internal {

class InMemoryFile {
public:
	InMemoryFile(const QByteArray &data = QByteArray()) : _data(data) {
	}

	zlib_filefunc_def funcs() {
		zlib_filefunc_def result;
		result.opaque = this;
		result.zopen_file = &InMemoryFile::Open;
		result.zerror_file = &InMemoryFile::Error;
		result.zread_file = &InMemoryFile::Read;
		result.zwrite_file = &InMemoryFile::Write;
		result.zclose_file = &InMemoryFile::Close;
		result.zseek_file = &InMemoryFile::Seek;
		result.ztell_file = &InMemoryFile::Tell;
		return result;
	}

	int error() const {
		return _error;
	}

	QByteArray result() const {
		return _data;
	}

private:
	voidpf open(const char *filename, int mode) {
		if (mode & ZLIB_FILEFUNC_MODE_WRITE) {
			if (mode & ZLIB_FILEFUNC_MODE_CREATE) {
				_data.clear();
			}
			_position = _data.size();
			_data.reserve(2 * 1024 * 1024);
		} else if (mode & ZLIB_FILEFUNC_MODE_READ) {
			_position = 0;
		}
		_error = 0;
		return this;
	}

	uLong read(voidpf stream, void* buf, uLong size) {
		uLong toRead = 0;
		if (!_error) {
			if (_data.size() > int(_position)) {
				toRead = qMin(size, uLong(_data.size() - _position));
				memcpy(buf, _data.constData() + _position, toRead);
				_position += toRead;
			}
			if (toRead < size) {
				_error = -1;
			}
		}
		return toRead;
	}

	uLong write(voidpf stream, const void* buf, uLong size) {
		if (_data.size() < int(_position + size)) {
			_data.resize(_position + size);
		}
		memcpy(_data.data() + _position, buf, size);
		_position += size;
		return size;
	}

	int close(voidpf stream) {
		auto result = _error;
		_position = 0;
		_error = 0;
		return result;
	}

	int error(voidpf stream) const {
		return _error;
	}

	long tell(voidpf stream) const {
		return _position;
	}

	long seek(voidpf stream, uLong offset, int origin) {
		if (!_error) {
			switch (origin) {
			case ZLIB_FILEFUNC_SEEK_SET: _position = offset; break;
			case ZLIB_FILEFUNC_SEEK_CUR: _position += offset; break;
			case ZLIB_FILEFUNC_SEEK_END: _position = _data.size() + offset; break;
			}
			if (int(_position) > _data.size()) {
				_error = -1;
			}
		}
		return _error;
	}

	static voidpf Open(voidpf opaque, const char* filename, int mode) {
		return static_cast<InMemoryFile*>(opaque)->open(filename, mode);
	}

	static uLong Read(voidpf opaque, voidpf stream, void* buf, uLong size) {
		return static_cast<InMemoryFile*>(opaque)->read(stream, buf, size);
	}

	static uLong Write(voidpf opaque, voidpf stream, const void* buf, uLong size) {
		return static_cast<InMemoryFile*>(opaque)->write(stream, buf, size);
	}

	static int Close(voidpf opaque, voidpf stream) {
		return static_cast<InMemoryFile*>(opaque)->close(stream);
	}

	static int Error(voidpf opaque, voidpf stream) {
		return static_cast<InMemoryFile*>(opaque)->error(stream);
	}

	static long Tell(voidpf opaque, voidpf stream) {
		return static_cast<InMemoryFile*>(opaque)->tell(stream);
	}

	static long Seek(voidpf opaque, voidpf stream, uLong offset, int origin) {
		return static_cast<InMemoryFile*>(opaque)->seek(stream, offset, origin);
	}

	uLong _position = 0;
	int _error = 0;
	QByteArray _data;

};

} // namespace internal

constexpr int kCaseSensitive = 1;
constexpr int kCaseInsensitive = 2;

class FileToRead {
public:
	FileToRead(const QByteArray &content) : _data(content) {
		auto funcs = _data.funcs();
		if (!(_handle = unzOpen2(nullptr, &funcs))) {
			_error = -1;
		}
	}

	int getGlobalInfo(unz_global_info *pglobal_info) {
		if (error() == UNZ_OK) {
			_error = _handle ? unzGetGlobalInfo(_handle, pglobal_info) : -1;
		}
		return _error;
	}

	int locateFile(const char *szFileName, int iCaseSensitivity) {
		if (error() == UNZ_OK) {
			_error = _handle ? unzLocateFile(_handle, szFileName, iCaseSensitivity) : -1;
		}
		return error();
	}

	int goToFirstFile() {
		if (error() == UNZ_OK) {
			_error = _handle ? unzGoToFirstFile(_handle) : -1;
		}
		return error();
	}

	int goToNextFile() {
		if (error() == UNZ_OK) {
			_error = _handle ? unzGoToNextFile(_handle) : -1;
		}
		return error();
	}

	int getCurrentFileInfo(
			unz_file_info *pfile_info,
			char *szFileName,
			uLong fileNameBufferSize,
			void *extraField,
			uLong extraFieldBufferSize,
			char *szComment,
			uLong commentBufferSize) {
		if (error() == UNZ_OK) {
			_error = _handle ? unzGetCurrentFileInfo(
				_handle,
				pfile_info,
				szFileName,
				fileNameBufferSize,
				extraField,
				extraFieldBufferSize,
				szComment,
				commentBufferSize
			) : -1;
		}
		return error();
	}

	QString getCurrentFileName() {
		unz_file_info info = { 0 };
		constexpr auto kMaxName = 128;
		char name[kMaxName + 1] = { 0 };
		const auto result = getCurrentFileInfo(
			&info,
			name,
			kMaxName,
			nullptr,
			0,
			nullptr,
			0);
		return (result == UNZ_OK) ? QString::fromUtf8(name) : QString();
	}

	int openCurrentFile() {
		if (error() == UNZ_OK) {
			_error = _handle ? unzOpenCurrentFile(_handle) : -1;
		}
		return error();
	}

	int readCurrentFile(voidp buf, unsigned len) {
		if (error() == UNZ_OK) {
			auto result = _handle ? unzReadCurrentFile(_handle, buf, len) : -1;
			if (result >= 0) {
				return result;
			} else {
				_error = result;
			}
		}
		return error();
	}

	int closeCurrentFile() {
		if (error() == UNZ_OK) {
			_error = _handle ? unzCloseCurrentFile(_handle) : -1;
		}
		return error();
	}

	QByteArray readCurrentFileContent(int fileSizeLimit) {
		unz_file_info fileInfo = { 0 };
		if (getCurrentFileInfo(&fileInfo, nullptr, 0, nullptr, 0, nullptr, 0) != UNZ_OK) {
			LOG(("Error: could not get current file info in a zip file."));
			return QByteArray();
		}

		auto size = fileInfo.uncompressed_size;
		if (size > static_cast<uint32>(fileSizeLimit)) {
			if (_error == UNZ_OK) _error = -1;
			LOG(("Error: current file is too large (should be less than %1, got %2) in a zip file.").arg(fileSizeLimit).arg(size));
			return QByteArray();
		}
		if (openCurrentFile() != UNZ_OK) {
			LOG(("Error: could not open current file in a zip file."));
			return QByteArray();
		}

		QByteArray result;
		result.resize(size);

		auto couldRead = readCurrentFile(result.data(), size);
		if (couldRead != static_cast<int>(size)) {
			LOG(("Error: could not read current file in a zip file, got %1.").arg(couldRead));
			return QByteArray();
		}

		if (closeCurrentFile() != UNZ_OK) {
			LOG(("Error: could not close current file in a zip file."));
			return QByteArray();
		}

		return result;
	}

	QByteArray readFileContent(const char *szFileName, int iCaseSensitivity, int fileSizeLimit) {
		if (locateFile(szFileName, iCaseSensitivity) != UNZ_OK) {
			LOG(("Error: could not locate '%1' in a zip file.").arg(szFileName));
			return QByteArray();
		}
		return readCurrentFileContent(fileSizeLimit);
	}

	void close() {
		if (_handle && unzClose(_handle) != UNZ_OK && _error == UNZ_OK) {
			_error = -1;
		}
		_handle = nullptr;
	}

	int error() const {
		if (auto dataError = _data.error()) {
			return dataError;
		}
		return _error;
	}

	void clearError() {
		_error = UNZ_OK;
	}

	~FileToRead() {
		close();
	}

private:
	internal::InMemoryFile _data;
	unzFile _handle = nullptr;
	int _error = 0;

};

class FileToWrite {
public:
	FileToWrite() {
		auto funcs = _data.funcs();
		if (!(_handle = zipOpen2(nullptr, APPEND_STATUS_CREATE, nullptr, &funcs))) {
			_error = -1;
		}
	}

	int openNewFile(
		const char *filename,
		const zip_fileinfo *zipfi,
		const void *extrafield_local,
		uInt size_extrafield_local,
		const void* extrafield_global,
		uInt size_extrafield_global,
		const char* comment,
		int method,
		int level
	) {
		if (error() == ZIP_OK) {
			_error = _handle ? zipOpenNewFileInZip(
				_handle,
				filename,
				zipfi,
				extrafield_local,
				size_extrafield_local,
				extrafield_global,
				size_extrafield_global,
				comment,
				method,
				level
			) : -1;
		}
		return error();
	}

	int writeInFile(const void* buf, unsigned len) {
		if (error() == ZIP_OK) {
			_error = _handle ? zipWriteInFileInZip(_handle, buf, len) : -1;
		}
		return error();
	}

	int closeFile() {
		if (error() == ZIP_OK) {
			_error = _handle ? zipCloseFileInZip(_handle) : -1;
		}
		return error();
	}

	void close() {
		if (_handle && zipClose(_handle, nullptr) != ZIP_OK && _error == ZIP_OK) {
			_error = -1;
		}
		_handle = nullptr;
	}

	int error() const {
		if (auto dataError = _data.error()) {
			return dataError;
		}
		return _error;
	}

	QByteArray result() const {
		return _data.result();
	}

	~FileToWrite() {
		close();
	}

private:
	internal::InMemoryFile _data;
	zipFile _handle = nullptr;
	int _error = 0;

};

} // namespace zlib
