/* 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */


#include "common/debug.h"
#include "common/textconsole.h"

#include "sword1/memman.h"
#include "sword1/resman.h"
#include "sword1/swordres.h"

#include "gui/message.h"

namespace Sword1 {
void guiFatalError(char *msg) {
	// Displays a dialog on-screen before terminating the engine.
	// TODO: We really need to setup a special palette for cases when
	// the engine is erroring before setting one... otherwise invisible cursor :)

	GUI::MessageDialog dialog(msg);
	dialog.runModal();
	error("%s", msg);
}

#define MAX_PATH_LEN 260

ResMan::ResMan(const char *fileName, bool isMacFile) {
	_openCluStart = _openCluEnd = NULL;
	_openClus = 0;
	_isBigEndian = isMacFile;
	_memMan = new MemMan();
	loadCluDescript(fileName);
}

ResMan::~ResMan() {
#if 0
	for (uint32 clusCnt = 0; clusCnt < _prj.noClu; clusCnt++) {
		Clu *cluster = _prj.clu[clusCnt];
		if (cluster) {
			for (uint32 grpCnt = 0; grpCnt < cluster->noGrp; grpCnt++) {
				Grp *group = cluster->grp[grpCnt];
				if (group) {
					for (uint32 resCnt = 0; resCnt < group->noRes; resCnt++) {
						if (group->resHandle[resCnt].cond == MEM_DONT_FREE) {
							warning("ResMan::~ResMan: Resource %02X.%04X.%02X is still open",
							        clusCnt + 1, grpCnt, resCnt);
						}
					}
				}
			}
		}
	}
	debug(0, "ResMan closed\n");
#endif
	flush();
	freeCluDescript();
	delete _memMan;
}

void ResMan::loadCluDescript(const char *fileName) {
	// The cluster description file is always little endian (even on the mac version, whose cluster files are big endian)
	Common::File file;
	file.open(fileName);

	if (!file.isOpen()) {
		char msg[512];
		sprintf(msg, "Couldn't open CLU description '%s'\n\nIf you are running from CD, please ensure you have read the ScummVM documentation regarding multi-cd games.", fileName);
		guiFatalError(msg);
	}


	_prj.noClu = file.readUint32LE();
	_prj.clu = new Clu[_prj.noClu];
	memset(_prj.clu, 0, _prj.noClu * sizeof(Clu));

	uint32 *cluIndex = (uint32 *)malloc(_prj.noClu * 4);
	file.read(cluIndex, _prj.noClu * 4);

	for (uint32 clusCnt = 0; clusCnt < _prj.noClu; clusCnt++)
		if (cluIndex[clusCnt]) {
			Clu *cluster = _prj.clu + clusCnt;
			file.read(cluster->label, MAX_LABEL_SIZE);

			cluster->file = NULL;
			cluster->noGrp = file.readUint32LE();
			cluster->grp = new Grp[cluster->noGrp];
			cluster->nextOpen = NULL;
			memset(cluster->grp, 0, cluster->noGrp * sizeof(Grp));
			cluster->refCount = 0;

			uint32 *grpIndex = (uint32 *)malloc(cluster->noGrp * 4);
			file.read(grpIndex, cluster->noGrp * 4);

			for (uint32 grpCnt = 0; grpCnt < cluster->noGrp; grpCnt++)
				if (grpIndex[grpCnt]) {
					Grp *group = cluster->grp + grpCnt;
					group->noRes = file.readUint32LE();
					group->resHandle = new MemHandle[group->noRes];
					group->offset = new uint32[group->noRes];
					group->length = new uint32[group->noRes];
					uint32 *resIdIdx = (uint32 *)malloc(group->noRes * 4);
					file.read(resIdIdx, group->noRes * 4);

					for (uint32 resCnt = 0; resCnt < group->noRes; resCnt++) {
						if (resIdIdx[resCnt]) {
							group->offset[resCnt] = file.readUint32LE();
							group->length[resCnt] = file.readUint32LE();
							_memMan->initHandle(group->resHandle + resCnt);
						} else {
							group->offset[resCnt] = 0xFFFFFFFF;
							group->length[resCnt] = 0;
							_memMan->initHandle(group->resHandle + resCnt);
						}
					}
					free(resIdIdx);
				}
			free(grpIndex);
		}
	free(cluIndex);

	if (_prj.clu[3].grp[5].noRes == 29)
		for (uint8 cnt = 0; cnt < 29; cnt++)
			_srIdList[cnt] = 0x04050000 | cnt;
}

void ResMan::freeCluDescript() {

	for (uint32 clusCnt = 0; clusCnt < _prj.noClu; clusCnt++) {
		Clu *cluster = _prj.clu + clusCnt;
		for (uint32 grpCnt = 0; grpCnt < cluster->noGrp; grpCnt++) {
			Grp *group = cluster->grp + grpCnt;
			if (group->resHandle != NULL) {
				for (uint32 resCnt = 0; resCnt < group->noRes; resCnt++)
					_memMan->freeNow(group->resHandle + resCnt);

				delete[] group->resHandle;
				delete[] group->offset;
				delete[] group->length;
			}
		}
		delete[] cluster->grp;
		delete cluster->file;
	}
	delete[] _prj.clu;
}

void ResMan::flush() {
	for (uint32 clusCnt = 0; clusCnt < _prj.noClu; clusCnt++) {
		Clu *cluster = _prj.clu + clusCnt;
		for (uint32 grpCnt = 0; grpCnt < cluster->noGrp; grpCnt++) {
			Grp *group = cluster->grp + grpCnt;
			for (uint32 resCnt = 0; resCnt < group->noRes; resCnt++)
				if (group->resHandle[resCnt].cond != MEM_FREED) {
					_memMan->setCondition(group->resHandle + resCnt, MEM_CAN_FREE);
					group->resHandle[resCnt].refCount = 0;
				}
		}
		if (cluster->file) {
			cluster->file->close();
			delete cluster->file;
			cluster->file = NULL;
			cluster->refCount = 0;
		}
	}
	_openClus = 0;
	_openCluStart = _openCluEnd = NULL;
	// the memory manager cached the blocks we asked it to free, so explicitly make it free them
	_memMan->flush();
}

void *ResMan::fetchRes(uint32 id) {
	MemHandle *memHandle = resHandle(id);
	if (!memHandle) {
		warning("fetchRes:: resource %d out of bounds", id);
		return NULL;
	}
	if (!memHandle->data)
		error("fetchRes:: resource %d is not open", id);
	return memHandle->data;
}

void *ResMan::openFetchRes(uint32 id) {
	resOpen(id);
	return fetchRes(id);
}

void ResMan::dumpRes(uint32 id) {
	char outn[30];
	sprintf(outn, "DUMP%08X.BIN", id);
	Common::DumpFile outf;
	if (outf.open(outn)) {
		resOpen(id);
		MemHandle *memHandle = resHandle(id);
		if (memHandle) {
			outf.write(memHandle->data, memHandle->size);
			outf.close();
		}
		resClose(id);
	}
}

Header *ResMan::lockScript(uint32 scrID) {
	if (!_scriptList[scrID / ITM_PER_SEC])
		error("Script id %d not found", scrID);
	scrID = _scriptList[scrID / ITM_PER_SEC];
#ifdef SCUMM_BIG_ENDIAN
	openScriptResourceBigEndian(scrID);
#else
	openScriptResourceLittleEndian(scrID);
#endif
	MemHandle *handle = resHandle(scrID);
	if (!handle)
		error("Script resource handle %d not found", scrID);
	return (Header *)handle->data;
}

void ResMan::unlockScript(uint32 scrID) {
	resClose(_scriptList[scrID / ITM_PER_SEC]);
}

void *ResMan::cptResOpen(uint32 id) {
#ifdef SCUMM_BIG_ENDIAN
	openCptResourceBigEndian(id);
#else
	openCptResourceLittleEndian(id);
#endif
	MemHandle *handle = resHandle(id);
	return handle != NULL ? handle->data : NULL;
}

void ResMan::resOpen(uint32 id) {  // load resource ID into memory
	MemHandle *memHandle = resHandle(id);
	if (!memHandle)
		return;
	if (memHandle->cond == MEM_FREED) { // memory has been freed
		uint32 size = resLength(id);
		_memMan->alloc(memHandle, size);
		Common::File *clusFile = resFile(id);
		assert(clusFile);
		clusFile->seek(resOffset(id));
		clusFile->read(memHandle->data, size);
		if (clusFile->err() || clusFile->eos()) {
			error("Can't read %d bytes from offset %d from cluster file %s\nResource ID: %d (%08X)", size, resOffset(id), _prj.clu[(id >> 24) - 1].label, id, id);
		}
	} else
		_memMan->setCondition(memHandle, MEM_DONT_FREE);

	memHandle->refCount++;
	if (memHandle->refCount > 20) {
		debug(1, "%d references to id %d. Guess there's something wrong.", memHandle->refCount, id);
	}
}

void ResMan::resClose(uint32 id) {
	MemHandle *handle = resHandle(id);
	if (!handle)
		return;
	if (!handle->refCount) {
		warning("Resource Manager fail: unlocking object with refCount 0. Id: %d", id);
	} else {
		handle->refCount--;
		if (!handle->refCount)
			_memMan->setCondition(handle, MEM_CAN_FREE);
	}
}

FrameHeader *ResMan::fetchFrame(void *resourceData, uint32 frameNo) {
	uint8 *frameFile = (uint8 *)resourceData;
	uint8 *idxData = frameFile + sizeof(Header);
	if (_isBigEndian) {
		if (frameNo >= READ_BE_UINT32(idxData))
			error("fetchFrame:: frame %d doesn't exist in resource.", frameNo);
		frameFile += READ_BE_UINT32(idxData + (frameNo + 1) * 4);
	} else {
		if (frameNo >= READ_LE_UINT32(idxData))
			error("fetchFrame:: frame %d doesn't exist in resource.", frameNo);
		frameFile += READ_LE_UINT32(idxData + (frameNo + 1) * 4);
	}
	return (FrameHeader *)frameFile;
}

Common::File *ResMan::resFile(uint32 id) {
	Clu *cluster = _prj.clu + ((id >> 24) - 1);
	if (cluster->file == NULL) {
		_openClus++;
		if (_openCluEnd == NULL) {
			_openCluStart = _openCluEnd = cluster;
		} else {
			_openCluEnd->nextOpen = cluster;
			_openCluEnd = cluster;
		}
		cluster->file = new Common::File();
		char fileName[15];
		// Supposes that big endian means mac cluster file and little endian means PC cluster file.
		// This works, but we may want to separate the file name from the endianess or try .CLM extension if opening.clu file fail.
		if (_isBigEndian)
			sprintf(fileName, "%s.CLM", _prj.clu[(id >> 24) - 1].label);
		else
			sprintf(fileName, "%s.CLU", _prj.clu[(id >> 24) - 1].label);
		cluster->file->open(fileName);
		if (!cluster->file->isOpen()) {
			char msg[512];
			sprintf(msg, "Couldn't open game cluster file '%s'\n\nIf you are running from CD, please ensure you have read the ScummVM documentation regarding multi-cd games.", fileName);
			guiFatalError(msg);
		}
		while (_openClus > MAX_OPEN_CLUS) {
			assert(_openCluStart);
			Clu *closeClu = _openCluStart;
			_openCluStart = _openCluStart->nextOpen;

			if (closeClu) {
				if (closeClu->file)
					closeClu->file->close();
				delete closeClu->file;
				closeClu->file = NULL;
				closeClu->nextOpen = NULL;
			}
			_openClus--;
		}
	}
	return cluster->file;
}

MemHandle *ResMan::resHandle(uint32 id) {
	if ((id >> 16) == 0x0405)
		id = _srIdList[id & 0xFFFF];
	uint8 cluster = (uint8)((id >> 24) - 1);
	uint8 group = (uint8)(id >> 16);

	// There is a known case of reading beyond array boundaries when trying to use
	// portuguese subtitles (cluster file 2, group 6) with a version that does not
	// contain subtitles for this languages (i.e. has only 6 languages and not 7).
	if (cluster >= _prj.noClu || group >= _prj.clu[cluster].noGrp)
		return NULL;

	return &(_prj.clu[cluster].grp[group].resHandle[id & 0xFFFF]);
}

uint32 ResMan::resLength(uint32 id) {
	if ((id >> 16) == 0x0405)
		id = _srIdList[id & 0xFFFF];
	uint8 cluster = (uint8)((id >> 24) - 1);
	uint8 group = (uint8)(id >> 16);

	if (cluster >= _prj.noClu || group >= _prj.clu[cluster].noGrp)
		return 0;

	return _prj.clu[cluster].grp[group].length[id & 0xFFFF];
}

uint32 ResMan::resOffset(uint32 id) {
	if ((id >> 16) == 0x0405)
		id = _srIdList[id & 0xFFFF];
	uint8 cluster = (uint8)((id >> 24) - 1);
	uint8 group = (uint8)(id >> 16);

	if (cluster >= _prj.noClu || group >= _prj.clu[cluster].noGrp)
		return 0;

	return _prj.clu[cluster].grp[group].offset[id & 0xFFFF];
}

void ResMan::openCptResourceBigEndian(uint32 id) {
	bool needByteSwap = false;
	if (!_isBigEndian) {
		// Cluster files are in little endian fomat.
		// If the resource are not in memory anymore, and therefore will be read
		// from disk, they will need to be byte swaped.
		MemHandle *memHandle = resHandle(id);
		if (memHandle)
			needByteSwap = (memHandle->cond == MEM_FREED);
	}
	resOpen(id);
	if (needByteSwap) {
		MemHandle *handle = resHandle(id);
		if (!handle)
			return;
		uint32 totSize = handle->size;
		uint32 *data = (uint32 *)((uint8 *)handle->data + sizeof(Header));
		totSize -= sizeof(Header);
		if (totSize & 3)
			error("Illegal compact size for id %d: %d", id, totSize);
		totSize /= 4;
		for (uint32 cnt = 0; cnt < totSize; cnt++) {
			*data = READ_LE_UINT32(data);
			data++;
		}
	}
}

void ResMan::openCptResourceLittleEndian(uint32 id) {
	bool needByteSwap = false;
	if (_isBigEndian) {
		// Cluster files are in big endian fomat.
		// If the resource are not in memory anymore, and therefore will be read
		// from disk, they will need to be byte swaped.
		MemHandle *memHandle = resHandle(id);
		if (memHandle)
			needByteSwap = (memHandle->cond == MEM_FREED);
	}
	resOpen(id);
	if (needByteSwap) {
		MemHandle *handle = resHandle(id);
		if (!handle)
			return;
		uint32 totSize = handle->size;
		uint32 *data = (uint32 *)((uint8 *)handle->data + sizeof(Header));
		totSize -= sizeof(Header);
		if (totSize & 3)
			error("Illegal compact size for id %d: %d", id, totSize);
		totSize /= 4;
		for (uint32 cnt = 0; cnt < totSize; cnt++) {
			*data = READ_BE_UINT32(data);
			data++;
		}
	}
}

void ResMan::openScriptResourceBigEndian(uint32 id) {
	bool needByteSwap = false;
	if (!_isBigEndian) {
		// Cluster files are in little endian fomat.
		// If the resource are not in memory anymore, and therefore will be read
		// from disk, they will need to be byte swaped.
		MemHandle *memHandle = resHandle(id);
		if (memHandle)
			needByteSwap = (memHandle->cond == MEM_FREED);
	}
	resOpen(id);
	if (needByteSwap) {
		MemHandle *handle = resHandle(id);
		if (!handle)
			return;
		// uint32 totSize = handle->size;
		Header *head = (Header *)handle->data;
		head->comp_length = FROM_LE_32(head->comp_length);
		head->decomp_length = FROM_LE_32(head->decomp_length);
		head->version = FROM_LE_16(head->version);
		uint32 *data = (uint32 *)((uint8 *)handle->data + sizeof(Header));
		uint32 size = handle->size - sizeof(Header);
		if (size & 3)
			error("Odd size during script endian conversion. Resource ID =%d, size = %d", id, size);
		size >>= 2;
		for (uint32 cnt = 0; cnt < size; cnt++) {
			*data = READ_LE_UINT32(data);
			data++;
		}
	}
}

void ResMan::openScriptResourceLittleEndian(uint32 id) {
	bool needByteSwap = false;
	if (_isBigEndian) {
		// Cluster files are in big endian fomat.
		// If the resource are not in memory anymore, and therefore will be read
		// from disk, they will need to be byte swaped.
		MemHandle *memHandle = resHandle(id);
		if (memHandle)
			needByteSwap = (memHandle->cond == MEM_FREED);
	}
	resOpen(id);
	if (needByteSwap) {
		MemHandle *handle = resHandle(id);
		if (!handle)
			return;
		// uint32 totSize = handle->size;
		Header *head = (Header *)handle->data;
		head->comp_length = FROM_BE_32(head->comp_length);
		head->decomp_length = FROM_BE_32(head->decomp_length);
		head->version = FROM_BE_16(head->version);
		uint32 *data = (uint32 *)((uint8 *)handle->data + sizeof(Header));
		uint32 size = handle->size - sizeof(Header);
		if (size & 3)
			error("Odd size during script endian conversion. Resource ID =%d, size = %d", id, size);
		size >>= 2;
		for (uint32 cnt = 0; cnt < size; cnt++) {
			*data = READ_BE_UINT32(data);
			data++;
		}
	}
}


uint32 ResMan::_srIdList[29] = { // the file numbers differ for the control panel file IDs, so we need this array
	OTHER_SR_FONT,      // SR_FONT
	0x04050000,         // SR_BUTTON
	OTHER_SR_REDFONT,   // SR_REDFONT
	0x04050001,         // SR_PALETTE
	0x04050002,         // SR_PANEL_ENGLISH
	0x04050003,         // SR_PANEL_FRENCH
	0x04050004,         // SR_PANEL_GERMAN
	0x04050005,         // SR_PANEL_ITALIAN
	0x04050006,         // SR_PANEL_SPANISH
	0x04050007,         // SR_PANEL_AMERICAN
	0x04050008,         // SR_TEXT_BUTTON
	0x04050009,         // SR_SPEED
	0x0405000A,         // SR_SCROLL1
	0x0405000B,         // SR_SCROLL2
	0x0405000C,         // SR_CONFIRM
	0x0405000D,         // SR_VOLUME
	0x0405000E,         // SR_VLIGHT
	0x0405000F,         // SR_VKNOB
	0x04050010,         // SR_WINDOW
	0x04050011,         // SR_SLAB1
	0x04050012,         // SR_SLAB2
	0x04050013,         // SR_SLAB3
	0x04050014,         // SR_SLAB4
	0x04050015,         // SR_BUTUF
	0x04050016,         // SR_BUTUS
	0x04050017,         // SR_BUTDS
	0x04050018,         // SR_BUTDF
	0x04050019,         // SR_DEATHPANEL
	0,
};

} // End of namespace Sword1