////////////////////////////////////////////////////////////////////////////////
// ArchiveViewer.cpp
// Screen for viewing archives
// Copyright Bjoern Ganster
// Developed since 2007
////////////////////////////////////////////////////////////////////////////////

// todo: code generally relies a lot on dynamic_cast ...

#include "BGBase.h"
#include "ArchiveViewer.h"
#include "Audio.h"
#include "BGStringTemplates.h"
#include "BipedMech.h"
#include "Config.h"
#include "dialogs.h"
#include "Font.h"
#include "MechWarrior3GameZArchive.h"
#include "MechWarrior3Reader.h"
#include "MechWarrior4Reader.h"
#include "LZdecode.h"
#include "MechVM.h"
#include "MechWarrior3ZBD.h"
#include "Sound.h"

#include <GL/glu.h>

#include <algorithm>

#define FILE_NUMBER FILE_NO_ARCHIVE_VIEWER_CPP

// Constructor
ArchiveViewer::ArchiveViewer(int _left, int _top, int _width, int _height)
: GLWindowContainer("ArchiveViewer", ArchiveViewer_ID, nullptr, _left, _top, _width, _height),
  label(nullptr),
  mwArchive (nullptr),
  PRJTable (nullptr),
  upDirButton(nullptr),
  texture(),
  mesh(nullptr),
  zoom(1.0),
  mode(0),
  soundOffset(0)
{
	SET_SCOPE;
	Audio* audio = getAudio();
	audio->unpause();
	mSoundMutex = SDL_CreateMutex();
	setAudioProvider(this);

	// Generate random palette
	mPal.randomize();

	RETURN;
}

// Destructor
ArchiveViewer::~ArchiveViewer() 
{
	SET_SCOPE;
	safeDelete(mwArchive);
	safeDelete(PRJTable);
	safeDelete(mesh);

	// Rip up the sound system!
	SDL_LockMutex(mSoundMutex);
	setAudioProvider(nullptr);
	for (auto iter = mQueuedSounds.begin(); iter != mQueuedSounds.end(); ++iter) {
		safeDelete(*iter);
	}
	SDL_UnlockMutex(mSoundMutex);
	SDL_DestroyMutex(mSoundMutex);

	RETURN;
}

// Set up archive viewer to view an archive
bool ArchiveViewer::exploreArchive(const PathChar* FN)
{
	SET_SCOPE;

	mwArchive = openArchive(FN);
	if (mwArchive != nullptr) {
		SET_OP;
		getMechVm()->addButton(this, 0, "..", MW2PRJ_upDirButtonID);
		getMechVm()->addButton(this, 1, "Exit", MW2PRJ_ExitButtonID);
		auto buttonHeight = getDefaultButtonHeight();
		label = new GLLabel(this, margin, buttonTop + buttonHeight + margin, 800, buttonHeight);

		SET_OP;
		Font* font = getDefaultFont();
		auto cw = font->getMaxCharWidth();
		PRJTable = new GLTableView(this, 20, buttonTop + 2*buttonHeight + 2*margin, 22 * cw, height - (buttonTop + 2*buttonHeight + 3*margin), MW2PRJ_TableID);
		PRJTable->setDataSource(this);
		PRJTable->setColWidth(2, 3*cw, 4*cw);
		PRJTable->setColWidth(1, 12*cw, 4*cw);
		PRJTable->setRowHeights(mwArchive->getEntryCount(), font->getCharHeight()+5);
		PRJTable->userData = MW2PRJ_TableID;
		PRJTable->setListener(this);
		add(PRJTable);

		RETURN true;
	} else {
		RETURN false;
	}
}

// Set up archive viewer to view a single file as a fake archive
// Useful for *SCN1.BWD that are written to the GBL main directory
bool ArchiveViewer::exploreSingleFile(const PathChar* FN)
{
	SET_SCOPE;
	getMechVm()->addButton(this, 0, "..", MW2PRJ_upDirButtonID);
	getMechVm()->addButton(this, 1, "Exit", MW2PRJ_ExitButtonID);

	label = new GLLabel(this, margin, buttonTop + getDefaultButtonHeight() + margin, 800, 30);
	label->setTitle(30);
	add(label);

	mwArchive = openSingleFileAsArchive(FN);
	Font* font = getDefaultFont();
	if (mwArchive != nullptr) {
		PRJTable = new GLTableView(this, 20, 100, 300, height - 120, MW2PRJ_TableID);
		PRJTable->setDataSource(this);
		PRJTable->setColWidth(2, 50, 50);
		PRJTable->setColWidth(1, 150, 50);
		PRJTable->setRowHeights(mwArchive->getEntryCount(), font->getCharHeight()+5);
		PRJTable->userData = MW2PRJ_TableID;
		PRJTable->setListener(this);
		add(PRJTable);

		RETURN true;
	} else {
		RETURN false;
	}
}

///////////////////////////////////////////////////////////////////////////////
// View external file

