/* 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/endian.h"
#include "common/file.h"
#include "common/textconsole.h"
#include "common/translation.h"
#include "sky/compact.h"
#include "gui/message.h"
#include <stddef.h>	// for ptrdiff_t

namespace Sky {

#define	SKY_CPT_SIZE	419427

#define OFFS(type,item) ((uint32)(((ptrdiff_t)(&((type *)42)->item))-42))
#define MK32(type,item) OFFS(type, item),0,0,0
#define MK16(type,item) OFFS(type, item),0
#define MK32_A5(type, item) MK32(type, item[0]), MK32(type, item[1]), \
	MK32(type, item[2]), MK32(type, item[3]), MK32(type, item[4])

static const uint32 compactOffsets[] = {
	MK16(Compact, logic),
	MK16(Compact, status),
	MK16(Compact, sync),
	MK16(Compact, screen),
	MK16(Compact, place),
	MK32(Compact, getToTableId),
	MK16(Compact, xcood),
	MK16(Compact, ycood),
	MK16(Compact, frame),
	MK16(Compact, cursorText),
	MK16(Compact, mouseOn),
	MK16(Compact, mouseOff),
	MK16(Compact, mouseClick),
	MK16(Compact, mouseRelX),
	MK16(Compact, mouseRelY),
	MK16(Compact, mouseSizeX),
	MK16(Compact, mouseSizeY),
	MK16(Compact, actionScript),
	MK16(Compact, upFlag),
	MK16(Compact, downFlag),
	MK16(Compact, getToFlag),
	MK16(Compact, flag),
	MK16(Compact, mood),
	MK32(Compact, grafixProgId),
	MK16(Compact, offset),
	MK16(Compact, mode),
	MK16(Compact, baseSub),
	MK16(Compact, baseSub_off),
	MK16(Compact, actionSub),
	MK16(Compact, actionSub_off),
	MK16(Compact, getToSub),
	MK16(Compact, getToSub_off),
	MK16(Compact, extraSub),
	MK16(Compact, extraSub_off),
	MK16(Compact, dir),
	MK16(Compact, stopScript),
	MK16(Compact, miniBump),
	MK16(Compact, leaving),
	MK16(Compact, atWatch),
	MK16(Compact, atWas),
	MK16(Compact, alt),
	MK16(Compact, request),
	MK16(Compact, spWidth_xx),
	MK16(Compact, spColor),
	MK16(Compact, spTextId),
	MK16(Compact, spTime),
	MK16(Compact, arAnimIndex),
	MK32(Compact, turnProgId),
	MK16(Compact, waitingFor),
	MK16(Compact, arTargetX),
	MK16(Compact, arTargetY),
	MK32(Compact, animScratchId),
	MK16(Compact, megaSet),
};

static const uint32 megaSetOffsets[] = {
	MK16(MegaSet, gridWidth),
	MK16(MegaSet, colOffset),
	MK16(MegaSet, colWidth),
	MK16(MegaSet, lastChr),
	MK32(MegaSet, animUpId),
	MK32(MegaSet, animDownId),
	MK32(MegaSet, animLeftId),
	MK32(MegaSet, animRightId),
	MK32(MegaSet, standUpId),
	MK32(MegaSet, standDownId),
	MK32(MegaSet, standLeftId),
	MK32(MegaSet, standRightId),
	MK32(MegaSet, standTalkId),
};

static const uint32 turnTableOffsets[] = {
	MK32_A5(TurnTable, turnTableUp),
	MK32_A5(TurnTable, turnTableDown),
	MK32_A5(TurnTable, turnTableLeft),
	MK32_A5(TurnTable, turnTableRight),
	MK32_A5(TurnTable, turnTableTalk),
};

#define COMPACT_SIZE (sizeof(compactOffsets)/sizeof(uint32))
#define MEGASET_SIZE (sizeof(megaSetOffsets)/sizeof(uint32))
#define TURNTABLE_SIZE (sizeof(turnTableOffsets)/sizeof(uint32))

SkyCompact::SkyCompact() {
	_cptFile = new Common::File();
	if (!_cptFile->open("sky.cpt")) {
		GUI::MessageDialog dialog(_("Unable to find \"sky.cpt\" file!\n"
								  "Please download it from www.scummvm.org"), _("OK"), NULL);
		dialog.runModal();
		error("Unable to find \"sky.cpt\" file\nPlease download it from www.scummvm.org");
	}

	uint16 fileVersion = _cptFile->readUint16LE();
	if (fileVersion != 0)
		error("unknown \"sky.cpt\" version");

	if (SKY_CPT_SIZE != _cptFile->size()) {
		GUI::MessageDialog dialog(_("The \"sky.cpt\" file has an incorrect size.\nPlease (re)download it from www.scummvm.org"), _("OK"), NULL);
		dialog.runModal();
		error("Incorrect sky.cpt size (%d, expected: %d)", _cptFile->size(), SKY_CPT_SIZE);
	}

	// set the necessary data structs up...
	_numDataLists = _cptFile->readUint16LE();
	_cptNames	  = (char***)malloc(_numDataLists * sizeof(char**));
	_dataListLen  = (uint16 *)malloc(_numDataLists * sizeof(uint16));
	_cptSizes	  = (uint16 **)malloc(_numDataLists * sizeof(uint16 *));
	_cptTypes	  = (uint16 **)malloc(_numDataLists * sizeof(uint16 *));
	_compacts	  = (Compact***)malloc(_numDataLists * sizeof(Compact**));

	for (int i = 0; i < _numDataLists; i++) {
		_dataListLen[i] = _cptFile->readUint16LE();
		_cptNames[i] = (char**)malloc(_dataListLen[i] * sizeof(char *));
		_cptSizes[i] = (uint16 *)malloc(_dataListLen[i] * sizeof(uint16));
		_cptTypes[i] = (uint16 *)malloc(_dataListLen[i] * sizeof(uint16));
		_compacts[i] = (Compact**)malloc(_dataListLen[i] * sizeof(Compact *));
	}

	uint32 rawSize = _cptFile->readUint32LE() * sizeof(uint16);
	uint16 *rawPos = _rawBuf = (uint16 *)malloc(rawSize);

	uint32 srcSize = _cptFile->readUint32LE() * sizeof(uint16);
	uint16 *srcBuf = (uint16 *)malloc(srcSize);
	uint16 *srcPos = srcBuf;
	_cptFile->read(srcBuf, srcSize);

	uint32 asciiSize = _cptFile->readUint32LE();
	char *asciiPos = _asciiBuf = (char *)malloc(asciiSize);
	_cptFile->read(_asciiBuf, asciiSize);

	// and fill them with the compact data
	for (uint32 lcnt = 0; lcnt < _numDataLists; lcnt++) {
		for (uint32 ecnt = 0; ecnt < _dataListLen[lcnt]; ecnt++) {
			_cptSizes[lcnt][ecnt] = READ_LE_UINT16(srcPos++);
			if (_cptSizes[lcnt][ecnt]) {
				_cptTypes[lcnt][ecnt] = READ_LE_UINT16(srcPos++);
				_compacts[lcnt][ecnt] = (Compact *)rawPos;
				_cptNames[lcnt][ecnt] = asciiPos;
				asciiPos += strlen(asciiPos) + 1;

				for (uint16 elemCnt = 0; elemCnt < _cptSizes[lcnt][ecnt]; elemCnt++)
					*rawPos++ = READ_LE_UINT16(srcPos++);
			} else {
				_cptTypes[lcnt][ecnt] = 0;
				_compacts[lcnt][ecnt] = NULL;
				_cptNames[lcnt][ecnt] = NULL;
			}
		}
	}
	free(srcBuf);

	uint16 numDlincs = _cptFile->readUint16LE();
	uint16 *dlincBuf = (uint16 *)malloc(numDlincs * 2 * sizeof(uint16));
	uint16 *dlincPos = dlincBuf;
	_cptFile->read(dlincBuf, numDlincs * 2 * sizeof(uint16));
	// these compacts don't actually exist but only point to other ones...
	uint16 cnt;
	for (cnt = 0; cnt < numDlincs; cnt++) {
		uint16 dlincId = READ_LE_UINT16(dlincPos++);
		uint16 destId = READ_LE_UINT16(dlincPos++);
		assert(((dlincId >> 12) < _numDataLists) && ((dlincId & 0xFFF) < _dataListLen[dlincId >> 12]) && (_compacts[dlincId >> 12][dlincId & 0xFFF] == NULL));
		_compacts[dlincId >> 12][dlincId & 0xFFF] = _compacts[destId >> 12][destId & 0xFFF];

		assert(_cptNames[dlincId >> 12][dlincId & 0xFFF] == NULL);
		_cptNames[dlincId >> 12][dlincId & 0xFFF] = asciiPos;
		asciiPos += strlen(asciiPos) + 1;
	}
	free(dlincBuf);

	// if this is v0.0288, parse this diff data
	uint16 numDiffs = _cptFile->readUint16LE();
	uint16 diffSize = _cptFile->readUint16LE();
	uint16 *diffBuf = (uint16 *)malloc(diffSize * sizeof(uint16));
	_cptFile->read(diffBuf, diffSize * sizeof(uint16));
	if (SkyEngine::_systemVars.gameVersion == 288) {
		uint16 *diffPos = diffBuf;
		for (cnt = 0; cnt < numDiffs; cnt++) {
			uint16 cptId = READ_LE_UINT16(diffPos++);
			uint16 *rawCpt = (uint16 *)fetchCpt(cptId);
			rawCpt += READ_LE_UINT16(diffPos++);
			uint16 len = READ_LE_UINT16(diffPos++);
			for (uint16 elemCnt = 0; elemCnt < len; elemCnt++)
				rawCpt[elemCnt] = READ_LE_UINT16(diffPos++);
		}
		assert(diffPos == (diffBuf + diffSize));
	}
	free(diffBuf);

	// these are the IDs that have to be saved into savegame files.
	_numSaveIds = _cptFile->readUint16LE();
	_saveIds = (uint16 *)malloc(_numSaveIds * sizeof(uint16));
	_cptFile->read(_saveIds, _numSaveIds * sizeof(uint16));
	for (cnt = 0; cnt < _numSaveIds; cnt++)
		_saveIds[cnt] = FROM_LE_16(_saveIds[cnt]);
	_resetDataPos = _cptFile->pos();
	
	checkAndFixOfficerBluntError();
}

SkyCompact::~SkyCompact() {
	free(_rawBuf);
	free(_asciiBuf);
	free(_saveIds);
	for (int i = 0; i < _numDataLists; i++) {
		free(_cptNames[i]);
		free(_cptSizes[i]);
		free(_cptTypes[i]);
		free(_compacts[i]);
	}
	free(_cptNames);
	free(_dataListLen);
	free(_cptSizes);
	free(_cptTypes);
	free(_compacts);
	_cptFile->close();
	delete _cptFile;
}

/* WORKAROUND for bug #2687: 
	The first release of scummvm with externalized, binary compact data has one broken 16 bit reference.
	When talking to Officer Blunt on ground level while in a crouched position, the game enters an
	unfinishable state because Blunt jumps into the lake and can no longer be interacted with.
	This fixes the problem when playing with a broken sky.cpt */
