////////////////////////////////////////////////////////////////////////////////
// MechWarrior3GameZArchive.cpp
// Parser for MW3 gamez.zbd files
// Copyright Bjoern Ganster
// Developed since 2022
////////////////////////////////////////////////////////////////////////////////

#include "MechWarrior3GameZArchive.h"
#include "MechWarrior3ZBD.h"
#include "XMLTree.h"

#include <iostream>

#define FILE_NUMBER FILE_NO_GAMEZ_ARCHIVE_CPP

// Adapted from MechWarrior3ZBD.cpp
struct MW3GameZNodeHeader {
	char partName[36];
	uint32_t flags;
	uint32_t data1, parentCountUnreliable;
	uint32_t zoneID, nodeType, dataPtr, meshIndex;
	uint32_t unknown[5];
	uint32_t parentCount, parentNodeArrayPointer;
	uint32_t childNodeCount, childNodeArrayPointer;
	uint32_t unknown2[27];
	uint32_t dataOffset;
}; // 212 bytes

struct MW3PMGameZNodeHeader {
	char partName[36];
	uint32_t flags;
	uint32_t unknown1, parentCount;
	uint32_t zoneID, nodeType, dataOffset, meshIndex;
	uint32_t unknown2[37];
}; // 212 bytes

struct Object3DData {
	uint32_t flags;
	float data[5];
	float rotationX, rotationY, rotationZ;
	float scaleX, scaleY, scaleZ;
	float rotationMatrix[9];
	float translationX, translationY, translationZ;
	// 48 zero bytes?
	uint32_t unknown[12];
};

struct LodData {
	uint32_t level;
	float rangeNearSq, rangeFar, rangeFarSq;
	uint8_t zeroes[44];
	float floatVal1, floatVal2;
	uint32_t unk68, unk72, unk76;
};

////////////////////////////////////////////////////////////////////////////////
// Implementation of an archive class for gamez.zbd files

MW3GameZArchive::MW3GameZArchive()
: MWArchive("GameZArchive", GameZArchive_ID),
  mMW3GameZHeader(nullptr),
  mMW3PMGameZHeader(nullptr)
{
}

MW3GameZArchive::~MW3GameZArchive()
{
	close();
}

// This appears once for all meshes in the file?
struct MW3GameZMeshHeader1 {
	uint32_t array_size;
	uint32_t count;
	uint32_t index_max;
};

// This appears at the start of each mesh?
struct MW3GameZMeshHeader2 {
	uint32_t unknown[3];
	uint32_t parentCount;
	uint32_t polygonCount;
	uint32_t vertexCount;
	uint32_t normalCount;
	uint32_t morphCount;
	uint32_t lightCount;
	uint32_t unknown2[4];
	uint32_t polygonsPtr, verticesPtr, normalsPtr, morphsPtr;
	uint32_t unknown3[6];
	uint32_t possibleDataOffset;
}; // 96 bytes

// This appears at the start of each mesh?
struct MW3PMGameZMeshHeader2 {
	uint32_t unknown[3];
	uint32_t parentCount;
	uint32_t polygonCount;
	uint32_t vertexCount;
	uint32_t normalCount;
	uint32_t morphCount;
	uint32_t lightCount;
	uint32_t unknown2[4];
	uint32_t polygonsPtr, verticesPtr, normalsPtr, morphsPtr;
	uint32_t unknown3[8];
	uint32_t possibleDataOffset;
}; // 104 bytes

