/* 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: https://scummvm.svn.sourceforge.net/svnroot/scummvm/scummvm/tags/release-1-1-1/engines/sci/engine/script.cpp $
 * $Id: script.cpp 48049 2010-02-13 17:44:58Z fingolfin $
 *
 */

#include "sci/sci.h"
#include "sci/resource.h"
#include "sci/engine/features.h"
#include "sci/engine/state.h"
#include "sci/engine/kernel.h"
#include "sci/engine/script.h"

#include "common/util.h"

namespace Sci {

#define END Script_None

opcode_format g_opcode_formats[128][4] = {
	/*00*/
	{Script_None}, {Script_None}, {Script_None}, {Script_None},
	/*04*/
	{Script_None}, {Script_None}, {Script_None}, {Script_None},
	/*08*/
	{Script_None}, {Script_None}, {Script_None}, {Script_None},
	/*0C*/
	{Script_None}, {Script_None}, {Script_None}, {Script_None},
	/*10*/
	{Script_None}, {Script_None}, {Script_None}, {Script_None},
	/*14*/
	{Script_None}, {Script_None}, {Script_None}, {Script_SRelative, END},
	/*18*/
	{Script_SRelative, END}, {Script_SRelative, END}, {Script_SVariable, END}, {Script_None},
	/*1C*/
	{Script_SVariable, END}, {Script_None}, {Script_None}, {Script_Variable, END},
	/*20*/
	{Script_SRelative, Script_Byte, END}, {Script_Variable, Script_Byte, END}, {Script_Variable, Script_Byte, END}, {Script_Variable, Script_SVariable, Script_Byte, END},
	/*24 (24=ret)*/
	{Script_End}, {Script_Byte, END}, {Script_Invalid}, {Script_Invalid},
	/*28*/
	{Script_Variable, END}, {Script_Invalid}, {Script_Byte, END}, {Script_Variable, Script_Byte, END},
	/*2C*/
	{Script_SVariable, END}, {Script_SVariable, Script_Variable, END}, {Script_None}, {Script_Invalid},
	/*30*/
	{Script_None}, {Script_Property, END}, {Script_Property, END}, {Script_Property, END},
	/*34*/
	{Script_Property, END}, {Script_Property, END}, {Script_Property, END}, {Script_Property, END},
	/*38*/
	{Script_Property, END}, {Script_SRelative, END}, {Script_SRelative, END}, {Script_None},
	/*3C*/
	{Script_None}, {Script_None}, {Script_None}, {Script_Word},
	/*40-4F*/
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	/*50-5F*/
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	/*60-6F*/
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	/*70-7F*/
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END},
	{Script_Global, END}, {Script_Local, END}, {Script_Temp, END}, {Script_Param, END}
};
#undef END

// TODO: script_adjust_opcode_formats should probably be part of the
// constructor (?) of a VirtualMachine or a ScriptManager class.
void script_adjust_opcode_formats(EngineState *s) {
	// TODO: Check that this is correct
	if (g_sci->_features->detectLofsType() != SCI_VERSION_0_EARLY) {
		g_opcode_formats[op_lofsa][0] = Script_Offset;
		g_opcode_formats[op_lofss][0] = Script_Offset;
	}

#ifdef ENABLE_SCI32
	// In SCI32, some arguments are now words instead of bytes
	if (getSciVersion() >= SCI_VERSION_2) {
		g_opcode_formats[op_calle][2] = Script_Word;
		g_opcode_formats[op_callk][1] = Script_Word;
		g_opcode_formats[op_super][1] = Script_Word;
		g_opcode_formats[op_send][0] = Script_Word;
		g_opcode_formats[op_self][0] = Script_Word;
		g_opcode_formats[op_call][1] = Script_Word;
		g_opcode_formats[op_callb][1] = Script_Word;
	}
#endif
}

#define INST_LOOKUP_CLASS(id) ((id == 0xffff)? NULL_REG : segMan->getClassAddress(id, SCRIPT_GET_LOCK, reg))


void SegManager::createClassTable() {
	Resource *vocab996 = _resMan->findResource(ResourceId(kResourceTypeVocab, 996), 1);

	if (!vocab996)
		error("SegManager: failed to open vocab 996");

	int totalClasses = vocab996->size >> 2;
	_classtable.resize(totalClasses);

	for (uint16 classNr = 0; classNr < totalClasses; classNr++) {
		uint16 scriptNr = READ_LE_UINT16(vocab996->data + classNr * 4 + 2);

		_classtable[classNr].reg = NULL_REG;
		_classtable[classNr].script = scriptNr;
	}

	_resMan->unlockResource(vocab996);
}