void ArchiveViewer::viewFile()
{
	SET_SCOPE;
	if (mwArchive == nullptr || PRJTable == nullptr) {
		return;
	}

	size_t fileNum = PRJTable->getSelectedRow();
	auto filetype = mwArchive->getFileType(fileNum);

	// The file size of gamez.zbd meshes isn't known like for normal files, so we can't treat
	// these files the same way as the other files
	if (filetype == FileType::MW3GameZMesh
	||  filetype == FileType::MW3PMGameZMesh) 
	{
		viewGameZMesh(fileNum, filetype);
		return;
	}

	BGCPString fileName;
	getSelectedFileName(fileName);
	MemoryBlock block;
	if(!getSelectedFile(block)) {
		return;
	}

	texture.resize(0, 0, GL_RGB);

	safeDelete(mesh);

	int fileType = mwArchive->getFileType(fileNum);
	if (fileType == FileType::unsupported) {
		fileType = getFileTypeFromExtension(fileName.getAsPath());
	}

	if (fileType == FileType::unsupported) {
		fileType = getFileTypeFromContent(&block);
		if (fileType == FileType::unsupported) {
			displayMsg("Unsupported file type");
			return;
		}
	}

	BGCPString labelText = BGCPString("Viewing ") + fileName;
	BGCPString outFN;
	mode = 0;

	switch (fileType) {
		case FileType::XEL:
			MechWarriorIIPRJ::loadXEL(block, mPal, texture);
			labelText += BGCPString(" (") + BGCPString::fromInt(texture.getWidth()) + BGCPString("x") + 
				BGCPString::fromInt(texture.getHeight()) + BGCPString(")");
			break;
		case FileType::WTB:
			viewMesh(&block, fileName.getAsPath());
			break;
		case FileType::OBJ:
			viewObj(fileName.getAsPath());
			break;
		case FileType::Mech:
			showMech(&block, fileName.getAsPath());
			break;
		case FileType::FNT:
			decodeFNT(block, texture);
			labelText += BGCPString(" (") + BGCPString::fromInt(texture.getWidth()) + BGCPString("x") + 
				BGCPString::fromInt(texture.getHeight()) + BGCPString(")");
			break;
		case FileType::SHP:
			clipBlock = block;
			mode = 1;
			frame = 0;
			MechWarriorIIPRJ::loadSHP(clipBlock, frame, mPal, texture);
			labelText += BGCPString(" (") + BGCPString::fromInt(texture.getWidth()) + BGCPString("x") + 
				BGCPString::fromInt(texture.getHeight()) + BGCPString(")");
			break;
		case FileType::SFL:
		{
			Sound sound;
			sound.decodeSFL(block);
			queueSound(sound);
		}
		break;
		case FileType::WAV:
			if (!playWAV(&block)) {
				printf("Playing " PathCharMod " failed\n", fileName.getAsPath());
			}
			break;
		case FileType::COL:
			memcpy(mPal.values, block.getPtr<uint8_t>(), std::min<int>(block.getSize(), 768));
			break;
		case FileType::EFA:
		{
			MemoryBlock* pcx = LZdecode(&block);
			MechWarriorIIPRJ::loadPCX(pcx->getPtr<unsigned char>(), pcx->getSize(), texture, mPal);
			delete pcx;
			labelText += BGCPString(" (") + BGCPString::fromInt(texture.getWidth()) + BGCPString("x") + 
				BGCPString::fromInt(texture.getHeight()) + BGCPString(")");
			break;
		}
		case FileType::TEX:
			MechWarriorIIPRJ::loadTEX(block.getPtr<unsigned char>(), block.getSize(), texture);
			labelText += BGCPString(" (") + BGCPString::fromInt(texture.getWidth()) + BGCPString("x") + 
				BGCPString::fromInt(texture.getHeight()) + BGCPString(")");
			break;
		case FileType::PCX:
			MechWarriorIIPRJ::loadPCX(block.getPtr<unsigned char>(), block.getSize(), texture, mPal);
			labelText += BGCPString(" (") + BGCPString::fromInt(texture.getWidth()) + BGCPString("x") + 
				BGCPString::fromInt(texture.getHeight()) + BGCPString(")");
			break;
		case FileType::MW2_555:
			Decode_MW2_555(block, texture);
			labelText += BGCPString(" (") + BGCPString::fromInt(texture.getWidth()) + BGCPString("x") + 
				BGCPString::fromInt(texture.getHeight()) + BGCPString(")");
			break;
		case FileType::MW3_555:
			Decode_MW3_555(block.getPtr<unsigned char>(), block.getSize(), texture, true);
			labelText += BGCPString(" (") + BGCPString::fromInt(texture.getWidth()) + BGCPString("x") + 
				BGCPString::fromInt(texture.getHeight()) + BGCPString(")");
			break;
	}
	displayMsg(labelText);

	RETURN;
}

///////////////////////////////////////////////////////////////////////////////
// View external file

bool ArchiveViewer::viewDir(const PathChar* dir)
{
	SET_SCOPE;
	getMechVm()->addButton(this, 0, "Exit", MW2PRJ_ExitButtonID);

	label = new GLLabel(this, margin, buttonTop + getDefaultButtonHeight() + margin, 800, 30);
	label->setTitle(30);
	add(label);

	mwArchive = openDirectoryAsArchive(dir);
	Font* font = getDefaultFont();
	if (mwArchive != nullptr) {
		PRJTable = new GLTableView(this, 20, 100, 300, height - 120, MW2PRJ_TableID);
		PRJTable->setDataSource(this);
		PRJTable->setColWidth(2, 50, 50);
		PRJTable->setColWidth(1, 150, 50);
		PRJTable->setRowHeights(mwArchive->getEntryCount(), font->getCharHeight()+5);
		PRJTable->userData = MW2PRJ_TableID;
		PRJTable->setListener(this);
		add(PRJTable);

		RETURN true;
	} else {
		RETURN false;
	}
}

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

void ArchiveViewer::unpackPrj()
{
	SET_SCOPE;
	displayMsg("Unpacking...");
	BGCPString saveFN;
	if (getSaveFileName (saveFN)) {
		unpackArchive (mwArchive, saveFN.getAsPath()); // also creates a file list ...
		displayMsg("Done");
	} else
		displayMsg("Aborted");
	RETURN;
}

///////////////////////////////////////////////////////////////////////////////
// TableView implementation: get number of rows and columns
// The numbers returned here should be precise, because other functions rely on the fact they can query data about cells declared legal here ...

void ArchiveViewer::getDimensions (size_t senderID, size_t& rowCount, size_t& colCount)
{
	if (senderID == MW2PRJ_TableID) {
		colCount = 3;
		rowCount = mwArchive->getEntryCount();
	}
}

///////////////////////////////////////////////////////////////////////////////
// TableView implementation: Get string for a cell

bool ArchiveViewer::getString (size_t senderID, int row, int col, BGCPString& str)
{
	SET_SCOPE;
    if (senderID == MW2PRJ_TableID) {
        if ((size_t) row < mwArchive->getEntryCount()) {
            if (col == 0) {
                str = BGCPString::fromInt(row);
            } else if (col == 1) {
                BGCPString entryName;
                if (mwArchive->getEntryName (row, str)) {
                    RETURN true;
                } else
                    str.clear();
            } else {
                if (!mwArchive->isDirectory(row)) {
                    str.clear();
                    size_t size = mwArchive->getFileSize(row);
                    str = BGCPString::sizeStr(size);
                } else
                    str = "Dir";
                RETURN true;
            }
        }
    }

    RETURN false;
}

///////////////////////////////////////////////////////////////////////////////
// Request context menu

