/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "common/config-manager.h"
#include "common/file.h"
#include "common/substream.h"
#include "common/macresman.h"
#include "common/memstream.h"

#include "director/director.h"
#include "director/archive.h"
#include "director/movie.h"
#include "director/window.h"

namespace Director {

// Base Archive code

Archive::Archive() {
	_stream = nullptr;
	_isBigEndian = true;
}

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

Common::String Archive::getFileName() const { return Director::getFileName(_pathName.toString(g_director->_dirSeparator)); }

bool Archive::openFile(const Common::Path &path) {
	Common::File *file = new Common::File();

	if (path.empty() || !file->open(path)) {
		warning("Archive::openFile(): Error opening file %s", path.toString(Common::Path::kNativeSeparator).c_str());
		delete file;
		return false;
	}

	_pathName = path.toString(g_director->_dirSeparator);

	if (!openStream(file)) {
		warning("Archive::openFile(): Error loading stream from file %s", path.toString(Common::Path::kNativeSeparator).c_str());
		close();
		return false;
	}
	return true;
}

void Archive::close() {
	listUnaccessedChunks();

	_types.clear();

	if (_stream)
		delete _stream;

	_stream = nullptr;
}

void Archive::listUnaccessedChunks() {
	Common::String s;

	for (const auto &type : _types) {
		bool accessed = false;

		for (const auto &res : type._value) {
			if (res._value.accessed) {
				accessed = true;
				break;
			}
		}

		if (!accessed) {
			switch (type._key) {
			case MKTAG('f','r','e','e'):	// Freed chunk
			case MKTAG('j','u','n','k'):	// Some unreferenced junk
			case MKTAG('T','H','U','M'):	// Cast thumbnail - authoring only
			case MKTAG('T','h','u','m'):	// Cast thumbnail - authoring only (D7+)
			case MKTAG('F','C','O','L'):	// Favorite colors - authoring only
			case MKTAG('S','C','R','F'):	// Shared Cast refs - authoring only
			case MKTAG('V','W','t','c'):	// Score time code
			case MKTAG('V','W','t','k'):	// Tape Key resource. Used as a lookup for labels in early Directors
				break;
			default:
				s += Common::String::format("%s: %d items\n", tag2str(type._key), type._value.size());
			}
		}
	}

	if (!s.empty())
		debugC(5, kDebugLoading, "Unaccessed Chunks in '%s':\n%s", _pathName.toString(g_director->_dirSeparator).c_str(), s.c_str());
}

uint32 Archive::getFileSize() {
	if (!_stream)
		return 0;

	return _stream->size();
}

bool Archive::hasResource(uint32 tag, int id) const {
	if (!_types.contains(tag))
		return false;

	if (id == -1)
		return true;

	return _types[tag].contains(id);
}

bool Archive::hasResource(uint32 tag, const Common::String &resName) const {
	if (!_types.contains(tag) || resName.empty())
		return false;

	const ResourceMap &resMap = _types[tag];

	for (ResourceMap::const_iterator it = resMap.begin(); it != resMap.end(); it++)
		if (it->_value.name.matchString(resName))
			return true;

	return false;
}

Common::SeekableReadStreamEndian *Archive::getFirstResource(uint32 tag) {
	return getResource(tag, getResourceIDList(tag)[0]);
}

Common::SeekableReadStreamEndian *Archive::getFirstResource(uint32 tag, uint16 parentId) {
	return getResource(tag, getResourceIDList(tag)[0]);
}

Common::SeekableReadStreamEndian *Archive::getResource(uint32 tag, uint16 id) {
	if (!_types.contains(tag))
		error("Archive::getResource(): Archive does not contain '%s' %d", tag2str(tag), id);

	const ResourceMap &resMap = _types[tag];

	if (!resMap.contains(id))
		error("Archive::getResource(): Archive does not contain '%s' %d", tag2str(tag), id);

	const Resource &res = resMap[id];
	auto stream = new Common::SeekableSubReadStream(_stream, res.offset, res.offset + res.size, DisposeAfterUse::NO);

	_types[tag][id].accessed = true; // Mark it as accessed

	return new Common::SeekableReadStreamEndianWrapper(stream, _isBigEndian, DisposeAfterUse::YES);
}

Resource Archive::getResourceDetail(uint32 tag, uint16 id) {
	if (!_types.contains(tag))
		error("Archive::getResourceDetail(): Archive does not contain '%s' %d", tag2str(tag), id);

	const ResourceMap &resMap = _types[tag];

	if (!resMap.contains(id))
		error("Archive::getResourceDetail(): Archive does not contain '%s' %d", tag2str(tag), id);

	return resMap[id];
}

uint32 Archive::getOffset(uint32 tag, uint16 id) const {
	if (!_types.contains(tag))
		error("Archive::getOffset(): Archive does not contain '%s' %d", tag2str(tag), id);

	const ResourceMap &resMap = _types[tag];

	if (!resMap.contains(id))
		error("Archive::getOffset(): Archive does not contain '%s' %d", tag2str(tag), id);

	return resMap[id].offset;
}

uint Archive::getResourceSize(uint32 tag, uint16 id) const {
	if (!_types.contains(tag))
		error("Archive::getResourceSize(): Archive does not contain '%s' %d", tag2str(tag), id);

	const ResourceMap &resMap = _types[tag];

	if (!resMap.contains(id))
		error("Archive::getResourceSize(): Archive does not contain '%s' %d", tag2str(tag), id);

	return resMap[id].size;
}

uint16 Archive::findResourceID(uint32 tag, const Common::String &resName, bool ignoreCase) const {
	if (!_types.contains(tag) || resName.empty())
		return 0xFFFF;

	const ResourceMap &resMap = _types[tag];

	for (ResourceMap::const_iterator it = resMap.begin(); it != resMap.end(); it++)
		if (it->_value.name.matchString(resName, ignoreCase))
			return it->_key;

	return 0xFFFF;
}

Common::String Archive::getName(uint32 tag, uint16 id) const {
	if (!_types.contains(tag))
		error("Archive::getName(): Archive does not contain '%s' %d", tag2str(tag), id);

	const ResourceMap &resMap = _types[tag];

	if (!resMap.contains(id))
		error("Archive::getName(): Archive does not contain '%s' %d", tag2str(tag), id);

	return resMap[id].name;
}

Common::SeekableReadStreamEndian *Archive::getMovieResourceIfPresent(uint32 tag) {
	if (g_director->getVersion() >= 400) {
		if (_movieChunks.contains(tag) && hasResource(tag, _movieChunks[tag]))
			return getResource(tag, _movieChunks[tag]);

	} else if (hasResource(tag, -1)) {
		return getFirstResource(tag);
	}

	return nullptr;
}

Common::Array<uint32> Archive::getResourceTypeList() const {
	Common::Array<uint32> typeList;

	for (TypeMap::const_iterator it = _types.begin(); it != _types.end(); it++)
		typeList.push_back(it->_key);

	return typeList;
}

Common::Array<uint16> Archive::getResourceIDList(uint32 type) const {
	Common::Array<uint16> idList;

	if (!_types.contains(type))
		return idList;

	const ResourceMap &resMap = _types[type];

	for (ResourceMap::const_iterator it = resMap.begin(); it != resMap.end(); it++)
		idList.push_back(it->_key);

	return idList;
}

uint32 Archive::convertTagToUppercase(uint32 tag) {
	uint32 newTag = toupper(tag >> 24) << 24;
	newTag |= toupper((tag >> 16) & 0xFF) << 16;
	newTag |= toupper((tag >> 8) & 0xFF) << 8;

	return newTag | toupper(tag & 0xFF);
}

void Archive::dumpChunk(Resource &res, Common::DumpFile &out) {
	byte *data = nullptr;
	uint dataSize = 0;

	Common::SeekableReadStreamEndian *resStream = getResource(res.tag, res.index);
	if (!resStream) {
		return;
	}
	uint32 len = resStream->size();

	if (res.tag == MKTAG('f','r','e','e') || res.tag == MKTAG('j','u','n','k')) {
		if (len == 0)
			return;
	}

	if (dataSize < len) {
		data = (byte *)malloc(resStream->size());
		dataSize = resStream->size();
	}

	Common::Path prepend = _pathName.empty() ? Common::Path(g_director->getEXEName()) : _pathName;
	Common::Path filename(Common::String::format("./dumps/%s-%s-%d", encodePathForDump(prepend.toString(g_director->_dirSeparator)).c_str(), tag2str(res.tag), res.index), '/');
	resStream->read(data, len);

	if (!out.open(filename, true)) {
		warning("Archive::dumpChunk(): Can not open dump file %s", filename.toString(Common::Path::kNativeSeparator).c_str());
	} else {
		out.write(data, len);
		out.flush();
		out.close();
	}

	delete resStream;
	free(data);
}

Common::String Archive::formatArchiveInfo() {
	Common::String result = "unknown, ";
	if (_isBigEndian)
		result += "big endian,";
	else
		result += "little endian, ";
	return result;
}

// Mac Archive code

MacArchive::MacArchive() : Archive(), _resFork(nullptr) {
}

MacArchive::~MacArchive() {
	delete _resFork;
}

uint32 MacArchive::getFileSize() {
	if (!_resFork)
		return 0;

	return _resFork->getResForkDataSize();
}

void MacArchive::close() {
	Archive::close();
	delete _resFork;
	_resFork = nullptr;
}

bool MacArchive::openFile(const Common::Path &path) {
	close();

	_resFork = new Common::MacResManager();

	if (path.empty() || !_resFork->open(path) || !_resFork->hasResFork()) {
		close();
		return false;
	}

	_pathName = _resFork->getBaseFileName();
	Common::String basename(_pathName.baseName());
	if (basename.hasSuffix(".bin")) {
		for (int i = 0; i < 4; i++)
			basename.deleteLastChar();
		_pathName = _pathName.getParent().appendComponent(basename);
	}

	readTags();

	return true;
}

bool MacArchive::openStream(Common::SeekableReadStream *stream, uint32 startOffset) {
	close();

	if (startOffset)
		error("MacArchive::openStream(): startOffset > 0 is not yet implemented");

	_resFork = new Common::MacResManager();
	stream->seek(startOffset);

	if (!_resFork->loadFromMacBinary(stream)) {
		warning("MacArchive::openStream(): Error loading Mac Binary");
		close();
		return false;
	}

	_pathName = "<stream>";
	_resFork->setBaseFileName(Common::Path(_pathName));

	readTags();

	return true;
}

void MacArchive::readTags() {
	Common::MacResTagArray tagArray = _resFork->getResTagArray();
	Common::DumpFile out;

	for (uint32 i = 0; i < tagArray.size(); i++) {
		ResourceMap resMap;
		Common::MacResIDArray idArray = _resFork->getResIDArray(tagArray[i]);

		for (uint32 j = 0; j < idArray.size(); j++) {
			// Avoid assigning invalid entries to _types, because other
			// functions will assume they exist and are valid if listed.
			Common::SeekableReadStream *readStream = _resFork->getResource(tagArray[i], idArray[j]);
			if (readStream == nullptr) {
				continue;
			}
			delete readStream;

			Resource &res = resMap[idArray[j]];

			res.offset = res.size = 0; // unused
			res.name = _resFork->getResName(tagArray[i], idArray[j]);
			res.tag = tagArray[i];
			res.index = idArray[j];
			res.accessed = false;
			debugC(3, kDebugLoading, "MacArchive::readTags(): Found MacArchive resource '%s' %d: %s", tag2str(tagArray[i]), idArray[j], res.name.c_str());
			if (ConfMan.getBool("dump_scripts"))
				dumpChunk(res, out);

			res.accessed = false; // Clear flag so stats are not spoiled
		}

		// Don't assign a 0-entry resMap to _types.
		if (resMap.size() > 0) {
			 _types[tagArray[i]] = resMap;
		}
	}
	if (debugChannelSet(5, kDebugLoading)) {
		debugC(5, kDebugLoading, "MacArchive::readTags(): Resources found:");
		for (const auto &it : _types) {
			debugC(5, kDebugLoading, "%s: %d", tag2str(it._key), it._value.size());
		}
	}
}

Common::SeekableReadStreamEndian *MacArchive::getResource(uint32 tag, uint16 id) {
	assert(_resFork);
	Common::SeekableReadStream *stream = _resFork->getResource(tag, id);

	if (stream == nullptr) {
		error("MacArchive::getResource(): Archive does not contain '%s' %d", tag2str(tag), id);
	}

	_types[tag][id].accessed = true; // Mark it as accessed

	return new Common::SeekableReadStreamEndianWrapper(stream, true, DisposeAfterUse::YES);
}

Common::String MacArchive::formatArchiveInfo() {
	Common::String result = "Mac resource fork, ";
	if (_isBigEndian)
		result += "big endian";
	else
		result += "little endian";
	return result;
}

// RIFF Archive code

bool RIFFArchive::openStream(Common::SeekableReadStream *stream, uint32 startOffset) {
	close();

	_startOffset = startOffset;

	stream->seek(startOffset);

	_stream = stream;

	uint32 tag = stream->readUint32BE();

	if (convertTagToUppercase(tag) != MKTAG('R', 'I', 'F', 'F')) {
		debugC(5, kDebugLoading, "RIFFArchive::openStream(): RIFF expected but got '%s'", tag2str(tag));
		return false;
	}

	stream->readUint32LE(); // size

	tag = stream->readUint32BE();

	if (convertTagToUppercase(tag) != MKTAG('R', 'M', 'M', 'P')) {
		debugC(5, kDebugLoading, "RIFFArchive::openStream(): RMMP expected but  got '%s'", tag2str(tag));
		return false;
	}

	tag = stream->readUint32BE();
	if (convertTagToUppercase(tag) != MKTAG('C', 'F', 'T', 'C')) {
		debugC(5, kDebugLoading, "RIFFArchive::openStream(): CFTC expected but  got '%s'", tag2str(tag));
		return false;
	}

	uint32 cftcSize = stream->readUint32LE();
	uint32 startPos = stream->pos();
	stream->readUint32LE(); // Chunk number, alsways 0

	Common::DumpFile out;

	while ((uint32)stream->pos() < startPos + cftcSize) {
		tag = convertTagToUppercase(stream->readUint32BE());

		uint32 size = stream->readUint32LE();
		uint32 id = stream->readUint32LE();
		uint32 offset = stream->readUint32LE();

		if (tag == 0)
			break;

		uint32 startResPos = stream->pos();
		stream->seek(startOffset + offset + 12);

		Common::String name = "";
		byte nameSize = stream->readByte();

		if (nameSize) {
			bool skip = false;
			for (uint8 i = 0; i < nameSize; i++) {
				byte b = stream->readByte();

				if (!b)
					skip = true;

				if (!skip)
					name += b;
			}
		}

		debugC(3, kDebugLoading, "Found RIFF resource '%s' %d: %d @ 0x%08x (0x%08x)", tag2str(tag), id, size, offset, startOffset + offset);

		Resource &res = _types[tag][id];
		res.index = id;
		res.offset = offset;
		res.size = size;
		res.name = name;
		res.tag = tag;
		res.accessed = false;

		if (ConfMan.getBool("dump_scripts"))
			dumpChunk(res, out);

		res.accessed = false; // Clear flag so stats are not spoiled

		stream->seek(startResPos);
	}

	if (debugChannelSet(5, kDebugLoading)) {
		debugC(5, kDebugLoading, "RIFFArchive::openStream(): Resources found:");
		for (const auto &it : _types) {
			debugC(5, kDebugLoading, "%s: %d", tag2str(it._key), it._value.size());
		}
	}
	return true;
}

Common::SeekableReadStreamEndian *RIFFArchive::getResource(uint32 tag, uint16 id) {
	if (!_types.contains(tag))
		error("RIFFArchive::getResource(): Archive does not contain '%s' %d", tag2str(tag), id);

	const ResourceMap &resMap = _types[tag];

	if (!resMap.contains(id))
		error("RIFFArchive::getResource(): Archive does not contain '%s' %d", tag2str(tag), id);

	const Resource &res = resMap[id];

	// Adjust to skip the resource header
	// FourCC, size and id (all 4 bytes each)
	// Resource size excludes FourCC and size.
	uint32 offset = res.offset + 12;
	uint32 size = res.size - 4;

	// Skip the Pascal string
	_stream->seek(_startOffset + offset);
	byte stringSize = _stream->readByte(); // 1 for this byte

	offset += stringSize + 1;
	size -= stringSize + 1;

	// 'DIB ' was observed to always have two 0 bytes
	// other resources such as 'SND ' need alignment to nearest word boundary.
	bool needsAlignment = offset & 1;
	if (needsAlignment || tag == MKTAG('D', 'I', 'B', ' ')) {
		offset++;
		size--;
	}

	debugC(4, kDebugLoading, "RIFFArchive::getResource() tag: %s id: %i offset: %i size: %i", tag2str(tag), id, res.offset, res.size);

	_types[tag][id].accessed = true; // Mark it as accessed

	auto stream = new Common::SeekableSubReadStream(_stream, _startOffset + offset, _startOffset + offset + size, DisposeAfterUse::NO);
	return new Common::SeekableReadStreamEndianWrapper(stream, true, DisposeAfterUse::YES);
}

Common::String RIFFArchive::formatArchiveInfo() {
	Common::String result = "RIFF, ";
	if (_isBigEndian)
		result += "big endian";
	else
		result += "little endian";
	return result;
}

// RIFX Archive code

RIFXArchive::RIFXArchive() : Archive() {
	_isBigEndian = true;
	_rifxType = 0;
	_ilsBodyOffset = 0;
}

RIFXArchive::~RIFXArchive() {
	for (auto &it : _ilsData)
		free(it._value);
}

// Parallel to DirectorFile::read from ProjectorRays
bool RIFXArchive::openStream(Common::SeekableReadStream *stream, uint32 startOffset) {
	if (stream != _stream) {
		close();
		_stream = stream;
	}

	stream->seek(startOffset);

	uint32 moreOffset = 0;

	_metaTag = stream->readUint32BE();

	if (_metaTag != MKTAG('R', 'I', 'F', 'X') &&
		_metaTag != MKTAG('X', 'F', 'I', 'R')) {
		// Check if it is MacBinary

		stream->seek(startOffset);

		if (Common::MacResManager::isMacBinary(*stream)) {
			warning("RIFXArchive::openStream: MacBinary detected, overriding");

			// We need to look at the resource fork to detect XCOD resources
			Common::SeekableSubReadStream *macStream = new Common::SeekableSubReadStream(stream, 0, stream->size());
			MacArchive *macArchive = new MacArchive();
			if (macArchive->openStream(macStream)) {
				g_director->getCurrentWindow()->probeResources(macArchive);
			} else {
				delete macStream;
			}
			delete macArchive;

			// Then read the data fork
			moreOffset = Common::MacResManager::getDataForkOffset();
			stream->seek(startOffset + moreOffset);

			_metaTag = stream->readUint32BE();
		}
	}

	if (_metaTag == MKTAG('R', 'I', 'F', 'X')) {
		_isBigEndian = true;
	} else if (SWAP_BYTES_32(_metaTag) == MKTAG('R', 'I', 'F', 'X')) {
		_metaTag = SWAP_BYTES_32(_metaTag);
		_isBigEndian = false;
	} else {
		warning("RIFXArchive::openStream(): RIFX or XFIR expected but %s found", tag2str(_metaTag));
		return false;
	}

	Common::SeekableReadStreamEndianWrapper endianStream(stream, _isBigEndian, DisposeAfterUse::NO);
	endianStream.seek(startOffset + moreOffset + 4);

	_size = endianStream.readUint32() + 8; // size

	// If it is an embedded file, dump it if requested.
	// Start by copying the movie data to a new buffer.
	byte *dumpData = nullptr;
	Common::SeekableMemoryWriteStream *dumpStream = nullptr;
	if (ConfMan.getBool("dump_scripts") && startOffset) {
		dumpData = (byte *)malloc(_size);
		dumpStream = new Common::SeekableMemoryWriteStream(dumpData, _size);
		stream->seek(startOffset);
		stream->read(dumpData, _size);
		stream->seek(startOffset + 8);

		// Add the padding data to match the file size
		endianStream.seek(_size - 4);
		uint32 _junk = endianStream.readUint32();
		if (_junk != 0) {
			dumpStream->seek(_size - 4);
			dumpStream->writeUint32BE(0);
		}

		endianStream.seek(startOffset + moreOffset + 8);
	}

	_rifxType = endianStream.readUint32();
	debugC(1, kDebugLoading, "RIFX: type: %s", tag2str(_rifxType));

	// Now read the memory map.
	// At the same time, we will patch the offsets in the dump data.
	bool readMapSuccess = false;
	switch (_rifxType) {
	case MKTAG('M', 'V', '9', '3'):
	case MKTAG('M', 'C', '9', '5'):
	case MKTAG('A', 'P', 'P', 'L'):
		readMapSuccess = readMemoryMap(endianStream, moreOffset, dumpStream, startOffset);
		break;

	case MKTAG('F', 'G', 'D', 'M'):
	case MKTAG('F', 'G', 'D', 'C'):
		readMapSuccess = readAfterburnerMap(endianStream, moreOffset);
		break;
	default:
		break;
	}

	_types[MKTAG('R', 'I', 'F', 'X')][0].accessed = true; // Mark it as accessed

	// Now that the dump data has been patched, actually dump it.
	if (dumpData) {
		Common::DumpFile out;

		char buf[256];
		Common::sprintf_s(buf, "./dumps/%s-%08x", encodePathForDump(g_director->getEXEName()).c_str(), startOffset);

		if (out.open(buf, true)) {
			out.write(dumpData, _size);
			out.flush();
			out.close();
		} else {
			warning("RIFXArchive::openStream(): Can not open dump file %s", buf);
		}
	}
	free(dumpData);
	delete dumpStream;

	// If we couldn't read the map, we can't do anything past this point.
	if (!readMapSuccess)
		return false;

	if (_rifxType == MKTAG('A', 'P', 'P', 'L')) {
		if (hasResource(MKTAG('F', 'i', 'l', 'e'), -1)) {
			// Replace this archive with the embedded archive.
			Common::Array<uint16> subFiles = getResourceIDList(MKTAG('F', 'i', 'l', 'e'));
			uint numFile;
			int32 fileOffset = 0;
			for (numFile = 0; numFile < subFiles.size(); numFile++) {
				uint32 fileId = subFiles[numFile];
				fileOffset = _resources[fileId]->offset;
				endianStream.seek(fileOffset + 8);
				uint32 tag = endianStream.readUint32();
				if (tag != MKTAG('X', 't', 'r', 'a') && tag != MKTAG('a', 'r', 't', 'X'))
					break;
			}
			if (numFile < subFiles.size()) {
				_types.clear();
				_resources.clear();
				return openStream(_stream, fileOffset);
			}
		}

		warning("RIFXArchive::openStream: No 'File' resource present in APPL archive");
		return false;
	}

	if (ConfMan.getBool("dump_scripts")) {
		debug("RIFXArchive::openStream: Dumping %d resources", _resources.size());

		Common::DumpFile out;

		for (uint i = 0; i < _resources.size(); i++) {
			if ((_rifxType == MKTAG('F', 'G', 'D', 'M') || _rifxType == MKTAG('F', 'G', 'D', 'C')) && _resources[i]->index < 3) {
				// This is in the initial load segment and can't be read like a normal chunk.
				continue;
			}
			dumpChunk(*_resources[i], out);
		}
	}

	// A KEY* must be present
	if (!hasResource(MKTAG('K', 'E', 'Y', '*'), -1)) {
		warning("RIFXArchive::openStream: No 'KEY*' resource present");
	} else {
		// Parse the KEY*
		Common::SeekableReadStreamEndian *keyStream = getFirstResource(MKTAG('K', 'E', 'Y', '*'), true);
		readKeyTable(*keyStream);
		delete keyStream;
	}

	// Parse the CAS* for each library, if present
	uint32 casTag = MKTAG('C', 'A', 'S', '*');
	if (_keyData.contains(casTag)) {
		for (auto &it : _keyData[casTag]) {
			for (auto &jt : it._value) {
				if (Common::SeekableReadStreamEndian *casStream = getResource(casTag, jt)) {
					readCast(*casStream, it._key);
					delete casStream;
				}
			}
		}
	}

	return true;
}

bool RIFXArchive::readMemoryMap(Common::SeekableReadStreamEndian &stream, uint32 moreOffset, Common::SeekableMemoryWriteStream *dumpStream, uint32 movieStartOffset) {
	if (stream.readUint32() != MKTAG('i', 'm', 'a', 'p'))
		return false;

	_imapLength = stream.readUint32(); // imap length
	_mapversion = stream.readUint32(); // version, seen 0 or 1
	uint32 mmapOffsetPos = stream.pos();
	uint32 mmapOffset = stream.readUint32() + moreOffset;

	if (dumpStream) {
		// If we're dumping the movie, patch this offset in the dump data.
		dumpStream->seek(mmapOffsetPos - movieStartOffset);
		if (stream.isBE())
			dumpStream->writeUint32BE(mmapOffset - movieStartOffset);
		else
			dumpStream->writeUint32LE(mmapOffset - movieStartOffset);
	}
	_version = stream.readUint32(); // 0 for 4.0, 0x4c1 for 5.0, 0x4c7 for 6.0, 0x708 for 8.5, 0x742 for 10.0
	debugC(2, kDebugLoading, "RIFXArchive::readMemoryMap: _mapversion: %d version: %x offset: 0x%x (%d)", _mapversion, _version, mmapOffset, mmapOffset);

	stream.seek(mmapOffset);
	if (stream.readUint32() != MKTAG('m', 'm', 'a', 'p')) {
		warning("RIFXArchive::readMemoryMap: mmap expected but not found");
		return false;
	}

	/* uint32 mmapLength = */ stream.readUint32(); // mmap length
	_mmapHeaderSize = stream.readUint16(); // header size
	_mmapEntrySize = stream.readUint16(); // size of map entry
	_totalCount = stream.readUint32(); // resCount + empty entries
	_resCount = stream.readUint32();
	stream.skip(8); // all 0xFF
	stream.readUint32(); // id of the first free resource, -1 if none.

	_resources.reserve(_resCount);

	for (uint32 i = 0; i < _resCount; i++) {
		uint32 tag = stream.readUint32();
		uint32 size = stream.readUint32();
		uint32 offsetPos = stream.pos();
		int32 offset = stream.readUint32() + moreOffset;
		if (dumpStream) {
			dumpStream->seek(offsetPos - movieStartOffset);
			if (stream.isBE())
				dumpStream->writeUint32BE(offset - movieStartOffset);
			else
				dumpStream->writeUint32LE(offset - movieStartOffset);
		}
		uint16 flags = stream.readUint16();
		uint16 unk1 = stream.readUint16();
		uint32 nextFreeResourceId = stream.readUint32(); // for free resources, the next id, flag like for imap and mmap resources

		debugC(3, kDebugLoading, "Found RIFX resource index %d: '%s', %d bytes @ 0x%08x (%d), flags: %x unk1: %x nextFreeResourceId: %d",
			i, tag2str(tag), size, offset, offset, flags, unk1, nextFreeResourceId);

		Resource &res = _types[tag][i];
		res.index = i;
		res.offset = offset;
		res.size = size;
		res.tag = tag;
		res.flags = flags;
		res.unk1 = unk1;
		res.nextFreeResourceID = nextFreeResourceId;
		res.accessed = false;
		_resources.push_back(&res);
	}

	for (auto &it : _types[MKTAG('m', 'm', 'a', 'p')])	{
		_types[MKTAG('m', 'm', 'a', 'p')][it._key].accessed = true; // Mark it as accessed
	}

	for (auto &it : _types[MKTAG('i', 'm', 'a', 'p')]) {
		_types[MKTAG('i', 'm', 'a', 'p')][it._key].accessed = true; // Mark it as accessed
	}

	if (debugChannelSet(5, kDebugLoading)) {
		debugC(5, kDebugLoading, "RIFXArchive::readMemoryMap: Resources found:");
		for (const auto &it : _types) {
			debugC(5, kDebugLoading, "%s: %d", tag2str(it._key), it._value.size());
		}
	}

	return true;
}

bool RIFXArchive::readAfterburnerMap(Common::SeekableReadStreamEndian &stream, uint32 moreOffset) {
	uint32 start, end;

	// File version
	if (stream.readUint32() != MKTAG('F', 'v', 'e', 'r')) {
		warning("RIFXArchive::readAfterburnerMap(): Fver expected but not found");
		return false;
	}

	_fverLength = readVarInt(stream);
	start = stream.pos();
	_afterBurnerVersion = readVarInt(stream);
	debugC(3, kDebugLoading, "Fver: version: %x", _afterBurnerVersion);
	end = stream.pos();

	if (end - start != _fverLength) {
		warning("RIFXArchive::readAfterburnerMap(): Expected Fver of length %d but read %d bytes", _fverLength, end - start);
		stream.seek(start + _fverLength);
	}

	// Compression types
	if (stream.readUint32() != MKTAG('F', 'c', 'd', 'r')) {
		warning("RIFXArchive::readAfterburnerMap(): Fcdr expected but not found");
		return false;
	}

	_fcdrLength = readVarInt(stream);
	stream.skip(_fcdrLength);

	// Afterburner map
	if (stream.readUint32() != MKTAG('A', 'B', 'M', 'P')) {
		warning("RIFXArchive::readAfterburnerMap(): ABMP expected but not found");
		return false;
	}
	_abmpLength = readVarInt(stream);
	_abmpEnd = stream.pos() + _abmpLength;
	_abmpCompressionType = readVarInt(stream);
	_abmpUncompLength = readVarInt(stream);
	_abmpActualUncompLength = _abmpUncompLength;
	debugC(3, kDebugLoading, "ABMP: length: %d compressionType: %d uncompressedLength: %d",
		_abmpLength, _abmpCompressionType, _abmpUncompLength);

	Common::SeekableReadStreamEndian *abmpStream = readZlibData(stream, _abmpEnd - stream.pos(), &_abmpActualUncompLength, _isBigEndian);
	if (!abmpStream) {
		warning("RIFXArchive::readAfterburnerMap(): Could not uncompress ABMP");
		return false;
	}
	if (_abmpUncompLength != _abmpActualUncompLength) {
		warning("ABMP: Expected uncompressed length %d but got length %d", _abmpUncompLength, _abmpActualUncompLength);
	}

	if (ConfMan.getBool("dump_scripts")) {
		Common::DumpFile out;

		char buf[256];
		Common::sprintf_s(buf, "./dumps/%s-%s", encodePathForDump(g_director->getEXEName()).c_str(), "ABMP");

		if (out.open(buf, true)) {
			byte *data = (byte *)malloc(abmpStream->size());

			abmpStream->read(data, abmpStream->size());
			out.write(data, abmpStream->size());
			out.flush();
			out.close();

			free(data);

			abmpStream->seek(0);
		} else {
			warning("RIFXArchive::readAfterburnerMap(): Can not open dump file %s", buf);
		}
	}

	uint32 abmpUnk1 = readVarInt(*abmpStream);
	uint32 abmpUnk2 = readVarInt(*abmpStream);
	uint32 resCount = readVarInt(*abmpStream);
	debugC(3, kDebugLoading, "ABMP: unk1: %d unk2: %d resCount: %d",
		abmpUnk1, abmpUnk2, resCount);

	Common::HashMap<uint32, Resource *> resourceMap;
	for (uint32 i = 0; i < resCount; i++) {
		uint32 resId = readVarInt(*abmpStream);
		int32 offset = readVarInt(*abmpStream);
		if (offset >= 0)
			offset += moreOffset;
		uint32 compSize = readVarInt(*abmpStream);
		uint32 uncompSize = readVarInt(*abmpStream);
		uint32 compressionType = readVarInt(*abmpStream);
		uint32 tag = abmpStream->readUint32();

		debugC(3, kDebugLoading, "Found RIFX resource index %d: '%s', %d bytes (%d uncompressed) @ pos 0x%08x (%d), compressionType: %d",
			resId, tag2str(tag), compSize, uncompSize, offset, offset, compressionType);

		Resource &res = _types[tag][resId];
		res.index = resId;
		res.offset = offset;
		res.size = compSize;
		res.uncompSize = uncompSize;
		res.compressionType = compressionType;
		res.tag = tag;
		res.accessed = false;
		_resources.push_back(&res);
		resourceMap[resId] = &res;
	}

	delete abmpStream;

	// Handle Initial Load Segment (ILS)
	if (!resourceMap.contains(2)) {
		warning("RIFXArchive::readAfterburnerMap(): Map has no entry for ILS");
		return false;
	}
	if (stream.readUint32() != MKTAG('F', 'G', 'E', 'I')) {
		warning("RIFXArchive::readAfterburnerMap(): FGEI expected but not found");
		return false;
	}

	Resource *ilsRes = resourceMap[2];
	uint32 ilsUnk1 = readVarInt(stream);
	debugC(3, kDebugLoading, "ILS: length: %d unk1: %d", ilsRes->size, ilsUnk1);
	_ilsBodyOffset = stream.pos();
	uint32 ilsLength = ilsRes->size;
	uint32 ilsActualUncompLength = ilsRes->uncompSize;
	Common::SeekableReadStreamEndian *ilsStream = readZlibData(stream, ilsLength, &ilsActualUncompLength, _isBigEndian);
	if (!ilsStream) {
		warning("RIFXArchive::readAfterburnerMap(): Could not uncompress FGEI");
		return false;
	}
	if (ilsRes->uncompSize != ilsActualUncompLength) {
		warning("ILS: Expected uncompressed length %d but got length %d", ilsRes->uncompSize, ilsActualUncompLength);
	}

	while (ilsStream->pos() < ilsStream->size()) {
		uint32 resId = readVarInt(*ilsStream);
		Resource *res = resourceMap[resId];

		debugC(3, kDebugLoading, "Loading ILS resource %d: '%s', %d bytes", resId, tag2str(res->tag), res->size);

		byte *data = (byte *)malloc(res->size);
		ilsStream->read(data, res->size);
		_ilsData[resId] = data;
	}

	delete ilsStream;

	return true;
}

void RIFXArchive::readCast(Common::SeekableReadStreamEndian &casStream, uint16 libResourceId) {
	int castTag = MKTAG('C', 'A', 'S', 't');

	uint casSize = casStream.size() / 4;

	debugCN(2, kDebugLoading, "CAS*: libResourceId %d, %d members [", libResourceId, casSize);

	for (uint i = 0; i < casSize; i++) {
		uint32 castIndex = casStream.readUint32BE();
		debugCN(2, kDebugLoading, "%d ", castIndex);

		if (castIndex == 0) {
			continue;
		}
		Resource &res = _types[castTag][castIndex];
		res.castId = i;
		res.libResourceId = libResourceId;
	}
	debugC(2, kDebugLoading, "]");
}

void RIFXArchive::readKeyTable(Common::SeekableReadStreamEndian &keyStream) {
	_keyTableEntrySize = keyStream.readUint16(); // Should always be 12 (3 uint32's)
	_keyTableEntrySize2 = keyStream.readUint16();
	_keyTableEntryCount = keyStream.readUint32(); // There are more entries than actually used
	_keyTableUsedCount = keyStream.readUint32();

	debugC(2, kDebugLoading, "KEY*: _keyTableEntrySize: %d _keyTableEntrySize2: %d _keyTableEntryCount: %d usedCount: %d", _keyTableEntrySize, _keyTableEntrySize2, _keyTableEntryCount, _keyTableUsedCount);

	ResourceMap &castResMap = _types[MKTAG('C', 'A', 'S', 't')];

	for (uint16 i = 0; i < _keyTableUsedCount; i++) {
		uint32 childIndex = keyStream.readUint32();
		uint32 parentIndex = keyStream.readUint32();
		uint32 childTag = keyStream.readUint32();

		debugC(2, kDebugLoading, "KEY*: childIndex: %d parentIndex: %d childTag: %s", childIndex, parentIndex, tag2str(childTag));

		if (!_keyData.contains(childTag)) {
			_keyData[childTag] = KeyMap();
		}
		if (!_keyData[childTag].contains(parentIndex)) {
			_keyData[childTag][parentIndex] = KeyArray();
		}
		_keyData[childTag][parentIndex].push_back(childIndex);

		if (childTag == MKTAG('C', 'A', 'S', '*')) {
			// A 'CAS*' resource contains indexes of 'CASt' resources that are linked to the
			// Resource at parentIndex, hence, mark the 'CAS*' libResourceId to parentIndex
			// This means all the 'CASt' resources are linked to the parent with index equal to 'CAS*' libResourceId
			_types[childTag][childIndex].libResourceId = parentIndex;
		}

		// Link cast members to their resources.
		if (castResMap.contains(parentIndex)) {
			castResMap[parentIndex].children.push_back(_types[childTag][childIndex]);
		} else if (castResMap.contains(childIndex)) { // sometimes parent and child index are reversed...
			castResMap[childIndex].children.push_back(_types[childTag][parentIndex]);
		}

		// Link the movie to its resources.
		// The movie has the hardcoded ID 1024, which may collide with a cast member's ID
		// when there are many chunks. This is not a problem since cast members and
		// movies use different resource types, so we can tell them apart.
		if (parentIndex == 1024) {
			_movieChunks.setVal(childTag, childIndex);
		}
	}
}

Common::SeekableReadStreamEndian *RIFXArchive::getFirstResource(uint32 tag) {
	return getResource(tag, getResourceIDList(tag)[0], false);
}

Common::SeekableReadStreamEndian *RIFXArchive::getFirstResource(uint32 tag, bool fileEndianness) {
	return getResource(tag, getResourceIDList(tag)[0], fileEndianness);
}

Common::SeekableReadStreamEndian *RIFXArchive::getFirstResource(uint32 tag, uint16 parentId) {
	if (!_keyData.contains(tag))
		return nullptr;
	if (!_keyData[tag].contains(parentId))
		return nullptr;
	if (_keyData[tag][parentId].empty())
		return nullptr;
	return getResource(tag, _keyData[tag][parentId][0], false);
}


Common::SeekableReadStreamEndian *RIFXArchive::getResource(uint32 tag, uint16 id) {
	return getResource(tag, id, false);
}

Common::SeekableReadStreamEndian *RIFXArchive::getResource(uint32 tag, uint16 id, bool fileEndianness) {
	if (!_types.contains(tag))
		error("RIFXArchive::getResource(): Archive does not contain '%s' %d", tag2str(tag), id);

	const ResourceMap &resMap = _types[tag];

	if (!resMap.contains(id))
		error("RIFXArchive::getResource(): Archive does not contain '%s' %d", tag2str(tag), id);

	_types[tag][id].accessed = true; // Mark it as accessed

	const Resource &res = resMap[id];
	bool bigEndian = fileEndianness ? _isBigEndian : true;

	if (_rifxType == MKTAG('F', 'G', 'D', 'M') || _rifxType == MKTAG('F', 'G', 'D', 'C')) {
		if (res.offset == -1) {
			return new Common::MemoryReadStreamEndian(_ilsData[id], res.uncompSize, bigEndian, DisposeAfterUse::NO);
		} else {
			_stream->seek(_ilsBodyOffset + res.offset);
			uint32 actualUncompLength = res.uncompSize;
			Common::SeekableReadStreamEndian *stream = readZlibData(*_stream, res.size, &actualUncompLength, _isBigEndian);
			if (!stream) {
				error("RIFXArchive::getResource(): Could not uncompress '%s' %d", tag2str(tag), id);
			}
			if (res.uncompSize != actualUncompLength) {
				warning("RIFXArchive::getResource(): For '%s' %d expected uncompressed length %d but got length %d",
					tag2str(tag), id, res.uncompSize, actualUncompLength);
			}
			return stream;
		}
	}

	uint32 offset = res.offset + 8;
	uint32 size = res.size;
	auto stream = new Common::SeekableSubReadStream(_stream, offset, offset + size, DisposeAfterUse::NO);

	return new Common::SeekableReadStreamEndianWrapper(stream, bigEndian, DisposeAfterUse::YES);
}

Resource RIFXArchive::getResourceDetail(uint32 tag, uint16 id) {
	if (!_types.contains(tag))
		error("RIFXArchive::getResourceDetail(): Archive does not contain '%s' %d", tag2str(tag), id);

	const ResourceMap &resMap = _types[tag];

	if (!resMap.contains(id))
		error("RIFXArchive::getResourceDetail(): Archive does not contain '%s' %d", tag2str(tag), id);

	return resMap[id];
}

Common::String RIFXArchive::formatArchiveInfo() {
	Common::String result = "RIFX, ";
	if (_isBigEndian)
		result += "big endian, ";
	else
		result += "little endian, ";
	result += "type ";
	result += tag2str(_rifxType);
	return result;
}

} // End of namespace Director