reg_t SegManager::getClassAddress(int classnr, ScriptLoadType lock, reg_t caller) {
	if (classnr == 0xffff)
		return NULL_REG;

	if (classnr < 0 || (int)_classtable.size() <= classnr || _classtable[classnr].script < 0) {
		error("[VM] Attempt to dereference class %x, which doesn't exist (max %x)", classnr, _classtable.size());
		return NULL_REG;
	} else {
		Class *the_class = &_classtable[classnr];
		if (!the_class->reg.segment) {
			getScriptSegment(the_class->script, lock);

			if (!the_class->reg.segment) {
				error("[VM] Trying to instantiate class %x by instantiating script 0x%x (%03d) failed;"
				          " Entering debugger.", classnr, the_class->script, the_class->script);
				return NULL_REG;
			}
		} else
			if (caller.segment != the_class->reg.segment)
				getScript(the_class->reg.segment)->incrementLockers();

		return the_class->reg;
	}
}

void SegManager::scriptInitialiseLocalsZero(SegmentId seg, int count) {
	Script *scr = getScript(seg);

	scr->_localsOffset = -count * 2; // Make sure it's invalid

	allocLocalsSegment(scr, count);
}

void SegManager::scriptInitialiseLocals(reg_t location) {
	Script *scr = getScript(location.segment);
	unsigned int count;

	VERIFY(location.offset + 1 < (uint16)scr->_bufSize, "Locals beyond end of script\n");

	if (getSciVersion() >= SCI_VERSION_1_1)
		count = READ_LE_UINT16(scr->_buf + location.offset - 2);
	else
		count = (READ_LE_UINT16(scr->_buf + location.offset - 2) - 4) >> 1;
	// half block size

	scr->_localsOffset = location.offset;

	if (!(location.offset + count * 2 + 1 < scr->_bufSize)) {
		warning("Locals extend beyond end of script: offset %04x, count %x vs size %x", location.offset, count, (uint)scr->_bufSize);
		count = (scr->_bufSize - location.offset) >> 1;
	}

	LocalVariables *locals = allocLocalsSegment(scr, count);
	if (locals) {
		uint i;
		byte *base = (byte *)(scr->_buf + location.offset);

		for (i = 0; i < count; i++)
			locals->_locals[i].offset = READ_LE_UINT16(base + i * 2);
	}
}

void SegManager::scriptRelocateExportsSci11(SegmentId seg) {
	Script *scr = getScript(seg);
	for (int i = 0; i < scr->_numExports; i++) {
		/* We are forced to use an ugly heuristic here to distinguish function
		   exports from object/class exports. The former kind points into the
		   script resource, the latter into the heap resource.  */
		uint16 location = READ_LE_UINT16((byte *)(scr->_exportTable + i));
		if ((location < scr->_heapSize - 1) && (READ_LE_UINT16(scr->_heapStart + location) == SCRIPT_OBJECT_MAGIC_NUMBER)) {
			WRITE_LE_UINT16((byte *)(scr->_exportTable + i), location + scr->_heapStart - scr->_buf);
		} else {
			// Otherwise it's probably a function export,
			// and we don't need to do anything.
		}
	}
}