const int ContextMenu_SaveAs = 0;
const int ContextMenu_ReplaceWith = 1;
const int ContextMenu_AddFiles = 2;
const int ContextMenu_SHP2BMP = 3;
const int ContextMenu_XEL2BMP = 4;
const int ContextMenu_SFL2WAV = 5;
const int ContextMenu_WTB2OBJ = 6;
const int ContextMenu_BWD2XML = 7;
const int ContextMenu_BWD2XML_rec = 8;
const int ContextMenu_ImportLevel = 9;
const int ContextMenu_TEX2BMP = 10;
const int ContextMenu_EFA2PCX = 11;
const int ContextMenu_EFA2BMP = 12;
const int ContextMenu_OBJ2WTB = 13;
const int ContextMenu_unpackAll = 14;
const int ContextMenu_CDDA2WAV = 15;
const int ContextMenu_FNT2BMP = 16;
const int ContextMenu_MW3GameZMesh2OBJ = 17;
const int ContextMenu_ExportZReaderAsXML = 18;
const int ContextMenu_MW3_ZBD2BMP = 19;

void ArchiveViewer::showContextMenu(VirtualClass* sender, int x, int y)
{
	SET_SCOPE;
	if (sender == PRJTable) {
		size_t fileNum = PRJTable->getSelectedRow();
		auto fileType = mwArchive->getFileType(fileNum);

		// If the file type is unknown, try to derive it from the extension
		if (fileType == FileType::unsupported) {
			BGCPString fileName;
			mwArchive->getEntryName(fileNum, fileName);
			fileType = getFileTypeFromExtension(fileName.getAsPath());
		}

		// If we still don't know the file type, try to determine it from its content
		if (fileType == FileType::unsupported) {
			MemoryBlock mb;
			mwArchive->getFile(fileNum, mb);
			fileType = getFileTypeFromContent(&mb);
		}

		//BGCPString fileName;
		//getSelectedFileName (fileName);
		//FileType fileType = getFileTypeFromExtension(fileName.getAsPath());
		GLContextMenu* cm = new GLContextMenu (this, x, y);
		cm->addMenuItem ("Save as ...", this, ContextMenu_SaveAs);
		cm->addMenuItem ("Extract all", this, ContextMenu_unpackAll);

		switch (fileType) {
		case FileType::XEL:
			cm->addMenuItem ("Export as BMP", this, ContextMenu_XEL2BMP);
			break;
		case FileType::SHP:
			cm->addMenuItem ("Export as BMP", this, ContextMenu_SHP2BMP);
			break;
		case FileType::WTB:
			cm->addMenuItem ("Export as OBJ", this, ContextMenu_WTB2OBJ);
			cm->addMenuItem ("Replace with OBJ", this, ContextMenu_OBJ2WTB);
			break;
		case FileType::SFL:
			cm->addMenuItem ("Export as WAV", this, ContextMenu_SFL2WAV);
			break;
		case FileType::BWD:
		case FileType::MW3GameZNodes:
		case FileType::MW3PMGameZNodes:
			//#ifdef _DEBUG
			cm->addMenuItem ("Import Level", this, ContextMenu_ImportLevel);
			//#endif
			break;
		case FileType::TEX:
			cm->addMenuItem ("Export as BMP", this, ContextMenu_TEX2BMP);
			break;
			/*case FileType::Mech:
			cm->addMenuItem (nullptr, "Save As ...", "", this, ContextMenu_SaveAs);
			break;
			case FileType::COL:
			cm->addMenuItem (nullptr, "Save As ...", "", this, ContextMenu_SaveAs);
			break;*/
		case FileType::EFA:
			cm->addMenuItem ("Save as PCX", this, ContextMenu_EFA2PCX);
			cm->addMenuItem ("Save as BMP", this, ContextMenu_EFA2BMP);
			break;
		case FileType::ISO_9660_AUDIO:
			cm->addMenuItem ("Save as WAV", this, ContextMenu_CDDA2WAV);
			break;
		case FileType::FNT:
			cm->addMenuItem("Save as BMP", this, ContextMenu_FNT2BMP);
			break;
		case MW3GameZMesh:
		case MW3PMGameZMesh:
			cm->addMenuItem("Save as OBJ", this, ContextMenu_MW3GameZMesh2OBJ);
			break;
		case MW3ZReaderContents:
			cm->addMenuItem("Export as XML", this, ContextMenu_ExportZReaderAsXML);
			break;
		case MW3_555:
			cm->addMenuItem("Export as BMP", this, ContextMenu_MW3_ZBD2BMP);
			break;
		}

		if (mwArchive->replaceFilePossible())
			cm->addMenuItem ("Replace with ...", this, ContextMenu_ReplaceWith);
		cm->addMenuItem ("Add files ...", this, ContextMenu_AddFiles);
		cm->userData = MW2PRJTableContextMenu;
		add (cm);
	}

	RETURN;
}

///////////////////////////////////////////////////////////////////////////////
// View file inside a PRJ file

void ArchiveViewer::getSelectedFileName (BGCPString& FN)
{
	size_t fileNum = PRJTable->getSelectedRow();
	mwArchive->getEntryName(fileNum, FN);
}

bool ArchiveViewer::getSelectedFile(MemoryBlock& block)
{
	SET_SCOPE;
    size_t fileNum = PRJTable->getSelectedRow();
    size_t fileSize = mwArchive->getFileSize(fileNum);

    if (!block.resize(fileSize)) {
		RETURN false;
	}

    if (mwArchive->getFileChunk(fileNum, 0, block.getPtr<char>(), fileSize)) {
        RETURN true;
    } else {
        RETURN false;
    }
}

///////////////////////////////////////////////////////////////////////////////
// Export files from PRJ

void ArchiveViewer::exportBWD (bool recursive)
{
	SET_SCOPE;
	MemoryBlock block;
	if (getSelectedFile(block)) {
		MechWarriorIIPRJ* mw2prj = dynamic_cast <MechWarriorIIPRJ*> (mwArchive);
		XMLTree xmlTree;
		XMLTreeIterator iter(&xmlTree, xmlTree.getRoot());
		iter.addChild("BWD");
		iter.goToChild(0);
		BGCPString fileName;
		getSelectedFileName (fileName);

		BWD2XML(block, iter, recursive, nullptr, nullptr, fileName, mw2prj);
		BGCPString outFN;
		if (getSaveFileName(outFN)) {
			bgDeleteFile(outFN.getAsPath());
			xmlTree.saveToFile(outFN.getAsPath(), CP_ASCII);
		}
	}
	RETURN;
}


