/*
 * unzip.cpp
 * ---------
 * Purpose: Implementation file for extracting modules from .zip archives, making use of MiniZip (from the zlib contrib package)
 * Notes  : (currently none)
 * Authors: OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */

#include "stdafx.h"

#include "../common/FileReader.h"
#include "unzip.h"
#include "../common/misc_util.h"
#include <algorithm>
#include <vector>

#if defined(MPT_WITH_ZLIB) && defined(MPT_WITH_MINIZIP)
#include <contrib/minizip/unzip.h>
#elif defined(MPT_WITH_MINIZ)
#include <miniz/miniz.h>
#endif


OPENMPT_NAMESPACE_BEGIN


#if defined(MPT_WITH_ZLIB) && defined(MPT_WITH_MINIZIP)


// Low-level file abstractions for in-memory file handling
struct ZipFileAbstraction
{
	static voidpf ZCALLBACK fopen64_mem(voidpf opaque, const void *, int mode)
	{
		FileReader &file = *static_cast<FileReader *>(opaque);
		if((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_WRITE)
		{
			return nullptr;
		} else
		{
			file.Rewind();
			return opaque;
		}
	}

	static uLong ZCALLBACK fread_mem(voidpf opaque, voidpf, void *buf, uLong size)
	{
		FileReader &file = *static_cast<FileReader *>(opaque);
		return mpt::saturate_cast<uLong>(file.ReadRaw(mpt::span(mpt::void_cast<std::byte*>(buf), size)).size());
	}

	static uLong ZCALLBACK fwrite_mem(voidpf, voidpf, const void *, uLong)
	{
		return 0;
	}

	static ZPOS64_T ZCALLBACK ftell64_mem(voidpf opaque, voidpf)
	{
		FileReader &file = *static_cast<FileReader *>(opaque);
		return file.GetPosition();
	}

	static long ZCALLBACK fseek64_mem(voidpf opaque, voidpf, ZPOS64_T offset, int origin)
	{
		FileReader &file = *static_cast<FileReader *>(opaque);
		ZPOS64_T destination = 0;
		switch(origin)
		{
		case ZLIB_FILEFUNC_SEEK_CUR:
			destination = static_cast<ZPOS64_T>(file.GetPosition()) + offset;
			break;
		case ZLIB_FILEFUNC_SEEK_END:
			destination = static_cast<ZPOS64_T>(file.GetLength()) + offset;
			break;
		case ZLIB_FILEFUNC_SEEK_SET:
			destination = offset;
			break;
		default:
			return -1;
		}
		if(!mpt::in_range<FileReader::off_t>(destination))
		{
			return 1;
		}
		return (file.Seek(static_cast<FileReader::off_t>(destination)) ? 0 : 1);
	}

	static int ZCALLBACK fclose_mem(voidpf, voidpf)
	{
		return 0;
	}

	static int ZCALLBACK ferror_mem(voidpf, voidpf)
	{
		return 0;
	}
};


CZipArchive::CZipArchive(FileReader &file)
	: ArchiveBase(file)
	, zipFile(nullptr)
{
	zlib_filefunc64_def functions =
	{
		ZipFileAbstraction::fopen64_mem,
		ZipFileAbstraction::fread_mem,
		ZipFileAbstraction::fwrite_mem,
		ZipFileAbstraction::ftell64_mem,
		ZipFileAbstraction::fseek64_mem,
		ZipFileAbstraction::fclose_mem,
		ZipFileAbstraction::ferror_mem,
		&inFile
	};

	zipFile = unzOpen2_64(nullptr, &functions);

	if(zipFile == nullptr)
	{
		return;
	}

	// read comment
	{
		unz_global_info info;
		if(unzGetGlobalInfo(zipFile, &info) == UNZ_OK)
		{
			if(info.size_comment > 0)
			{
				if(info.size_comment < Util::MaxValueOfType(info.size_comment))
				{
					info.size_comment++;
				}
				std::vector<char> commentData(info.size_comment);
				if(unzGetGlobalComment(zipFile, commentData.data(), info.size_comment) >= 0)
				{
					commentData[info.size_comment - 1] = '\0';
					comment = mpt::ToUnicode(mpt::IsUTF8(commentData.data()) ? mpt::Charset::UTF8 : mpt::Charset::CP437, commentData.data());
				}
			}
		}
	}

	// read contents
	unz_file_pos curFile;
	int status = unzGoToFirstFile(zipFile);
	unzGetFilePos(zipFile, &curFile);

	while(status == UNZ_OK)
	{
		ArchiveFileInfo fileinfo;

		fileinfo.type = ArchiveFileType::Normal;
		
		unz_file_info info;
		char name[256];
		unzGetCurrentFileInfo(zipFile, &info, name, sizeof(name), nullptr, 0, nullptr, 0);
		fileinfo.name = mpt::PathString::FromUnicode(mpt::ToUnicode((info.flag & (1<<11)) ? mpt::Charset::UTF8 : mpt::Charset::CP437, std::string(name)));
		fileinfo.size = info.uncompressed_size;

		unzGetFilePos(zipFile, &curFile);
		fileinfo.cookie1 = curFile.pos_in_zip_directory;
		fileinfo.cookie2 = curFile.num_of_file;

		contents.push_back(fileinfo);

		status = unzGoToNextFile(zipFile);
	}

}


CZipArchive::~CZipArchive()
{
	unzClose(zipFile);
}


bool CZipArchive::ExtractFile(std::size_t index)
{
	if(index >= contents.size())
	{
		return false;
	}

	data.clear();

	unz_file_pos bestFile;
	unz_file_info info;

	bestFile.pos_in_zip_directory = static_cast<uLong>(contents[index].cookie1);
	bestFile.num_of_file = static_cast<uLong>(contents[index].cookie2);

	if(unzGoToFilePos(zipFile, &bestFile) == UNZ_OK && unzOpenCurrentFile(zipFile) == UNZ_OK)
	{
		unzGetCurrentFileInfo(zipFile, &info, nullptr, 0, nullptr, 0, nullptr, 0);
		
		try
		{
			data.resize(info.uncompressed_size);
		} catch(...)
		{
			unzCloseCurrentFile(zipFile);
			return false;
		}
		unzReadCurrentFile(zipFile, data.data(), info.uncompressed_size);
		unzCloseCurrentFile(zipFile);

		return true;
	}

	return false;
}


#elif defined(MPT_WITH_MINIZ)


CZipArchive::CZipArchive(FileReader &file) : ArchiveBase(file)
{
	zipFile = new mz_zip_archive();
	
	mz_zip_archive *zip = static_cast<mz_zip_archive*>(zipFile);
	
	(*zip) = {};
	const auto fileData = file.GetRawData();
	if(!mz_zip_reader_init_mem(zip, fileData.data(), fileData.size(), 0))
	{
		delete zip;
		zip = nullptr;
		zipFile = nullptr;
	}

	if(!zip)
	{
		return;
	}

	for(mz_uint i = 0; i < mz_zip_reader_get_num_files(zip); ++i)
	{
		ArchiveFileInfo info;
		info.type = ArchiveFileType::Invalid;
		mz_zip_archive_file_stat stat = {};
		if(mz_zip_reader_file_stat(zip, i, &stat))
		{
			info.type = ArchiveFileType::Normal;
			info.name = mpt::PathString::FromUnicode(mpt::ToUnicode((stat.m_bit_flag & (1<<11)) ? mpt::Charset::UTF8 : mpt::Charset::CP437, stat.m_filename));
			info.size = stat.m_uncomp_size;
		}
		if(mz_zip_reader_is_file_a_directory(zip, i))
		{
			info.type = ArchiveFileType::Special;
		} else if(mz_zip_reader_is_file_encrypted(zip, i))
		{
			info.type = ArchiveFileType::Special;
		}
		contents.push_back(info);
	}

}


CZipArchive::~CZipArchive()
{
	mz_zip_archive *zip = static_cast<mz_zip_archive*>(zipFile);

	if(zip)
	{
		mz_zip_reader_end(zip);

		delete zip;
		zipFile = nullptr;
	}

}


bool CZipArchive::ExtractFile(std::size_t index)
{
	mz_zip_archive *zip = static_cast<mz_zip_archive*>(zipFile);

	if(index >= contents.size())
	{
		return false;
	}

	mz_uint bestFile = index;

	mz_zip_archive_file_stat stat = {};
	mz_zip_reader_file_stat(zip, bestFile, &stat);
	if(stat.m_uncomp_size >= std::numeric_limits<std::size_t>::max())
	{
		return false;
	}
	try
	{
		data.resize(static_cast<std::size_t>(stat.m_uncomp_size));
	} catch(...)
	{
		return false;
	}
	if(!mz_zip_reader_extract_to_mem(zip, bestFile, data.data(), static_cast<std::size_t>(stat.m_uncomp_size), 0))
	{
		return false;
	}
	comment = mpt::ToUnicode(mpt::Charset::CP437, std::string(stat.m_comment, stat.m_comment + stat.m_comment_size));
	return true;
}


#endif // MPT_WITH_ZLIB || MPT_WITH_MINIZ


OPENMPT_NAMESPACE_END