void SegManager::scriptInitialiseObjectsSci11(SegmentId seg) {
	Script *scr = getScript(seg);
	byte *seeker = scr->_heapStart + 4 + READ_LE_UINT16(scr->_heapStart + 2) * 2;

	while (READ_LE_UINT16(seeker) == SCRIPT_OBJECT_MAGIC_NUMBER) {
		if (READ_LE_UINT16(seeker + 14) & SCRIPT_INFO_CLASS) {
			int classpos = seeker - scr->_buf;
			int species = READ_LE_UINT16(seeker + 10);

			if (species < 0 || species >= (int)_classtable.size()) {
				error("Invalid species %d(0x%x) not in interval [0,%d) while instantiating script %d",
				          species, species, _classtable.size(), scr->_nr);
				return;
			}

			_classtable[species].reg.segment = seg;
			_classtable[species].reg.offset = classpos;
		}
		seeker += READ_LE_UINT16(seeker + 2) * 2;
	}

	seeker = scr->_heapStart + 4 + READ_LE_UINT16(scr->_heapStart + 2) * 2;
	while (READ_LE_UINT16(seeker) == SCRIPT_OBJECT_MAGIC_NUMBER) {
		reg_t reg;
		Object *obj;

		reg.segment = seg;
		reg.offset = seeker - scr->_buf;
		obj = scr->scriptObjInit(reg);

#if 0
		if (obj->_variables[5].offset != 0xffff) {
			obj->_variables[5] = INST_LOOKUP_CLASS(obj->_variables[5].offset);
			baseObj = getObject(obj->_variables[5]);
			obj->variable_names_nr = baseObj->variables_nr;
			obj->_baseObj = baseObj->_baseObj;
		}
#endif

		// Copy base from species class, as we need its selector IDs
		obj->setSuperClassSelector(
			getClassAddress(obj->getSuperClassSelector().offset, SCRIPT_GET_LOCK, NULL_REG));

		// Set the -classScript- selector to the script number.
		// FIXME: As this selector is filled in at run-time, it is likely
		// that it is supposed to hold a pointer. The Obj::isKindOf method
		// uses this selector together with -propDict- to compare classes.
		// For the purpose of Obj::isKindOf, using the script number appears
		// to be sufficient.
		obj->setClassScriptSelector(make_reg(0, scr->_nr));

		seeker += READ_LE_UINT16(seeker + 2) * 2;
	}
}



int script_instantiate_common(ResourceManager *resMan, SegManager *segMan, int script_nr, Resource **script, Resource **heap, int *was_new) {
	*was_new = 1;

	*script = resMan->findResource(ResourceId(kResourceTypeScript, script_nr), 0);
	if (getSciVersion() >= SCI_VERSION_1_1)
		*heap = resMan->findResource(ResourceId(kResourceTypeHeap, script_nr), 0);

	if (!*script || (getSciVersion() >= SCI_VERSION_1_1 && !heap)) {
		warning("Script 0x%x requested but not found", script_nr);
		if (getSciVersion() >= SCI_VERSION_1_1) {
			if (*heap)
				warning("Inconsistency: heap resource WAS found");
			else if (*script)
				warning("Inconsistency: script resource WAS found");
		}
		return 0;
	}

	SegmentId seg_id = segMan->getScriptSegment(script_nr);
	Script *scr = segMan->getScriptIfLoaded(seg_id);
	if (scr) {
		if (!scr->isMarkedAsDeleted()) {
			scr->incrementLockers();
			return seg_id;
		} else {
			scr->freeScript();
		}
	} else {
		scr = segMan->allocateScript(script_nr, &seg_id);
		if (!scr) {  // ALL YOUR SCRIPT BASE ARE BELONG TO US
			error("Not enough heap space for script size 0x%x of script 0x%x (Should this happen?)", (*script)->size, script_nr);
			return 0;
		}
	}

	scr->init(script_nr, resMan);

	reg_t reg;
	reg.segment = seg_id;
	reg.offset = 0;

	// Set heap position (beyond the size word)
	scr->setLockers(1);
	scr->setExportTableOffset(0);
	scr->setSynonymsOffset(0);
	scr->setSynonymsNr(0);

	*was_new = 0;

	return seg_id;
}