// Import a level from a BWD
void ArchiveViewer::importLevel()
{
	SET_SCOPE;

	size_t fileNum = PRJTable->getSelectedRow();
	auto fileType = mwArchive->getFileType(fileNum);

	switch (fileType) {
	case FileType::MW3PMGameZNodes:
		{
			MW3GameZArchive* gameZArchive = dynamic_cast<MW3GameZArchive*>(mwArchive);
			if (gameZArchive != nullptr) {
				gameZArchive->importPMGameZNodes(fileNum);
				RETURN;
			}
		}
		break;
	case FileType::MW3GameZNodes:
		{
			MW3GameZArchive* gameZArchive = dynamic_cast<MW3GameZArchive*>(mwArchive);
			if (gameZArchive != nullptr) {
				gameZArchive->importGameZNodes(fileNum);
				RETURN;
			}
		}
		break;
	default:
		// Other types are handled below ...
		break;
	}

	BGCPString outputDir = getDataDir();
	outputDir.addPathSep("levels");
	bgmkdir(outputDir.getAsPath());

	BGCPString BWDFN;
	getSelectedFileName (BWDFN);
	outputDir.addPathSep(BWDFN);
	bgmkdir(outputDir.getAsPath());

	MechWarriorIIPRJ* mw2prj = dynamic_cast <MechWarriorIIPRJ*> (mwArchive);
	if (mw2prj == nullptr) {
		RETURN;
	}

	MemoryBlock block;
	if (!getSelectedFile(block)) {
		RETURN;
	}

	XMLTree xmlTree;
	XMLTreeIterator iter(&xmlTree, xmlTree.getRoot());
	iter.addChild("BWD");
	iter.goToChild(0);

	// Call import function
	BWD2XML(block, iter, true, outputDir.getAsPath(), nullptr, BWDFN, mw2prj);
	BGCPString outFN = BWDFN + ".xml";
	BGCPString fullFN = outputDir;
	fullFN.addPathSep(outFN);
	bgDeleteFile (fullFN.getAsPath());
	xmlTree.saveToFile(fullFN.getAsPath(), CP_ASCII);

	getConfig()->addKeyValuePair("GameName", BWDFN);
	getConfig()->addKeyValuePair("StartMethod", "StartLevel");
	getConfig()->addKeyValuePair("GamePath", outputDir);

	BGCPString xmlFN = "levels";
	xmlFN.addPathSep();
	xmlFN += BWDFN;
	xmlFN.addPathSep();
	xmlFN += outFN;
	getConfig()->addKeyValuePair("LevelFN", xmlFN);
	RETURN;
}

void ArchiveViewer::exportXEL ()
{
	SET_SCOPE;
	MemoryBlock block;
	if (getSelectedFile(block)) {
		if (MechWarriorIIPRJ::loadXEL(block, mPal, texture)) {
			BGCPString outFN;
			if (getSaveFileName(outFN)) {
				bgDeleteFile(outFN.getAsPath());
				TextureDim w = texture.getWidth();
				TextureDim h = texture.getHeight();
				texture.saveToFile(outFN.getAsPath(), 0, 0, w, h);
			}
		} else {
			messageBox(getVersionStr(), "Unsupported image");
		}
	}
	RETURN;
}

void ArchiveViewer::exportTEX ()
{
	SET_SCOPE;
    MemoryBlock block;
    if (getSelectedFile(block)) {
        if (MechWarriorIIPRJ::loadTEX(block.getPtr<unsigned char>(), block.getSize(), texture)) {
            BGCPString outFN;
            if (getSaveFileName(outFN)) {
                bgDeleteFile(outFN.getAsPath());
                TextureDim w = texture.getWidth();
                TextureDim h = texture.getHeight();
                texture.saveToFile(outFN.getAsPath(), 0, 0, w, h);
            }
        }
    }
	RETURN;
}

void ArchiveViewer::exportEFA (int format)
{
	SET_SCOPE;
    MemoryBlock block;
    if (getSelectedFile(block)) {
        MemoryBlock* pcx = LZdecode(&block);
        if (pcx != nullptr) {
            BGCPString outFN;
            if (getSaveFileName (outFN)) {
                bgDeleteFile (outFN.getAsPath());
                if (format == ContextMenu_EFA2PCX) {
                    pcx->saveToFile (outFN.getAsPath());
                } else {
                    if (MechWarriorIIPRJ::loadPCX(pcx->getPtr<unsigned char>(), pcx->getSize(), texture, mPal)) {
                        texture.saveToFile(outFN.getAsPath(), 0, 0, texture.getWidth(), texture.getHeight());
                    }
                }
            }
            delete pcx;
        }
    }
	RETURN;
}

// Decode CD-DA to wav
void ArchiveViewer::exportCddaAsWave()
{
	SET_SCOPE;
	MemoryBlock block;
	if (getSelectedFile(block)) {
		BGCPString outFN;
		if (getSaveFileName (outFN)) {
			Sound track;
			track.decodeCDDA(block);
			track.saveToWave(outFN.getAsPath());
		}
	}
	RETURN;
}

void ArchiveViewer::exportFntAsBmp()
{
	SET_SCOPE;
	MemoryBlock block;
	if (getSelectedFile(block)) {
		BGCPString outFN;
		if (getSaveFileName (outFN)) {
			Texture texture;
			decodeFNT(block, texture);
			texture.saveToFile(outFN.getAsPath(), 0, 0, texture.getWidth(), texture.getHeight());
		}
	}
	RETURN;
}

void ArchiveViewer::exportMW3_555AsBmp()
{
	SET_SCOPE;
	MemoryBlock block;
	if (getSelectedFile(block)) {
		BGCPString outFN;
		if (getSaveFileName (outFN)) {
			Texture texture;
			Decode_MW3_555(block.getPtr<unsigned char>(), block.getSize(), texture, true);
			texture.saveToFile(outFN.getAsPath(), 0, 0, texture.getWidth(), texture.getHeight());
		}
	}
	RETURN;
}