#define SCUMMVM_BROKEN_TALK_INDEX 158
void SkyCompact::checkAndFixOfficerBluntError() {
	// Retrieve the table with the animation ids to use for talking
	uint16 *talkTable = (uint16*)fetchCpt(CPT_TALK_TABLE_LIST);
	if (talkTable[SCUMMVM_BROKEN_TALK_INDEX] == ID_SC31_GUARD_TALK) {
		debug(1, "SKY.CPT with Officer Blunt bug encountered, fixing talk gfx.");
		talkTable[SCUMMVM_BROKEN_TALK_INDEX] = ID_SC31_GUARD_TALK2;
	}
}

// needed for some workaround where the engine has to check if it's currently processing joey, for example
bool SkyCompact::cptIsId(Compact *cpt, uint16 id) {
	return (cpt == fetchCpt(id));
}

Compact *SkyCompact::fetchCpt(uint16 cptId) {
	if (cptId == 0xFFFF) // is this really still necessary?
		return NULL;
	assert(((cptId >> 12) < _numDataLists) && ((cptId & 0xFFF) < _dataListLen[cptId >> 12]));

	debug(8, "Loading Compact %s [%s] (%04X=%d,%d)", _cptNames[cptId >> 12][cptId & 0xFFF], nameForType(_cptTypes[cptId >> 12][cptId & 0xFFF]), cptId, cptId >> 12, cptId & 0xFFF);

	return _compacts[cptId >> 12][cptId & 0xFFF];
}

