////////////////////////////////////////////////////////////////////////////////
// Archive.h
// Abstract base class for MechWarrior archive files
// Copyright Bjoern Ganster
// Developed since 2008
////////////////////////////////////////////////////////////////////////////////

#include "BGBase.h"
#include "BGStringTemplates.h"
#include "BGCPString.h"
#include "Archive.h"
#include "FileList.h"

#include "MechWarriorIIPRJ.h"
#include "MechWarrior3AnimFile.h"
#include "Database_MW2.h"
#include "MechWarrior3GameZArchive.h"
#include "MechWarrior3Reader.h"
#include "MechWarrior4Reader.h"
#include "MechWarrior3ZBD.h"
#include "ISO9660.h"
#include "ZFSF.h"

#define FILE_NUMBER FILE_NO_ARCHIVE_CPP

////////////////////////////////////////////////////////////////////////////////
// Check if type is an archive

bool isArchiveType (FileType type)
{
	if (type == FileType::PRJ
	||  type == FileType::MW2_DB
	||  type == FileType::ZBD
	||  type == FileType::ISO_9660)
	{
		return true;
	} else {
		return false;
	}
}

////////////////////////////////////////////////////////////////////////////////
// Open archive

MWArchive* openArchive (const PathChar* archivePath)
{
	if (bgstrlen(archivePath) == 0) {
		return nullptr;
	}

	SET_SCOPE;
	size_t pos = 0;
	MWArchive* arch = NULL;
	if (bgtextscan(archivePath, "gamez.zbd", 0, bgstrlen(archivePath), pos)) {
		arch = new MW3GameZArchive();
	} else if (bgtextscan(archivePath, "anim.zbd", 0, bgstrlen(archivePath), pos)) {
		arch = new MechWarrior3AnimFile();
	} else if (bgtextscan(archivePath, "reader.zbd", 0, bgstrlen(archivePath), pos)) {
		arch = new MW3ReaderZbdArchive();
	} else {
		SET_OP;
		// Determine archive type
		FileType ft = getFileTypeFromExtension (archivePath);

		// Create and open archive
		switch (ft) {
		case FileType::ZFS:
			arch = new ZFSF();
			break;
		case FileType::PRJ:
			arch = new MechWarriorIIPRJ();
			break;
		case FileType::MW2_DB: 
			arch = new Database_MW2();
			break;
		case FileType::ZBD:
			arch = new MechWarrior3ZBD();
			break;
		case FileType::ISO_9660:
			arch = new ISO9660();
			break;
		case FileType::MW4ResourceFile:
			arch = new MW4ResourceFileReader();
			break;
		default:
			RETURN nullptr;
		}
	}

	SET_OP;
	bool openedSuccessfully = false;
	if (arch != NULL) {
		// If we can open the file read-write, that's better, then we can alter it ...
		if (!arch->open (archivePath, false)) {
			// Try opening the file read-only, necessary for files on CD
			openedSuccessfully = arch->open(archivePath, true);
		} else {
			openedSuccessfully = true;
		}

		if (!openedSuccessfully) {
			delete arch;
			arch = NULL;
		}
	}

	RETURN arch;
}

////////////////////////////////////////////////////////////////////////////////
// Unpack archive

void unpackArchive (MWArchive* archive, const PathChar* target)
{
	SET_SCOPE;
	if (archive != NULL) {
		archive->exportToDirectory (target);
		BGCPString listFN = BGCPString(target, CP_SYSTEM) + ".txt";
		archive->createFileList (listFN.getAsPath());
	}
	RETURN;
}

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

MWArchive::MWArchive(const char* className, int ID)
: VirtualClass(className, MWArchiveID)
{
}

MWArchive::~MWArchive() 
{
}

////////////////////////////////////////////////////////////////////////////////
// Export all files in a mw2.prj file