// Export selected file from MW3 gamez.zbd as obj
// todo: texture and uv support requires working with the materials ...
void ArchiveViewer::exportMW3GameZMeshAsObj()
{
	SET_SCOPE;
	MW3GameZArchive* gameZArchive = dynamic_cast<MW3GameZArchive*>(mwArchive);
	if (gameZArchive == nullptr) {
		RETURN;
	}

	BGCPString fileNoStr;
	size_t fileNum = PRJTable->getSelectedRow();
	mwArchive->getEntryName(fileNum, fileNoStr);
	auto fileType = mwArchive->getFileType(fileNum);
	fileNoStr = fileNoStr.copyPart(4, 10);
	auto fileNo = fileNoStr.toInt();
	fileNo += 3; // account for the fake files ...

	Mesh mesh;
	if (fileType == MW3GameZMesh) {
		gameZArchive->decodeGameZMesh((size_t)fileNo, &mesh);
	} else if (fileType == MW3PMGameZMesh) {
		gameZArchive->decodeGameZPMMesh((size_t)fileNo, &mesh);
	}
	BGCPString outFN;
	if (getSaveFileName (outFN)) {
		mesh.saveToObj(outFN.getAsPath(), true);
	}
	RETURN;
}

// Export MW3 zreader archive as XML (entry point)
void ArchiveViewer::exportMW3ZReaderAsXml()
{
	SET_SCOPE;
	MW3ReaderZbdArchive* readerArchive = dynamic_cast<MW3ReaderZbdArchive*>(mwArchive);
	if (readerArchive == nullptr) {
		RETURN;
	}

	XMLTree xml;
	XMLTreeIterator xmlIter(&xml, xml.getRoot());
	size_t newChild = xmlIter.addChild("reader");
	xmlIter.goToChild(newChild);
	size_t offset = 0x10; // skip header???
	size_t fileSize = readerArchive->getFileSize(0);
	while (offset < fileSize) {
		readerArchive->exportMW3ZReaderAsXmlRec(offset, xmlIter, 0);
	}

	BGCPString outFN;
	if (getSaveFileName (outFN)) {
		xml.saveToFile(outFN.getAsPath(), CP_ASCII);
	}

	RETURN;
}

// Was used previously to create new WTB files that the MW2 sim would accept
MemoryBlock* createWTB (Mesh* mesh)
{
	return nullptr;

	/*    if (mesh->getPointCount() > 32767 || mesh->getPolygonCount() > 32768) {
        return nullptr;
    }

    WORD color = 27648; //128;

    // Compute WTB size
    size_t wtbSize = 32 + mesh->getPointCount() * 16;

    for (size_t i = 0; i < mesh->getPolygonCount(); ++i) {
        MeshPolygon* poly = mesh->getPolygon(i);
        if (poly->getPointCount() < 5)
            wtbSize += 12;
        else
            wtbSize += 18;
    }

    MemoryBlock* wtb = new MemoryBlock(wtbSize);

    // Fill WTB header
    memcpy (wtb->getPtr<char>(), "WTBO", 4);
    wtb->fill(4, 20, 0); // unknown fields
    wtb->setWord(0x18, (WORD) mesh->getPointCount());
    wtb->setWord(0x1a, (WORD) mesh->getPolygonCount());
    wtb->setDWord(0x1c, 2); // 31stcc, BM1_HEAD: 3

    // Copy vertices
    size_t offset = 32;
    for (size_t i = 0; i < mesh->getPointCount(); ++i) {
        Point3D p; 
        mesh->getPoint(i, p);
        wtb->setDWord(offset, (DWORD) p.x);
        wtb->setDWord(offset+4, (DWORD) p.y);
        wtb->setDWord(offset+8, (DWORD) p.z);
        WORD u = random(0, 128);
        WORD v = random(0, 128);
        wtb->setWord(offset+12, u);
        wtb->setWord(offset+14, v);
        offset += 16;
    }

    // Copy polygons
    for (size_t i = 0; i < mesh->getPolygonCount(); ++i) {
        MeshPolygon* poly = mesh->getPolygon(i);
        if (poly->getPointCount() < 8) {
            wtb->setWord(offset, color);
            wtb->setWord(offset+2, (WORD) poly->getPointCount());

            for (size_t j = 0; j < poly->getPointCount(); ++j) {
                wtb->setWord(offset+4+2*j, (WORD) poly->getPoint(j));
            }

            if (poly->getPointCount() < 5)
                offset += 12;
            else
                offset += 18;
        }
    }

    return wtb;*/
}

// Replace a WTB file inside a PRJ with an external OBJ file
void ArchiveViewer::replaceWTBwithOBJ()
{
	BGCPString objFN, outFN;
	if (getOpenFileName(objFN)) {
		MemoryBlock inFile;
		Mesh mesh;
		if (mesh.loadFromObj(objFN.getAsPath())) {
			MemoryBlock* wtb = createWTB (&mesh);
			if (wtb != nullptr) {
				outFN = objFN;
				outFN += ".WTB";
				wtb->saveToFile(outFN.getAsPath());
				delete wtb;
			}
		}
	 }
}

///////////////////////////////////////////////////////////////////////////////
// Export filters

void ArchiveViewer::exportWTB()
{
	MemoryBlock block;
	if (getSelectedFile(block)) {
		Mesh mesh;
		if (MechWarriorIIPRJ::loadWTB(&block, &mesh, 0.01)) {
			mesh.calculateNormals();
			Point3D min, max;
			if (!mesh.getMinCoords(min) || !mesh.getMaxCoords(max)) {
				COUT << "Refuse to export an empty mesh" << std::endl;
				return;
			}
			//Point3D min = mesh.getMinCoords();
			//Point3D max = mesh.getMaxCoords();
			meshCenter = 0.5 * (max + min);
			viewDist = 2 * (max - min).length();

			BGCPString outFN;
			if (getSaveFileName(outFN)) {
				bgDeleteFile(outFN.getAsPath());
				mesh.saveToObj(outFN.getAsPath());
			}
		} else {
			BGCPString fn;
			size_t fileNum = PRJTable->getSelectedRow();
			mwArchive->getEntryName(fileNum, fn);
			BGCPString msg = "Error exporting ";
			msg += fn;
			displayMsg(msg);
		}
	}
}

void ArchiveViewer::exportSFL ()
{
	SET_SCOPE;
    MemoryBlock block;
    if (getSelectedFile(block)) {
		Sound sound;
		sound.decodeSFL(block);
        BGCPString outFN;
        if (getSaveFileName(outFN)) {
            bgDeleteFile(outFN.getAsPath());
            sound.saveToWave(outFN.getAsPath());
        }
    }
	RETURN;
}