Compact *SkyCompact::fetchCptInfo(uint16 cptId, uint16 *elems, uint16 *type, char *name) {
	assert(((cptId >> 12) < _numDataLists) && ((cptId & 0xFFF) < _dataListLen[cptId >> 12]));
	if (elems)
		*elems = _cptSizes[cptId >> 12][cptId & 0xFFF];
	if (type)
		*type  = _cptTypes[cptId >> 12][cptId & 0xFFF];
	if (name) {
		if (_cptNames[cptId >> 12][cptId & 0xFFF] != NULL)
			strcpy(name, _cptNames[cptId >> 12][cptId & 0xFFF]);
		else
			strcpy(name, "(null)");
	}
	return fetchCpt(cptId);
}

const char *SkyCompact::nameForType(uint16 type) {
	if (type >= NUM_CPT_TYPES)
		return "unknown";
	else
		return _typeNames[type];
}

uint16 *SkyCompact::getSub(Compact *cpt, uint16 mode) {
	switch (mode) {
	case 0:
		return &(cpt->baseSub);
	case 2:
		return &(cpt->baseSub_off);
	case 4:
		return &(cpt->actionSub);
	case 6:
		return &(cpt->actionSub_off);
	case 8:
		return &(cpt->getToSub);
	case 10:
		return &(cpt->getToSub_off);
	case 12:
		return &(cpt->extraSub);
	case 14:
		return &(cpt->extraSub_off);
	default:
		error("Invalid Mode (%d)", mode);
	}
}