// Open file
bool MW3GameZArchive::open(const PathChar* PRJFN, bool readOnly)
{
	SET_SCOPE;

	if (!mMb.loadFromFile(PRJFN)) {
		RETURN false;
	}

	if (mMb.getSize() < sizeof(MW3GameZHeader)) {
		close();
		RETURN false;
	}

	mMW3GameZHeader = mMb.getPtr<MW3GameZHeader>(0, false);
	if (mMW3GameZHeader == nullptr || mMW3GameZHeader->signature != 0x2971222) {
		close();
		RETURN false;
	}

	if (mMW3GameZHeader->version == 0x1b) {
		SET_OP;
		addFile("textures", mMW3GameZHeader->materialsOffset - mMW3GameZHeader->textureOffset, mMW3GameZHeader->textureOffset);
		addFile("materials", mMW3GameZHeader->meshesOffset - mMW3GameZHeader->materialsOffset, mMW3GameZHeader->materialsOffset);
		addFile("nodes", mMb.getSize() - mMW3GameZHeader->nodesOffset, mMW3GameZHeader->nodesOffset, FileType::MW3GameZNodes);
		//addFile("terrain", sizeof(GameZNodeHeader),  mMW3GameZHeader->nodesOffset, FileType::MW3Terrain);

		MW3GameZMeshHeader1* meshMainHeader = mMb.getPtr<MW3GameZMeshHeader1>(mMW3GameZHeader->meshesOffset, false);
		if (meshMainHeader != nullptr) {
			for (size_t i = 0; i < meshMainHeader->count; ++i) {
				size_t meshHeaderOffset = mMW3GameZHeader->meshesOffset + sizeof(MW3GameZMeshHeader1) + i * sizeof(MW3GameZMeshHeader2);
				BGCPString meshName = BGCPString("Mesh") + BGCPString::fromInt(i);
				addFile(meshName, 0, meshHeaderOffset, FileType::MW3GameZMesh);
				//MW3GameZMeshHeader2* meshHeader = mMb.getPtr<MW3GameZMeshHeader2>(meshHeaderOffset);
			}
		}

		RETURN true;
	} else if (mMW3GameZHeader->version == 0x29) {
		// MW3:PM
		SET_OP;
		mMW3PMGameZHeader = mMb.getPtr<MW3PMGameZHeader>(0, false);
		if (mMW3PMGameZHeader == nullptr) {
			RETURN false;
		}
		mMW3GameZHeader = nullptr;
		addFile("textures", mMW3PMGameZHeader->materialsOffset - mMW3PMGameZHeader->textureOffset, mMW3PMGameZHeader->textureOffset);
		addFile("materials", mMW3PMGameZHeader->meshesOffset - mMW3PMGameZHeader->materialsOffset, mMW3PMGameZHeader->materialsOffset);
		//addFile("meshes", pmHeader.nodesOffset - pmHeader.meshesOffset, pmHeader.nodesOffset); // Actually a container for the mesh files?
		addFile("nodes", mMb.getSize() - mMW3PMGameZHeader->nodesOffset, mMW3PMGameZHeader->nodesOffset, FileType::MW3PMGameZNodes);

		MW3GameZMeshHeader1* meshMainHeader = mMb.getPtr<MW3GameZMeshHeader1>(mMW3PMGameZHeader->meshesOffset, false);
		if (meshMainHeader != nullptr) {
			for (size_t i = 0; i < meshMainHeader->count; ++i) {
				size_t meshHeaderOffset = mMW3PMGameZHeader->meshesOffset + sizeof(MW3GameZMeshHeader1) + i * sizeof(MW3PMGameZMeshHeader2);
				BGCPString meshName = BGCPString("Mesh") + BGCPString::fromInt(i);
				addFile(meshName, 0, meshHeaderOffset, FileType::MW3PMGameZMesh);
				//MW3GameZMeshHeader2* meshHeader = mMb.getPtr<MW3GameZMeshHeader2>(meshHeaderOffset);
			}
		}

		RETURN true;
	} else {
		close();
		RETURN false;
	}
}

// Close file
void MW3GameZArchive::close()
{
	mMb.clear();
	mEntries.clear();
}

// Read bufSize bytes from file numbered index at offset into buf
bool MW3GameZArchive::getFileChunk(size_t index, size_t offset, void* buf, size_t bufSize)
{
	SET_SCOPE;
	if (index >= mEntries.size()) {
		RETURN false;
	}
	if (offset > mEntries[index].fileSize) {
		RETURN false;
	}
	if (offset + bufSize > mEntries[index].fileSize) {
		bufSize = mEntries[index].fileSize - offset;
	}

	auto read = mMb.read(mEntries[index].fileOffset+offset, bufSize, buf);
	RETURN (read > 0);
}

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

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

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

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

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

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

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

// Add files to archive
void MW3GameZArchive::addFileToArchive(const PathChar* /*FN*/) 
{
}

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

// Decode a mesh stored in a MW3 gamez.zbd to something MechVM understands ...
bool MW3GameZArchive::decodeGameZMesh(size_t fileNo, Mesh* mesh)
{
	int UnknownPolygonDataSize = 12;
	bool flipPolies = true;
	//Mesh* newMesh = new Mesh("Mesh", MeshID, mesh);
	mesh->setName(BGCPString("mesh") + BGCPString::fromInt(fileNo-3));

	SET_SCOPE;
	if (fileNo > mEntries.size()) {
		RETURN false;
	}

	if (mMW3PMGameZHeader != nullptr) {
		RETURN decodeGameZPMMesh(fileNo, mesh);
	}

	// We stored the offset to the mesh header as the file offset. Now, retrieve the mesh header from the stored offset ...
	MW3GameZMeshHeader2* meshHeader = mMb.getPtr<MW3GameZMeshHeader2>(mEntries[fileNo].fileOffset);

	if (meshHeader == nullptr) {
		RETURN false;
	}

	if (meshHeader->vertexCount == 0 || meshHeader->vertexCount > 1000) {
		RETURN false;
	}

	// Read vertices
	SET_OP;
	for (size_t i = 0; i < meshHeader->vertexCount; ++i) {
		auto px = mMb.get<float>(meshHeader->possibleDataOffset + 12*i);
		auto py = mMb.get<float>(meshHeader->possibleDataOffset + 12*i + 4);
		auto pz = mMb.get<float>(meshHeader->possibleDataOffset + 12*i + 8);
		mesh->addPoint(px, py, pz);
	}

	// Read polygons
	size_t polygonHeadersOffset = meshHeader->possibleDataOffset + 12 * meshHeader->vertexCount + 12 * meshHeader->normalCount + 
		12 * meshHeader->morphCount + 80 * meshHeader->lightCount;
	size_t polygonDataOffset = polygonHeadersOffset + meshHeader->polygonCount * sizeof(LocalPolygonHeader);

	for (size_t i = 0; i < meshHeader->polygonCount; ++i) {
		SET_OP;
		const LocalPolygonHeader* polygonHeader = mMb.getPtr<LocalPolygonHeader>(polygonHeadersOffset, false);
		if (polygonHeader != nullptr) {
			MeshPolygon* poly = mesh->addPolygon(polygonHeader->vertexCount);
			uint32_t lastIndex = -1;
			for (size_t j = 0; j < polygonHeader->vertexCount; ++j) {
				uint32_t vertexIndex = mMb.get<uint32_t>(polygonDataOffset);
				polygonDataOffset += 4;
				if (flipPolies) {
					poly->setVertexIndex(polygonHeader->vertexCount-j-1, vertexIndex);
				} else {
					poly->setVertexIndex(j, vertexIndex);
				}
				lastIndex = vertexIndex;
			}

			polygonHeadersOffset += sizeof(LocalPolygonHeader);

			// Skip data
			if (polygonHeader->hasPointIndex2)
				polygonDataOffset += 4*polygonHeader->vertexCount; // PointIndex2

			// Read texture coordinates
			if (polygonHeader->flags > 0) {
				//readTextureCoords(flt, mesh, poly, polygonDataOffset, gInvertPolygons);
				polygonDataOffset += 8 * polygonHeader->vertexCount;
			}

			// Skip unknown data
			polygonDataOffset += UnknownPolygonDataSize*polygonHeader->vertexCount;
		}
	}

	//mesh->saveToObj(L"c:\\temp\\mesh.obj");

	RETURN true;
}

