/* ScummVM - Scumm Interpreter
 * Copyright (C) 2004-2005 The ScummVM project
 *
 * The ReInherit Engine is (C)2000-2003 by Daniel Balsom.
 *
 * 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.
 *
 * $Header$
 *
 */

// Scripting module thread management component
#include "saga/saga.h"

#include "saga/gfx.h"
#include "saga/actor.h"
#include "saga/console.h"
#include "saga/interface.h"

#include "saga/script.h"

#include "saga/stream.h"
#include "saga/scene.h"
#include "saga/resnames.h"

namespace Saga {

ScriptThread *Script::createThread(uint16 scriptModuleNumber, uint16 scriptEntryPointNumber) {
	ScriptThread *newThread;

	loadModule(scriptModuleNumber);
	if (_modules[scriptModuleNumber].entryPointsCount <= scriptEntryPointNumber) {
		error("Script::createThread wrong scriptEntryPointNumber");
	}

	newThread = _threadList.pushFront().operator->();
	newThread->_flags = kTFlagNone;
	newThread->_stackSize = DEFAULT_THREAD_STACK_SIZE;
	newThread->_stackBuf = (uint16 *)malloc(newThread->_stackSize * sizeof(*newThread->_stackBuf));
	newThread->_stackTopIndex = newThread->_stackSize - 2;
	newThread->_instructionOffset = _modules[scriptModuleNumber].entryPoints[scriptEntryPointNumber].offset;
	newThread->_commonBase = _commonBuffer;
	newThread->_staticBase = _commonBuffer + _modules[scriptModuleNumber].staticOffset;
	newThread->_moduleBase = _modules[scriptModuleNumber].moduleBase;
	newThread->_moduleBaseSize = _modules[scriptModuleNumber].moduleBaseSize;

	newThread->_strings = &_modules[scriptModuleNumber].strings;

	if (_vm->getGameType() == GType_IHNM)
		newThread->_voiceLUT = &_globalVoiceLUT;
	else
		newThread->_voiceLUT = &_modules[scriptModuleNumber].voiceLUT;

	return newThread;
}

void Script::wakeUpActorThread(int waitType, void *threadObj) {
	ScriptThread *thread;
	ScriptThreadList::iterator threadIterator;

	for (threadIterator = _threadList.begin(); threadIterator != _threadList.end(); ++threadIterator) {
		thread = threadIterator.operator->();
		if ((thread->_flags & kTFlagWaiting) && (thread->_waitType == waitType) && (thread->_threadObj == threadObj)) {
			thread->_flags &= ~kTFlagWaiting;
		}
	}
}

void Script::wakeUpThreads(int waitType) {
	ScriptThread *thread;
	ScriptThreadList::iterator threadIterator;

	for (threadIterator = _threadList.begin(); threadIterator != _threadList.end(); ++threadIterator) {
		thread = threadIterator.operator->();
		if ((thread->_flags & kTFlagWaiting) && (thread->_waitType == waitType)) {
			thread->_flags &= ~kTFlagWaiting;
		}
	}
}

void Script::wakeUpThreadsDelayed(int waitType, int sleepTime) {
	ScriptThread *thread;
	ScriptThreadList::iterator threadIterator;

	for (threadIterator = _threadList.begin(); threadIterator != _threadList.end(); ++threadIterator) {
		thread = threadIterator.operator->();
		if ((thread->_flags & kTFlagWaiting) && (thread->_waitType == waitType)) {
			thread->_waitType = kWaitTypeDelay;
			thread->_sleepTime = sleepTime;
		}
	}
}

void Script::executeThreads(uint msec) {
	ScriptThread *thread;
	ScriptThreadList::iterator threadIterator;

	if (_vm->_interface->_statusTextInput) {
		return;
	}

	threadIterator = _threadList.begin();

	while (threadIterator != _threadList.end()) {
		thread = threadIterator.operator->();

		if (thread->_flags & (kTFlagFinished | kTFlagAborted)) {
			if (thread->_flags & kTFlagFinished)
				setPointerVerb();

			if (_vm->getGameType() == GType_IHNM) {
				thread->_flags &= ~kTFlagFinished;
				thread->_flags |= kTFlagAborted;
				++threadIterator;
			} else {
				threadIterator = _threadList.erase(threadIterator);
			}
			continue;
		}

		if (thread->_flags & kTFlagWaiting) {

			switch (thread->_waitType) {
			case kWaitTypeDelay:
				if (thread->_sleepTime < msec) {
					thread->_sleepTime = 0;
				} else {
					thread->_sleepTime -= msec;
				}

				if (thread->_sleepTime == 0)
					thread->_flags &= ~kTFlagWaiting;
				break;

			case kWaitTypeWalk:
				{
					ActorData *actor;
					actor = (ActorData *)thread->_threadObj;
					if (actor->_currentAction == kActionWait) {
						thread->_flags &= ~kTFlagWaiting;
					}
				}
				break;

			case kWaitTypeWaitFrames: // IHNM
				if (thread->_frameWait < _vm->_frameCount)
					thread->_flags &= ~kTFlagWaiting;
				break;
			}
		}

		if (!(thread->_flags & kTFlagWaiting)) {
			if (runThread(thread, STHREAD_TIMESLICE)) {
				break;
			}
		}

		++threadIterator;
	}

}

void Script::abortAllThreads(void) {
	ScriptThread *thread;
	ScriptThreadList::iterator threadIterator;

	threadIterator = _threadList.begin();

	while (threadIterator != _threadList.end()) {
		thread = threadIterator.operator->();
		thread->_flags |= kTFlagAborted;
		++threadIterator;
	}
	executeThreads(0);
}

void Script::completeThread(void) {
	int limit = (_vm->getGameType() == GType_IHNM) ? 100 : 40;

	for (int i = 0; i < limit && !_threadList.isEmpty(); i++)
		executeThreads(0);
}

bool Script::runThread(ScriptThread *thread, uint instructionLimit) {
	const char*operandName;
	uint instructionCount;
	uint16 savedInstructionOffset;

	byte *addr;
	byte mode;
	uint16 jmpOffset1;
	int16 iparam1;
	int16 iparam2;
	int16 iparam3;

	bool disContinue;
	byte argumentsCount;
	uint16 functionNumber;
	uint16 checkStackTopIndex;
	ScriptFunctionType scriptFunction;

	int operandChar;
	int i;

	MemoryReadStream scriptS(thread->_moduleBase, thread->_moduleBaseSize);

	scriptS.seek(thread->_instructionOffset);

	for (instructionCount = 0; instructionCount < instructionLimit; instructionCount++) {
		if (thread->_flags & (kTFlagAsleep))
			break;

		savedInstructionOffset = thread->_instructionOffset;
		operandChar = scriptS.readByte();


#define CASEOP(opName)	case opName:												\
							if (operandChar == opName) {							\
								operandName = #opName;								\
								debug(2, operandName);								\
								_vm->_console->DebugPrintf("%s\n", operandName);	\
							}

		debug(8, "Executing thread offset: %lu (%x) stack: %d", thread->_instructionOffset, operandChar, thread->pushedSize());
		operandName="";
		switch (operandChar) {
		CASEOP(opNextBlock)
			// Some sort of "jump to the start of the next memory
			// page" instruction, I think.
			thread->_instructionOffset = (((thread->_instructionOffset) >> 10) + 1) << 10;
			break;

// STACK INSTRUCTIONS
		CASEOP(opDup)
			thread->push(thread->stackTop());
			break;
		CASEOP(opDrop)
			thread->pop();
			break;
		CASEOP(opZero)
			thread->push(0);
			break;
		CASEOP(opOne)
			thread->push(1);
			break;
		CASEOP(opConstint)
		CASEOP(opStrlit)
			iparam1 = scriptS.readSint16LE();
			thread->push(iparam1);
			debug(8, "0x%X", iparam1);
			break;

// DATA INSTRUCTIONS
		CASEOP(opGetFlag)
			addr = thread->baseAddress(scriptS.readByte());
			iparam1 = scriptS.readSint16LE();
			addr += (iparam1 >> 3);
			iparam1 = (1 << (iparam1 & 7));
			thread->push((*addr) & iparam1 ? 1 : 0);
			break;
		CASEOP(opGetInt)
			mode = scriptS.readByte();
			addr = thread->baseAddress(mode);
			iparam1 = scriptS.readSint16LE();
			addr += iparam1;
			thread->push(readUint16(addr, mode));
			debug(8, "0x%X", readUint16(addr, mode));
			break;
		CASEOP(opPutFlag)
			addr = thread->baseAddress(scriptS.readByte());
			iparam1 = scriptS.readSint16LE();
			addr += (iparam1 >> 3);
			iparam1 = (1 << (iparam1 & 7));
			if (thread->stackTop()) {
				*addr |= iparam1;
			} else {
				*addr &= ~iparam1;
			}
			break;
		CASEOP(opPutInt)
			mode = scriptS.readByte();
			addr = thread->baseAddress(mode);
			iparam1 = scriptS.readSint16LE();
			addr += iparam1;
			writeUint16(addr, thread->stackTop(), mode);
			break;
		CASEOP(opPutFlagV)
			addr = thread->baseAddress(scriptS.readByte());
			iparam1 = scriptS.readSint16LE();
			addr += (iparam1 >> 3);
			iparam1 = (1 << (iparam1 & 7));
			if (thread->pop()) {
				*addr |= iparam1;
			} else {
				*addr &= ~iparam1;
			}
			break;
		CASEOP(opPutIntV)
			mode = scriptS.readByte();
			addr = thread->baseAddress(mode);
			iparam1 = scriptS.readSint16LE();
			addr += iparam1;
			writeUint16(addr, thread->pop(), mode);
			break;

// FUNCTION CALL INSTRUCTIONS
		CASEOP(opCall)
			argumentsCount = scriptS.readByte();
			iparam1 = scriptS.readByte();
			if (iparam1 != kAddressModule) {
				error("Script::runThread iparam1 != kAddressModule");
			}
			addr = thread->baseAddress(iparam1);
			iparam1 = scriptS.readSint16LE();
			addr += iparam1;
			thread->push(argumentsCount);

			jmpOffset1 = scriptS.pos();
			// NOTE: The original pushes the program
			// counter as a pointer here. But I don't think
			// we will have to do that.
			thread->push(jmpOffset1);
			// NOTE2: program counter is 32bit - so we should "emulate" it size - because kAddressStack relies on it
			thread->push(0);
			thread->_instructionOffset = iparam1;

			break;
		CASEOP(opCcall)
		CASEOP(opCcallV)
			argumentsCount = scriptS.readByte();
			functionNumber = scriptS.readUint16LE();
			if (functionNumber >= ((_vm->getGameType() == GType_IHNM) ?
								   IHNM_SCRIPT_FUNCTION_MAX : ITE_SCRIPT_FUNCTION_MAX)) {
				error("Script::runThread() Invalid script function number (%d)", functionNumber);
			}

			debug(2, "Calling #%d %s argCount=%i", functionNumber, _scriptFunctionsList[functionNumber].scriptFunctionName, argumentsCount);
			scriptFunction = _scriptFunctionsList[functionNumber].scriptFunction;
			checkStackTopIndex = thread->_stackTopIndex + argumentsCount;
			disContinue = false;
			(this->*scriptFunction)(thread, argumentsCount, disContinue);
			if (disContinue) {
				return true;
			}
			if (scriptFunction == &Saga::Script::sfScriptGotoScene ||
				scriptFunction == &Saga::Script::sfVsetTrack) {
				return true; // cause abortAllThreads called and _this_ thread destroyed
			}

			thread->_stackTopIndex = checkStackTopIndex;

			if (operandChar == opCcall) {// CALL function
				thread->push(thread->_returnValue);
			}

			if (thread->_flags & kTFlagAsleep)
				instructionCount = instructionLimit;	// break out of loop!
			break;
		CASEOP(opEnter)
			thread->push(thread->_frameIndex);
			thread->_frameIndex = thread->_stackTopIndex;
			thread->_stackTopIndex -= (scriptS.readSint16LE() / 2);
			break;
		CASEOP(opReturn)
			thread->_returnValue = thread->pop();
		CASEOP(opReturnV)
			thread->_stackTopIndex = thread->_frameIndex;
			thread->_frameIndex = thread->pop();
			if (thread->pushedSize() == 0) {
				thread->_flags |= kTFlagFinished;
				return true;
			} else {
				thread->pop(); //cause it 0
				thread->_instructionOffset = thread->pop();

				// Pop all the call parameters off the stack
				iparam1 = thread->pop();
				while (iparam1--) {
					thread->pop();
				}

				if (operandChar == opReturn) {
					thread->push(thread->_returnValue);
				}
			}
			break;

// BRANCH INSTRUCTIONS
		CASEOP(opJmp)
			jmpOffset1 = scriptS.readUint16LE();
			thread->_instructionOffset = jmpOffset1;
			break;
		CASEOP(opJmpTrueV)
			jmpOffset1 = scriptS.readUint16LE();
			if (thread->pop()) {
				thread->_instructionOffset = jmpOffset1;
			}
			break;
		CASEOP(opJmpFalseV)
			jmpOffset1 = scriptS.readUint16LE();
			if (!thread->pop()) {
				thread->_instructionOffset = jmpOffset1;
			}
			break;
		CASEOP(opJmpTrue)
			jmpOffset1 = scriptS.readUint16LE();
			if (thread->stackTop()) {
				thread->_instructionOffset = jmpOffset1;
			}
			break;
		CASEOP(opJmpFalse)
			jmpOffset1 = scriptS.readUint16LE();
			if (!thread->stackTop()) {
				thread->_instructionOffset = jmpOffset1;
			}
			break;
		CASEOP(opJmpSwitch)
			iparam1 = scriptS.readSint16LE();
			iparam2 = thread->pop();
			while (iparam1--) {
				iparam3 = scriptS.readUint16LE();
				thread->_instructionOffset = scriptS.readUint16LE();
				if (iparam3 == iparam2) {
					break;
				}
			}
			if (iparam1 < 0) {
				thread->_instructionOffset = scriptS.readUint16LE();
			}
			break;
		CASEOP(opJmpRandom)
			// Supposedly the number of possible branches.
			// The original interpreter ignores it.
			scriptS.readUint16LE();
			iparam1 = scriptS.readSint16LE();
			iparam1 = _vm->_rnd.getRandomNumber(iparam1 - 1);
			while (1) {
				iparam2 = scriptS.readSint16LE();
				thread->_instructionOffset = scriptS.readUint16LE();

				iparam1 -= iparam2;
				if (iparam1 < 0) {
					break;
				}
			}
			break;

// UNARY INSTRUCTIONS
		CASEOP(opNegate)
			thread->push(-thread->pop());
			break;
		CASEOP(opNot)
			thread->push(!thread->pop());
			break;
		CASEOP(opCompl)
			thread->push(~thread->pop());
			break;

		CASEOP(opIncV)
			mode = scriptS.readByte();
			addr = thread->baseAddress(mode);
			iparam1 = scriptS.readSint16LE();
			addr += iparam1;
			iparam1 = readUint16(addr, mode);
			writeUint16(addr, iparam1 + 1, mode);
			break;
		CASEOP(opDecV)
			mode = scriptS.readByte();
			addr = thread->baseAddress(mode);
			iparam1 = scriptS.readSint16LE();
			addr += iparam1;
			iparam1 = readUint16(addr, mode);
			writeUint16(addr, iparam1 - 1, mode);
			break;
		CASEOP(opPostInc)
			mode = scriptS.readByte();
			addr = thread->baseAddress(mode);
			iparam1 = scriptS.readSint16LE();
			addr += iparam1;
			iparam1 = readUint16(addr, mode);
			thread->push(iparam1);
			writeUint16(addr, iparam1 + 1, mode);
			break;
		CASEOP(opPostDec)
			mode = scriptS.readByte();
			addr = thread->baseAddress(mode);
			iparam1 = scriptS.readSint16LE();
			addr += iparam1;
			iparam1 = readUint16(addr, mode);
			thread->push(iparam1);
			writeUint16(addr, iparam1 - 1, mode);
			break;

// ARITHMETIC INSTRUCTIONS
		CASEOP(opAdd)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			iparam1 += iparam2;
			thread->push(iparam1);
			break;
		CASEOP(opSub)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			iparam1 -= iparam2;
			thread->push(iparam1);
			break;
		CASEOP(opMul)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			iparam1 *= iparam2;
			thread->push(iparam1);
			break;
		CASEOP(opDiv)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			iparam1 /= iparam2;
			thread->push(iparam1);
			break;
		CASEOP(opMod)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			iparam1 %= iparam2;
			thread->push(iparam1);
			break;

// COMPARISION INSTRUCTIONS
		CASEOP(opEq)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			thread->push((iparam1 == iparam2) ? 1 : 0);
			debug(8, "0x%X 0x%X", iparam1, iparam2);
			break;
		CASEOP(opNe)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			thread->push((iparam1 != iparam2) ? 1 : 0);
			break;
		CASEOP(opGt)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			thread->push((iparam1 > iparam2) ? 1 : 0);
			break;
		CASEOP(opLt)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			thread->push((iparam1 < iparam2) ? 1 : 0);
			break;
		CASEOP(opGe)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			thread->push((iparam1 >= iparam2) ? 1 : 0);
			break;
		CASEOP(opLe)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			thread->push((iparam1 <= iparam2) ? 1 : 0);
			break;

// SHIFT INSTRUCTIONS
		CASEOP(opRsh)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			iparam1 >>= iparam2;
			thread->push(iparam1);
			break;
		CASEOP(opLsh)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			iparam1 <<= iparam2;
			thread->push(iparam1);
			break;

// BITWISE INSTRUCTIONS
		CASEOP(opAnd)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			iparam1 &= iparam2;
			thread->push(iparam1);
			break;
		CASEOP(opOr)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			iparam1 |= iparam2;
			thread->push(iparam1);
			break;
		CASEOP(opXor)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			iparam1 ^= iparam2;
			thread->push(iparam1);
			break;

// LOGICAL INSTRUCTIONS
		CASEOP(opLAnd)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			thread->push((iparam1 && iparam2) ? 1 : 0);
			break;
		CASEOP(opLOr)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			thread->push((iparam1 || iparam2) ? 1 : 0);
			break;
		CASEOP(opLXor)
			iparam2 = thread->pop();
			iparam1 = thread->pop();
			thread->push(((iparam1 && !iparam2) || (!iparam1 && iparam2)) ? 1 : 0);
			break;

// GAME INSTRUCTIONS
		CASEOP(opSpeak) {
				int stringsCount;
				uint16 actorId;
				uint16 speechFlags;
				int sampleResourceId = -1;
				int16 first;
				const char *strings[ACTOR_SPEECH_STRING_MAX];

				if (_vm->_actor->isSpeaking()) {
					thread->wait(kWaitTypeSpeech);
					return false;
				}

				stringsCount = scriptS.readByte();
				actorId = scriptS.readUint16LE();
				speechFlags = scriptS.readByte();
				scriptS.readUint16LE(); // x,y skip

				if (stringsCount == 0)
					error("opSpeak stringsCount == 0");

				if (stringsCount > ACTOR_SPEECH_STRING_MAX)
					error("opSpeak stringsCount=0x%X exceed ACTOR_SPEECH_STRING_MAX", stringsCount);

				iparam1 = first = thread->stackTop();
				for (i = 0; i < stringsCount; i++) {
					 iparam1 = thread->pop();
					 strings[i] = thread->_strings->getString(iparam1);
				}
				// now data contains last string index

				if (_vm->getGameId() == GID_ITE_DISK_G) { // special ITE dos
					if ((_vm->_scene->currentSceneNumber() == ITE_DEFAULT_SCENE) &&
						(iparam1 >= 288) && (iparam1 <= (RID_SCENE1_VOICE_138 - RID_SCENE1_VOICE_009 + 288))) {
						sampleResourceId = RID_SCENE1_VOICE_009 + iparam1 - 288;
					}
				} else {
					if (thread->_voiceLUT->voicesCount > first) {
						sampleResourceId = thread->_voiceLUT->voices[first];
					}
				}

				if (sampleResourceId < 0 || sampleResourceId > 4000)
					sampleResourceId = -1;

				if (_vm->getGameType() == GType_ITE && !sampleResourceId)
					sampleResourceId = -1;

				_vm->_actor->actorSpeech(actorId, strings, stringsCount, sampleResourceId, speechFlags);

				if (!(speechFlags & kSpeakAsync)) {
					thread->wait(kWaitTypeSpeech);
				}
			}
			break;
		CASEOP(opDialogBegin)
			if (_conversingThread) {
				thread->wait(kWaitTypeDialogBegin);
				return false;
			}
			_conversingThread = thread;
			_vm->_interface->converseClear();
			break;
		CASEOP(opDialogEnd)
			if (thread == _conversingThread) {
				_vm->_interface->activate();
				_vm->_interface->setMode(kPanelConverse);
				thread->wait(kWaitTypeDialogEnd);
				return false;
			}
			break;
		CASEOP(opReply) {
				const char *str;
				byte replyNum;
				byte flags;
				replyNum = scriptS.readByte();
				flags = scriptS.readByte();
				iparam1 = 0;

				if (flags & kReplyOnce) {
					iparam1 = scriptS.readSint16LE();
					addr = thread->_staticBase + (iparam1 >> 3);
					if (*addr & (1 << (iparam1 & 7))) {
						break;
					}
				}

				str = thread->_strings->getString(thread->pop());
				if (_vm->_interface->converseAddText(str, replyNum, flags, iparam1))
					warning("Error adding ConverseText (%s, %d, %d, %d)", str, replyNum, flags, iparam1);
			}
			break;
		CASEOP(opAnimate)
			scriptS.readUint16LE();
			scriptS.readUint16LE();
			jmpOffset1 = scriptS.readByte();
			thread->_instructionOffset += jmpOffset1;
			break;

		default:
			error("Script::runThread() Invalid opcode encountered 0x%X", operandChar);
		}

		if (thread->_flags & (kTFlagFinished | kTFlagAborted)) {
			error("Wrong flags %d in thread", thread->_flags);
		}

		// Set instruction offset only if a previous instruction didn't branch
		if (savedInstructionOffset == thread->_instructionOffset) {
			thread->_instructionOffset = scriptS.pos();
		} else {
			if (thread->_instructionOffset >= scriptS.size()) {
				error("Script::runThread() Out of range script execution");
			}

			scriptS.seek(thread->_instructionOffset);
		}
	}
	return false;
}

} // End of namespace Saga