uint16 *SkyCompact::getGrafixPtr(Compact *cpt) {
	uint16 *gfxBase = (uint16 *)fetchCpt(cpt->grafixProgId);
	if (gfxBase == NULL)
		return NULL;

	return gfxBase + cpt->grafixProgPos;
}

/**
 * Returns the n'th mega set specified by \a megaSet from Compact \a cpt.
 */
MegaSet *SkyCompact::getMegaSet(Compact *cpt) {
	switch (cpt->megaSet) {
	case 0:
		return &cpt->megaSet0;
	case NEXT_MEGA_SET:
		return &cpt->megaSet1;
	case NEXT_MEGA_SET*2:
		return &cpt->megaSet2;
	case NEXT_MEGA_SET*3:
		return &cpt->megaSet3;
	default:
		error("Invalid MegaSet (%d)", cpt->megaSet);
	}
}

/**
 \brief Returns the turn table for direction \a dir
	from Compact \a cpt in \a megaSet.

 Functionally equivalent to:
 \verbatim
 clear eax
 mov al,20
 mul (cpt[esi]).c_dir
 add ax,(cpt[esi]).c_mega_set
 lea eax,(cpt[esi+eax]).c_turn_table_up
 \endverbatim
*/
uint16 *SkyCompact::getTurnTable(Compact *cpt, uint16 dir) {
	MegaSet *m = getMegaSet(cpt);
	TurnTable *turnTable = (TurnTable *)fetchCpt(m->turnTableId);
	switch (dir) {
	case 0:
		return turnTable->turnTableUp;
	case 1:
		return turnTable->turnTableDown;
	case 2:
		return turnTable->turnTableLeft;
	case 3:
		return turnTable->turnTableRight;
	case 4:
		return turnTable->turnTableTalk;
	default:
		error("No TurnTable (%d) in MegaSet (%d)", dir, cpt->megaSet);
	}
}

