/* 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.
 *
 * $URL$
 * $Id$
 *
 * This file contains the handle based Memory Manager code
 */

#define BODGE

#include "common/file.h"

#include "tinsel/drives.h"
#include "tinsel/dw.h"
#include "tinsel/scn.h"			// name of "index" file
#include "tinsel/handle.h"
#include "tinsel/heapmem.h"			// heap memory manager


// these are included only so the relevant structs can be used in convertLEStructToNative()
#include "tinsel/anim.h"
#include "tinsel/multiobj.h"
#include "tinsel/film.h"
#include "tinsel/object.h"
#include "tinsel/palette.h"
#include "tinsel/text.h"
#include "tinsel/timers.h"
#include "tinsel/tinsel.h"
#include "tinsel/scene.h"

namespace Tinsel {

//----------------- EXTERNAL GLOBAL DATA --------------------

#ifdef DEBUG
bool bLockedScene = 0;
#endif


//----------------- LOCAL DEFINES --------------------

struct MEMHANDLE {
	char szName[12];	//!< 00 - file name of graphics file
	int32 filesize;		//!< 12 - file size and flags
	MEM_NODE *pNode;	//!< 16 - memory node for the graphics
	uint32 flags2;
};


/** memory allocation flags - stored in the top bits of the filesize field */
enum {
	fPreload	= 0x01000000L,	//!< preload memory
	fDiscard	= 0x02000000L,	//!< discard memory
	fSound		= 0x04000000L,	//!< sound data
	fGraphic	= 0x08000000L,	//!< graphic data
	fCompressed	= 0x10000000L,	//!< compressed data
	fLoaded		= 0x20000000L	//!< set when file data has been loaded
};
#define	FSIZE_MASK	0x00FFFFFFL	//!< mask to isolate the filesize
#define	MALLOC_MASK	0xFF000000L	//!< mask to isolate the memory allocation flags
//#define	HANDLEMASK		0xFF800000L	//!< get handle of address

//----------------- LOCAL GLOBAL DATA --------------------

// handle table gets loaded from index file at runtime
static MEMHANDLE *handleTable = 0;

// number of handles in the handle table
static uint numHandles = 0;

static uint32 cdPlayHandle = (uint32)-1;

static int	cdPlayFileNum, cdPlaySceneNum;
static SCNHANDLE cdBaseHandle = 0, cdTopHandle = 0;
static Common::File cdGraphStream;

static char szCdPlayFile[100];

//----------------- FORWARD REFERENCES --------------------

static void LoadFile(MEMHANDLE *pH, bool bWarn);	// load a memory block as a file


/**
 * Loads the graphics handle table index file and preloads all the
 * permanent graphics etc.
 */
void SetupHandleTable(void) {
	bool t2Flag = (TinselVersion == TINSEL_V2);
	int RECORD_SIZE = t2Flag ? 24 : 20;

	int len;
	uint i;
	MEMHANDLE *pH;
	Common::File f;

	if (f.open(INDEX_FILENAME)) {
		// get size of index file
		len = f.size();

		if (len > 0) {
			if ((len % RECORD_SIZE) != 0) {
				// index file is corrupt
				error(FILE_IS_CORRUPT, INDEX_FILENAME);
			}

			// calc number of handles
			numHandles = len / RECORD_SIZE;

			// allocate memory for the index file
			handleTable = (MEMHANDLE *)calloc(numHandles, sizeof(struct MEMHANDLE));

			// make sure memory allocated
			assert(handleTable);

			// load data
			for (i = 0; i < numHandles; i++) {
				f.read(handleTable[i].szName, 12);
				handleTable[i].filesize = f.readUint32LE();
				// The pointer should always be NULL. We don't
				// need to read that from the file.
				handleTable[i].pNode = NULL;
				f.seek(4, SEEK_CUR);
				// For Discworld 2, read in the flags2 field
				handleTable[i].flags2 = t2Flag ? f.readUint32LE() : 0;
			}

			if (f.ioFailed()) {
				// index file is corrupt
				error(FILE_IS_CORRUPT, INDEX_FILENAME);
			}

			// close the file
			f.close();
		} else {	// index file is corrupt
			error(FILE_IS_CORRUPT, INDEX_FILENAME);
		}
	} else {	// cannot find the index file
		error(CANNOT_FIND_FILE, INDEX_FILENAME);
	}

	// allocate memory nodes and load all permanent graphics
	for (i = 0, pH = handleTable; i < numHandles; i++, pH++) {
		if (pH->filesize & fPreload) {
			// allocate a fixed memory node for permanent files
			pH->pNode = MemoryAlloc(DWM_FIXED, sizeof(MEM_NODE) + (pH->filesize & FSIZE_MASK));

			// make sure memory allocated
			assert(pH->pNode);

			// Initialise the MEM_NODE structure
			memset(pH->pNode, 0, sizeof(MEM_NODE));

			// load the data
			LoadFile(pH, true);
		}
#ifdef BODGE
		else if ((pH->filesize & FSIZE_MASK) == 8) {
			pH->pNode = NULL;
		}
#endif
		else {
			// allocate a discarded memory node for other files
			pH->pNode = MemoryAlloc(
				DWM_MOVEABLE | DWM_DISCARDABLE | DWM_NOALLOC,
				pH->filesize & FSIZE_MASK);

			// make sure memory allocated
			assert(pH->pNode);
		}
	}
}

void FreeHandleTable(void) {
	if (handleTable) {
		free(handleTable);
		handleTable = NULL;
	}
	if (cdGraphStream.isOpen())
		cdGraphStream.close();
}

/**
 * Loads a memory block as a file.
 */
void OpenCDGraphFile(void) {
	if (cdGraphStream.isOpen())
		cdGraphStream.close();

	// As the theory goes, the right CD will be in there!

	cdGraphStream.clearIOFailed();
	cdGraphStream.open(szCdPlayFile);
	if (cdGraphStream.ioFailed())
		error(CANNOT_FIND_FILE, szCdPlayFile);
}

void LoadCDGraphData(MEMHANDLE *pH) {
	// read the data
	uint bytes;
	byte *addr;
	int	retries = 0;

	assert(!(pH->filesize & fCompressed));

	// Can't be preloaded
	assert(!(pH->filesize & fPreload));

	// discardable - lock the memory
	addr = (byte *)MemoryLock(pH->pNode);

	// make sure address is valid
	assert(addr);

	// Move to correct place in file and load the required data
	cdGraphStream.seek(cdBaseHandle & OFFSETMASK, SEEK_SET);
	bytes = cdGraphStream.read(addr, (cdTopHandle - cdBaseHandle) & OFFSETMASK);

	// New code to try and handle CD read failures 24/2/97
	while (bytes != ((cdTopHandle - cdBaseHandle) & OFFSETMASK) && retries++ < MAX_READ_RETRIES) 	{
		// Try again
		cdGraphStream.seek(cdBaseHandle & OFFSETMASK, SEEK_SET);
		bytes = cdGraphStream.read(addr, (cdTopHandle - cdBaseHandle) & OFFSETMASK);
	}

	// discardable - unlock the memory
	MemoryUnlock(pH->pNode);

	// set the loaded flag
	pH->filesize |= fLoaded;

	// clear the loading flag
//	pH->filesize &= ~fLoading;

	if (bytes != ((cdTopHandle-cdBaseHandle) & OFFSETMASK))
		// file is corrupt
		error(FILE_READ_ERROR, "CD play file");
}

/**
 * Called immediatly preceding a CDplay().
 * Prepares the ground so that when LockMem() is called, the
 * appropriate section of the extra scene file is loaded.
 * @param start			Handle of start of range
 * @param next			Handle of end of range + 1
 */
void LoadExtraGraphData(SCNHANDLE start, SCNHANDLE next) {
	if (cdPlayFileNum == cdPlaySceneNum && start == cdBaseHandle)
		return;

	OpenCDGraphFile();

	if ((handleTable + cdPlayHandle)->pNode->pBaseAddr != NULL)
		MemoryDiscard((handleTable + cdPlayHandle)->pNode); // Free it

	// It must always be the same
	assert(cdPlayHandle == (start >> SCNHANDLE_SHIFT));
	assert(cdPlayHandle == (next >> SCNHANDLE_SHIFT));

	cdBaseHandle = start;
	cdTopHandle = next;
}

void SetCdPlaySceneDetails(int fileNum, const char *fileName) {
	cdPlaySceneNum = fileNum;
	strcpy(szCdPlayFile, fileName);
}

void SetCdPlayHandle(int fileNum) {
	cdPlayHandle = fileNum;
}


/**
 * Loads a memory block as a file.
 * @param pH			Memory block pointer
 * @param bWarn			If set, treat warnings as errors
 */
void LoadFile(MEMHANDLE *pH, bool bWarn) {
	Common::File f;
	char szFilename[sizeof(pH->szName) + 1];

	if (pH->filesize & fCompressed) {
		error("Compression handling has been removed!");
	}

	// extract and zero terminate the filename
	strncpy(szFilename, pH->szName, sizeof(pH->szName));
	szFilename[sizeof(pH->szName)] = 0;

	if (f.open(szFilename)) {
		// read the data
		int bytes;
		uint8 *addr;

		if (pH->filesize & fPreload)
			// preload - no need to lock the memory
			addr = (uint8 *)pH->pNode + sizeof(MEM_NODE);
		else {
			// discardable - lock the memory
			addr = (uint8 *)MemoryLock(pH->pNode);
		}
#ifdef DEBUG
		if (addr == NULL) {
			if (pH->filesize & fPreload)
				// preload - no need to lock the memory
				addr = (uint8 *)pH->pNode;
			else {
				// discardable - lock the memory
				addr = (uint8 *)MemoryLock(pH->pNode);
			}
		}
#endif

		// make sure address is valid
		assert(addr);

		bytes = f.read(addr, pH->filesize & FSIZE_MASK);

		// close the file
		f.close();

		if ((pH->filesize & fPreload) == 0) {
			// discardable - unlock the memory
			MemoryUnlock(pH->pNode);
		}

		// set the loaded flag
		pH->filesize |= fLoaded;

		if (bytes == (pH->filesize & FSIZE_MASK)) {
			return;
		}

		if (bWarn)
			// file is corrupt
			error(FILE_IS_CORRUPT, szFilename);
	}

	if (bWarn)
		// cannot find file
		error(CANNOT_FIND_FILE, szFilename);
}

/**
 * Returns the address of a image, given its memory handle.
 * @param offset			Handle and offset to data
 */
uint8 *LockMem(SCNHANDLE offset) {
	uint32 handle = offset >> SCNHANDLE_SHIFT;	// calc memory handle to use
	MEMHANDLE *pH;			// points to table entry

	// range check the memory handle
	assert(handle < numHandles);

	pH = handleTable + handle;

	if (pH->filesize & fPreload) {
		if (TinselV2)
			// update the LRU time (new in this file)
			pH->pNode->lruTime = DwGetCurrentTime();

		// permanent files are already loaded
		return (uint8 *)pH->pNode + sizeof(MEM_NODE) + (offset & OFFSETMASK);
	} else if (handle == cdPlayHandle) {
		// Must be in currently loaded/loadable range
		if (offset < cdBaseHandle || offset >= cdTopHandle)
			error("Overlapping (in time) CD-plays!");

		if (pH->pNode->pBaseAddr && (pH->filesize & fLoaded))
			// already allocated and loaded
			return pH->pNode->pBaseAddr + ((offset - cdBaseHandle) & OFFSETMASK);

		if (pH->pNode->pBaseAddr == NULL)
			// must have been discarded - reallocate the memory
			MemoryReAlloc(pH->pNode, cdTopHandle-cdBaseHandle,
				DWM_MOVEABLE | DWM_DISCARDABLE);

		if (pH->pNode->pBaseAddr == NULL)
			error("Out of memory");

		LoadCDGraphData(pH);

		// make sure address is valid
		assert(pH->pNode->pBaseAddr);

		// update the LRU time (new in this file)
		pH->pNode->lruTime = DwGetCurrentTime();

		return pH->pNode->pBaseAddr + ((offset - cdBaseHandle) & OFFSETMASK);

	} else {
		if (pH->pNode->pBaseAddr && (pH->filesize & fLoaded))
			// already allocated and loaded
			return pH->pNode->pBaseAddr + (offset & OFFSETMASK);

		if (pH->pNode->pBaseAddr == NULL)
			// must have been discarded - reallocate the memory
			MemoryReAlloc(pH->pNode, pH->filesize & FSIZE_MASK,
				DWM_MOVEABLE | DWM_DISCARDABLE);

		if (pH->pNode->pBaseAddr == NULL)
			error("Out of memory");

		LoadFile(pH, true);

		// make sure address is valid
		assert(pH->pNode->pBaseAddr);

		return pH->pNode->pBaseAddr + (offset & OFFSETMASK);
	}
}

/**
 * Called to make the current scene non-discardable.
 * @param offset			Handle and offset to data
 */
void LockScene(SCNHANDLE offset) {

	uint32 handle = offset >> SCNHANDLE_SHIFT;	// calc memory handle to use
	MEMHANDLE *pH;					// points to table entry

#ifdef DEBUG
	assert(!bLockedScene); // Trying to lock more than one scene
#endif

	// range check the memory handle
	assert(handle < numHandles);

	pH = handleTable + handle;

	// compact the heap to avoid fragmentation while scene is non-discardable
	HeapCompact(MAX_INT, false);

	if ((pH->filesize & fPreload) == 0) {
		// change the flags for the node
		// WORKAROUND: The original didn't include the DWM_LOCKED flag. It's being
		// included because the method is 'LockScene' so it's presumed that the
		// point of this was that the scene's memory block be locked
		MemoryReAlloc(pH->pNode, pH->filesize & FSIZE_MASK, DWM_MOVEABLE | DWM_LOCKED);
#ifdef DEBUG
		bLockedScene = true;
#endif
	}
}

/**
 * Called to make the current scene discardable again.
 * @param offset			Handle and offset to data
 */
void UnlockScene(SCNHANDLE offset) {

	uint32 handle = offset >> SCNHANDLE_SHIFT;	// calc memory handle to use
	MEMHANDLE *pH;					// points to table entry

	// range check the memory handle
	assert(handle < numHandles);

	pH = handleTable + handle;

	if ((pH->filesize & fPreload) == 0) {
		// change the flags for the node
		MemoryReAlloc(pH->pNode, pH->filesize & FSIZE_MASK, DWM_MOVEABLE | DWM_DISCARDABLE);
#ifdef DEBUG
		bLockedScene = false;
#endif
	}
}

/*----------------------------------------------------------------------*/

#ifdef BODGE

/**
 * Validates that a specified handle pointer is valid
 * @param offset			Handle and offset to data
 */
bool ValidHandle(SCNHANDLE offset) {
	uint32 handle = offset >> SCNHANDLE_SHIFT;	// calc memory handle to use
	MEMHANDLE *pH;					// points to table entry

	// range check the memory handle
	assert(handle < numHandles);

	pH = handleTable + handle;

	return (pH->filesize & FSIZE_MASK) != 8;
}
#endif

/**
 * TouchMem
 * @param offset			Handle and offset to data
 */
void TouchMem(SCNHANDLE offset) {
	MEMHANDLE *pH;					// points to table entry
	uint32 handle = offset >> SCNHANDLE_SHIFT;	// calc memory handle to use

	if (offset != 0) {
		pH = handleTable + handle;

		// update the LRU time whether its loaded or not!
		pH->pNode->lruTime = DwGetCurrentTime();
	}
}

/**
 * Returns true if the given handle is into the cd graph data
 * @param offset			Handle and offset to data
 */
bool IsCdPlayHandle(SCNHANDLE offset) {
	uint32 handle = offset >> SCNHANDLE_SHIFT;	// calc memory handle to use

	// range check the memory handle
	assert(handle < numHandles);

	return (handle == cdPlayHandle);
}

/**
 * Returns the CD for a given scene handle
 */
int CdNumber(SCNHANDLE offset) {
	uint handle = offset >> SCNHANDLE_SHIFT;	// calc memory handle to use

	// range check the memory handle
	assert(handle < numHandles);

	MEMHANDLE *pH = handleTable + handle;

	if (!TinselV2)
		return 1;

	return GetCD(pH->flags2 & fAllCds);
}

} // end of namespace Tinsel