/* Copyright (C) 1994-1998 Revolution Software Ltd.
 * Copyright (C) 2003-2005 The ScummVM project
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * $Header$
 */

#include "common/stdafx.h"
#include "common/file.h"
#include "sword2/sword2.h"
#include "sword2/defs.h"
#include "sword2/console.h"
#include "sword2/logic.h"
#include "sword2/memory.h"
#include "sword2/resman.h"
#include "sword2/router.h"
#include "sword2/sound.h"

#define Debug_Printf _vm->_debugger->DebugPrintf

namespace Sword2 {

// Welcome to the easy resource manager - written in simple code for easy
// maintenance
// 
// The resource compiler will create two files
//
//	resource.inf which is a list of ascii cluster file names
//	resource.tab which is a table which tells us which cluster a resource
//	is located in and the number within the cluster

#if !defined(__GNUC__)
	#pragma START_PACK_STRUCTS
#endif

struct CdInf {
	uint8 clusterName[20];	// Null terminated cluster name.
	uint8 cd;		// Cd cluster is on and whether it is on the local drive or not.
} GCC_PACK;

#if !defined(__GNUC__)
	#pragma END_PACK_STRUCTS
#endif

ResourceManager::ResourceManager(Sword2Engine *vm) {
	_vm = vm;

	// Until proven differently, assume we're on CD 1. This is so the start
	// dialog will be able to play any music at all.
	_curCd = 1;

	// We read in the resource info which tells us the names of the
	// resource cluster files ultimately, although there might be groups
	// within the clusters at this point it makes no difference. We only
	// wish to know what resource files there are and what is in each

	File file;
	uint32 size;
	byte *temp;

	_totalClusters = 0;
	_resConvTable = NULL;

	if (!file.open("resource.inf"))
		error("Cannot open resource.inf");

	size = file.size();

	// Get some space for the incoming resource file - soon to be trashed
	temp = (byte *) malloc(size);

	if (file.read(temp, size) != size) {
		file.close();
		error("init cannot *READ* resource.inf");
	}

	file.close();

	// Ok, we've loaded in the resource.inf file which contains a list of
	// all the files now extract the filenames.

	// Using this method the Gode generated resource.inf must have #0d0a on
	// the last entry

	uint32 i = 0;
	uint32 j = 0;

	do {
		// item must have an #0d0a
		while (temp[i] != 13) {
			_resFiles[_totalClusters].fileName[j] = temp[i];
			i++;
			j++;
		}

		// NULL terminate our extracted string
		_resFiles[_totalClusters].fileName[j] = '\0';
		_resFiles[_totalClusters].numEntries = -1;
		_resFiles[_totalClusters].entryTab = NULL;

		// Reset position in current slot between entries, skip the
		// 0x0a in the source and increase the number of clusters.

		j = 0;
		i += 2;
		_totalClusters++;

		// TODO: put overload check here
	} while (i != size);

	free(temp);

	// Now load in the binary id to res conversion table
	if (!file.open("resource.tab"))
		error("Cannot open resource.tab");

	// Find how many resources
	size = file.size();

	_totalResFiles = size / 4;

	// Table seems ok so malloc some space
	_resConvTable = (uint16 *) malloc(size);

	for (i = 0; i < size / 2; i++)
		_resConvTable[i] = file.readUint16LE();

	if (file.ioFailed()) {
		file.close();
		error("Cannot read resource.tab");
	}

	file.close();

	if (!file.open("cd.inf"))
		error("Cannot open cd.inf");

	CdInf *cdInf = new CdInf[_totalClusters];

	for (i = 0; i < _totalClusters; i++) {
		file.read(cdInf[i].clusterName, sizeof(cdInf[i].clusterName));
		cdInf[i].cd = file.readByte();
		
		if (file.ioFailed())
			error("Cannot read cd.inf");
	}

	file.close();

	for (i = 0; i < _totalClusters; i++) {
		for (j = 0; j < _totalClusters; j++) {
			if (scumm_stricmp((char *) cdInf[j].clusterName, _resFiles[i].fileName) == 0)
				break;
		}

		if (j == _totalClusters)
			error("%s is not in cd.inf", _resFiles[i].fileName);

		_resFiles[i].cd = cdInf[j].cd;
	}

	delete [] cdInf;

	debug(1, "%d resources in %d cluster files", _totalResFiles, _totalClusters);
	for (i = 0; i < _totalClusters; i++)
		debug(2, "filename of cluster %d: -%s", i, _resFiles[i].fileName);

	_resList = (Resource *) malloc(_totalResFiles * sizeof(Resource));

	for (i = 0; i < _totalResFiles; i++) {
		_resList[i].ptr = NULL;
		_resList[i].size = 0;
		_resList[i].refCount = 0;
		_resList[i].prev = _resList[i].next = NULL;
	}
	_cacheStart = _cacheEnd = NULL;
	_usedMem = 0;
}

ResourceManager::~ResourceManager() {
	Resource *res = _cacheStart;
	while (res) {
		_vm->_memory->memFree(res->ptr);
		res = res->next;
	}
	free(_resList);
	free(_resConvTable);
}

// Quick macro to make swapping in-place easier to write

#define SWAP16(x)	x = SWAP_BYTES_16(x)
#define SWAP32(x)	x = SWAP_BYTES_32(x)

void convertEndian(byte *file, uint32 len) {
	int i;
	StandardHeader *hdr = (StandardHeader *) file;
	
	file += sizeof(StandardHeader);

	SWAP32(hdr->compSize);
	SWAP32(hdr->decompSize);

	switch (hdr->fileType) {
	case ANIMATION_FILE: {
		AnimHeader *animHead = (AnimHeader *) file;

		SWAP16(animHead->noAnimFrames);
		SWAP16(animHead->feetStartX);
		SWAP16(animHead->feetStartY);
		SWAP16(animHead->feetEndX);
		SWAP16(animHead->feetEndY);
		SWAP16(animHead->blend);

		CdtEntry *cdtEntry = (CdtEntry *) (file + sizeof(AnimHeader));
		for (i = 0; i < animHead->noAnimFrames; i++, cdtEntry++) {
			SWAP16(cdtEntry->x);
			SWAP16(cdtEntry->y);
			SWAP32(cdtEntry->frameOffset);

			FrameHeader *frameHeader = (FrameHeader *) (file + cdtEntry->frameOffset);
			// Quick trick to prevent us from incorrectly applying the endian
			// fixes multiple times. This assumes that frames are less than 1 MB
			// and have height/width less than 4096.
			if ((frameHeader->compSize & 0xFFF00000) ||
				(frameHeader->width & 0xF000) ||
				(frameHeader->height & 0xF000)) {
				SWAP32(frameHeader->compSize);
				SWAP16(frameHeader->width);
				SWAP16(frameHeader->height);
			}
		}
		break;
	}
	case SCREEN_FILE: {
		MultiScreenHeader *mscreenHeader = (MultiScreenHeader *) file;

		SWAP32(mscreenHeader->palette);
		SWAP32(mscreenHeader->bg_parallax[0]);
		SWAP32(mscreenHeader->bg_parallax[1]);
		SWAP32(mscreenHeader->screen);
		SWAP32(mscreenHeader->fg_parallax[0]);
		SWAP32(mscreenHeader->fg_parallax[1]);
		SWAP32(mscreenHeader->layers);
		SWAP32(mscreenHeader->paletteTable);
		SWAP32(mscreenHeader->maskOffset);

		// screenHeader
		ScreenHeader *screenHeader = (ScreenHeader *) (file + mscreenHeader->screen);

		SWAP16(screenHeader->width);
		SWAP16(screenHeader->height);
		SWAP16(screenHeader->noLayers);

		// layerHeader
		LayerHeader *layerHeader = (LayerHeader *) (file + mscreenHeader->layers);
		for (i = 0; i < screenHeader->noLayers; i++, layerHeader++) {
			SWAP16(layerHeader->x);
			SWAP16(layerHeader->y);
			SWAP16(layerHeader->width);
			SWAP16(layerHeader->height);
			SWAP32(layerHeader->maskSize);
			SWAP32(layerHeader->offset);
		}

		// backgroundParallaxLayer
		Parallax *parallax;
		int offset;
		offset = mscreenHeader->bg_parallax[0];
		if (offset > 0) {
			parallax = (Parallax *) (file + offset);
			SWAP16(parallax->w);
			SWAP16(parallax->h);
		}

		offset = mscreenHeader->bg_parallax[1];
		if (offset > 0) {
			parallax = (Parallax *) (file + offset);
			SWAP16(parallax->w);
			SWAP16(parallax->h);
		}

		// backgroundLayer
		offset = mscreenHeader->screen + sizeof(ScreenHeader);
		if (offset > 0) {
			parallax = (Parallax *) (file + offset);
			SWAP16(parallax->w);
			SWAP16(parallax->h);
		}

		// foregroundParallaxLayer
		offset = mscreenHeader->fg_parallax[0];
		if (offset > 0) {
			parallax = (Parallax *) (file + offset);
			SWAP16(parallax->w);
			SWAP16(parallax->h);
		}

		offset = mscreenHeader->fg_parallax[1];
		if (offset > 0) {
			parallax = (Parallax *) (file + offset);
			SWAP16(parallax->w);
			SWAP16(parallax->h);
		}
		break;
	}
	case GAME_OBJECT: {
		ObjectHub *objectHub = (ObjectHub *) file;

		objectHub->type = (int) SWAP_BYTES_32(objectHub->type);
		SWAP32(objectHub->logic_level);

		for (i = 0; i < TREE_SIZE; i++) {
			SWAP32(objectHub->logic[i]);
			SWAP32(objectHub->script_id[i]);
			SWAP32(objectHub->script_pc[i]);
		}
		break;
	}
	case WALK_GRID_FILE: {
		WalkGridHeader *walkGridHeader = (WalkGridHeader *) file;

		SWAP32(walkGridHeader->numBars);
		SWAP32(walkGridHeader->numNodes);

		BarData *barData = (BarData *) (file + sizeof(WalkGridHeader));
		for (i = 0; i < walkGridHeader->numBars; i++) {
			SWAP16(barData->x1);
			SWAP16(barData->y1);
			SWAP16(barData->x2);
			SWAP16(barData->y2);
			SWAP16(barData->xmin);
			SWAP16(barData->ymin);
			SWAP16(barData->xmax);
			SWAP16(barData->ymax);
			SWAP16(barData->dx);
			SWAP16(barData->dy);
			SWAP32(barData->co);
			barData++;
		}

		uint16 *node = (uint16 *) (file + sizeof(WalkGridHeader) + walkGridHeader->numBars * sizeof(BarData));
		for (i = 0; i < walkGridHeader->numNodes * 2; i++) {
			SWAP16(*node);
			node++;
		}

		break;
	}
	case GLOBAL_VAR_FILE:
		break;
	case PARALLAX_FILE_null:
		break;
	case RUN_LIST: {
		uint32 *list = (uint32 *) file;
		while (*list) {
			SWAP32(*list);
			list++;
		}
		break;
	}
	case TEXT_FILE: {
		TextHeader *textHeader = (TextHeader *) file;
		SWAP32(textHeader->noOfLines);
		break;
	}
	case SCREEN_MANAGER:
		break;
	case MOUSE_FILE:
		break;
	case ICON_FILE:
		break;
	}
}

/**
 * Returns the address of a resource. Loads if not in memory. Retains a count.
 */

byte *ResourceManager::openResource(uint32 res, bool dump) {
	assert(res < _totalResFiles);

	// Is the resource in memory already? If not, load it.

	if (!_resList[res].ptr) {
		// Fetch the correct file and read in the correct portion.
		uint16 cluFileNum = _resConvTable[res * 2]; // points to the number of the ascii filename
		assert(cluFileNum != 0xffff);

		// Relative resource within the file
		// First we have to find the file via the _resConvTable
		uint16 actual_res = _resConvTable[(res * 2) + 1];
		
		debug(5, "openResource %s res %d", _resFiles[cluFileNum].fileName, res);

		// If we're loading a cluster that's only available from one
		// of the CDs, remember which one so that we can play the
		// correct music.

		if ((_resFiles[cluFileNum].cd == CD1) || (_resFiles[cluFileNum].cd == CD2))
			_curCd = _resFiles[cluFileNum].cd;

		// Actually, as long as the file can be found we don't really
		// care which CD it's on. But if we can't find it, keep asking
		// for the CD until we do.

		File *file = openCluFile(cluFileNum);

		if (_resFiles[cluFileNum].entryTab == NULL) {
			// we didn't read from this file before, get its index table
			readCluIndex(cluFileNum, file);
		}

		uint32 pos = _resFiles[cluFileNum].entryTab[actual_res * 2 + 0];
		uint32 len = _resFiles[cluFileNum].entryTab[actual_res * 2 + 1];

		file->seek(pos, SEEK_SET);

		debug(6, "res len %d", len);

		// Ok, we know the length so try and allocate the memory.
		_resList[res].ptr = _vm->_memory->memAlloc(len, res);
		_resList[res].size = len;
		_resList[res].refCount = 0;

		file->read(_resList[res].ptr, len);

		if (dump) {
			StandardHeader *header = (StandardHeader *) _resList[res].ptr;
			char buf[256];
			const char *tag;
			File out;

			switch (header->fileType) {
			case ANIMATION_FILE:
				tag = "anim";
				break;
			case SCREEN_FILE:
				tag = "layer";
				break;
			case GAME_OBJECT:
				tag = "object";
				break;
			case WALK_GRID_FILE:
				tag = "walkgrid";
				break;
			case GLOBAL_VAR_FILE:
				tag = "globals";
				break;
			case PARALLAX_FILE_null:
				tag = "parallax";	// Not used!
				break;
			case RUN_LIST:
				tag = "runlist";
				break;
			case TEXT_FILE:
				tag = "text";
				break;
			case SCREEN_MANAGER:
				tag = "screen";
				break;
			case MOUSE_FILE:
				tag = "mouse";
				break;
			case WAV_FILE:
				tag = "wav";
				break;
			case ICON_FILE:
				tag = "icon";
				break;
			case PALETTE_FILE:
				tag = "palette";
				break;
			default:
				tag = "unknown";
				break;
			}

#if defined(MACOS_CARBON)
			sprintf(buf, ":dumps:%s-%d.dmp", tag, res);
#else
			sprintf(buf, "dumps/%s-%d.dmp", tag, res);
#endif

			if (!out.exists(buf, "")) {
				if (out.open(buf, File::kFileWriteMode, ""))
					out.write(_resList[res].ptr, len);
			}
		}

		// close the cluster
		file->close();
		delete file;

		_usedMem += len;
		checkMemUsage();

#ifdef SCUMM_BIG_ENDIAN
		convertEndian(_resList[res].ptr, len);
#endif
	} else if (_resList[res].refCount == 0)
		removeFromCacheList(_resList + res);

	_resList[res].refCount++;

	return _resList[res].ptr;
}

void ResourceManager::closeResource(uint32 res) {
	assert(res < _totalResFiles);
	assert(_resList[res].refCount > 0);

	_resList[res].refCount--;
	if (_resList[res].refCount == 0)
		addToCacheList(_resList + res);

	// It's tempting to free the resource immediately when refCount
	// reaches zero, but that'd be a mistake. Closing a resource does not
	// mean "I'm not going to use this resource any more". It means that
	// "the next time I use this resource I'm going to ask for a new
	// pointer to it".
	//
	// Since the original memory manager had to deal with memory
	// fragmentation, keeping a resource open - and thus locked down to a
	// specific memory address - was considered a bad thing.
}

void ResourceManager::removeFromCacheList(Resource *res) {
	if (_cacheStart == res)
		_cacheStart = res->next;

	if (_cacheEnd == res)
		_cacheEnd = res->prev;

	if (res->prev)
		res->prev->next = res->next;
	if (res->next)
		res->next->prev = res->prev;
	res->prev = res->next = NULL;
}

void ResourceManager::addToCacheList(Resource *res) {
	res->prev = NULL;
	res->next = _cacheStart;
	if (_cacheStart)
		_cacheStart->prev = res;
	_cacheStart = res;
	if (!_cacheEnd)
		_cacheEnd = res;
}

File *ResourceManager::openCluFile(uint16 fileNum) {
	File *file = new File;
	while (!file->open(_resFiles[fileNum].fileName)) {
		// If the file is supposed to be on hard disk, or we're
		// playing a demo, then we're in trouble if the file
		// can't be found!

		if ((_vm->_features & GF_DEMO) || (_resFiles[fileNum].cd & LOCAL_PERM))
			error("Could not find '%s'", _resFiles[fileNum].fileName);

		getCd(_resFiles[fileNum].cd & 3);
	}
	return file;
}

void ResourceManager::readCluIndex(uint16 fileNum, File *file) {
	if (_resFiles[fileNum].entryTab == NULL) {
		// we didn't read from this file before, get its index table
		if (file == NULL)
			file = openCluFile(fileNum);
		else
			file->incRef();

		// 1st DWORD of a cluster is an offset to the look-up table
		uint32 table_offset = file->readUint32LE();
		debug(6, "table offset = %d", table_offset);
		uint32 tableSize = file->size() - table_offset; // the table is stored at the end of the file
		file->seek(table_offset);

		assert((tableSize % 8) == 0);
		_resFiles[fileNum].entryTab = (uint32*)malloc(tableSize);
		_resFiles[fileNum].numEntries = tableSize / 8;
		file->read(_resFiles[fileNum].entryTab, tableSize);
		if (file->ioFailed())
			error("unable to read index table from file %s\n", _resFiles[fileNum].fileName);
#ifdef SCUMM_BIG_ENDIAN
		for (int tabCnt = 0; tabCnt < _resFiles[fileNum].numEntries * 2; tabCnt++)
			_resFiles[fileNum].entryTab[tabCnt] = FROM_LE_32(_resFiles[fileNum].entryTab[tabCnt]);
#endif
		file->decRef();
	}
}

/**
 * Returns true if resource is valid, otherwise false.
 */

bool ResourceManager::checkValid(uint32 res) {
	// Resource number out of range
	if (res >= _totalResFiles)
		return false;

	// Points to the number of the ascii filename
	uint16 parent_res_file = _resConvTable[res * 2];

	// Null & void resource
	if (parent_res_file == 0xffff)
		return false;

	return true;
}

/**
 * Returns the total file length of a resource - i.e. all headers are included
 * too.
 */

uint32 ResourceManager::fetchLen(uint32 res) {
	if (_resList[res].ptr)
		return _resList[res].size;

	// Does this ever happen?
	warning("fetchLen: Resource %u is not loaded; reading length from file", res);

	// Points to the number of the ascii filename
	uint16 parent_res_file = _resConvTable[res * 2];

	// relative resource within the file
	uint16 actual_res = _resConvTable[(res * 2) + 1];

	// first we have to find the file via the _resConvTable
	// open the cluster file

	if (_resFiles[parent_res_file].entryTab == NULL) {
		readCluIndex(parent_res_file);
	}
	return _resFiles[parent_res_file].entryTab[actual_res * 2 + 1];
}

void ResourceManager::checkMemUsage() {
	while (_usedMem > MAX_MEM_CACHE) {
		// we're using up more memory than we wanted to. free some old stuff.
		// Newly loaded objects are added to the start of the list,
		// we start freeing from the end, to free the oldest items first
		if (_cacheEnd) {
			Resource *tmp = _cacheEnd;
			assert((tmp->refCount == 0) && (tmp->ptr) && (tmp->next == NULL));
			removeFromCacheList(tmp);

			_vm->_memory->memFree(tmp->ptr);
			tmp->ptr = NULL;
			_usedMem -= tmp->size;
		} else {
			warning("%d bytes of memory used, but cache list is empty!\n");
			return;
		}
	}
}

void ResourceManager::remove(int res) {
	if (_resList[res].ptr) {
		removeFromCacheList(_resList + res);

		_vm->_memory->memFree(_resList[res].ptr);
		_resList[res].ptr = NULL;
		_resList[res].refCount = 0;
		_usedMem -= _resList[res].size;
	}
}

/**
 * Remove all res files from memory - ready for a total restart. This includes
 * the player object and global variables resource.
 */

void ResourceManager::removeAll() {
	// We need to clear the FX queue, because otherwise the sound system
	// will still believe that the sound resources are in memory, and that
	// it's ok to close them.

	_vm->_sound->clearFxQueue();

	for (uint i = 0; i < _totalResFiles; i++)
		remove(i);
}

/**
 * Remove all resources from memory.
 */

void ResourceManager::killAll(bool wantInfo) {
	int nuked = 0;

	// We need to clear the FX queue, because otherwise the sound system
	// will still believe that the sound resources are in memory, and that
	// it's ok to close them.

	_vm->_sound->clearFxQueue();

	for (uint i = 0; i < _totalResFiles; i++) {
		// Don't nuke the global variables or the player object!
		if (i == 1 || i == CUR_PLAYER_ID)
			continue;

		if (_resList[i].ptr) {
			StandardHeader *header = (StandardHeader *) _resList[i].ptr;

			if (wantInfo)
				Debug_Printf("Nuked %5d: %s\n", i, header->name);

			remove(i);
			nuked++;
		}
	}

	if (wantInfo)
		Debug_Printf("Expelled %d resources\n", nuked);
}

/**
 * Like killAll but only kills objects (except George & the variable table of
 * course) - ie. forcing them to reload & restart their scripts, which
 * simulates the effect of a save & restore, thus checking that each object's
 * re-entrant logic works correctly, and doesn't cause a statuette to
 * disappear forever, or some plaster-filled holes in sand to crash the game &
 * get James in trouble again.
 */

void ResourceManager::killAllObjects(bool wantInfo) {
	int nuked = 0;

	for (uint i = 0; i < _totalResFiles; i++) {
		// Don't nuke the global variables or the player object!
		if (i == 1 || i == CUR_PLAYER_ID)
			continue;

		if (_resList[i].ptr) {
			StandardHeader *header = (StandardHeader *) _resList[i].ptr;

			if (header->fileType == GAME_OBJECT) {
				if (wantInfo)
					Debug_Printf("Nuked %5d: %s\n", i, header->name);

				remove(i);
				nuked++;
			}
		}
	}

	if (wantInfo)
		Debug_Printf("Expelled %d resources\n", nuked);
}

int ResourceManager::whichCd() {
	return _curCd;
}

void ResourceManager::getCd(int cd) {
	byte *textRes;

	// Stop any music from playing - so the system no longer needs the
	// current CD - otherwise when we take out the CD, Windows will
	// complain!

	_vm->_sound->stopMusic(true);

	textRes = openResource(2283);
	_vm->_screen->displayMsg(_vm->fetchTextLine(textRes, 5 + cd) + 2, 0);
	closeResource(2283);

	// The original code probably determined automagically when the correct
	// CD had been inserted, but our backend doesn't support that, and
	// anyway I don't know if all systems allow that sort of thing. So we
	// wait for the user to press any key instead, or click the mouse.
	//
	// But just in case we ever try to identify the CDs by their labels,
	// they should be:
	//
	// CD1: "RBSII1" (or "PCF76" for the PCF76 version, whatever that is)
	// CD2: "RBSII2"
}

} // End of namespace Sword2