struct GameZPMMeshPolygonHeader {
	uint8_t vertexCount;
	uint8_t polygonMode, flags2, flags3;
	uint32_t zero;
	uint32_t ptr1;
	uint32_t ptr2;
	uint32_t hasPointIndex2; // Indices into normals?
	uint32_t ptr3, ptr4, ptr5, ptr6, flags4;
}; // 40 bytes

// Decode a mesh stored in a MW3:PM gamez.zbd to something MechVM understands ...
bool MW3GameZArchive::decodeGameZPMMesh(size_t fileNo, Mesh* mesh)
{
	SET_SCOPE;
	if (fileNo > mEntries.size()) {
		RETURN false;
	}

	// We stored the offset to the mesh header as the file offset. Now, retrieve the mesh header from the stored offset ...
	auto headerOffset = mEntries[fileNo].fileOffset;
	MW3PMGameZMeshHeader2* meshHeader = mMb.getPtr<MW3PMGameZMeshHeader2>(headerOffset, false);

	if (meshHeader == nullptr|| meshHeader->vertexCount == 0 || meshHeader->vertexCount > 1000) {
		RETURN false;
	}

	// Read vertices
	SET_OP;
	for (size_t i = 0; i < meshHeader->vertexCount; ++i) {
		auto px = mMb.get<float>(meshHeader->possibleDataOffset + 12*i);
		auto py = mMb.get<float>(meshHeader->possibleDataOffset + 12*i + 4);
		auto pz = mMb.get<float>(meshHeader->possibleDataOffset + 12*i + 8);
		mesh->addPoint(px, py, pz);
	}

	// Read polygons
	size_t polygonHeadersOffset = meshHeader->possibleDataOffset + 12 * meshHeader->vertexCount + 12 * meshHeader->normalCount + 
		12 * meshHeader->morphCount + 80 * meshHeader->lightCount;
	size_t polygonDataOffset = polygonHeadersOffset + meshHeader->polygonCount * sizeof(GameZPMMeshPolygonHeader);

	for (size_t i = 0; i < meshHeader->polygonCount; ++i) {
		SET_OP;
		GameZPMMeshPolygonHeader* polygonHeader = mMb.getPtr<GameZPMMeshPolygonHeader>(polygonHeadersOffset, false);

		if (polygonHeader != nullptr) {
			if (polygonHeader->polygonMode != 0x30) {
				// Normal polygon
				MeshPolygon* poly = mesh->addPolygon(polygonHeader->vertexCount);
				for (size_t j = 0; j < polygonHeader->vertexCount; ++j) {
					uint32_t vertexIndex = mMb.get<uint32_t>(polygonDataOffset);
					polygonDataOffset += 4;
					poly->setVertexIndex(j, vertexIndex);
				}
			} else {
				// Triangle Strip
				for (size_t j = 0; j+2 < polygonHeader->vertexCount; ++j) {
					uint32_t vi1 = mMb.get<uint32_t>(polygonDataOffset);
					uint32_t vi2 = mMb.get<uint32_t>(polygonDataOffset + 4);
					uint32_t vi3 = mMb.get<uint32_t>(polygonDataOffset + 8);

					if (j % 2 == 0) {
						mesh->addTriangle(vi3, vi2, vi1);
					} else {
						mesh->addTriangle(vi1, vi2, vi3);
					}
					polygonDataOffset += 4; // jump one vertex index forward, although 3 vertex indices are used
				}
				polygonDataOffset += 8;
			}

			// Testing ...
			polygonHeadersOffset += sizeof(GameZPMMeshPolygonHeader);
			polygonDataOffset += 4 + 20 * polygonHeader->vertexCount;
			if (polygonHeader->ptr2 != 0) {
				polygonDataOffset += 4 * polygonHeader->vertexCount;
			}
		}
	}

	//mesh->saveToObj(L"c:\\temp\\mesh.obj");

	RETURN true;
}