void ArchiveViewer::exportSHP(const Palette& pal)
{
	SET_SCOPE;
    MemoryBlock block;
    if (getSelectedFile(block)) {
        if (MechWarriorIIPRJ::loadSHP(block, 0, mPal, texture)) {
            BGCPString outFN;
            if (getSaveFileName (outFN)) {
                bgDeleteFile (outFN.getAsPath());
                TextureDim w = texture.getWidth();
                TextureDim h = texture.getHeight();
                texture.saveToFile(outFN.getAsPath(), 0, 0, w, h);
            }
        } else {
            messageBox(getVersionStr(), "Unsupported image");
        }
    }
	RETURN;
}

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

void ArchiveViewer::viewMesh (const MemoryBlock* mb, const PathChar* /*fileName*/)
{
    if (mesh != nullptr) {
        delete mesh;
	}

    mesh = new Mesh();
    if (MechWarriorIIPRJ::loadWTB(mb, mesh, 1.0)) {
        mesh->calculateNormals();
        Point3D min, max;
		mesh->cacheTransformedVertices(Matrix());
        if(mesh->getMinCoords(min) && mesh->getMaxCoords(max)) {
	        meshCenter = 0.5*(max+min);
	        viewDist = 2*(max-min).length();
		}
    } else {
        BGCPString fn;
        size_t fileNum = PRJTable->getSelectedRow();
        mwArchive->getEntryName (fileNum, fn);
        BGCPString msg = "Error opening ";
        msg += fn;
        displayMsg(msg);
    }
}

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

void ArchiveViewer::viewObj(const PathChar* fileName)
{
	if (mesh != nullptr) {
		delete mesh;
	}

	mesh = new Mesh();
	if (mesh->loadFromObj(fileName)) {
		mesh->calculateNormals();
		Point3D min, max;
		mesh->cacheTransformedVertices(Matrix());
		if(mesh->getMinCoords(min) && mesh->getMaxCoords(max)) {
			meshCenter = 0.5*(max+min);
			viewDist = 2*(max-min).length();
		}
	} else {
		BGCPString fn;
		size_t fileNum = PRJTable->getSelectedRow();
		mwArchive->getEntryName (fileNum, fn);
		BGCPString msg = "Error opening ";
		msg += fn;
		displayMsg(msg);
	}
}

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

void ArchiveViewer::showMech (const MemoryBlock* mb, const PathChar* fileName)
{
    BipedMech* mech = new BipedMech();
    mech->loadFromMW2MEK(mb);

    // Print mech data
    BGCPString str (fileName, CP_SYSTEM);
    str += " (";
    str += BGCPString::fromDouble(mech->getWeight());
    str += "t/";
    str += BGCPString::fromInt(mech->getMaxWeight());
    str += "t)";

    label->setTitle(str);

    str.print();
    COUT << std::endl;

    int heatSinks = mech->getInteger (BipedMech::Info_Heat_Sinks);
    WeaponType HeatSinkType = mech->getHeatSinkType();
    if (HeatSinkType == WeaponType::IS_Heat_Sink)
        printf ("Heat Sinks: %i\n", heatSinks);
    else
        printf ("Heat Sinks: %i (%i)\n", heatSinks, 2*heatSinks);

    int walkingHexes = mech->getInteger (BipedMech::Info_WalkingHexes);
    int runningHexes = mech->getInteger (BipedMech::Info_RunningHexes);
    int jumpJets = mech->getInteger (BipedMech::Info_JumpJets);
    printf ("Walking: %i, Running: %i, Jumping: %i\n", walkingHexes, 
        runningHexes, jumpJets);

    int EngineType = mech->getInteger (BipedMech::Info_EngineType);
    //int tonnage = mech->getInteger (BipedMech::Info_Tonnage);
    int EngineRating = mech->getInteger (BipedMech::Info_EngineRating);
    if (EngineType == ENGINE_TYPE_STD)
        printf ("Engine: %i\n", EngineRating);
    else 
        printf ("Engine: %iXL\n", EngineRating);

    WeaponType InternalStructureType = mech->getInternalStructureType();
    if (InternalStructureType != WeaponType::Unused_Critical_Slot)
        printf ("Endo Steel\n");
    int ArmorType = mech->getInteger (BipedMech::Info_Armor_Type);
    if (ArmorType == ARMOR_TYPE_FERRO_FIBROUS)
        printf ("Ferro Fibrous\n");

    printf ("\nSection: <Front Armor> <Rear Armor> <Internal Structure>\n");
    for (int i = 0; i < Biped_Mech_Sections; i++) {
        mech->getSectionName(i).print();
        printf (": %i, %i, %i\n", 
            mech->getArmor(i, Vehicle::Armor_Front), 
            mech->getArmor(i, Vehicle::Armor_Rear), mech->getInternalStructure(i));
    }
    printf ("\n");

    // Print criticals
    for (int i = 0; i < Biped_Mech_Sections; i++) {
        mech->getSectionName(i).print();
        COUT << std::endl;
        for (int j = 0; j < mech->getCriticalCount (i); j++) {
            WeaponType critical = mech->getCriticals(i, j);
            const MWEquipment* w = getWeaponStats(critical);
            if (w != nullptr) {
                if (j < 9)
                    printf (" ");
                printf ("  %i: %s (%ft)\n", j+1, w->getLongName(), 
                    w->TonnageTimes100/100.0);
                /*if (techType == Biped_Mech_Tech_Type_IS_Level_1)
                printf (" (IS1)");
                else if (techType == Biped_Mech_Tech_Type_IS_Level_2)
                printf (" (IS2)");
                else if (techType == Biped_Mech_Tech_Type_IS_Clan)
                printf (" (Clan)");
                printf (", code: %i\n", ID);*/
            }
        }
    }
    printf ("\n");
    delete mech;
}

bool ArchiveViewer::playWAV (const MemoryBlock* mb)
{
	SET_SCOPE;
	Sound sound;
	if (!sound.decodeWAV(*mb)) {
		RETURN false;
	}
	queueSound(sound);
	RETURN true;
}

///////////////////////////////////////////////////////////////////////////////
// Called to set new layout

void ArchiveViewer::setGeometry (int /*left*/, int /*top*/, int newWidth, int newHeight)
{
	GLWindow::setGeometry (0, 0, newWidth, newHeight);

	if (PRJTable != nullptr) {
		int width = newWidth-margin-PRJTable->getLeft();
		if (width > 250)
			width = 250;
		PRJTable->setGeometry (PRJTable->getLeft(), PRJTable->getTop(),
			width, newHeight-120);
	}
}