void MWArchive::exportToDirectory(const PathChar* saveFN)
{
	SET_SCOPE;
	bgmkdir (saveFN);
	size_t count = getEntryCount();

	for (size_t i = 0; i < count; i++) {
		if (isDirectory (i)) {
			BGCPString dir(saveFN, CP_SYSTEM);
			BGCPString subDirName;
			getEntryName(i, subDirName);
			dir.addPathSep(subDirName);
			bgmkdir (dir.getAsPath());
			enterSubDir(i);
			exportToDirectory (dir.getAsPath());
			leaveDir();
		} else {
			BGCPString FN(saveFN, CP_SYSTEM);
			BGCPString prjFN;
			getEntryName(i, prjFN);
			FN.addPathSep(prjFN);
			//FN += ".";
			//FN += subDirName; // suffix
			size_t fileSize = getFileSize(i);
			if (fileSize > 0) {
				char* memFile = new char[fileSize];
				getFileChunk(i, 0, memFile, fileSize);
				FileCache fc;
				fc.create(FN.getAsPath());
				fc.write(0, memFile, fileSize);
				delete memFile;
			}
		}
	}
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Create a list of files

void MWArchive::createFileListEntry(const BGCPString& subDirName, size_t index, BGCPString& list)
{
	SET_SCOPE;
	BGCPString prjFN;
	getEntryName(index, prjFN);
	size_t fileSize = getFileSize(index);
	size_t fileOffset = getFileOffset (index);

	BGCPString line;
	if (!subDirName.empty()) {
		line += subDirName;
		line += "\\";
		line += prjFN;
	} else {
		line = prjFN;
	}
	line += "\t";
	line += BGCPString::fromInt(index);
	line += "\t";
	line += BGCPString::fromInt(fileSize);
	line += "\t";
	line += BGCPString::fromInt(fileOffset);
	line += BGCPString("\n", CP_SYSTEM);

	list += line;
	RETURN;
}

void MWArchive::createFileList(const PathChar* fn)
{
	SET_SCOPE;
	size_t count = getEntryCount();
	BGCPString list;

	//const TCHAR tabChar = 9;
	BGCPString line = "Filename\tEntry Number\tFile size\tFile offset\n";
	list += line;

	// Simple traversion supports only one nested directory, but so do
	// archive types that are currently supported
	for (size_t i = 0; i < count; i++) {
		if (isDirectory (i)) {
			BGCPString subDirName;
			getEntryName(i, subDirName);
			enterSubDir(i);
			for (size_t j = 0; j < getEntryCount(); j++) {
				createFileListEntry (subDirName, j, list);
			}
			leaveDir();
		} else {
			BGCPString dummy;
			createFileListEntry (dummy, i, list);
		}
	}

	list.saveToFile(fn, CP_SYSTEM);
	RETURN;
}

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

void MWArchive::addFile(const BGCPString& name, size_t size, size_t offset, FileType fileType)
{
	SET_SCOPE;
	ArchiveEntry entry;
	entry.filename = name;
	entry.fileOffset = offset;
	entry.fileSize = size;
	entry.fileType = fileType;
	mEntries.push_back(entry);
	RETURN;
}

void MWArchive::clearFileEntries()
{
	SET_SCOPE;
	mEntries.clear();
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Get name of entry

bool MWArchive::getEntryName (size_t index, BGCPString& name)
{
	SET_SCOPE;
    bool result = false;

    if (index < mEntries.size()) {
        name = mEntries[index].filename;
        result = true;
    } else {
        name.clear();
    }

    RETURN result;
}

////////////////////////////////////////////////////////////////////////////////
// Return index of file in current directory, or entries+1 if not found

size_t MWArchive::getEntryNumberByName(const BGCPString& name)
{
	SET_SCOPE;
	BGCPString lowerName = name.lowerStringASCII();
	BGCPString buf;
	for (size_t i = 0; i < getEntryCount(); i++) {
		if (getEntryName(i, buf)) {
			buf = buf.lowerStringASCII();
			if (lowerName == buf) {
				RETURN i;
			}
		}
	}

	// Nothing found
	RETURN getEntryCount()+1;
}

////////////////////////////////////////////////////////////////////////////////
// Get memory block for a file inside the archive

bool MWArchive::getFile(size_t fileNum, MemoryBlock& mb)
{
	SET_SCOPE;
	if (fileNum < getEntryCount()) {
		size_t fileSize = mEntries[fileNum].fileSize;
		if (mb.resize(fileSize)) {
			RETURN getFileChunk(fileNum, 0, mb.getPtr<char>(0, false), fileSize);
		}
	}

	RETURN false;
}

////////////////////////////////////////////////////////////////////////////////
// Open single file as Archive
// (used e.g. for opening loose BWD files such as those written by GBL)

class SingleFileFakeArchive: public MWArchive {
public:
	// Constructor
	SingleFileFakeArchive()
	: MWArchive("SingleFileFakeArchive", SingleFileFakeArchiveID)
	{
	}

	// Destructor
	~SingleFileFakeArchive() OVERRIDE
	{
	}

	// Open single file as a fake archive ... useful for some GBL BWDs ...
	bool open(const PathChar* fn, bool /*readOnly*/) OVERRIDE
	{
		SET_SCOPE;
		bool result = mFile.loadFromFile(fn);
		if (result) {
			SET_OP;
			mFN = getFilename(fn);

			ArchiveEntry entry;
			entry.fileSize = mFile.getSize();
			entry.fileOffset = 0;
			entry.filename = BGCPString(fn, CP_SYSTEM);
			entry.fileType = FileType::BWD;
			mEntries.push_back(entry);

			RETURN true;
		} else {
			RETURN false;
		}
	}

	// Close file
	void close() OVERRIDE
	{
	}

	// Get offset of file start inside prj file
	size_t getFileOffset(size_t index) OVERRIDE
	{
		return mFile.getSize();
	}

	// Read bufSize bytes from file numbered index at offset into buf
	bool getFileChunk(size_t index, size_t offset, void* buf, size_t bufSize) OVERRIDE
	{
		SET_SCOPE;
		auto read = mFile.read(offset, bufSize, buf);
		if (read == bufSize) {
			RETURN true;
		} else {
			RETURN false;
		}
	}

private:
	MemoryBlock mFile;
	BGCPString mFN;
};

MWArchive* openSingleFileAsArchive(const PathChar* FN)
{
	SET_SCOPE;
	SingleFileFakeArchive* newArch = new SingleFileFakeArchive();

	if (newArch->open(FN, false)) {
		RETURN newArch;
	} else {
		delete newArch;
		RETURN nullptr;
	}
}

////////////////////////////////////////////////////////////////////////////////
// Open directory as Archive

class DirectoryAsFakeArchive: public MWArchive {
public:
	// Constructor
	DirectoryAsFakeArchive()
	: MWArchive("SingleFileFakeArchive", SingleFileFakeArchiveID)
	{
	}

	// Destructor
	~DirectoryAsFakeArchive() OVERRIDE
	{
	}

	// Open single file as a fake archive ... useful for some GBL BWDs ...
	bool open(const PathChar* dir, bool) OVERRIDE
	{
		SET_SCOPE;
		mBaseDir = BGCPString(dir, CP_SYSTEM);
		BGCPString dirMask = mBaseDir;
		dirMask.addPathSep("*");
		
		FileList fl;
		if (fl.start(dirMask.getAsPath())) {
			do {
				ArchiveEntry entry;
				entry.fileSize = (size_t) fl.getSize();
				entry.fileOffset = 0;
				entry.filename = BGCPString(fl.getFileName(), CP_SYSTEM);
				entry.fileType = FileType::unsupported;
				mEntries.push_back(entry);
			} while (++fl);
		}

		if (mEntries.size() > 0) {
			RETURN true;
		} else {
			RETURN false;
		}
	}

	// Close file
	void close() OVERRIDE
	{
	}

	// Read bufSize bytes from file numbered index at offset into buf
	bool getFileChunk(size_t index, size_t offset, void* buf, size_t bufSize) OVERRIDE
	{
		SET_SCOPE;
		MemoryBlock mb;
		if (index > mFileNames.size() ) {
			RETURN false;
		}

		if (!mb.loadFromFile(mFileNames[index].getAsPath())) {
			RETURN false;
		}

		if (offset > mb.getSize()) {
			RETURN false;

		}

		size_t toCopy = std::min (mb.getSize() - offset, bufSize);
		memcpy(buf, mb.getPtr<char>(offset, false), toCopy);
		RETURN true;
	}

private:
	BGCPString mBaseDir;
	std::vector<BGCPString> mFileNames;
	std::vector<size_t> mFileSizes;
};

MWArchive* openDirectoryAsArchive(const PathChar* dir)
{
	SET_SCOPE;
	DirectoryAsFakeArchive* newArch = new DirectoryAsFakeArchive();

	if (newArch->open(dir, true)) {
		RETURN newArch;
	} else {
		delete newArch;
		RETURN nullptr;
	}
}

// Get number of entries in directory
size_t MWArchive::getEntryCount() const
{
	return mEntries.size();
}

// Get size of entry, 0: directory or error
size_t MWArchive::getFileSize(size_t index)
{
	if (index < mEntries.size()) {
		return mEntries[index].fileSize;
	} else {
		return 0;
	}
}

// Get offset of file start inside prj file
size_t MWArchive::getFileOffset(size_t index)
{
	if (index < mEntries.size()) {
		return mEntries[index].fileOffset;
	} else {
		return 0;
	}
}

// Enter subdirectory
bool MWArchive::enterSubDir(size_t /*index*/)
{
	return false; // Not supported by archive type
}

bool MWArchive::enterSubDir(const char* /*dirName*/)
{
	return false; // Not supported by archive type
}

// Leave subdirectory
bool MWArchive::leaveDir()
{
	return false; // Not supported by archive type
}

// Check if currently in sub directory
bool MWArchive::inSubDir() const
{
	return false; // Not supported by archive type
}

// Replace  file
bool MWArchive::replaceFile(size_t /*fileNum*/, const PathChar* /*srcFN*/)
{
	return false;
}

// Check if archive type supports file replacement
bool MWArchive::replaceFilePossible() const
{
	return false;
}

// Check if entry index is a directory
bool MWArchive::isDirectory(size_t /*index*/) const
{
	return false;
}

void MWArchive::addFileToArchive(const PathChar* /*FN*/)
{ }

bool MWArchive::canAddFilesToArchive() const
{
	return false;
}

// Get memory block for a file inside the archive
bool MWArchive::getFile(const char* fn, MemoryBlock& mb)
{
	size_t fno = getEntryNumberByName(fn);
	return getFile(fno, mb);
}

int MWArchive::getFileType(size_t index)
{
	if (index < mEntries.size()) {
		return mEntries[index].fileType;
	} else {
		return FileType::unsupported;
	}
}