// Import a scene graph from a gamez.zbd
void MW3GameZArchive::importGameZNodesRec(size_t offset, XMLTreeIterator& iter, int recursionLevel)
{
	SET_SCOPE;
	const bool exportFlags = false;

	// Scan for certain file offsets of interest ...
	/*if (offset < 6463496 && offset + sizeof(GameZNodeHeader) >= 6463496) {
		COUT << "Point of interest found ..." << std::endl;
	}*/

	if (offset + sizeof(MW3GameZNodeHeader) > mMb.getSize()) {
		RETURN;
	}

	mChildNodeOffsets.insert(offset);

	const MW3GameZNodeHeader* nodeHeader = mMb.getPtr<const MW3GameZNodeHeader>(offset, false);

	if (nodeHeader == nullptr) {
		RETURN;
	}

	if (recursionLevel > 0 && nodeHeader->parentCount == 0) {
		COUT << "Since this warning is never shown, nodeHeader->parentCount will be > 0 for all child nodes. However, some nodes " 
		        "claim nodeHeader->parentCount == 1 although they have no parent???" << std::endl;
	}

	// Stopwords: dbase, destroy, destroyed, sinker, shadow, shdw (do not export these?)

	/*if (nodeHeader->parentCount != nodeHeader->parentCountCopy) {
		COUT << "parent count difference: " << nodeHeader->parentCount << "!=" << nodeHeader->parentCountCopy << std::endl;
	}*/

	SET_OP;
	size_t childArrayOffset = 0;
	BGCPString nodeTypeStr;
	switch (nodeHeader->nodeType) {
		case 0: RETURN; //nodeTypeStr = "empty"; break;
		case 1: nodeTypeStr = "camera"; break;
		case 2:
			{
				nodeTypeStr = "world";
				SET_OP;
				const MW3GameZNodeHeader* nextNodeHeader = mMb.getPtr<const MW3GameZNodeHeader>(offset+sizeof(MW3GameZNodeHeader), false);
				if (nextNodeHeader == nullptr) {
					RETURN;
				}
				childArrayOffset = nextNodeHeader->dataOffset - 4 * (nodeHeader->childNodeCount + 1);
				break;
			}
		case 3: nodeTypeStr = "window"; break;
		case 4: nodeTypeStr = "display"; break;
		case 5: 
			if (nodeHeader->childNodeCount > 0) {
				nodeTypeStr = "transform";
			} else {
				if (nodeHeader->meshIndex > mEntries.size()) {
					//COUT << "Empty Object3D node?" << std::endl;
					RETURN;
				}
				nodeTypeStr = "object";
			}
			childArrayOffset = nodeHeader->dataOffset + sizeof(Object3DData) + 4 * nodeHeader->parentCount;
			break;
		case 6: nodeTypeStr = "transform"; //"lod";
			childArrayOffset = nodeHeader->dataOffset + 80 + 4 * nodeHeader->parentCount;
			break;
		case 7: nodeTypeStr = "sequence???"; break;
		case 8: nodeTypeStr = "animate"; break;
		case 9: nodeTypeStr = "light"; break;
		case 10: nodeTypeStr = "sound"; break;
		case 11: nodeTypeStr = "switch"; break;
		default: RETURN; // error?
	}

	SET_OP;
	auto childNo = iter.addChild(nodeTypeStr);
	iter.goToChild(childNo);
	iter.addAttribute("description", nodeHeader->partName);

	// Closely inspect object3d nodes ...
	if (nodeHeader->nodeType == 5) {
		SET_OP;
		const Object3DData* moreData = mMb.getPtr<const Object3DData>(nodeHeader->dataOffset, false);
		if (moreData == nullptr) {
			RETURN;
		}

		if (moreData->flags > 0 && exportFlags) {
			iter.addIntAttribute("moreFlags", moreData->flags);
		}

		if (moreData->rotationX < -0.01 || moreData->rotationX > 0.01) {
			iter.addDoubleAttribute("rx", 180 * moreData->rotationX / M_PI);
		}
		if (moreData->rotationY < -0.01 || moreData->rotationY > 0.01) {
			iter.addDoubleAttribute("ry", 180 * moreData->rotationY / M_PI);
		}
		if (moreData->rotationZ < -0.01 || moreData->rotationZ > 0.01) {
			iter.addDoubleAttribute("rz", 180 * moreData->rotationZ / M_PI);
		}

		if (moreData->scaleX < 0.99 || moreData->scaleX > 1.01) {
			iter.addDoubleAttribute("sx", moreData->scaleX);
		}
		if (moreData->scaleY < 0.99 || moreData->scaleY > 1.01) {
			iter.addDoubleAttribute("sy", moreData->scaleY);
		}
		if (moreData->scaleZ < 0.99 || moreData->scaleZ > 1.01) {
			iter.addDoubleAttribute("sz", moreData->scaleZ);
		}

		if (moreData->translationX < -0.01 || moreData->translationX > 0.01) {
			iter.addDoubleAttribute("tx", moreData->translationX);
		}
		if (moreData->translationY < -0.01 || moreData->translationY > 0.01) {
			iter.addDoubleAttribute("ty", moreData->translationY);
		}
		if (moreData->translationZ < -0.01 || moreData->translationZ > 0.01) {
			iter.addDoubleAttribute("tz", moreData->translationZ);
		}
	}

	if (nodeHeader->nodeType == 6) {
		const LodData* lodData = mMb.getPtr<const LodData>(nodeHeader->dataOffset, false);
		if (lodData != nullptr) {
			iter.addDoubleAttribute("v1", lodData->floatVal1);
			iter.addDoubleAttribute("v2", lodData->floatVal2);
		}
	}

	if (nodeHeader->flags > 0 && exportFlags) {
		SET_OP;
		BGCPString nodeFlagsStr;
		nodeFlagsStr = BGCPString::fromInt(nodeHeader->flags, 2) + BGCPString("=");
		if ((nodeHeader->flags & 4) > 0) nodeFlagsStr += "active,";
		if ((nodeHeader->flags & 8) > 0) nodeFlagsStr += "altitudeSurface,";
		if ((nodeHeader->flags & 16) > 0) nodeFlagsStr += "intersectSurface,";
		if ((nodeHeader->flags & 32) > 0) nodeFlagsStr += "intersectBBox,";
		if ((nodeHeader->flags & 128) > 0) nodeFlagsStr += "landmark,";
		if ((nodeHeader->flags & 512) > 0) nodeFlagsStr += "hasMesh,";
		if ((nodeHeader->flags & (1 << 15)) > 0) nodeFlagsStr += "terrain,";
		if ((nodeHeader->flags & (1 << 16)) > 0) nodeFlagsStr += "canModify,";
		iter.addAttribute("flags", nodeFlagsStr);
	}

	if (nodeHeader->meshIndex < mEntries.size()) {
		SET_OP;
		if (nodeHeader->childNodeCount == 0) {
			iter.addIntAttribute("zbdmesh", nodeHeader->meshIndex);
		} else {
			auto childNo = iter.addChild("object");
			iter.goToChild(childNo);
			iter.addIntAttribute("zbdmesh", nodeHeader->meshIndex);
			iter.goToParent();
		}
	}

	if (nodeHeader->parentCount == 0 && recursionLevel > 0) {
		cout << "Node says it has no parents, although it does? " << nodeHeader->nodeType << ", " << offset << std::endl;
	}

	if (childArrayOffset == 0) {
		iter.goToParent();
		RETURN;
	}

	// Visit child nodes
	SET_OP;
	if (childArrayOffset > 0 && childArrayOffset < mMb.getSize()) {
		for (size_t i = 0; i < nodeHeader->childNodeCount; ++i) {
			int32_t childNodeIndex = mMb.get<uint32_t>(childArrayOffset + 4 *i);

			// Let's see if there are negative indices ...
			if (childNodeIndex < 0) {
				COUT << "Unexpected, found a negative index: " << childNodeIndex << std::endl;
			} else {
				uint32_t childNodeOffset = mMW3GameZHeader->nodesOffset + sizeof(MW3GameZNodeHeader) * childNodeIndex;
				importGameZNodesRec (childNodeOffset, iter, recursionLevel+1);
			}
		}
	}

	SET_OP;
	iter.goToParent();
	RETURN;
}

