/* 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "common/ptr.h"

#include "mediastation/mediastation.h"
#include "mediastation/mediascript/codechunk.h"
#include "mediastation/mediascript/collection.h"
#include "mediastation/debugchannels.h"

namespace MediaStation {

CodeChunk::CodeChunk(Chunk &chunk) {
	uint lengthInBytes = chunk.readTypedUint32();
	debugC(5, kDebugLoading, "CodeChunk::CodeChunk(): Length 0x%x (@0x%llx)", lengthInBytes, static_cast<long long int>(chunk.pos()));
	_bytecode = static_cast<ParameterReadStream *>(chunk.readStream(lengthInBytes));
}

ScriptValue CodeChunk::executeNextBlock() {
	uint blockSize = _bytecode->readTypedUint32();
	uint startingPos = _bytecode->pos();

	ScriptValue returnValue;
	ExpressionType expressionType = static_cast<ExpressionType>(_bytecode->readTypedUint16());
	while (expressionType != kExpressionTypeEmpty && !_returnImmediately) {
		returnValue = evaluateExpression(expressionType);
		expressionType = static_cast<ExpressionType>(_bytecode->readTypedUint16());
	}

	// Verify we consumed the right number of script bytes.
	if (!_returnImmediately) {
		uint bytesRead = _bytecode->pos() - startingPos;
		if (bytesRead != blockSize) {
			error("%s: Expected to have read %d script bytes, actually read %d", __func__, blockSize, bytesRead);
		}
	}
	return returnValue;
}

void CodeChunk::skipNextBlock() {
	uint lengthInBytes = _bytecode->readTypedUint32();
	_bytecode->skip(lengthInBytes);
}

ScriptValue CodeChunk::execute(Common::Array<ScriptValue> *args) {
	_args = args;
	ScriptValue returnValue = executeNextBlock();

	// Rewind the stream once we're finished, in case we need to execute
	// this code again!
	_bytecode->seek(0);
	_locals.clear();
	// We don't own the args, so we will prevent a potentially out-of-scope
	// variable from being re-accessed.
	_args = nullptr;

	return returnValue;
}

ScriptValue CodeChunk::evaluateExpression() {
	ExpressionType expressionType = static_cast<ExpressionType>(_bytecode->readTypedUint16());
	ScriptValue returnValue = evaluateExpression(expressionType);
	return returnValue;
}

ScriptValue CodeChunk::evaluateExpression(ExpressionType expressionType) {
	debugCN(5, kDebugScript, "(%s) ", expressionTypeToStr(expressionType));

	ScriptValue returnValue;
	switch (expressionType) {
	case kExpressionTypeEmpty:
		break;

	case kExpressionTypeOperation:
		returnValue = evaluateOperation();
		break;

	case kExpressionTypeValue:
		returnValue = evaluateValue();
		break;

	case kExpressionTypeVariable:
		returnValue = evaluateVariable();
		break;

	default:
		error("%s: Got unimplemented expression type %s (%d)", __func__,
			expressionTypeToStr(expressionType), static_cast<uint>(expressionType));
	}
	return returnValue;
}

ScriptValue CodeChunk::evaluateOperation() {
	Opcode opcode = static_cast<Opcode>(_bytecode->readTypedUint16());
	debugCN(5, kDebugScript, "%s ", opcodeToStr(opcode));

	ScriptValue returnValue;
	switch (opcode) {
	case kOpcodeIf:
		evaluateIf();
		break;

	case kOpcodeIfElse:
		evaluateIfElse();
		break;

	case kOpcodeAssignVariable:
		evaluateAssign();
		break;

	case kOpcodeOr:
	case kOpcodeXor:
	case kOpcodeAnd:
	case kOpcodeEquals:
	case kOpcodeNotEquals:
	case kOpcodeLessThan:
	case kOpcodeGreaterThan:
	case kOpcodeLessThanOrEqualTo:
	case kOpcodeGreaterThanOrEqualTo:
	case kOpcodeAdd:
	case kOpcodeSubtract:
	case kOpcodeMultiply:
	case kOpcodeDivide:
	case kOpcodeModulo:
		returnValue = evaluateBinaryOperation(opcode);
		break;

	case kOpcodeNegate:
		returnValue = evaluateUnaryOperation();
		break;

	case kOpcodeCallFunction:
		returnValue = evaluateFunctionCall();
		break;

	case kOpcodeCallMethod:
		returnValue = evaluateMethodCall();
		break;

	case kOpcodeDeclareLocals:
		evaluateDeclareLocals();
		break;

	case kOpcodeReturn:
		returnValue = evaluateReturn();
		break;

	case kOpcodeReturnNoValue:
		evaluateReturnNoValue();
		break;

	case kOpcodeWhile:
		evaluateWhileLoop();
		break;

	case kOpcodeCallFunctionInVariable:
		returnValue = evaluateFunctionCall(true);
		break;

	case kOpcodeCallMethodInVariable:
		returnValue = evaluateMethodCall(true);
		break;

	default:
		error("%s: Got unimplemented opcode %s (%d)", __func__, opcodeToStr(opcode), static_cast<uint>(opcode));
	}
	return returnValue;
}

ScriptValue CodeChunk::evaluateValue() {
	OperandType operandType = static_cast<OperandType>(_bytecode->readTypedUint16());
	debugCN(5, kDebugScript, "%s ", operandTypeToStr(operandType));

	ScriptValue returnValue;
	switch (operandType) {
	case kOperandTypeBool: {
		int b = _bytecode->readTypedByte();
		if (b != 0 && b != 1) {
			error("%s: Got invalid literal bool value %d", __func__, b);
		}
		debugC(5, kDebugScript, "%d ", b);
		returnValue.setToBool(b == 1 ? true : false);
		return returnValue;
	}

	case kOperandTypeFloat: {
		double f = _bytecode->readTypedDouble();
		debugC(5, kDebugScript, "%f ", f);
		returnValue.setToFloat(f);
		return returnValue;
	}

	case kOperandTypeInt: {
		int i = _bytecode->readTypedSint32();
		debugC(5, kDebugScript, "%d ", i);
		// Ints are stored internally as doubles.
		returnValue.setToFloat(static_cast<double>(i));
		return returnValue;
	}

	case kOperandTypeString: {
		// This is indeed a raw string, not a string wrapped in a datum!
		uint size = _bytecode->readTypedUint16();
		Common::String string = _bytecode->readString('\0', size);
		debugC(5, kDebugScript, "%s ", string.c_str());
		returnValue.setToString(string);
		return returnValue;
	}

	case kOperandTypeParamToken: {
		uint literal = _bytecode->readTypedUint16();
		debugC(5, kDebugScript, "%d ", literal);
		returnValue.setToParamToken(literal);
		return returnValue;
	}

	case kOperandTypeActorId: {
		uint actorId = _bytecode->readTypedUint16();
		debugC(5, kDebugScript, "%d ", actorId);
		returnValue.setToActorId(actorId);
		return returnValue;
	}

	case kOperandTypeTime: {
		double d = _bytecode->readTypedTime();
		debugC(5, kDebugScript, "%f ", d);
		returnValue.setToTime(d);
		return returnValue;
	}

	case kOperandTypeVariable: {
		returnValue = ScriptValue(_bytecode);
		return returnValue;
	}

	case kOperandTypeFunctionId: {
		uint functionId = _bytecode->readTypedUint16();
		debugC(5, kDebugScript, "%d ", functionId);
		returnValue.setToFunctionId(functionId);
		return returnValue;
	}

	case kOperandTypeMethodId: {
		BuiltInMethod methodId = static_cast<BuiltInMethod>(_bytecode->readTypedUint16());
		debugC(5, kDebugScript, "%s ", builtInMethodToStr(methodId));
		returnValue.setToMethodId(methodId);
		return returnValue;
	}

	default:
		error("%s: Got unknown ScriptValue type %s (%d)", __func__, operandTypeToStr(operandType), static_cast<uint>(operandType));
	}
}

ScriptValue CodeChunk::evaluateVariable() {
	ScriptValue *variable = readAndReturnVariable();
	return *variable;
}

ScriptValue *CodeChunk::readAndReturnVariable() {
	uint id = _bytecode->readTypedUint16();
	VariableScope scope = static_cast<VariableScope>(_bytecode->readTypedUint16());
	debugC(5, kDebugScript, "%d (%s)", id, variableScopeToStr(scope));

	ScriptValue returnValue;
	switch (scope) {
	case kVariableScopeGlobal: {
		ScriptValue *variable = g_engine->getVariable(id);
		if (variable == nullptr) {
			error("%s: Global variable %d doesn't exist", __func__, id);
		}
		return variable;
	}

	case kVariableScopeLocal: {
		uint index = id - 1;
		return &_locals.operator[](index);
	}

	case kVariableScopeIndirectParameter: {
		ScriptValue indexValue = evaluateExpression();
		uint index = static_cast<uint>(indexValue.asFloat() + id);
		return &_args->operator[](index);
	}

	case kVariableScopeParameter: {
		uint index = id - 1;
		if (_args == nullptr) {
			error("%s: Requested a parameter in a code chunk that has no parameters", __func__);
		}
		return &_args->operator[](index);
	}

	default:
		error("%s: Got unknown variable scope %s (%d)", __func__, variableScopeToStr(scope), static_cast<uint>(scope));
	}
}

void CodeChunk::evaluateIf() {
	debugCN(5, kDebugScript, "\n    condition: ");
	ScriptValue condition = evaluateExpression();
	if (condition.getType() != kScriptValueTypeBool) {
		error("%s: Expected bool condition, got %s", __func__, scriptValueTypeToStr(condition.getType()));
	}

	if (condition.asBool()) {
		executeNextBlock();
	} else {
		skipNextBlock();
	}
}

void CodeChunk::evaluateIfElse() {
	debugCN(5, kDebugScript, "\n    condition: ");
	ScriptValue condition = evaluateExpression();
	if (condition.getType() != kScriptValueTypeBool) {
		error("%s: Expected bool condition, got %s", __func__, scriptValueTypeToStr(condition.getType()));
	}

	if (condition.asBool()) {
		executeNextBlock();
		skipNextBlock();
	} else {
		skipNextBlock();
		executeNextBlock();
	}
}

ScriptValue CodeChunk::evaluateAssign() {
	debugCN(5, kDebugScript, "Variable ");
	ScriptValue *targetVariable = readAndReturnVariable();

	debugC(5, kDebugScript, "  Value: ");
	ScriptValue value = evaluateExpression();

	if (value.getType() == kScriptValueTypeEmpty) {
		error("%s: Attempt to assign an empty value to a variable", __func__);
	}

	if (targetVariable != nullptr) {
		*targetVariable = value;
		return value;
	} else {
		error("%s: Attempt to assign to null variable", __func__);
	}
}

ScriptValue CodeChunk::evaluateBinaryOperation(Opcode op) {
	debugCN(5, kDebugScript, "\n    lhs: ");
	ScriptValue value1 = evaluateExpression();
	debugCN(5, kDebugScript, "    rhs: ");
	ScriptValue value2 = evaluateExpression();

	ScriptValue returnValue;
	switch (op) {
	case kOpcodeOr:
		returnValue.setToBool(value1 || value2);
		break;

	case kOpcodeXor:
		returnValue.setToBool(value1 ^ value2);
		break;

	case kOpcodeAnd:
		returnValue.setToBool(value1 && value2);
		break;

	case kOpcodeEquals:
		returnValue.setToBool(value1 == value2);
		break;

	case kOpcodeNotEquals:
		returnValue.setToBool(value1 != value2);
		break;

	case kOpcodeLessThan:
		returnValue.setToBool(value1 < value2);
		break;

	case kOpcodeGreaterThan:
		returnValue.setToBool(value1 > value2);
		break;

	case kOpcodeLessThanOrEqualTo:
		returnValue.setToBool(value1 <= value2);
		break;

	case kOpcodeGreaterThanOrEqualTo:
		returnValue.setToBool(value1 >= value2);
		break;

	case kOpcodeAdd:
		returnValue = value1 + value2;
		break;

	case kOpcodeSubtract:
		returnValue = value1 - value2;
		break;

	case kOpcodeMultiply:
		returnValue = value1 * value2;
		break;

	case kOpcodeDivide:
		returnValue = value1 / value2;
		break;

	case kOpcodeModulo:
		returnValue = value1 % value2;
		break;

	default:
		error("%s: Got unknown binary operation opcode %s", __func__, opcodeToStr(op));
	}
	return returnValue;
}

ScriptValue CodeChunk::evaluateUnaryOperation() {
	// The only supported unary operation seems to be negation.
	ScriptValue value = evaluateExpression();
	debugCN(5, kDebugScript, "    value: ");
	return -value;
}

ScriptValue CodeChunk::evaluateFunctionCall(bool isIndirect) {
	uint functionId, paramCount = 0;
	if (isIndirect) {
		paramCount = _bytecode->readTypedUint16();
		ScriptValue value = evaluateExpression();
		functionId = value.asFunctionId();
	} else {
		functionId = _bytecode->readTypedUint16();
		paramCount = _bytecode->readTypedUint16();
	}

	return evaluateFunctionCall(functionId, paramCount);
}

ScriptValue CodeChunk::evaluateFunctionCall(uint functionId, uint paramCount) {
	debugC(5, kDebugScript, "%d (%d params)", functionId, paramCount);

	Common::Array<ScriptValue> args;
	for (uint i = 0; i < paramCount; i++) {
		debugCN(5, kDebugScript, "  Param %d: ", i);
		ScriptValue arg = evaluateExpression();
		args.push_back(arg);
	}

	ScriptValue returnValue = g_engine->getFunctionManager()->call(functionId, args);
	return returnValue;
}

ScriptValue CodeChunk::evaluateMethodCall(bool isIndirect) {
	BuiltInMethod method;
	uint paramCount = 0;
	if (isIndirect) {
		paramCount = _bytecode->readTypedUint16();
		ScriptValue value = evaluateExpression();
		method = value.asMethodId();
	} else {
		method = static_cast<BuiltInMethod>(_bytecode->readTypedUint16());
		paramCount = _bytecode->readTypedUint16();
	}

	return evaluateMethodCall(method, paramCount);
}

ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount) {
	// In Media Station, all methods are built-in - there aren't
	// custom objects or methods individual titles can
	// define. Functions, however, CAN be title-defined.
	// But here, we're only looking for built-in methods.
	debugC(5, kDebugScript, "%s (%d params)", builtInMethodToStr(method), paramCount);
	debugCN(5, kDebugScript, "  Self: ");

	ScriptValue target = evaluateExpression();
	Common::Array<ScriptValue> args;
	for (uint i = 0; i < paramCount; i++) {
		debugCN(5, kDebugScript, "  Param %d: ", i);
		ScriptValue arg = evaluateExpression();
		args.push_back(arg);
	}

	ScriptValue returnValue;
	switch (target.getType()) {
	case kScriptValueTypeActorId: {
		if (target.asActorId() == 0) {
			// It seems to be valid to call a method on a null actor ID, in
			// which case nothing happens. Still issue warning for traceability.
			warning("%s: Attempt to call method on a null actor ID", __func__);
			return returnValue;
		} else {
			// This is a regular actor that we can process directly.
			uint actorId = target.asActorId();
			Actor *targetActor = g_engine->getActorById(actorId);
			if (targetActor == nullptr) {
				error("%s: Attempt to call method on actor ID %d, which isn't loaded", __func__, target.asActorId());
			}
			returnValue = targetActor->callMethod(method, args);
			return returnValue;
		}
	}

	case kScriptValueTypeCollection: {
		Common::SharedPtr<Collection> collection = target.asCollection();
		returnValue = collection->callMethod(method, args);
		return returnValue;
	}

	default:
		error("Attempt to call method on unimplemented value type %s (%d)",
			scriptValueTypeToStr(target.getType()), static_cast<uint>(target.getType()));
	}
}

void CodeChunk::evaluateDeclareLocals() {
	uint localVariableCount = _bytecode->readTypedUint16();
	if (localVariableCount <= 0) {
		error("Got non-positive local variable count");
	}
	debugC(5, kDebugScript, "%d", localVariableCount);
	_locals = Common::Array<ScriptValue>(localVariableCount);
}

ScriptValue CodeChunk::evaluateReturn() {
	ScriptValue returnValue = evaluateExpression();
	_returnImmediately = true;
	return returnValue;
}

void CodeChunk::evaluateReturnNoValue() {
	_returnImmediately = true;
}

void CodeChunk::evaluateWhileLoop() {
	uint loopStartPosition = _bytecode->pos();
	uint iterationCount = 0;

	while (true) {
		// Seek to the top of the loop bytecode.
		_bytecode->seek(loopStartPosition);
		ScriptValue condition = evaluateExpression();
		if (condition.getType() != kScriptValueTypeBool) {
			error("Expected loop condition to be bool, not %s", scriptValueTypeToStr(condition.getType()));
		}

		if (++iterationCount >= MAX_LOOP_ITERATION_COUNT) {
			error("Exceeded max loop iteration count");
		}

		if (condition.asBool()) {
			executeNextBlock();
		} else {
			skipNextBlock();
			break;
		}
	}
}

CodeChunk::~CodeChunk() {
	_locals.clear();

	// We don't own the args, so we don't need to delete it.
	_args = nullptr;

	delete _bytecode;
	_bytecode = nullptr;
}

} // End of namespace MediaStation