void *SkyCompact::getCompactElem(Compact *cpt, uint16 off) {
	if (off < COMPACT_SIZE)
		return((uint8 *)cpt + compactOffsets[off]);
	off -= COMPACT_SIZE;

	if (off < MEGASET_SIZE)
		return((uint8 *)&(cpt->megaSet0) + megaSetOffsets[off]);

	off -= MEGASET_SIZE;
	if (off < TURNTABLE_SIZE)
		return ((uint8 *)fetchCpt(cpt->megaSet0.turnTableId) + turnTableOffsets[off]);

	off -= TURNTABLE_SIZE;
	if (off < MEGASET_SIZE)
		return((uint8 *)&(cpt->megaSet1) + megaSetOffsets[off]);

	off -= MEGASET_SIZE;
	if (off < TURNTABLE_SIZE)
		return ((uint8 *)fetchCpt(cpt->megaSet1.turnTableId) + turnTableOffsets[off]);

	off -= TURNTABLE_SIZE;
	if (off < MEGASET_SIZE)
		return((uint8 *)&(cpt->megaSet2) + megaSetOffsets[off]);

	off -= MEGASET_SIZE;
	if (off < TURNTABLE_SIZE)
		return ((uint8 *)fetchCpt(cpt->megaSet2.turnTableId) + turnTableOffsets[off]);

	off -= TURNTABLE_SIZE;
	if (off < MEGASET_SIZE)
		return((uint8 *)&(cpt->megaSet3) + megaSetOffsets[off]);

	off -= MEGASET_SIZE;
	if (off < TURNTABLE_SIZE)
		return ((uint8 *)fetchCpt(cpt->megaSet3.turnTableId) + turnTableOffsets[off]);
	off -= TURNTABLE_SIZE;

	error("Offset %X out of bounds of compact", (int)(off + COMPACT_SIZE + 4 * MEGASET_SIZE + 4 * TURNTABLE_SIZE));
}

uint8 *SkyCompact::createResetData(uint16 gameVersion) {
	_cptFile->seek(_resetDataPos);
	uint32 dataSize = _cptFile->readUint16LE() * sizeof(uint16);
	uint16 *resetBuf = (uint16 *)malloc(dataSize);
	_cptFile->read(resetBuf, dataSize);
	uint16 numDiffs = _cptFile->readUint16LE();
	for (uint16 cnt = 0; cnt < numDiffs; cnt++) {
		uint16 version = _cptFile->readUint16LE();
		uint16 diffFields = _cptFile->readUint16LE();
		if (version == gameVersion) {
			for (uint16 diffCnt = 0; diffCnt < diffFields; diffCnt++) {
				uint16 pos = _cptFile->readUint16LE();
				resetBuf[pos] = TO_LE_16(_cptFile->readUint16LE());
			}
			return (uint8 *)resetBuf;
		} else
			_cptFile->seek(diffFields * 2 * sizeof(uint16), SEEK_CUR);
	}
	free(resetBuf);
	error("Unable to find reset data for Beneath a Steel Sky Version 0.0%03d", gameVersion);
}

// - debugging functions

uint16 SkyCompact::findCptId(void *cpt) {
	for (uint16 listCnt = 0; listCnt < _numDataLists; listCnt++)
		for (uint16 elemCnt = 0; elemCnt < _dataListLen[listCnt]; elemCnt++)
			if (_compacts[listCnt][elemCnt] == cpt)
				return (listCnt << 12) | elemCnt;
	// not found
	debug(1, "Id for Compact %p wasn't found", cpt);
	return 0;
}

uint16 SkyCompact::findCptId(const char *cptName) {
	for (uint16 listCnt = 0; listCnt < _numDataLists; listCnt++)
		for (uint16 elemCnt = 0; elemCnt < _dataListLen[listCnt]; elemCnt++)
			if (_cptNames[listCnt][elemCnt] != 0)
				if (scumm_stricmp(cptName, _cptNames[listCnt][elemCnt]) == 0)
					return (listCnt << 12) | elemCnt;
	// not found
	debug(1, "Id for Compact %s wasn't found", cptName);
	return 0;
}

uint16 SkyCompact::giveNumDataLists() {
	return _numDataLists;
}

uint16 SkyCompact::giveDataListLen(uint16 listNum) {
	if (listNum >= _numDataLists) // list doesn't exist
		return 0;
	else
		return _dataListLen[listNum];
}

const char *const SkyCompact::_typeNames[NUM_CPT_TYPES] = {
	"null",
	"COMPACT",
	"TURNTABLE",
	"ANIM SEQ",
	"UNKNOWN",
	"GETTOTABLE",
	"AR BUFFER",
	"MAIN LIST"
};

} // End of namespace Sky