// Import a scene graph from a MW3 gamez.zbd
void MW3GameZArchive::importGameZNodes(size_t fileNo)
{
	if (mMW3GameZHeader == nullptr) {
		return;
	}

	SET_SCOPE;
	XMLTree xmlTree;
	XMLTreeIterator iter(&xmlTree, xmlTree.getRoot());
	auto childNo = iter.addChild("level");
	iter.goToChild(childNo);

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

	const MW3PMGameZNodeHeader* nodeHeader = mMb.getPtr<const MW3PMGameZNodeHeader>(mMW3GameZHeader->nodesOffset, false);
	if (nodeHeader == nullptr) {
		RETURN;
	}
	size_t nodeCount = (nodeHeader->dataOffset - mMW3GameZHeader->nodesOffset) / sizeof(MW3PMGameZNodeHeader);

	// Check theories over all nodes ...
	/*for (size_t i = 1; i < nodeCount; ++i) {
		size_t nodeOffset = mMW3GameZHeader->nodesOffset + sizeof(GameZNodeHeader) *i;
		const GameZNodeHeader* nodeHeader = mMb.getPtr<GameZNodeHeader>(nodeOffset, false);
		if (nodeHeader == nullptr) {
			RETURN;
		}
		if (nodeHeader->parentCount != 1 && nodeHeader->nodeType > 0) {
			cout << "Failed for " << i << std::endl;
		}
	}*/

	// Simply traverse the node headers, but only export nodes that do not have a parent ...
	SET_OP;
	for (size_t i = 0; i < nodeCount; ++i) {
		size_t nodeOffset = mMW3GameZHeader->nodesOffset + sizeof(MW3GameZNodeHeader) * i;
		/*const MW3GameZNodeHeader* nodeHeader = mMb.getPtr<const MW3GameZNodeHeader>(nodeOffset, false);
		if (nodeHeader == nullptr) {
			RETURN;
		}*/

		// Scan for certain file offsets of interest ...
		/*if (nodeOffset < 6463496 && nodeOffset + sizeof(GameZNodeHeader) >= 6463496) {
			COUT << "Point of interest found ..." << std::endl;
		}*/

		//if (nodeHeader->parentCount == 0 && nodeHeader->nodeType > 0) { // too restrictive, but we need a way to filter out duplicates ...
		if (mChildNodeOffsets.find(nodeOffset) == mChildNodeOffsets.end()) {
			importGameZNodesRec(nodeOffset, iter, 0);
		}
	}

	// Try to traverse the scene from the root
	//importGameZNodesRec(mMW3GameZHeader->nodesOffset, iter, 0);

	BGCPString xmlExportFN(outputDir);
	xmlExportFN.addPathSep("level.xml");
	xmlTree.saveToFile(xmlExportFN.getAsPath(), CP_ASCII);
	RETURN;
}