///////////////////////////////////////////////////////////////////////////////
// Receive messages

void ArchiveViewer::receiveMessage (VirtualClass* sender, int /*MessageType*/, VirtualClass* data)
{
	GLWindow* win = dynamic_cast <GLWindow*> (sender);
	Font* font = getDefaultFont();
    if (win != nullptr) {
        switch (win->userData) {
            case MW2PRJ_upDirButtonID:
                mwArchive->leaveDir();
                PRJTable->setRowHeights(mwArchive->getEntryCount(), font->getCharHeight()+5);
                PRJTable->clearSelection();
                break;
            case MW2PRJ_TableID: // Selection changed
            {
                int row = PRJTable->getSelectedRow();
                if (mwArchive->isDirectory (row)) {
                    mwArchive->enterSubDir(row);
                    PRJTable->setRowHeights(mwArchive->getEntryCount(), font->getCharHeight()+5);
                    PRJTable->clearSelection();
                } else {
                    viewFile();
                }
            }
            break;
            case MW2PRJ_ExitButtonID:
                getMechVm()->restore();
                getMechVm()->deleteLater((VirtualClass*)this);
                break;
        }
    } else {
        GLContextMenuEntry* entry = dynamic_cast <GLContextMenuEntry*> (sender);
        if (entry != nullptr) {
            switch (entry->ID) {
                case ContextMenu_SaveAs:
                {
                    // Context menu: Save As ...
                    MemoryBlock block;
					if (getSelectedFile(block)) {
                        BGCPString oldFN;
                        getSelectedFileName (oldFN);
                        BGCPString FN = oldFN;
                        getSaveFileName (FN);
                        block.saveToFile(FN.getAsPath());
                    }
                }
                break;
                case ContextMenu_ReplaceWith:
                    if (mwArchive->canAddFilesToArchive()) {
                        BGCPString fn;
                        if (getOpenFileName (fn)) {
                            size_t fileNum = PRJTable->getSelectedRow();
                            mwArchive->replaceFile (fileNum, fn.getAsPath());
                            delete mwArchive;
                            mwArchive = nullptr;
                            delete PRJTable;
                            clear();
                            getMechVm()->createToolbar(this);
                        }
                    }
                    break;
                case ContextMenu_AddFiles:
                {
                    BGCPString fn;
                    if (getOpenFileName (fn)) {
                        mwArchive->addFileToArchive (fn.getAsPath());
                        delete mwArchive;
                        mwArchive = nullptr;
                        delete PRJTable;
                        clear();
                        getMechVm()->createToolbar(this);
                    }
                }
                break;
                case ContextMenu_SHP2BMP:
                    exportSHP(mPal);
                    break;
                case ContextMenu_XEL2BMP:
                    exportXEL();
                    break;
                case ContextMenu_SFL2WAV:
                    exportSFL();
                    break;
                case ContextMenu_WTB2OBJ:
                    exportWTB();
                    break;
                case ContextMenu_BWD2XML:
                    exportBWD(false);
                    break;
                case ContextMenu_BWD2XML_rec:
                    exportBWD(true);
                    break;
                case ContextMenu_ImportLevel:
                    importLevel();
                    break;
                case ContextMenu_TEX2BMP:
                    exportTEX();
                    break;
                case ContextMenu_EFA2PCX:
                    exportEFA(ContextMenu_EFA2PCX);
                    break;
                case ContextMenu_EFA2BMP:
                    exportEFA(ContextMenu_EFA2BMP);
                    break;
                case ContextMenu_OBJ2WTB:
                    replaceWTBwithOBJ();
                    break;
				case ContextMenu_CDDA2WAV:
					exportCddaAsWave();
					break;
				case ContextMenu_FNT2BMP:
					exportFntAsBmp();
					break;
				case ContextMenu_MW3GameZMesh2OBJ:
					exportMW3GameZMeshAsObj();
					break;
				case ContextMenu_ExportZReaderAsXML:
					exportMW3ZReaderAsXml();
					break;
				case ContextMenu_unpackAll:
					unpackPrj();
					break;
				case ContextMenu_MW3_ZBD2BMP:
					exportMW3_555AsBmp();
					break;
			}
		}
	}
}

// Render
void ArchiveViewer::render(int x, int y, int w, int h)
{
    // Render mesh
    if (mesh != nullptr) {
        // Set viewing projection
        glMatrixMode (GL_PROJECTION);
        glLoadIdentity ();
        //int w = getWidth();
        //int h = getHeight();
        gluPerspective (60, ((double) w) / ((double) h), viewDist*0.01, viewDist* 100);
        glMatrixMode (GL_MODELVIEW);

        // Render
        glColor3d(1, 1, 1);
        glViewport (220, 0, w-230, h);
        glLoadIdentity();
        gluLookAt (0, 0, viewDist,
            meshCenter.x, meshCenter.y, meshCenter.z, 
            0, 1, 0);
        //glTranslated(0, 0, 1000);
        glEnable(GL_NORMALIZE);
        glRotated (360*(GetTickCount()%2000)/2000.0, 0, 1, 0);

        // Activate light source
        glEnable (GL_LIGHTING);
        glEnable (GL_COLOR_MATERIAL);
        //glEnable (GL_TEXTURE_2D);
        //glFrontFace (GL_CCW);
        //glDisable (GL_LIGHT_MODEL_TWO_SIDE);
        //glCullFace (GL_BACK);
        //glEnable (GL_TEXTURE_2D);

        GLfloat pos[4] = {0.0f, 1.0f, 0.0f, 0.0f};
        GLfloat color[3] = {0.7f, 0.7f, 0.7f};
        GLfloat ambient[3] = {0.3f, 0.3f, 0.3f};
        GLfloat k0 = 1.0;
        GLfloat k1 = 0.0;
        GLfloat k2 = 0.0;

        glEnable (GL_LIGHT0);
        glLightfv (GL_LIGHT0, GL_POSITION, pos);
        glLightfv (GL_LIGHT0, GL_AMBIENT, ambient);
        glLightfv (GL_LIGHT0, GL_DIFFUSE, color);
        glLightfv (GL_LIGHT0, GL_SPECULAR, color);
        glLightfv (GL_LIGHT0, GL_CONSTANT_ATTENUATION, &k0);
        glLightfv (GL_LIGHT0, GL_LINEAR_ATTENUATION, &k1);
        glLightfv (GL_LIGHT0, GL_QUADRATIC_ATTENUATION, &k2);

        glEnable (GL_DEPTH_TEST);
        mesh->render();
        glDisable (GL_LIGHT0);
        glDisable (GL_LIGHTING);
    }

    // Render texture
    if (texture.getWidth() > 0 && texture.getHeight() > 0) {
        texture.use();
        //int x = (int) (zoom * width / 2 + 200);
        //int y = (int) (zoom * height / 2 + 100);
        glDisable(GL_DEPTH_TEST);
        glEnable(GL_TEXTURE_2D);
        glColor3d(1.0, 1.0, 1.0);
        glViewport (0, 0, w, h);
        glLoadIdentity();
        glBegin(GL_POLYGON);
        texture.glTexCoord(0, 0);
        //glVertex2d (-0.5,-0.5);
        glVertex2d (-(double)(zoom * texture.getWidth()) / (double)(width), 
            -(double)(zoom * texture.getHeight()) / (double)(height));
        texture.glTexCoord(1, 0);
        //glVertex2d (0.5, -0.5);
        glVertex2d ( (double)(zoom * texture.getWidth()) / (double)(width), 
            -(double)(zoom * texture.getHeight()) / (double)(height));
        texture.glTexCoord(1,1);
        //glVertex2d (0.5, 0.5);
        glVertex2d ((double)(zoom * texture.getWidth()) / (double)(width), 
            (double)(zoom * texture.getHeight()) / (double)(height));
        texture.glTexCoord(0, 1);
        //glVertex2d (-0.5, 0.5);
        glVertex2d (-(double)(zoom * texture.getWidth()) / (double)(width), 
            (double)(zoom * texture.getHeight()) / (double)(height));
        glEnd();
    }

    GLWindowContainer::render(x, y, w, h);
}