int script_instantiate_sci0(ResourceManager *resMan, SegManager *segMan, int script_nr) {
	int objtype;
	unsigned int objlength;
	int relocation = -1;
	int magic_pos_adder; // Usually 0; 2 for older SCI versions
	Resource *script;
	int was_new;
	bool oldScriptHeader = (getSciVersion() == SCI_VERSION_0_EARLY);

	const int seg_id = script_instantiate_common(resMan, segMan, script_nr, &script, NULL, &was_new);

	if (was_new)
		return seg_id;

	Script *scr = segMan->getScript(seg_id);

	if (oldScriptHeader) {
		//
		int locals_nr = READ_LE_UINT16(script->data);

		// Old script block
		// There won't be a localvar block in this case
		// Instead, the script starts with a 16 bit int specifying the
		// number of locals we need; these are then allocated and zeroed.

		scr->mcpyInOut(0, script->data, script->size);
		magic_pos_adder = 2;  // Step over the funny prefix

		if (locals_nr)
			segMan->scriptInitialiseLocalsZero(seg_id, locals_nr);

	} else {
		scr->mcpyInOut(0, script->data, script->size);
		magic_pos_adder = 0;
	}

	// Now do a first pass through the script objects to find the
	// export table and local variable block

	reg_t reg;
	reg.segment = seg_id;
	reg.offset = magic_pos_adder;

	objlength = 0;

	do {
		reg_t data_base;
		reg_t addr;
		reg.offset += objlength; // Step over the last checked object
		objtype = scr->getHeap(reg.offset);
		if (!objtype)
			break;

		objlength = scr->getHeap(reg.offset + 2);

		// This happens in some demos (e.g. the EcoQuest 1 demo). Not sure what is the
		// actual cause of it, but the scripts of these demos can't be loaded properly
		// and we're stuck forever in this loop, as objlength never changes
		if (!objlength) {
			warning("script_instantiate_sci0: objlength is 0, unable to parse script");
			return 0;
		}

		data_base = reg;
		data_base.offset += 4;

		addr = data_base;

		switch (objtype) {
		case SCI_OBJ_EXPORTS: {
			scr->setExportTableOffset(data_base.offset);
		}
		break;

		case SCI_OBJ_SYNONYMS:
			scr->setSynonymsOffset(addr.offset);   // +4 is to step over the header
			scr->setSynonymsNr((objlength) / 4);
			break;

		case SCI_OBJ_LOCALVARS:
			segMan->scriptInitialiseLocals(data_base);
			break;

		case SCI_OBJ_CLASS: {
			int classpos = addr.offset - SCRIPT_OBJECT_MAGIC_OFFSET;
			int species;
			species = scr->getHeap(addr.offset - SCRIPT_OBJECT_MAGIC_OFFSET + SCRIPT_SPECIES_OFFSET);
			if (species < 0 || species >= (int)segMan->_classtable.size()) {
				if (species == (int)segMan->_classtable.size()) {
					// Happens in the LSL2 demo
					warning("Applying workaround for an off-by-one invalid species access");
					segMan->_classtable.resize(segMan->_classtable.size() + 1);
				} else {
					warning("Invalid species %d(0x%x) not in interval "
							  "[0,%d) while instantiating script %d\n",
							  species, species, segMan->_classtable.size(),
							  script_nr);
					return 0;
				}
			}

			segMan->_classtable[species].reg = addr;
			segMan->_classtable[species].reg.offset = classpos;
			// Set technical class position-- into the block allocated for it
		}
		break;

		default:
			break;
		}
	} while (objtype != 0);

	// And now a second pass to adjust objects and class pointers, and the general pointers

	objlength = 0;
	reg.offset = magic_pos_adder; // Reset counter

	do {
		reg_t addr;
		reg.offset += objlength; // Step over the last checked object
		objtype = scr->getHeap(reg.offset);
		if (!objtype)
			break;

		objlength = scr->getHeap(reg.offset + 2);

		addr = reg;
		addr.offset += 4; // Step over header

		switch (objtype) {
		case SCI_OBJ_CODE:
			scr->scriptAddCodeBlock(addr);
			break;
		case SCI_OBJ_OBJECT:
		case SCI_OBJ_CLASS: { // object or class?
			Object *obj = scr->scriptObjInit(addr);

			// Instantiate the superclass, if neccessary
			obj->setSpeciesSelector(INST_LOOKUP_CLASS(obj->getSpeciesSelector().offset));

			Object *baseObj = segMan->getObject(obj->getSpeciesSelector());

			if (baseObj) {
				obj->setVarCount(baseObj->getVarCount());
				// Copy base from species class, as we need its selector IDs
				obj->_baseObj = baseObj->_baseObj;

				obj->setSuperClassSelector(INST_LOOKUP_CLASS(obj->getSuperClassSelector().offset));
			} else {
				warning("Failed to locate base object for object at %04X:%04X; skipping", PRINT_REG(addr));

				scr->scriptObjRemove(addr);
			}
		} // if object or class
		break;
		case SCI_OBJ_POINTERS: // A relocation table
			relocation = addr.offset;
			break;

		default:
			break;
		}

	} while (objtype != 0 && reg.offset < script->size - 2);

	if (relocation >= 0)
		scr->scriptRelocate(make_reg(seg_id, relocation));

	return reg.segment;		// instantiation successful
}