// Inform zinder?
struct MW3PMWorldNodeData {
	uint32_t unk0, partitionsUsed, partitionCount, partitionPtr;
	uint32_t fog;
	float color[3];
	float fogNear, fogFar, fogLatitudeHigh, fogAltitudeLow, fogDensity;
	float areaLeft, areaBottom, areaWith, areaHeight, areaRight, areaTop;
	uint32_t unk76;
	uint32_t virtPartitionData1[5];
	float virtPartitionData2[9];
	uint32_t virtPrtitionXCount, virtPartitionYCount, virtPartitionPtr;
	float unk148[3];
	uint32_t unk160[12]; // notify zinder?
};

struct PMWorldPartionData {
	int32_t unk0, unk4;
	float partX, partY;
	float xMin, zMin, yMin, xMax, zMax, yMax, xMid, zMid, yMid, diagonal;
	uint16_t unk56, count;
	uint32_t ptr, unk64[6];
};

// Import a scene graph from a gamez.zbd
void MW3GameZArchive::importPMGameZNodesRec(size_t offset, XMLTreeIterator& iter, int recursionLevel)
{
	SET_SCOPE;
	const bool exportFlags = false;

	if (offset + sizeof(MW3PMGameZNodeHeader) > mMb.getSize()) {
		COUT << "Implausible node offset" << std::endl;
		RETURN;
	}

	if (mChildNodeOffsets.find(offset) != mChildNodeOffsets.end()) {
		COUT << "previously visited node..." << std::endl;
		RETURN;
	}

	mChildNodeOffsets.insert(offset);

	const MW3PMGameZNodeHeader* nodeHeader = mMb.getPtr<MW3PMGameZNodeHeader>(offset, false);
	if (nodeHeader == nullptr) {
		RETURN;
	}

	/*if (nodeHeader->parentCount != nodeHeader->parentCountCopy) {
		COUT << "parent count difference: " << nodeHeader->parentCount << "!=" << nodeHeader->parentCountCopy << std::endl;
	}*/

	// todo: let each node give node give an estimate of how much data it should have. Then compare that to the amount data indicated 
	// by the next node. If there is a difference, print a warning.

	SET_OP;
	size_t childArrayOffset = 0;
	BGCPString nodeTypeStr;
	switch (nodeHeader->nodeType) {
		case 0: RETURN; //nodeTypeStr = "empty"; break;
		case 1: nodeTypeStr = "camera"; break;
		case 2: nodeTypeStr = "world";
			{
				// For MW3:PM, we cannot use the heuristic of obtaining the location of the child offsets from the next node.
				// Instead, we have to calculate the offset from the data in the world data.
				
				/*SET_OP;
				const GameZNodeHeader* nextNodeHeader = mMb.getPtr<GameZNodeHeader>(offset+sizeof(GameZNodeHeader), false);
				if (nextNodeHeader == nullptr) {
					RETURN;
				}
				childArrayOffset = nextNodeHeader->dataOffset - 4 * (nodeHeader->childNodeCount + 1);*/
				size_t dataOffset = nodeHeader->dataOffset;
				MW3PMWorldNodeData* moreData = mMb.getPtr<MW3PMWorldNodeData>(nodeHeader->dataOffset, false);
				dataOffset+= sizeof(MW3PMWorldNodeData);

				// Check the partition data
				if (moreData != nullptr) {
					for (size_t i = 0; i < moreData->partitionCount; ++i) {
						const PMWorldPartionData* partData = mMb.getPtr<const PMWorldPartionData>(dataOffset, false);
						if (partData->unk0 != 0x100 || partData->unk4 != 0xffFFffFF) {
							COUT << "Something's wrong???" << std::endl;
						}
						if (partData != nullptr) {
							dataOffset += sizeof(PMWorldPartionData);
							dataOffset += 12 * partData->count;
						}
					}

					// todo: is a list of parent nodes stored somewhere?
					//childArrayOffset = nodeHeader->dataOffset + sizeof(MW3PMWorldNodeData) + moreData->partitionCount * sizeof(PMWorldPartionData);
					childArrayOffset = dataOffset;
				}
				break;
			}
		case 3: nodeTypeStr = "window"; break;
		case 4: nodeTypeStr = "display"; break;
		case 5: 
			//if (nodeHeader->childNodeCount > 0) {
				nodeTypeStr = "transform";
			/*} else {
				if (nodeHeader->meshIndex > mEntries.size()) {
					//COUT << "Empty Object3D node?" << std::endl;
					RETURN;
				}
				nodeTypeStr = "object";
			}*/
			childArrayOffset = nodeHeader->dataOffset + sizeof(Object3DData) + 4 * nodeHeader->parentCount;
			break;
		case 6: nodeTypeStr = "lod";
			childArrayOffset = nodeHeader->dataOffset + 80 + 4 * nodeHeader->parentCount;
			break;
		case 7: nodeTypeStr = "sequence???"; break;
		case 8: nodeTypeStr = "animate"; break;
		case 9: nodeTypeStr = "light"; break;
		case 10: nodeTypeStr = "sound"; break;
		case 11: nodeTypeStr = "switch"; break;
		default: COUT << "Unknown node type: " << nodeHeader->nodeType << std::endl; RETURN; // error?
	}

	/*if (nodeHeader->nodeType == 5) {
		if (nodeHeader->childNodeCount == 0 && nodeHeader->meshIndex > mEntries.size()) {
			COUT << "Empty Object3D node?" << std::endl;
			RETURN;
		}
	}*/

	SET_OP;
	auto childNo = iter.addChild(nodeTypeStr);
	iter.goToChild(childNo);
	iter.addAttribute("description", nodeHeader->partName);

	// Closely inspect object3d nodes ...
	if (nodeHeader->nodeType == 5) {
		const Object3DData* moreData = mMb.getPtr<const Object3DData>(nodeHeader->dataOffset, false);
		if (moreData != nullptr) {
			if (moreData->rotationX < -0.01 || moreData->rotationX > 0.01) {
				iter.addDoubleAttribute("rx", 180 * moreData->rotationX / M_PI);
			}
			if (moreData->rotationY < -0.01 || moreData->rotationY > 0.01) {
				iter.addDoubleAttribute("ry", 180 * moreData->rotationY / M_PI);
			}
			if (moreData->rotationZ < -0.01 || moreData->rotationZ > 0.01) {
				iter.addDoubleAttribute("rz", 180 * moreData->rotationZ / M_PI);
			}

			if (moreData->scaleX < 0.99 || moreData->scaleX > 1.01) {
				iter.addDoubleAttribute("sx", moreData->scaleX);
			}
			if (moreData->scaleY < 0.99 || moreData->scaleY > 1.01) {
				iter.addDoubleAttribute("sy", moreData->scaleY);
			}
			if (moreData->scaleZ < 0.99 || moreData->scaleZ > 1.01) {
				iter.addDoubleAttribute("sz", moreData->scaleZ);
			}

			if (moreData->translationX < -0.01 || moreData->translationX > 0.01) {
				iter.addDoubleAttribute("tx", moreData->translationX);
			}
			if (moreData->translationY < -0.01 || moreData->translationY > 0.01) {
				iter.addDoubleAttribute("ty", moreData->translationY);
			}
			if (moreData->translationZ < -0.01 || moreData->translationZ > 0.01) {
				iter.addDoubleAttribute("tz", moreData->translationZ);
			}
		}
	}

	if (nodeHeader->flags > 0 && exportFlags) {
		BGCPString nodeFlagsStr;
		nodeFlagsStr = BGCPString::fromInt(nodeHeader->flags, 2) + BGCPString("=");
		if ((nodeHeader->flags & 4) > 0) nodeFlagsStr += "active,";
		if ((nodeHeader->flags & 8) > 0) nodeFlagsStr += "altitudeSurface,";
		if ((nodeHeader->flags & 16) > 0) nodeFlagsStr += "intersectSurface,";
		if ((nodeHeader->flags & 32) > 0) nodeFlagsStr += "intersectBBox,";
		if ((nodeHeader->flags & 128) > 0) nodeFlagsStr += "landmark,";
		if ((nodeHeader->flags & 512) > 0) nodeFlagsStr += "hasMesh,";
		if ((nodeHeader->flags & (1 << 15)) > 0) nodeFlagsStr += "terrain,";
		if ((nodeHeader->flags & (1 << 16)) > 0) nodeFlagsStr += "canModify,";
		iter.addAttribute("flags", nodeFlagsStr);
	}

	if (nodeHeader->meshIndex < mEntries.size()) {
		//if (nodeHeader->childNodeCount == 0) {
			iter.addIntAttribute("zbdmesh", nodeHeader->meshIndex);
		/*} else {
			auto childNo = iter.addChild("object");
			iter.goToChild(childNo);
			iter.addIntAttribute("zbdmesh", nodeHeader->meshIndex);
			iter.goToParent();
		}*/
	}

	if (nodeHeader->parentCount == 0 && recursionLevel > 0) {
		cout << "Node says it has no parents, although it does? " << nodeHeader->nodeType << ", " << offset << std::endl;
	}

	if (childArrayOffset == 0 || childArrayOffset + 4 * sizeof(size_t) > mMb.getSize()) {
		iter.goToParent();
		RETURN;
	}

	// Visit child nodes
	// todo: not sure where the number of child nodes is actually stored ...
	SET_OP;
	/*if (childArrayOffset > 0 && childArrayOffset < mMb.getSize() && childArrayOffset + 4 * nodeHeader->childNodeCount < mMb.getSize()) {
		for (size_t i = 0; i < nodeHeader->childNodeCount; ++i) {
			int32_t childNodeIndex = mMb.get<uint32_t>(childArrayOffset + 4 *i);

			// Let's see if there are negative indices ...
			if (childNodeIndex < 0) {
				COUT << "Unexpected, found a negative index: " << childNodeIndex << std::endl;
			} else {
				uint32_t childNodeOffset = mMW3PMGameZHeader->nodesOffset + sizeof(MW3PMGameZNodeHeader) * childNodeIndex;
				importPMGameZNodesRec (childNodeOffset, iter, recursionLevel+1);
			}
		}
	} else {
		COUT << "implausible child nodes" << std::endl;
	}*/

	iter.goToParent();

	RETURN;
}

