////////////////////////////////////////////////////////////////////////////////
// MechWarrior II PRJ file system
// Copyright Bjoern Ganster
// Developed since 2007
////////////////////////////////////////////////////////////////////////////////

#include "MechWarriorIIPRJ.h"
#include "Mesh.h"
#include "FileCache.h"

#include <iostream>

#define FILE_NUMBER FILE_NO_MECHWARRIOR_II_PRJ_CPP

////////////////////////////////////////////////////////////////////////////////
// Constructor

MechWarriorIIPRJ::MechWarriorIIPRJ()
: MWArchive("MechWarriorIIPRJ", MechWarriorIIPRJ_ID)
{
	m_offset = 0;
	m_mainDirEntries = NULL;
}

////////////////////////////////////////////////////////////////////////////////
// Destructor

MechWarriorIIPRJ::~MechWarriorIIPRJ()
{
	SET_SCOPE;
	close();
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Open file

bool MechWarriorIIPRJ::open (const PathChar* PRJFN, bool readOnly)
{
	SET_SCOPE;
	bool openedSuccessfully;
	if (readOnly) {
		openedSuccessfully = fc.openReadOnly(PRJFN);
	} else {
		openedSuccessfully = fc.openReadWrite(PRJFN);
	}

	if (!openedSuccessfully || fc.getFileSize() < 1024) {
		RETURN false;
	}

	// Read main directory
	SET_OP;
	bool MainDirReadSuccessfully = false;
	char sig[4];
	if (fc.getFileSize() > 4) {
		fc.read (0, sig, 4);
		if (memcmp (sig, "PROJ", 4) == 0) {
			if (!fc.read (0x18, (char*) (&m_mainDirEntryCount), 2))
				return false;
			size_t entrySize = 24*m_mainDirEntryCount;
			m_mainDirEntries = new MW2_PRJ_main_directory_entry[m_mainDirEntryCount];
			if (!fc.read (50, (char*) m_mainDirEntries, entrySize))
				return false;
			MainDirReadSuccessfully = readMainDir();
			m_dirHeaders.resize(m_mainDirEntryCount);
			m_dirEntries.resize(m_mainDirEntryCount);
		}
	}

	// Read subdirectory data
	if (!MainDirReadSuccessfully) {
		RETURN false;
	}

	SET_OP;
	for (size_t i = 0; i < m_mainDirEntryCount; i++) {
		size_t dirEntryOffset = m_mainDirEntries[i].dirOffset;
		MW2_PRJ_directory_header& dirHeader = m_dirHeaders[i];
		fc.read (dirEntryOffset, (char*) (&dirHeader), sizeof (dirHeader));
		size_t dirEntryCount = dirHeader.entries;
		MW2_PRJ_Directory_Entries* entries = new MW2_PRJ_Directory_Entries [dirEntryCount];
		fc.read (dirEntryOffset+sizeof(MW2_PRJ_directory_header), 
			(char*) entries, sizeof(MW2_PRJ_Directory_Entries)*(dirEntryCount));
		m_dirEntries[i] = entries;
	}
	RETURN true;
}

////////////////////////////////////////////////////////////////////////////////
// Close file

void MechWarriorIIPRJ::close()
{
	SET_SCOPE;
	m_offset = 0xffffFFFF;

	fc.close();

	if (m_mainDirEntries != NULL) {
		for (size_t i = 0; i < m_mainDirEntryCount; i++) {
			safeDelete(m_dirEntries[i]);
		}
		safeDelete(m_mainDirEntries);
	}
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Read main directory entries

bool MechWarriorIIPRJ::readMainDir()
{
	SET_SCOPE;
	clearFileEntries();
	m_offset = 0x18; // Marks main directory

	// Read main directory entry names
	for (int i = 0; i < ((int) m_mainDirEntryCount)-1; i++) {
		MW2_PRJ_main_directory_entry& entry = m_mainDirEntries[i];
		BGCPString str(entry.dirName, 4, CP_ASCII);
		addFile (str, 0, entry.dirOffset);
	}

	RETURN true;
}

////////////////////////////////////////////////////////////////////////////////
// Enter subdirectory

bool MechWarriorIIPRJ::enterSubDir (size_t index)
{
   if (index >= m_mainDirEntryCount) {
      return false;
   }

   SET_SCOPE;
   m_dirName.assign(m_dirHeaders[index].DirName, 4, CP_ASCII);
   clearFileEntries();

   // Load & check directory header
   m_offset = m_mainDirEntries[index].dirOffset;
   MW2_PRJ_directory_header& dirHeader = m_dirHeaders[index];

   if (dirHeader.entries > 0) {
      // Read entries
      size_t totalEntries = dirHeader.entries; // Count
      MW2_PRJ_Directory_Entries* entryBuf = m_dirEntries[index];
      MW2_PRJ_local_file_header localHeader;

      for (size_t i = 0; i < totalEntries; i++) {
         // Read file name from local file header
         // Do not filter out NULL entries, leads to problems for files
         // referenced by number in the PRJ or for replacing files!
         bool OK = false;
         if (entryBuf[i].offset > 0 
         &&  entryBuf[i].offset + entryBuf[i].size < fc.getFileSize())
         {
            if (fc.read(entryBuf[i].offset, (char*) (&localHeader), sizeof(localHeader)))
            {
               OK = true;
               BGCPString fn = localHeader.nameWithExt;
               addFile (fn, entryBuf[i].size-62, entryBuf[i].offset);
            } 
         }
         if (!OK) {
            addFile ("", 0, 0);
		 }
      }
   }
   RETURN true;
}

bool MechWarriorIIPRJ::enterSubDir (const char* newDirName)
{
   if (!m_dirName.empty()) {
      if (m_dirName == BGCPString(newDirName, CP_ASCII)) {
         return true; // already in that directory
	  } else {
         leaveDir();
	  }
   }

   BGCPString buf;
   size_t i = 0; 
   while (i < getEntryCount()) {
      getEntryName(i, buf);
      if (buf == newDirName) {
         enterSubDir (i);
         return true;
      } else
         i++;
   }

   return false;
}

////////////////////////////////////////////////////////////////////////////////
// Find directory

size_t MechWarriorIIPRJ::findDirectory(const char* dirName)
{
   size_t i = 0;
   while (i < m_mainDirEntryCount) {
      BGCPString testDirName(m_mainDirEntries[i].dirName, 4, CP_ASCII);
      if (testDirName == dirName) {
         return i;
      } else
         i++;
   }

   return m_mainDirEntryCount;
}

////////////////////////////////////////////////////////////////////////////////
// Get file name for a file in another directory w/ochanging into that directory

bool MechWarriorIIPRJ::getEntryNameFromOtherDir (const char* dirName, 
   size_t index, char* buf, size_t bufSize)
{
   size_t dirNum = findDirectory(dirName);
   if (dirNum == m_mainDirEntryCount)
      return false;

   // Get offset to local header
   MW2_PRJ_Directory_Entries& dirEntry = m_dirEntries[dirNum][index];
   MW2_PRJ_local_file_header localHeader;
   if (fc.read(dirEntry.offset, (char*) (&localHeader), sizeof(localHeader))) {
      bgstrcpy((char*) buf, (char*) (&localHeader.nameWithExt), bufSize);
      return true;
   } else
      return false;
}

////////////////////////////////////////////////////////////////////////////////
// Get file from another directory withoutchanging into that directory

MemoryBlock* MechWarriorIIPRJ::getFileFromOtherDir (const char* dirName, 
   size_t index)
{
	size_t dirNum = findDirectory(dirName);
	if (dirNum == m_mainDirEntryCount) {
		return nullptr;
    }

   // Get file offset
   size_t fileOffset = m_dirEntries[dirNum][index].offset;
   size_t fileSize = m_dirEntries[dirNum][index].size;

   // Read
   MemoryBlock* mb = new MemoryBlock (fileSize);
   if (!fc.read(fileOffset+sizeof(MW2_PRJ_local_file_header), 
                (char*) mb->getPtr<unsigned char>(), fileSize)) 
   {
      delete mb;
      return nullptr;
   } else {
      return mb;
    }
}

////////////////////////////////////////////////////////////////////////////////
// Leave subdirectory

bool MechWarriorIIPRJ::leaveDir()
{
   m_dirName.clear();
   return readMainDir();
}

////////////////////////////////////////////////////////////////////////////////
// Read bufSize bytes from file numbered index at offset into buf

bool MechWarriorIIPRJ::getFileChunk(size_t index, size_t readOffset, void* buf, 
                                    size_t bufSize)
{
   size_t startOfFileOffset = getFileOffset (index);
//   printf("Loaded %i bytes from offset %i\n", bufSize, startOfFileOffset);
   return fc.read(startOfFileOffset + readOffset + 62, (char*) buf, bufSize);
}

////////////////////////////////////////////////////////////////////////////////
// Load MW2 PRJ file into a MemoryBlock

bool MechWarriorIIPRJ::loadFile (const BGCPString& FN, MemoryBlock& mb)
//MemoryBlock* MechWarriorIIPRJ::loadFile (const BGCPString& FN)
{
   size_t entryCount = getEntryCount();
   size_t entryNum = entryCount+1;

   // Find index of file to load
   for (size_t i = 0; i < entryCount; i++) {
      BGCPString fnBuf;
      getEntryName (i, fnBuf);
      if (fnBuf == FN)
         entryNum = i;
   }

   // Load file
   bool result = false;
   if (entryNum < entryCount) {
      size_t size = getFileSize (entryNum);
	  mb.resize(size);
      if (getFileChunk(entryNum, 0, mb.getPtr<unsigned char>(0), size))
         result = true;
   }

   return result;
}

////////////////////////////////////////////////////////////////////////////////
// Save PRJ file to new file name

bool MechWarriorIIPRJ::saveToFile (const TCHAR* /*fn*/)
{
   return false; 
}

////////////////////////////////////////////////////////////////////////////////
// Replace numbered file in current directory

/*bool MechWarriorIIPRJ::setFile (size_t fileNum, const char* fileBuf, size_t fileSize)
{
   size_t fileOffset = getFileOffset (fileNum);
   //return fc.read(startOfFileOffset + readOffset + 62, (char*) buf, bufSize);
   return fc.write(fileOffset+62, fileBuf, fileSize);
}*/

///////////////////////////////////////////////////////////////////////////////
// Replace  file

bool MechWarriorIIPRJ::replaceFile (size_t fileNum, const PathChar* srcFN)
{
   MemoryBlock block;
   block.loadFromFile(srcFN);
   size_t oldSize = getFileSize (fileNum);
   BGCPString oldFN;
   getEntryName (fileNum, oldFN);
   DWORD oldOffset = (DWORD) getFileOffset (fileNum);

   if (fileNum >= getEntryCount())
      return false;

   if (block.getSize() <= oldSize) {
      printf ("Replacing " PathCharMod " with " PathCharMod " in-place\n", oldFN.getAsPath(), srcFN);
      fc.write(oldOffset+62, (char*) block.getPtr<unsigned char>(0), block.getSize());
   } else {
      printf ("Replacing " PathCharMod " by appending " PathCharMod "\n", oldFN.getAsPath(), srcFN);

      // Update directory entry
      MW2_PRJ_Directory_Entries entry;
      entry.offset = fc.getFileSize();
      entry.size = block.getSize()+62;
      size_t dirEntryOffset = m_offset + sizeof(MW2_PRJ_directory_header) + 
                              fileNum * sizeof(entry);
      fc.write(dirEntryOffset, (char*) (&entry), sizeof(entry));

      // Update old local file header and copy it to the end PRJ
      MW2_PRJ_local_file_header header;
      fc.read(oldOffset, (char*) (&header), sizeof(header));
      header.SizeOfThisFile = block.getSize() + 54;
      fc.append((char*) (&header), sizeof(header));

      // Append new file to the PRJ
      fc.append((const char*) block.getPtr<unsigned char>(0), block.getSize());
   }

   return true;
}

///////////////////////////////////////////////////////////////////////////////
// Add files to archive

void MechWarriorIIPRJ::addFileToArchive (const PathChar* FN)
{ 
   MemoryBlock block;
   if (!block.loadFromFile(FN)) {
      return;
   }

   // Load directory header
   MW2_PRJ_directory_header dirHeader;
   fc.read(m_offset, (char*) (&dirHeader), sizeof(dirHeader));

   // Fill local header for new file
   MW2_PRJ_local_file_header localFileHeader;
   localFileHeader.marker[0] = 'D';
   localFileHeader.marker[1] = 'A';
   localFileHeader.marker[2] = 'T';
   localFileHeader.marker[3] = 'A';
   localFileHeader.FileNumberInParentDir = (WORD)getEntryCount();

   size_t pos;
   bgstrrscan<PathChar>(FN, OSPathSep, bgstrlen(FN)-1, 0, pos);
   const PathChar* fnWOPath = FN+pos+1;
   bgstrcpy(localFileHeader.nameWithExt, fnWOPath, sizeof(localFileHeader.nameWithExt));
   bgstrcpy((char*) &localFileHeader.nameWithExt, fnWOPath, sizeof(localFileHeader.nameWithExt));
   bgstrcpy((char*) &localFileHeader.nameWithoutExt, fnWOPath, sizeof(localFileHeader.nameWithoutExt));
   replace (localFileHeader.nameWithoutExt, '.', (char) 0);
   bgstrcpy(localFileHeader.parentDir, m_dirName.getPtr<char>(), sizeof(localFileHeader.parentDir));
   localFileHeader.SizeOfThisFile = block.getSize();

   // Write local header
   size_t localHeaderOffset = fc.getFileSize();
   fc.append ((const char*) (&localFileHeader), sizeof (localFileHeader));

   // Append new file
   fc.append ((const char*) block.getPtr<unsigned char>(), block.getSize());

   // Write new directory header
   ++dirHeader.entries;
   DWORD newDirOffset = fc.getFileSize();
   fc.append ((char*) (&dirHeader), sizeof(dirHeader));

   // todo: Copy old directory entries and append new entry
   DWORD* entryBuf = new DWORD [2*dirHeader.entries];
   fc.read (m_offset+30, (char*) entryBuf, 8*(dirHeader.entries-1));
   entryBuf[2*dirHeader.entries-2] = localHeaderOffset;
   entryBuf[2*dirHeader.entries-1] = block.getSize();
   fc.append ((char*) entryBuf, 8*dirHeader.entries);
   delete entryBuf;

   // Find and update offset to new directory in the main directory table
   int MainDirEntryCount = 0; // Clear higher-order bits
   fc.read (m_offset, (char*) (&MainDirEntryCount), 2);
   MainDirEntryCount--;
   for (int i = 0; i < MainDirEntryCount; i++) {
      size_t dirOffsetInMainDir = 54 + 24*i;
      DWORD testedOffset;
      fc.read (dirOffsetInMainDir, (char*) (&testedOffset), sizeof (DWORD));
      if (testedOffset == m_offset) {
         fc.write (dirOffsetInMainDir, (const char*) (&newDirOffset), 4);
      }
   }
}

////////////////////////////////////////////////////////////////////////////////
// XEL file format

#ifndef TitanFarPatcher
bool MechWarriorIIPRJ::loadXEL(const MemoryBlock& mb, const Palette& pal, Texture& texture)
{
	SET_SCOPE;
	if (mb.getSize() < 4) {
		COUT << "XEL too small" << std::endl;
		RETURN false;
	}

	TextureDim xsize = mb.get<uint16_t>(0);
	TextureDim ysize = mb.get<uint16_t>(2);
	size_t size = xsize*ysize+4;

	if (mb.getSize() != size) {
		COUT << "Unexpected XEL file size, expected " << size << "but got " << mb.getSize() << std::endl;
		RETURN false;
	}

	texture.resize (xsize, ysize, GL_RGBA);
	for (TextureDim y = 0; y < ysize; y++) {
		for (TextureDim x = 0; x < xsize; x++) {
			unsigned char c = mb.get<uint8_t>(x+(ysize-y-1)*xsize+4);
			if (c != 0xFF) {
				uint8_t r, g, b;
				pal.getRGB(c, r, g, b);
				texture.setRGBA(x, y, 4*r, 4*g, 4*b, 255);
			} else
				texture.setRGBA(x, y, 255, 255, 255, 0);
		}
	}

	RETURN true;
}
#endif

////////////////////////////////////////////////////////////////////////////////
// TEX file introduced in MW2:Mercs

#ifndef TitanFarPatcher
bool MechWarriorIIPRJ::loadTEX (const unsigned char* buf, size_t bufSize, Texture& texture)
{
	if (bufSize < 20
		|| *((DWORD*) (buf)) != 0x32322e31)
	{
		return NULL;
	}

	SET_SCOPE;
	WORD format = *((WORD*) (&buf[4]));
	WORD xsize = *((WORD*) (&buf[6]));
	WORD ysize = *((WORD*) (&buf[8]));
	const size_t colorTableStart = 12;
	WORD colorTableSize = *((WORD*) (&buf[10]));
	const size_t colorEntrySize = 4;
	WORD texelStart = colorTableStart+colorEntrySize*colorTableSize;
	size_t fsize = texelStart+xsize*ysize;

	if (fsize != bufSize) {
		RETURN false;
	}

	SET_OP;
	texture.resize(xsize, ysize, GL_RGBA);
	for (unsigned int y = 0; y < ysize; y++) {
		for (unsigned int x = 0; x < xsize; x++) {
			auto col = buf[texelStart+y*xsize+x];
			if (col < colorTableSize) {
				size_t i = colorTableStart+colorEntrySize*col;
				auto b = buf[i];
				auto g = buf[++i];
				auto r = buf[++i];
				auto a = buf[++i];
				texture.setRGBA(xsize-x-1, ysize-y-1, r, g, b, a);
			}
		}
	}

	RETURN true;
}
#endif

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

#ifndef TitanFarPatcher
bool MechWarriorIIPRJ::loadSHP (const MemoryBlock& mb, size_t picNum, const Palette& defaultPalette, Texture& texture)
{
	// Read and check basic numbers
	if (mb.getSize() < 29) {
		return false;
	}

	// MW2 PCX have this header, other PCX may not ...
	SET_SCOPE;
	const unsigned char* buf = mb.getPtr<unsigned char>(0);
	if (buf[0] != '1'
	||  buf[1] != '.'
	||  buf[2] != '1'
	||  buf[3] != '0')
	{
		RETURN false;
	}

	size_t picCount = mb.get<DWORD>(4);
	picNum = picNum % picCount;
	if (12 + 16 * picNum >= mb.getSize())
	{
		RETURN false;
	}

	size_t frameStart = mb.get<DWORD>(8+8*picNum);
	if (frameStart + 24 >= mb.getSize()) {
		RETURN false;
	}

	size_t frameEndOffset = mb.getSize();
	if (picNum + 1 < picCount) {
		frameEndOffset = mb.get<DWORD>(16+8*picNum);
	}

	TextureDim xsize = mb.get<WORD>(frameStart+16);
	TextureDim ysize = mb.get<WORD>(frameStart+20);

	// Check header
	if (xsize > 0
	&&  ysize > 0
	&&  xsize < 1000
	&&  ysize < 1000)
	{
		// Load palette
		Palette pal;
		size_t palOffset = mb.get<DWORD>(12);
		if (palOffset > 0) {
			// Use in-file palette
			size_t colorCount = mb.get<uint8_t>(palOffset);
			const double colorScale = 2.0;
			for (size_t i = 0; i <= colorCount; i++)
			{
				unsigned char j = mb.get<uint8_t>(palOffset+4*i);
				uint8_t r = (char) (colorScale * mb.get<uint8_t>(palOffset+4*i+1));
				uint8_t g = (char) (colorScale * mb.get<uint8_t>(palOffset+4*i+2));
				uint8_t b = (char) (colorScale * mb.get<uint8_t>(palOffset+4*i+3));
				pal.setRGB(j, r, g, b);
			}
		} else {
			pal = defaultPalette;
		}

		// Init pixels
		xsize++; ysize++;
		texture.resize(xsize, ysize, GL_RGBA);
		TextureDim x = 0, y = 0;
		for (y = 0; y < texture.getHeight(); y++)
			for (x = 0; x < texture.getWidth(); x++)
				texture.setRGBA(x, y, 0, 0, 0, 0); // untouched areas stay transparent

		// Load pixels
		x = 0;
		y = texture.getHeight()-1;
		size_t offset = frameStart+24;
		while (y >= 0 && y < ysize && offset < frameEndOffset) {
			unsigned char ctrl = buf[offset];
			unsigned char data = buf[offset+1];

			if (ctrl == 0) {
				x = 0; y--; offset++;
			} else if (ctrl == 1) {
				// Jump forward - pixels stay transparent
				if (x + data < xsize) {
					x += data;
					offset += 2;
				} else {
					y -= (x + data) / xsize;
					x = 0;
				}
			} else if ((ctrl & 1) == 0) {
				// Write data (ctrl/2) times
				for (int j = 0; j < (ctrl >> 1); j++) {
					uint8_t r, g, b;
					uint8_t a = 255;
					pal.getRGB(data, r, g, b);
					texture.setRGBA(x, y, r, g, b, a);
					if (x < xsize) {
						x++;
					} else {
						x = 0;
						--y;
					}
				}
				offset += 2;
			} else {
				// Write (ctrl/2) bytes
				offset++;
				for (int j = 0; j < (ctrl >> 1); j++) {
					unsigned char c = buf[offset];
					uint8_t r, g, b;
					uint8_t a = 255;
					pal.getRGB(c, r, g, b);
					texture.setRGBA(x, y, r, g, b, a);
					x++;
					offset++;
				}
			}
		}

		RETURN true;
	} else {
		RETURN false;
	}
}
#endif

////////////////////////////////////////////////////////////////////////////////
// Read bufSize bytes from file numbered index at offset into buf

#ifndef TitanFarPatcher
bool MechWarriorIIPRJ::loadPCX (const unsigned char* buf, size_t bufSize, Texture& texture, Palette& pal)
{
	if (bufSize < 768 + 128) {
		return false; // must contain header and palette
	}

	int w = *((WORD*)(&buf[8]))  + 1;
	int h = *((WORD*)(&buf[10])) + 1;

	// Length of decoded scanlines should be even ...
	if ((w & 1) == 1)
		w++;

	// Load the palette
	memcpy(pal.values, buf+bufSize-768, 768);

	// Load the image
	texture.resize(w, h, GL_RGB);
	size_t offset = 128;
	int x = 0;
	int y = h - 1;
	while (y > 0 && offset < bufSize) {
		unsigned char v = buf[offset];
		offset++;
		if (v < 0xC0) {
			uint8_t r, g, b;
			pal.getRGB(v, r, g, b);
			texture.setRGB(x, y, r, g, b);
			x++;
			if (x >= w) {
				x = 0;
				y--;
			}
		} else {
			// Repeat byte x-C0h times
			unsigned char c = buf[offset++];
			uint8_t r, g, b;
			pal.getRGB(c, r, g, b);
			while (v > 0xc0) {
				texture.setRGB(x, y, r, g, b);
				x++;
				if (x >= w) {
					x = 0;
					y--;
				}
				if (y >= h)
					v = 0;
				else
					v--;
			}
		}
	}

	return true;
}
#endif

////////////////////////////////////////////////////////////////////////////////
// Read mesh from WTB
// MW2MechImporter::loadLimb is similar, but also supports textures, texture coordinates and texture managers

#ifndef TitanFarPatcher
bool MechWarriorIIPRJ::loadWTB(const MemoryBlock* block, Mesh* mesh, double scale)
{
	if (block->getSize() <= 32
	||  memcmp (block->getPtr<char>(0), "WTBO", 4) != 0)
	{
		printf ("Not a WTBO file?\n");
		return false;
	}

	bool flipPolygons = true; // Change polygon winding
	//bool flipPolygons = false; // Do not change polygon winding
	size_t pointRecordSize = 40;
	size_t pointBaseOffset = 32;
	size_t shortPolyRecordSize = 40;
	size_t longPolyRecordSize = 46;
	size_t longPolyRecordVertices = 5;
	size_t pointCountInPolygon = 30;
	size_t firstPointInPolygon = 32;

	size_t pointCount = block->get<WORD> (0x18);
	size_t polygonCount = block->get<WORD> (0x1a);
	size_t bc = pointCount*pointRecordSize + polygonCount*shortPolyRecordSize + pointBaseOffset;

	auto subtype = block->get<uint8_t>(0x1c);
	if (subtype > 0) {
		// Try alternate access constants
		pointRecordSize = 16;
		pointBaseOffset = 32;
		shortPolyRecordSize = 12;
		longPolyRecordSize = 18;
		longPolyRecordVertices = 5;
		pointCountInPolygon = 2;
		firstPointInPolygon = 4;
		bc = pointCount*pointRecordSize + polygonCount*shortPolyRecordSize + pointBaseOffset;
	}

	// Read vertices
	size_t offset = pointBaseOffset;
	size_t i = 0;
	while (i < pointCount && offset < block->getSize()) {
		double x = scale * block->get<int> (offset);
		double y = scale * block->get<int> (offset+4);
		double z = scale * block->get<int> (offset+8);
		mesh->addPoint(x, y, z);
		offset += pointRecordSize;
		++i;
	}

	// Read polygons
	auto zeroTexCoords = mesh->addTexCoords(0.0, 0.0);
	while (offset + shortPolyRecordSize <= block->getSize()
	&&     mesh->getPolygonCount() < polygonCount
	&&     offset < block->getSize())
	{
		size_t verticesInThisPoly = block->get<WORD> (offset+pointCountInPolygon);
		if (offset + 2*verticesInThisPoly+firstPointInPolygon <= block->getSize()
			&&  verticesInThisPoly > 2
			&&  verticesInThisPoly <= (longPolyRecordSize -firstPointInPolygon) / 2)
		{
			MeshPolygon* poly = mesh->addPolygon(verticesInThisPoly);
			for (size_t i = 0; i < verticesInThisPoly; i++) {
				size_t polyIndex = i;
				if (flipPolygons)
					polyIndex = verticesInThisPoly - i - 1;
				size_t vertexNum = block->get<WORD> (offset+firstPointInPolygon+i*2);
				if (vertexNum >= mesh->getVertexCount()) 
					vertexNum = 0;
				poly->setVertexIndex(polyIndex, vertexNum);

				// Retrieve uv coordinates
				// There is one pair of texture coordinates per vertex, but with the creation of texture atlases, 
				// we need to manage texture coordinates per polygon and per vertex
				size_t ti = zeroTexCoords;
				//if (manText != nullptr && manText->texture != nullptr) {
				size_t uvOffset = pointBaseOffset + vertexNum * pointRecordSize;
				int u = block->get<int16_t>(uvOffset + 12);
				int v = block->get<int16_t>(uvOffset + 14);
				//manText->checkUVs(u, v);
				//double ud = ((double)u) / ((double)manText->texture->getWidth());
				//double vd = ((double)v) / ((double)manText->texture->getHeight());
				ti = mesh->addTexCoords(u, v);
				//}
				poly->setTexCoordIndex(polyIndex, ti);
			}
		}

		// Find next polygon
		if (verticesInThisPoly < longPolyRecordVertices) {
			offset += shortPolyRecordSize;
		} else {
			offset += longPolyRecordSize;
		}
	}

	return true;
}
#endif

////////////////////////////////////////////////////////////////////////////////
// Export BWD file as XML

struct ObjectData {
	BGCPString mw2prj, description, destroyedGeometry;
	LocalTransform localTransform;
	int health, num;
	bool alternativeRep;
};

void dumpObjects(std::vector<ObjectData>& objectData, XMLTreeIterator& iter)
{
	// Export object data
	for (size_t i = 0; i < objectData.size(); ++i) {
		if (!objectData[i].alternativeRep) {
			size_t childNum = iter.addChild("object");
			iter.goToChild(childNum);

			iter.addAttribute("mw2prj", objectData[i].mw2prj);

			if (objectData[i].localTransform.sx != 1.0) {
				iter.addDoubleAttribute("sx", objectData[i].localTransform.sx);
			}
			if (objectData[i].localTransform.sy != 1.0) {
				iter.addDoubleAttribute("sy", objectData[i].localTransform.sy);
			}
			if (objectData[i].localTransform.sz != 1.0) {
				iter.addDoubleAttribute("sz", objectData[i].localTransform.sz);
			}
			if (objectData[i].localTransform.rx != 0) {
				iter.addDoubleAttribute("rx", -objectData[i].localTransform.rx);
			}
			if (objectData[i].localTransform.ry != 0) {
				iter.addDoubleAttribute("ry", -objectData[i].localTransform.ry);
			}
			if (objectData[i].localTransform.rz != 0) {
				iter.addDoubleAttribute("rz", -objectData[i].localTransform.rz);
			}
			if (objectData[i].localTransform.tx != 0) {
				iter.addDoubleAttribute("tx", objectData[i].localTransform.tx + 0.005, 2);
			}
			if (objectData[i].localTransform.ty != 0) {
				iter.addDoubleAttribute("ty", objectData[i].localTransform.ty + 0.005, 2);
			}
			if (objectData[i].localTransform.tz != 0) {
				iter.addDoubleAttribute("tz", objectData[i].localTransform.tz + 0.005, 2);
			}

			if (objectData[i].health > 0) {
				iter.addAttribute("health", objectData[i].health);
			}
			if (objectData[i].description != "none" && !objectData[i].description.empty()) {
				iter.addAttribute("description", objectData[i].description);
			}

			if (!objectData[i].destroyedGeometry.empty()) {
				iter.addAttribute("destroyedGeometry", objectData[i].destroyedGeometry);
			}

			iter.goToParent();
		}
	}

	objectData.clear();
}


#ifndef TitanFarPatcher
bool BWD2XML (const MemoryBlock& mb, XMLTreeIterator& iter, 
	bool followIncludes, const PathChar* WTB_outputDir, const LocalTransform* parentTransform,
	const BGCPString& BWDFN, MechWarriorIIPRJ* prj)
{
	// Check signature
	if (mb.get<uint8_t>(0) != 'B'
	||  mb.get<uint8_t>(1) != 'W'
	||  mb.get<uint8_t>(2) != 'D')
	{
		return false;
	}

	// Exclude stuff that never changes (BUL, XPLO, ...)
	if (BWDFN == "mw2_sht1" || BWDFN == "mw2_sht2") {
		return false;
	}

	size_t offset = 0x34;
	bool insideRepresentation = false;
	std::vector<ObjectData> objectData;
	double translationScale = 0.01;
	std::vector<LocalTransform> localTransforms;
	LocalTransform blkxTransform;

    // Visit all BWD tags and update XML tree 
    while (offset < mb.getSize()) {
        BGCPString tagName((const char*) mb.getPtr<unsigned char>(offset), 4, CP_ASCII);
        size_t tagSize = mb.get<DWORD>(offset+4);

        if (tagName == "BLK") {
			// Kallisti says the coordinates here only define a bounding box ...
            size_t childNum = iter.addChild("block");
            iter.goToChild(childNum);
        } else if (tagName == "BLKX") {
            size_t childNum = iter.addChild("transform");
            iter.goToChild(childNum);
            blkxTransform.sx = mb.get<int>(offset+8);
            blkxTransform.sy = mb.get<int>(offset+12);
            blkxTransform.sz = mb.get<int>(offset+16);
            blkxTransform.rx = mb.get<int>(offset+20) / 65536.0;
            blkxTransform.ry = mb.get<int>(offset+24) / 65536.0;
            blkxTransform.rz = mb.get<int>(offset+28) / 65536.0;
            blkxTransform.tx = 0.01 * mb.get<int>(offset+32);
            blkxTransform.ty = 0.01 * mb.get<int>(offset+36);
            blkxTransform.tz = 0.01 * mb.get<int>(offset+40);
			iter.goToParent();
        } else if ((tagName =="BMPJ"  && tagSize == 20)
		||         (tagName == "BMID" && tagSize == 12)) 
		{
			if (prj != nullptr) {
				if (prj->bmIdFn != BWDFN) {
					if (!prj->bmIdFn.empty()) {
						size_t childNum = iter.addChild("BitmapID");
						iter.goToChild(childNum);
						iter.addAttribute("filename", prj->bmIdFn);
						iter.goToParent();
					}
					prj->bmIdFn = BWDFN;
				}
			}
        /*} else if (tagName == "BTHG") {
            size_t childNum = iter.addChild("BTHG");
            iter.goToChild(childNum);
            int16 val = mb.get<WORD> (offset + 8);
            iter.addIntAttribute ("num", val);
            iter.goToParent();*/
        } else if (tagName == "ENDB") {
			dumpObjects(objectData, iter);
            iter.goToParent();
        } else if (tagName == "ENDR") {
            iter.goToParent();
            insideRepresentation = false;
        } else if (tagName == "INCL" && tagSize == 20) {
            BGCPString fn((const char*) mb.getPtr<unsigned char>(offset + 10), 9, CP_ASCII);
            if (followIncludes && prj != nullptr) {
                WORD fileNo = mb.get<WORD>(offset+8);
                MemoryBlock otherMB;
                if (prj->getFile(fileNo, otherMB)) {
                    //printf("Entering %s, %i\n", fn.getChars(), fileNo);
                    BWD2XML(otherMB, iter, true, WTB_outputDir, &blkxTransform, fn, prj);
                   //printf ("Done with %s, %i\n", fn.getChars(), fileNo);
                /*} else {
                    printf("Unable to get %s\n", fn.getChars());*/
                }
            } else {
                size_t childNum = iter.addChild("include");
                iter.goToChild(childNum);
                iter.addAttribute("file", fn);
                int16 val = mb.get<WORD> (offset + 8);
                iter.addIntAttribute ("num", val);
                iter.goToParent();
            }
        } else if (tagName == "MUSI") {
            size_t childNum = iter.addChild("music");
            iter.goToChild(childNum);
			auto track = mb.get<uint16_t>(offset+8);
			iter.addAttribute("track", track);
            iter.goToParent();
        } else if (tagName == "NAVP") {
            size_t childNum = iter.addChild("nav");
            iter.goToChild(childNum);
			auto navpx = mb.get<int32_t>(offset+8);
			auto navpy = mb.get<int32_t>(offset+12);
			auto navpz = mb.get<int32_t>(offset+16);
			auto groupID = mb.get<uint16_t>(offset+16);
			const char* description = mb.getPtr<char>(offset+36);
			iter.addDoubleAttribute("x", 0.01 * navpx + 0.005, 2);
			iter.addDoubleAttribute("y", 0.01 * navpy + 0.005, 2);
			iter.addDoubleAttribute("z", 0.01 * navpz + 0.005, 2);
			iter.addIntAttribute("group", groupID);
			iter.addAttribute("description", description);
            iter.goToParent();
        } else if (tagName == "OBJ") {
			ObjectData newObj;
            int16 fileNum = mb.get<int16_t>(offset+56);
            char buf[50];
			if (prj != nullptr) {
				prj->getEntryNameFromOtherDir ("POLY", fileNum, buf, 50);

				// Export WTB file?
				if (WTB_outputDir != NULL) {
					MemoryBlock* mb = prj->getFileFromOtherDir("POLY", fileNum);
					if (mb != NULL) {
						Mesh mesh;
						if (prj->loadWTB(mb, &mesh, 0.01)) {
							// We were able to decode the OBJ file, so save it
							BGCPString objFN = buf;
							objFN += ".obj";
							BGCPString objFNwithPath(WTB_outputDir, CP_SYSTEM);
							objFNwithPath.addPathSep(objFN);
							mesh.saveToObj(objFNwithPath.getAsPath());

							// Export fixed mesh using fix algorithm 2
							Mesh mesh2;
							mesh2.copyMesh(&mesh);
							mesh2.findMeshEdges();
							if (mesh2.fillHoles2()) {
								objFNwithPath = BGCPString(WTB_outputDir, CP_SYSTEM);
								objFNwithPath.addPathSep(buf);
								objFNwithPath += "-fixed2.obj";
								mesh2.saveToObj(objFNwithPath.getAsPath());
							}

							// Export fixed mesh using fix algorithm 2
							Mesh mesh3;
							mesh3.copyMesh(&mesh);
							mesh3.findMeshEdges();
							if (mesh3.fillHoles3(Point3D(0.0, -10.0, 0.0))) {
								objFNwithPath = BGCPString(WTB_outputDir, CP_SYSTEM);
								objFNwithPath.addPathSep(buf);
								objFNwithPath += "-fixed3.obj";
								mesh3.saveToObj(objFNwithPath.getAsPath());
							}

							// Export mesh that should not have holes ...
							mesh.findMeshEdges();
							if (mesh.fillHoles()) {
								objFNwithPath = BGCPString(WTB_outputDir, CP_SYSTEM);
								objFNwithPath.addPathSep(buf);
								objFNwithPath += "-fixed.obj";
								mesh.saveToObj(objFNwithPath.getAsPath());
							}
						} else {
							// Decoding the WTB file failed, so store it for analysis
							log("Error exporting ", BGCPString(buf, CP_ASCII));
							BGCPString wtbFN(WTB_outputDir, CP_SYSTEM);
							wtbFN.addPathSep(buf);
							mb->saveToFile(wtbFN.getAsPath());
						}
						delete mb;
					}
				}
			}

			newObj.num = mb.get<int16_t>(offset+8);
			newObj.mw2prj = BGCPString(buf);
			newObj.alternativeRep = false;

			// Check if relative placement is needed ...
			int rel = mb.get<int16_t>(offset+10);
			//iter.addAttribute("rel", rel);
			if (rel >= 0) {
				for (size_t i = 0; i < localTransforms.size(); ++i) {
					if (localTransforms[i].ID == rel) {
						newObj.localTransform = localTransforms[i];
					}
				}
			} else if (parentTransform != nullptr) {
				newObj.localTransform = *parentTransform;
			}
			newObj.localTransform.ID = newObj.num;

            newObj.localTransform.sx *= mb.get<int32_t>(offset+14);
            newObj.localTransform.sy *= mb.get<int32_t>(offset+18);
            newObj.localTransform.sz *= mb.get<int32_t>(offset+22);

            newObj.localTransform.rx += mb.get<int32_t>(offset+26) / 65536.0;
            newObj.localTransform.ry += mb.get<int32_t>(offset+30) / 65536.0;
			newObj.localTransform.rz += mb.get<int32_t>(offset+34) / 65536.0;

            newObj.localTransform.tx += translationScale * mb.get<int32_t>(offset+38);
            newObj.localTransform.ty += translationScale * mb.get<int32_t>(offset+42);
            newObj.localTransform.tz += translationScale * mb.get<int32_t>(offset+46);

			localTransforms.push_back(newObj.localTransform);
			objectData.push_back(newObj);
        } else if (tagName == "OBJL") {
            size_t childNum = iter.addChild("ObjectLink");
            iter.goToChild(childNum);
            int16 val1 = mb.get<int16_t> (offset+8);
            int16 val2 = mb.get<int16_t> (offset+10);
            iter.addIntAttribute ("val1", val1);
            iter.addIntAttribute ("val2", val2);
            iter.goToParent();
        } else if (tagName == "PALG") {
            size_t childNum = iter.addChild("palette");
            iter.goToChild(childNum);
            iter.addAttribute ("filename", BWDFN);
            iter.goToParent();
        } else if (tagName == "ORDR") {
            size_t childNum = iter.addChild("order");
            iter.goToChild(childNum);
			const char* order = mb.getPtr<char>(offset+8);
			iter.addText(order);
            iter.goToParent();
        } else if (tagName == "REPR") {
            if (insideRepresentation) 
                iter.goToParent();
            else
                insideRepresentation = true;
            size_t childNum = iter.addChild("Representation");
            iter.goToChild(childNum);
        } else if (tagName == "SDSC") {
            size_t childNum = iter.addChild("limits");
            iter.goToChild(childNum);
			auto maxStarTonnage = mb.get<uint32_t>(offset+12);
			auto maxMechs = mb.get<uint32_t>(offset+20);
			iter.addAttribute("tonnage", maxStarTonnage);
			iter.addAttribute("mechs", maxMechs);
            iter.goToParent();
        } else if (tagName == "STAR") {
			size_t i = 0;
			while (i*24+8 < tagSize) {
				size_t childNum = iter.addChild("star");
				iter.goToChild(childNum);
				auto alliance = mb.get<uint32_t>(offset+i*24+8);
				auto disposition = mb.get<uint32_t>(offset+i*24+12);
				const char* formation = mb.getPtr<char>(offset+i*24+16);
				iter.addAttribute("alliance", alliance);
				iter.addAttribute("disposition", disposition);
				iter.addAttribute("formation", formation);
				iter.goToParent();
				++i;
			}
        } else if (tagName == "SUPS") {
            size_t childNum = iter.addChild("startupScreen");
            iter.goToChild(childNum);
			const char* fn = mb.getPtr<char>(offset+8);
			iter.addAttribute("fn", fn);
            iter.goToParent();
        } else if (tagName == "THNG") {
            size_t childNum = iter.addChild("THNG");
            iter.goToChild(childNum);
            DWORD val = mb.get<DWORD> (offset+8);
            iter.addIntAttribute ("value", val);
            iter.goToParent();
        /*} else if (tagName.equals("TSK")) {
            DWORD val = mb.get<DWORD> (offset+8);
            int16 val2 = mb.get<int16_t> (offset+8);
            if (val != 3 || val2 != 3) {
                size_t childNum = iter.addChild("TSK");
                iter.goToChild(childNum);
                BGString str;
                str.assignInt(val);
                iter.addText (str.getChars());
                str.assignInt(val2);
                iter.addText (str.getChars());
                iter.addText ((const char*) mb.getPtr<unsigned char>(offset+14));
                iter.goToParent();
            }*/
        /*} else if (tagName == "XPLO" && tagSize == 16) {
            size_t childNum = iter.addChild("explosion");
            iter.goToChild(childNum);
            for (int i = 0; i < 4; i++) {
                BGCPString attrStr = BGCPString("val") + BGCPString::fromInt(i);
                auto val = mb.get<WORD>(offset+8+2*i);
                iter.addIntAttribute(attrStr.get<char>(), val);
            }
            iter.goToParent();*/
        } else if (tagName == "GT") {
            //size_t childNum = iter.addChild("GameThing");
            //iter.goToChild(childNum);

            WORD objRef = mb.get<WORD>(offset+8);
            //iter.addAttribute("objref", objRef);
			ObjectData* refObjectData = nullptr;
			for (size_t i = 0; i < objectData.size(); ++i) {
				if (objectData[i].num == objRef) {
					refObjectData = &objectData[i];
				}
			}

			if (refObjectData != nullptr) {
				objRef = mb.get<WORD>(offset+10);
				if (objRef != 0xFFff) {
					for (size_t i = 0; i < objectData.size(); ++i) {
						if (objectData[i].num == objRef) {
							objectData[i].alternativeRep = true;
							refObjectData->destroyedGeometry = objectData[i].mw2prj;
						}
					}
				}

				refObjectData->health = mb.get<WORD>(offset+24);

				//WORD goalFlags = mb.get<WORD>(offset+26);
				//iter.addAttribute("goalFlags", goalFlags);

				//WORD chunkyFlag = mb.get<WORD>(offset+28);
				//iter.addAttribute("chunkyFlag", chunkyFlag);

				refObjectData->description = (char*) mb.getPtr<char>(offset+32);
			}
        } else if (tagName == "PLNT") {
            size_t childNum = iter.addChild("planet");
            iter.goToChild(childNum);

            double gravity = mb.get<DWORD>(offset+8) / 65536.0;
            iter.addDoubleAttribute("gravity", gravity);

			int temperatureInCelsius = mb.get<int32_t>(offset + 36);
			iter.addAttribute("temperature", temperatureInCelsius);

			// todo: there's more data here:
			// 12-15: atmosphere (unused?)
			// 16-19: days in a year
			// 20-23: seconds in a day
			// 24-27: axial tilt
			// 28-31: circumference - related to "cowardice will not be tolerated"?
			// and more ...

            iter.goToParent();
        } else if (tagName == "GPS") {
            size_t childNum = iter.addChild("spawn");
            iter.goToChild(childNum);

			auto gunnery = mb.get<WORD>(offset+16);
			auto piloting = mb.get<WORD>(offset+24);
			BGCPString chassisName(mb.getPtr<char>(offset+36), CP_ASCII);
			BGCPString configName(mb.getPtr<char>(offset+45), CP_ASCII);
			BGCPString targetText(mb.getPtr<char>(offset+54), CP_ASCII);
			BGCPString inspectionText(mb.getPtr<char>(offset+76), CP_ASCII);
			auto triggerRange = mb.get<WORD>(offset+20);
			auto range2 = mb.get<WORD>(offset+18);

			iter.addAttribute("description", targetText);
			iter.addAttribute("inspectionText", inspectionText);
			iter.addAttribute("chassis", chassisName);
			iter.addAttribute("config", configName);
			iter.addAttribute("triggerRange", triggerRange);
            iter.addAttribute("piloting", piloting);
            iter.addAttribute("gunnery", gunnery);

            iter.goToParent();
		} else if (tagName == "TSK"
		||         tagName == "FTBL"
		||         tagName == "BSEC"
		||         tagName == "BMEN"
		||         tagName == "BMPJ"
		||         tagName == "XPLO"
		||         tagName == "BTHG") 
		{
			// Ignored tags ...
        } else {
            size_t childNum = iter.addChild(tagName);
            iter.goToChild(childNum);
            iter.addIntAttribute ("length", tagSize);
            iter.goToParent();

            BGCPString str = "Unknown tag ";
            str += tagName;
            str += ", length ";
            str += BGCPString::fromInt (tagSize);
            str.printLn();
        }

        offset += tagSize;
    }

	if (prj != nullptr) {
		if (!prj->bmIdFn.empty()) {
			size_t childNum = iter.addChild("BitmapID");
			iter.goToChild(childNum);
			iter.addAttribute("filename", prj->bmIdFn);
			iter.goToParent();
			prj->bmIdFn.clear();
		}
	}

	dumpObjects(objectData, iter);

    return true;
}
#endif

////////////////////////////////////////////////////////////////////////////////
// Convert SFL to Wav file

#define MAX_BLOCKSIZE 256 // well, _i_ haven't seen one larger than this :P

// utility function.  linearly interpolates the string of (size)-many bytes in
// ptr to get a string of (size*2)-many bytes
// todo: one byte missing at the end?
void interpolate2(unsigned char* incoming, int size)
{
    unsigned char buffer[MAX_BLOCKSIZE * 4];
    unsigned char *bufferptr = buffer, *dataptr = incoming;

    // while we still have bytes left...
    while (dataptr+1 - incoming < size) {
        // ... interpolate
        *bufferptr = *dataptr;
        *(bufferptr+1) = (*dataptr + *(dataptr+1))/2;
        bufferptr += 2;
        dataptr++;
    }

    memcpy(incoming, buffer, size*2);

    return;
}

// same thing, but outputs a string of length (size*4) bytes
// todo: three bytes missing at the end?
void interpolate4(unsigned char* incoming, int size) {
    unsigned char buffer[MAX_BLOCKSIZE * 4];
    unsigned char *bufferptr = buffer, *dataptr = incoming;

    // while we still have bytes left...
    while (dataptr+1 - incoming < size) {
        // ... interpolate
        *bufferptr = *dataptr;
        *(bufferptr+1) = (*dataptr * 3 + *(dataptr+1))/4;
        *(bufferptr+2) = (*dataptr + *(dataptr+1))/2;
        *(bufferptr+3) = (*dataptr + *(dataptr+1) * 3)/4;
        bufferptr += 4;
        dataptr++;
    }

    memcpy(incoming, buffer, size*4);

    return;
}

// factored out code that does the table and dpcm decoding, look for invocation
// in the block decoding loop in sflx.
#define MAX_DELTAS      16      // 4 bits max means 16 deltas max
#define SILENCE         0x80    // pcm is shifted by 128, this is secretly zero
int process_dpcm(const unsigned char *inptr, unsigned char *frame, int framesize,
                 unsigned char* accumulator, int bits_per_word) {
    long deltas[MAX_DELTAS];
    int frame_index = 0;
    int ret = 0;

    // grab our deltas
    int delta_index;
    for (delta_index = 0; delta_index < (1 << bits_per_word); delta_index++) {
        deltas[delta_index] = (inptr[ret++] << 1) - SILENCE;
    }

    do {
        int word_index;
        int in_word = *(inptr + ret);
        ret++;

        for (word_index = 0; word_index < (8 / bits_per_word); word_index++) {
            *accumulator += (unsigned char)(deltas[(in_word >> word_index*bits_per_word) &
                       ((1 << bits_per_word) - 1)]);
            frame[frame_index++] = *accumulator;
        }
    } while (frame_index < framesize);

    return ret;
}

// a number of flag masks used in the sflx format
#define HALF_FRAME      0x40
#define QUARTER_FRAME   0x80
#define CONTROL_MASK    0x0f
#define CONTROL_ZEROES  0x00
#define CONTROL_NOP     0x01
#define CONTROL_RAW     0x05
#define CONTROL_DPCM1   0x02 // use one bit to choose a delta
#define CONTROL_DPCM2   0x03 // use two bits to choose a delta
#define CONTROL_DPCM4   0x04 // use four bits to choose a delta

// sits at the start of a sflx file.  the __attribute__ compiler tag is to
// signal gcc not to pad this out to 16 bytes.  don't know if there's an msvc
// equivalent, since that's what MechVM seems to be using.
#pragma pack(2) // MW2 file data are WORD aligned?
typedef struct {
    char title[4];
    long filesize;
    long block_count;
    short blocksize;
} SFLX_HEADER;

// decodes an SFLX file sitting in incoming, writes pcm to outgoing
// todo: move this to sound, but first: SFL2Wav needs to use Sound instead ...
char* sflx(const char *incoming, char *outgoing) 
{
    // keep track of where we're writing to and reading from
    unsigned char* outptr = (unsigned char*)outgoing;
    const unsigned char* inptr = (const unsigned char*)incoming;
    // used for the D in DPCM :)
    unsigned char accumulator = SILENCE;

    // decode the header to see how many blocks we're expecting
    SFLX_HEADER *header = (SFLX_HEADER*)incoming;
    long blocksize = header->blocksize;
    long iter = header->block_count;
    inptr += sizeof(SFLX_HEADER);

    // now we loop through the blocks
    for (int frameNo = 0; frameNo < header->block_count; ++frameNo) {
        unsigned char frame[MAX_BLOCKSIZE * 4];
        int framesize;
        int header = *inptr;

        // the basic structure of the block is to have a header at the front,
        // which contains info about ...
        //  1) what sample rate we're storing, and
        //  2) what decoding method to use.
        // this is the information that we strip out first.

        // use the header to decode how large we want our frame to be
		auto frameType = (header & CONTROL_MASK);
        if (header & HALF_FRAME) {
            framesize = blocksize / 2;
		} else if (header & QUARTER_FRAME) {
            framesize = blocksize / 4;
		} else {
            framesize = blocksize;
		}

        inptr++; // start looking at actual data
        switch (frameType) { // various decoding methods follow:
            case CONTROL_ZEROES:
                // this control code means we fill the frame with silence
                memset(frame, SILENCE, framesize);
                accumulator = SILENCE;
            break;

            case CONTROL_NOP:
                // don't do anything.
            break;

            // from here, all the dpcm blocks are similarly structured.
            // following their one-byte header, which contains information
            // about the granularity of the sound to follow (to be explained in
            // a moment), they contain a table of signed values we call deltas.
            // after we read in the delta table, all the data that follows is
            // broken down into 1, 2, or 4-bit "words".  each of these words
            // selects which delta value to use, which we add to whatever our
            // pcm was just sitting at (hence "delta") and then loop.

            case CONTROL_DPCM1:
                inptr += process_dpcm(inptr, frame, framesize, &accumulator, 1);
            break;

            case CONTROL_DPCM2:
                inptr += process_dpcm(inptr, frame, framesize, &accumulator, 2);
            break;

            case CONTROL_DPCM4:
                inptr += process_dpcm(inptr, frame, framesize, &accumulator, 4);
            break;

            case CONTROL_RAW:
                memcpy(frame, inptr, framesize);
                inptr += framesize;
                accumulator = frame[framesize - 1]; // +155 is a good value for AllPoints.sfl ...
            break;

            // shouldn't ever get here, i don't think.
            default:
                fprintf(stderr, "Error decoding SFLX file (%d).\n", *(inptr-1) & CONTROL_MASK);
            break;
        }

        // now we need to rescale if this segment was sampled at a lower rate
        if (header & HALF_FRAME)
            interpolate2(frame, blocksize);
        else if (header & QUARTER_FRAME)
            interpolate4(frame, blocksize);

        // finally, store the data to outptr
        memcpy(outptr, frame, blocksize);
        outptr += blocksize;
    }

    return (char*) outgoing;
}

MemoryBlock* MechWarriorIIPRJ::SFL2Wav (const MemoryBlock* mb)
{
	if (mb->getSize() < 20) {
		return nullptr;
	}

	DWORD blockCount = mb->get<DWORD>(8);
	size_t maxBlockSize = mb->get<WORD>(12);
	DWORD BitsPerSample = 8; // 8, 16, or 24
	WORD channels = 1; // 1 for mono, 2 for stereo
	DWORD SampleRate = 11025;
	//DWORD BlockAlign = SampleRate*BitsPerSample/8;
	WORD formatTag = 1;

	// Create header for copy in WAV format
	// http://de.wikipedia.org/wiki/WAV_(Format)
	MemoryBlock* wavBlock = new MemoryBlock(blockCount * maxBlockSize + 44);
	//ZeroMemory(wavBlock->getPtr(), wavBlock->getSize());
	wavBlock->fill (0, blockCount * maxBlockSize + 44, 0);
	wavBlock->write(0, (unsigned char*) "RIFF    WAVE", 12);
	wavBlock->set<DWORD>(4, (DWORD) wavBlock->getSize()-8);

	// fmt chunk
	wavBlock->write(12, (unsigned char*) "fmt ", 4);
	wavBlock->set<DWORD> (16, 16); // length of remaining fmt header
	wavBlock->set<WORD> (20, formatTag); 
	wavBlock->set<WORD> (22, channels);
	wavBlock->set<DWORD> (24, SampleRate); // sample rate
	wavBlock->set<DWORD> (28, SampleRate); //*BlockAlign);
	wavBlock->set<DWORD> (32, 1); //BlockAlign); 
	wavBlock->set<DWORD> (34, BitsPerSample); // mono

	// Data chunk
	wavBlock->write (36, (unsigned char*) "data", 4); // mono
	size_t wavOffset = 44;
	//size_t offset = 14;
	//DWORD blockNum = 0;
	//size_t blockOffset = maxBlockSize;
	//unsigned char c, lastChar = 128;

	sflx(mb->getPtr<char>(), wavBlock->getPtr<char>(wavOffset));

	wavBlock->set<DWORD>(4,  (DWORD) wavBlock->getSize()-8); // file size
	wavBlock->set<DWORD>(40, (DWORD) wavBlock->getSize()-44); // data chunk size
	//wavBlock->resize (wavOffset);
	return wavBlock;
}

////////////////////////////////////////////////////////////////////////////////
// Decode MW2 555 image

bool Decode_MW2_555 (const MemoryBlock& mb, Texture& texture)
{
	SET_SCOPE;
	if (mb.getSize() < 4) {
		RETURN NULL;
	}

	TextureDim w = mb.get<WORD>(0);
	TextureDim h = mb.get<WORD>(2);

	if (mb.getSize() < (size_t)(2*w*h+4)) {
		RETURN false;
	}

	texture.resize(w, h, GL_RGBA);

	// b5g5r5a1
	uint8_t rMask = 31;
	uint8_t rShift = 10;
	uint8_t rFactor = 8;
	uint8_t gMask = 31;
	uint8_t gShift = 5;
	uint8_t gFactor = 8;
	uint8_t bMask = 31;
	uint8_t bShift = 0;
	uint8_t bFactor = 8;
	uint8_t aMask = 1;
	uint8_t aShift = 15;
	uint8_t aFactor = 255;

	bool highestBitSet = false;
	for (TextureDim x = 0; x < w; x++) {
		for (TextureDim y = 0; y < h; y++) {
			size_t index = 2*(y*w+x)+4;
			auto val = mb.get<uint16_t>(index);
			if (val >> 15 > 0) {
				highestBitSet = true;
			}
		}
	}

	// b4g4r4a4
	if (highestBitSet) {
		rMask = 15;
		rShift = 8;
		rFactor = 16;
		gMask = 15;
		gShift = 4;
		gFactor = 16;
		bMask = 15;
		bShift = 0;
		bFactor = 16;
		aMask = 15;
		aShift = 12;
		aFactor = 16;
	}

	for (TextureDim x = 0; x < w; x++) {
		for (TextureDim y = 0; y < h; y++) {
			size_t index = 2*(y*w+x)+4;
			auto val = mb.get<uint16_t>(index);
			uint8_t r = rFactor * ((val >> rShift) & rMask);
			uint8_t g = gFactor * ((val >> gShift) & gMask);
			uint8_t b = bFactor * ((val >> bShift) & bMask);
			uint8_t a = aFactor * ((val >> aShift) & aMask);

			texture.setRGBA (x, h-y-1, r, g, b, a);
		}
	}

	RETURN true;
}


////////////////////////////////////////////////////////////////////////////////
// Decode MW2 font (true type fonts are used)

struct FNTHeader {
	char sig[4];
	DWORD pointerArraySize;
	DWORD fontHeight;
	DWORD unknown;
};

void decodeFNT(const MemoryBlock& block, Texture& texture)
{
	// Check file integrity
	SET_SCOPE;
	if (block.getSize() < 16) {
		COUT << "Illegal font file: size " << block.getSize() << std::endl;
		RETURN; // we need at least a header ...
	}

	const FNTHeader* header = block.getPtr<const FNTHeader>(0);
	if (header->sig[0] != 0x31
	||  header->sig[1] != 0x2E
	||  header->sig[2] != 0
	||  header->sig[3] != 0
	||  header->pointerArraySize < 32
	||  header->pointerArraySize > 256)
	{
		COUT << "Illegal font file header" << std::endl;
		RETURN;
	}

	// There has to be a header, 4 bytes per character for the pointer and at least another byte for every character
	if (block.getSize() < 16 + 5 * header->pointerArraySize) {
		COUT << "Font file: insufficient data, size " << block.getSize() << std::endl;
		RETURN;
	}

	// Get max character width for a character
	SET_OP;
	size_t max = 0;
	size_t maxCharWidth = 0;
	for (size_t i = 0; i < header->pointerArraySize-1; ++i) {
		auto offset = block.get<DWORD>(16 + 4*i);
		auto charWidth = block.get<DWORD>(offset);
		if (charWidth > maxCharWidth) {
			maxCharWidth = charWidth;
		}
	}

	// Decode bitmaps
	SET_OP;
	TextureDim w = 16 * maxCharWidth;
	TextureDim h = (header->pointerArraySize + 15) / 16 * header->fontHeight;
	if (!texture.resize(w, h, GL_RGB)) {
		COUT << "Failed to resize font texture" << std::endl;
		RETURN;
	}
	texture.fillRectI(0, 0, w, h, 0, 0, 0);

	SET_OP;
	for (size_t i = 0; i < header->pointerArraySize; ++i) {
		auto charOffset = block.get<DWORD>(16 + 4 * i);
		auto charWidth = block.get<DWORD>(charOffset);
		TextureDim x1 = (i % 16) * maxCharWidth;
		TextureDim y1 = (i / 16) * header->fontHeight;
		charOffset += 4;

		for (TextureDim y2 = 0; y2 < header->fontHeight; ++y2) {
			for (TextureDim x2 = 0; x2 < charWidth; ++x2) {
				auto c = block.get<BYTE>(charOffset);
				int v = 0;
				v = (c == header->unknown) ? 0 : 255;
				texture.setRGB(x1+x2, h-y1-y2+2*header->fontHeight-1, v, v, v);
				++charOffset;
			}
		}
	}

	COUT << "Decoded font from MW2.PRJ, char size " << maxCharWidth << "x" << header->fontHeight << std::endl;

	RETURN;
}