int script_instantiate_sci11(ResourceManager *resMan, SegManager *segMan, int script_nr) {
	Resource *script, *heap;
	int _heapStart;
	reg_t reg;
	int was_new;

	const int seg_id = script_instantiate_common(resMan, segMan, script_nr, &script, &heap, &was_new);

	if (was_new)
		return seg_id;

	Script *scr = segMan->getScript(seg_id);

	_heapStart = script->size;
	if (script->size & 2)
		_heapStart ++;

	scr->mcpyInOut(0, script->data, script->size);
	scr->mcpyInOut(_heapStart, heap->data, heap->size);

	if (READ_LE_UINT16(script->data + 6) > 0)
		scr->setExportTableOffset(6);

	reg.segment = seg_id;
	reg.offset = _heapStart + 4;
	segMan->scriptInitialiseLocals(reg);

	segMan->scriptRelocateExportsSci11(seg_id);
	segMan->scriptInitialiseObjectsSci11(seg_id);

	reg.offset = READ_LE_UINT16(heap->data);
	scr->heapRelocate(reg);

	return seg_id;
}

int script_instantiate(ResourceManager *resMan, SegManager *segMan, int script_nr) {
	if (getSciVersion() >= SCI_VERSION_1_1)
		return script_instantiate_sci11(resMan, segMan, script_nr);
	else
		return script_instantiate_sci0(resMan, segMan, script_nr);
}

void script_uninstantiate_sci0(SegManager *segMan, int script_nr, SegmentId seg) {
	bool oldScriptHeader = (getSciVersion() == SCI_VERSION_0_EARLY);
	reg_t reg = make_reg(seg, oldScriptHeader ? 2 : 0);
	int objtype, objlength;
	Script *scr = segMan->getScript(seg);

	// Make a pass over the object in order uninstantiate all superclasses
	objlength = 0;

	do {
		reg.offset += objlength; // Step over the last checked object

		objtype = scr->getHeap(reg.offset);
		if (!objtype)
			break;
		objlength = scr->getHeap(reg.offset + 2);  // use SEG_UGET_HEAP ??

		reg.offset += 4; // Step over header

		if ((objtype == SCI_OBJ_OBJECT) || (objtype == SCI_OBJ_CLASS)) { // object or class?
			int superclass;

			reg.offset -= SCRIPT_OBJECT_MAGIC_OFFSET;

			superclass = scr->getHeap(reg.offset + SCRIPT_SUPERCLASS_OFFSET); // Get superclass...

			if (superclass >= 0) {
				int superclass_script = segMan->_classtable[superclass].script;

				if (superclass_script == script_nr) {
					if (scr->getLockers())
						scr->decrementLockers();  // Decrease lockers if this is us ourselves
				} else
					script_uninstantiate(segMan, superclass_script);
				// Recurse to assure that the superclass lockers number gets decreased
			}

			reg.offset += SCRIPT_OBJECT_MAGIC_OFFSET;
		} // if object or class

		reg.offset -= 4; // Step back on header

	} while (objtype != 0);
}

void script_uninstantiate(SegManager *segMan, int script_nr) {
	SegmentId segment = segMan->getScriptSegment(script_nr);
	Script *scr = segMan->getScriptIfLoaded(segment);

	if (!scr) {   // Is it already loaded?
		//warning("unloading script 0x%x requested although not loaded", script_nr);
		// This is perfectly valid SCI behaviour
		return;
	}

	scr->decrementLockers();   // One less locker

	if (scr->getLockers() > 0)
		return;

	// Free all classtable references to this script
	for (uint i = 0; i < segMan->_classtable.size(); i++)
		if (segMan->_classtable[i].reg.segment == segment)
			segMan->_classtable[i].reg = NULL_REG;

	if (getSciVersion() < SCI_VERSION_1_1)
		script_uninstantiate_sci0(segMan, script_nr, segment);
	// FIXME: Add proper script uninstantiation for SCI 1.1

	if (scr->getLockers())
		return; // if xxx.lockers > 0

	// Otherwise unload it completely
	// Explanation: I'm starting to believe that this work is done by SCI itself.
	scr->markDeleted();

	debugC(kDebugLevelScripts, "Unloaded script 0x%x.", script_nr);

	return;
}


} // End of namespace Sci