// Import a scene graph from a MW3:PM gamez.zbd
void MW3GameZArchive::importPMGameZNodes(size_t fileNo)
{
	if (mMW3PMGameZHeader == nullptr) {
		return;
	}

	SET_SCOPE;
	XMLTree xmlTree;
	XMLTreeIterator iter(&xmlTree, xmlTree.getRoot());
	auto childNo = iter.addChild("level");
	iter.goToChild(childNo);

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

	const MW3PMGameZNodeHeader* nodeHeader = mMb.getPtr<const MW3PMGameZNodeHeader>(mMW3PMGameZHeader->nodesOffset, false);
	if (nodeHeader == nullptr) {
		RETURN;
	}
	size_t nodeCount = (nodeHeader->dataOffset - mMW3PMGameZHeader->nodesOffset) / sizeof(mMW3PMGameZHeader);

	// Check theories over all nodes ...
	/*for (size_t i = 1; i < nodeCount; ++i) {
		size_t nodeOffset = mMW3GameZHeader->nodesOffset + sizeof(GameZNodeHeader) *i;
		const GameZNodeHeader* nodeHeader = mMb.getPtr<GameZNodeHeader>(nodeOffset, false);
		if (nodeHeader == nullptr) {
			RETURN;
			}
		if (nodeHeader->parentCount != 1 && nodeHeader->nodeType > 0) {
			cout << "Failed for " << i << std::endl;
		}
	}*/

	// Simply traverse the node headers, but only export nodes that do not have a parent ...
	SET_OP;
	size_t i = 0;
	while (i < mMW3PMGameZHeader->nodeArraySize) {
		size_t nodeOffset = mMW3PMGameZHeader->nodesOffset + sizeof(MW3PMGameZNodeHeader) * i;
		const MW3PMGameZNodeHeader* nodeHeader = mMb.getPtr<const MW3PMGameZNodeHeader>(nodeOffset, false);
		if (nodeHeader != nullptr) {
			if (mChildNodeOffsets.find(nodeOffset) == mChildNodeOffsets.end()) {
				importPMGameZNodesRec(nodeOffset, iter, 0);
			}
			++i;
		} else {
			i = nodeCount; // abort
		}
	}

	SET_OP;
	BGCPString xmlExportFN(outputDir);
	xmlExportFN.addPathSep("level.xml");
	xmlTree.saveToFile(xmlExportFN.getAsPath(), CP_ASCII);
	RETURN;
}