////////////////////////////////////////////////////////////////////////////////
// Misc functions

void ArchiveViewer::displayMsg (const BGCPString& msg)
{
    msg.printLn();
    if (label != nullptr) {
        label->setTitle (msg);
    }
    getMechVm()->renderScene();
}

////////////////////////////////////////////////////////////////////////////////
// Keyboard event handler

void ArchiveViewer::keypressed (BGKey key, bool /*shiftHeld*/, bool /*ctrlHeld*/, bool down, bool repeat)
{
	if (mode == 1 && down) {
		MechWarriorIIPRJ::loadSHP(clipBlock, frame, mPal, texture);
		++frame;
	}
}

////////////////////////////////////////////////////////////////////////////////
// Provide audio via SDL callback

// Add sound to queue
void ArchiveViewer::queueSound(const Sound& sound)
{
	SET_SCOPE;
	SDL_LockMutex(mSoundMutex);

	Audio* audio = getAudio();
	const SDL_AudioSpec& deviceAudioSpec = audio->getAudioSpec();

	Sound* compatibleSound = new Sound();
	compatibleSound->copyConvert(sound, deviceAudioSpec);

	mQueuedSounds.push_back(compatibleSound);

	SDL_UnlockMutex(mSoundMutex);
	RETURN;
}

void ArchiveViewer::provideAudio(void* userdata, Uint8* stream, int len)
{
	if (stream == nullptr || len == 0)
		return;

	SET_SCOPE;
	SDL_LockMutex(mSoundMutex);

	// Check if a sound is over
	if (mQueuedSounds.size() > 0) {
		Sound* sound = mQueuedSounds[0];
		if (sound == nullptr || soundOffset >= sound->mData.getSize()) {
			safeDelete(mQueuedSounds[0]);
			soundOffset = 0;
			mQueuedSounds.erase(mQueuedSounds.begin());
		}
	}

	// Copy sound into output
	size_t bytesToPlay = 0;
	if (mQueuedSounds.size() > 0) {
		Sound* sound = mQueuedSounds[0];
		if (sound != nullptr) {
			bytesToPlay = std::min<size_t>(len, sound->mData.getSize() - soundOffset);
			char* p = sound->mData.getPtr<char>(soundOffset);
			SDL_memcpy(stream, p, bytesToPlay);
			soundOffset += bytesToPlay;
		}
	}

	// Set the rest to zero
	if (bytesToPlay < (size_t)len) {
		SDL_memset(stream+bytesToPlay, 0, len-bytesToPlay);
	}

	SDL_UnlockMutex(mSoundMutex);
	RETURN;
}

// Decode and view a mesh from a MW3 gameZ.ZBD
void ArchiveViewer::viewGameZMesh(size_t fileNum, int subType)
{
	SET_SCOPE;
	MW3GameZArchive* gameZArchive = dynamic_cast <MW3GameZArchive*>(mwArchive);
	if (gameZArchive == nullptr || fileNum > mwArchive->getEntryCount()) {
		RETURN;
	}

	safeDelete(mesh);
	mesh = new Mesh();

	if (gameZArchive->decodeGameZMesh(fileNum, mesh)) {
		mesh->calculateNormals();
		Point3D min, max;
		mesh->cacheTransformedVertices(Matrix());
		if(mesh->getMinCoords(min) && mesh->getMaxCoords(max)) {
			meshCenter = 0.5*(max+min);
			viewDist = 2*(max-min).length();
		}
	} else {
		displayMsg("Error opening MW3 gamez.zbd mesh");
	}

	RETURN;
}

// Not sure what exactly this is - it's not terrain data, as thought previously
/*void ArchiveViewer::viewMW3Terrain()
{
	SET_SCOPE;
	MW3GameZArchive* gameZArchive = dynamic_cast <MW3GameZArchive*>(mwArchive);
	if (gameZArchive == nullptr) {
		RETURN;
	}

	safeDelete(mesh);
	mesh = new Mesh();

	if (gameZArchive->decodeTerrain(mesh)) {
		mesh->calculateNormals();
		Point3D min, max;
		mesh->cacheTransformedVertices(Matrix());
		if(mesh->getMinCoords(min) && mesh->getMaxCoords(max)) {
			meshCenter = 0.5*(max+min);
			viewDist = 2*(max-min).length();
		}
	} else {
		displayMsg("Error opening MW3 gamez.zbd mesh");
	}

	RETURN;
}*/