// ???
struct MW3Partition {
	uint32_t unknown;
	float x1, z1, x1b, y1, z2, x2, y2, z3, x3, y3, z4, x4, y4;
	uint8_t unknown2[20];
};

// Decode terrain stored in a gamez-zbd world node
// No, that must be another kind of data ...
/*bool MW3GameZArchive::decodeTerrain(Mesh* mesh)
{
	if (mesh == nullptr) {
		return false;
	}

	SET_SCOPE;
	const GameZNodeHeader* worldNodeHeader = mMb.getPtr<GameZNodeHeader>(mMW3GameZHeader->nodesOffset, false);
	if (worldNodeHeader == nullptr) {
		RETURN false;
	}
	auto polyCount = mMb.get<uint32_t>(worldNodeHeader->dataOffset + 8);

	// According to Zinder's documentation, these are partitions, controlling what nodes are visible from a given position
	SET_OP;
	for (size_t i = 0; i < polyCount; ++i) {
		size_t dataOffset = worldNodeHeader->dataOffset + i * sizeof(MW3TerrainPolygon);
		//MW3TerrainPolygon* poly = mMb.getPtr<MW3TerrainPolygon>(dataOffset, false);
		MW3TerrainPolygon poly;
		mMb.read(dataOffset, sizeof(MW3TerrainPolygon), &poly);
	}

	//mesh->saveToObj(L"c:\\temp\\terrain.obj");

	RETURN true;
}*/

// True if the file belongs to MW3:PM, false for MW3
bool MW3GameZArchive::isPiratesMoonFile() const
{
	if (mMW3PMGameZHeader != nullptr) {
		return true;
	} else {
		return false;
	}
}
