/* ScummVM Tools
 *
 * ScummVM Tools 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 "disassembler.h"
#include "opcodes.h"

#include "common/util.h"
#include "common/endian.h"

namespace Groovie {

/**
 * Value representing an array of values.
 */
class ValuesArrayValue : public Value {
protected:
	const ValueList _values;

public:
	/**
	 * Constructor for ValuesArrayValue.
	 *
	 * @param values std::deque of values (left-to-right).
	 */
	ValuesArrayValue(ValueList values) : _values(values) { }
	//TODO: implement dup()?

	std::ostream &print(std::ostream &output) const;
};

std::ostream &ValuesArrayValue::print(std::ostream &output) const {
	output << "[";
	for (ValueList::const_iterator i = _values.begin(); i != _values.end(); ++i) {
		if (i != _values.begin())
			output << ", ";
		output << *i;
	}
	output << "]";
	return output;
}

class GroovieJumpInstruction : public Instruction {
public:
	bool isJump() const { return true; }
	uint32 getDestAddress() const;
	void processInst(ValueStack &stack, Engine *engine, CodeGenerator *codeGen) {}
};

uint32 GroovieJumpInstruction::getDestAddress() const {
	std::vector<ValuePtr>::const_iterator i = _params.begin();
	while (i != _params.end()) {
		if ((*i)->isAddress())
			return (*i)->getUnsigned();
		++i;
	}
	return 0;
}

class GroovieCondJumpInstruction : public GroovieJumpInstruction {
public:
	bool isCondJump() const { return true; }
};

class GroovieUncondJumpInstruction : public GroovieJumpInstruction {
public:
	bool isUncondJump() const { return true; }
};


// GroovieDisassembler

GroovieDisassembler::GroovieDisassembler(InstVec &insts, const std::vector<GroovieOpcode> &opcodes) :
	Reassembler(insts), _opcodes(opcodes), _firstBit(false) {
}

InstPtr GroovieDisassembler::readInstruction() {
	// Read the next opcode
	byte opcode;
	try {
		opcode = _f.readByte();
	} catch (Common::FileException e) {
		return NULL;
	}
	if (_f.eos())
		return NULL;

	return createInstruction(opcode);
}

InstPtr GroovieDisassembler::createInstruction(byte opcode) {
	// Extract the first bit
	_firstBit = opcode & 0x80;
	opcode &= 0x7F;

	// Verify it's a valid opcode
	if (opcode >= _opcodes.size())
		throw UnknownOpcodeException(_address, opcode);

	// Create the new instruction
	InstPtr i;
	switch (_opcodes[opcode].type) {
	case kUnaryOpPreInst:
		i = new UnaryOpPrefixStackInstruction();
		break;
	case kBinaryOpInst:
		i = new BinaryOpStackInstruction();
		break;
	case kKernelCallInst:
		i = new KernelCallStackInstruction();
		break;
	case kReturnInst:
		i = new ReturnInstruction();
		break;
	case kJumpInst:
		i = new GroovieUncondJumpInstruction();
		break;
	case kCondJumpInst:
		i = new GroovieCondJumpInstruction();
		break;
	case kCallInst:
		// TODO: mark beginning of functions?
		i = new KernelCallStackInstruction();
		break;
	}
	if (!i)
		return NULL;

	// Fill the instruction information
	i->_opcode = opcode;
	i->_address = _address++;
	i->_stackChange = 0;
	i->_name = _opcodes[opcode].name;

	// Handle special cases
	switch (opcode) {
	case 0x0B: // Input Loop Start
		// save its address so the Input Loop End can jump here
		_inputLoopStart = i->_address;
		break;
	case 0x13: // Input Loop End
		// Jump to the address of the Input Loop Start
		i->_params.push_back(new AddressValue(_inputLoopStart));
		break;
	}

	// Read the current instruction's parameters
	readParams(i, _opcodes[opcode].params);
	return i;
}

void GroovieDisassembler::readParams(InstPtr inst, const char *typeString) {
	while (*typeString) {
		inst->_params.push_back(readParameter(*typeString));
		typeString++;
	}
}

ValuePtr GroovieDisassembler::readParameter(char type) {
	ValuePtr retval = NULL;
	switch (type) {
	case '1': // 8 bits
		retval = new IntValue(_f.readByte(), false);
		_address++;
		break;
	case '2': // 16 bits
		retval = new IntValue(_f.readUint16LE(), false);
		_address += 2;
		break;
	case '3': // 8 or 16 bits
		if (_firstBit)
			retval = readParameter('1');
		else
			retval = readParameter('2');
		break;
	case '4': // 32 bits
		retval = new IntValue(_f.readUint32LE(), false);
		_address += 4;
		break;
	case '@': // Address
		retval = new AddressValue(_f.readUint16LE());
		_address += 2;
		if (retval->getUnsigned() > _maxTargetAddress)
			_maxTargetAddress = retval->getUnsigned();
		break;
	case 'A': // Array
		retval = readParameterArray();
		break;
	case 'S': // Script name
		retval = readParameterScriptName();
		break;
	case 'V': // Video name
		retval = readParameterVideoName();
		break;
	case 'C': // Indexed value
		retval = readParameterIndexed(false, true, true);
		break;
	case 'B': // First Bit
		retval = new IntValue(_firstBit, false);
		break;
	default:
		std::cout << "  UNKNOWN param type: " << type << std::endl;
	}
	return retval;
}

ValuePtr GroovieDisassembler::readParameterIndexed(bool allow7C, bool limitVal, bool limitVar) {
	ValuePtr result;
	uint8 data = _f.readByte();
	_address++;

	if (limitVal) {
		_firstBit = data & 0x80;
		data &= 0x7F;
	}

	if (allow7C && (data == 0x7C)) {
		// Index a bidimensional array:
		// M[part1, part2]
		// M[0x19 + 10 * part1 + part2]

		ValuePtr part1 = readParameterIndexed(false, false, false);
		ValuePtr part2 = readParameterIndexed(false, true, true);

		ValueList idxs;
		idxs.push_back(part1);
		idxs.push_back(part2);
		result = new ArrayValue("M", idxs);
	} else if (data == 0x23) {
		// Index an array:
		// M[data - 0x61]

		data = _f.readByte();
		_address++;
		if (limitVar) {
			_firstBit = data & 0x80;
			data &= 0x7F;
		}

		ValueList idxs;
		idxs.push_back(new IntValue(data - 0x61, false));
		result = new ArrayValue("m", idxs);// TODO: can probably make writeParameterIndex smart enough to not need the lowercase m here
	} else {
		// Immediate value
		result = new IntValue(data - 0x30, true);
	}
	return result;
}

ValuePtr GroovieDisassembler::readParameterArray() {
	ValueList values;
	do {
		values.push_back(readParameterIndexed(true, true, true));
	} while (!_firstBit);

	return new ValuesArrayValue(values);
}

ValuePtr GroovieDisassembler::readParameterScriptName() {
	std::string result = "";
	byte c;
	_address++;
	while ((c = _f.readByte())) {
		result += (char)c;
		_address++;
	}

	// Return the read string
	return new StringValue(result);
}

ValuePtr GroovieDisassembler::readParameterVideoName() {
	ValueList values;

	std::string stringLiteral = "";
	byte data;
	_address++;
	while ((data = _f.readByte())) {
		if (data == 0x7C || data == 0x23) {
			// Check if it breaks a previous string literal
			if (!stringLiteral.empty()) {
				values.push_back(new StringValue(stringLiteral));
				stringLiteral = "";
			}

			ValueList idxs;
			if (data == 0x7C) {
				// Indexing a bidimensional array
				idxs.push_back(readParameterIndexed(false, false, false));
				idxs.push_back(readParameterIndexed(false, false, false));
				values.push_back(new ArrayValue("M", idxs));
			} else if (data == 0x23) {
				// Indexing an unidimensional array
				data = _f.readByte();
				_address++;
				idxs.push_back(new IntValue(data - 0x61, false));
				values.push_back(new ArrayValue("m", idxs));// TODO: can probably make writeParameterVideoName smart enough to not need the lowercase m here
			}
			// TODO BinaryOpValue: M[...] + 0x30
			//values.push_back(new ArrayValue("M", idxs));
		} else {
			// To lowercase?
			if (data >= 0x41 && data <= 0x5A) {
				data += 0x20;
			}
			// Append the current character at the end of the string
			stringLiteral += (char)data;
		}
		_address++;
	}

	// Check if there's a spare string literal
	if (!stringLiteral.empty())
		values.push_back(new StringValue(stringLiteral));

	// Return the array built of string parts
	return new ValuesArrayValue(values);
}

void GroovieDisassembler::doDisassemble() throw (UnknownOpcodeException) {
	// Prepare to read the input script
	_f.seek(0, SEEK_SET);

	// Reset the status
	_addressBase = 0;
	_address = _addressBase;
	_maxTargetAddress = 0;
	_inputLoopStart = 0;

	bool cont = true;
	while (cont) {
		InstPtr i = readInstruction();

		if (i)
			_insts.push_back(i);
		else
			cont = false;
	}

	// HACK: insert additional NOPs, since some jumps target beyond
	// the end of the script
	while (_maxTargetAddress >= _address)
		_insts.push_back(createInstruction(0));
}

GroovieOpcode GroovieDisassembler::getInstruction(const std::string &name) throw(std::exception) {
	for(auto &i : _opcodes) {
		if(name == i.name) {
			return i;
		}
	}
	throw std::runtime_error("Unable to find instruction: " + name);
}

void GroovieDisassembler::doAssembly(const std::string &label, std::string &instruction, const std::vector<std::string> &args, const std::string &comment) throw(std::exception) {
	// find the matching instruction name
	GroovieOpcode inst = getInstruction(instruction);
	
	// build list of labels, parse arguments, and write bytes to _binary
	std::vector<byte> bytes;
	size_t jumpAddrStart;// where the address is stored so it can be overwritten when we have the full list of labels
	std::string jumpToLabel;

	_firstBit = false;
	jumpAddrStart = writeParams(bytes, inst.params, args, jumpToLabel);
	// use writeParams to guess _firstBit, then we write the opcode at the end
	bytes.insert(bytes.begin(), inst.opcode | (_firstBit<<7));
	jumpAddrStart++; // increment since we pushed a byte to the front

	addInstruction(bytes, inst.type, jumpAddrStart, 2, label, jumpToLabel);
}

size_t GroovieDisassembler::writeParams(std::vector<byte> &bytes, const char *typeString, const std::vector<std::string> &args, std::string &jumpToLabel) {
	size_t jumpAddrStart = 0;
	assert(args.size() >= strlen(typeString));
	for(int i=0; typeString[i]; i++) {
		writeParameter(typeString[i], bytes, args[i], jumpAddrStart, jumpToLabel);
	}
	return jumpAddrStart;
}

void GroovieDisassembler::writeParameter(char type, std::vector<byte> &bytes, const std::string &arg, size_t &jumpAddrStart, std::string &jumpToLabel) {
	int i;
	uint16 i16;
	uint32 u32;

	switch (type) {
	case '1': // 8 bits
		i = std::stoi(arg);
		bytes.push_back(i);
		break;
	case '2': // 16 bits
		i16 = std::stoi(arg);
		i16 = TO_LE_16(i16);
		bytes.push_back(i16);
		bytes.push_back(i16 >> 8);
		break;
	case '3': // 8 or 16 bits
		i = std::stoi(arg);
		if(i <= 0xFF || _firstBit) {
			_firstBit = true;
			bytes.push_back(i);
		} else {
			_firstBit = false;
			i16 = TO_LE_16(i);
			bytes.push_back(i16);
			bytes.push_back(i16 >> 8);
		}
		break;
	case '4': // 32 bits
		u32 = std::stoul(arg);
		u32 = TO_LE_32(u32);
		bytes.push_back(u32);
		bytes.push_back(u32 >> 8);
		bytes.push_back(u32 >> 16);
		bytes.push_back(u32 >> 24);
		break;
	case '@': // Address
		jumpAddrStart = bytes.size();
		// if arg is in 0xF3DE format, convert to 0000f3de
		if(arg.find("0x") == 0) {
			u32 = std::stoul(arg, 0, 16);
			assert((boost::format("0x%X") % u32).str() == arg);
			jumpToLabel = (boost::format("%08x") % u32).str();
		}
		else
			jumpToLabel = arg;
		bytes.push_back(0);
		bytes.push_back(0);
		break;
	case 'A': // Array
		// substring to remove the [ and ]
		writeParameterArray(bytes, arg.substr(1, arg.length()-2));
		break;
	case 'S': // Script name
		// ignore the quotes around it
		for(size_t j=1; j<arg.length()-1; j++)
			bytes.push_back(arg[j]);
		bytes.push_back(0);
		break;
	case 'V': // Video name
		// substring to remove the [ and ]
		writeParameterVideoName(bytes, arg.substr(1, arg.length()-2));
		break;
	case 'C': // Indexed value
		writeParameterIndexed(false, true, true, bytes, arg);
		break;
	case 'B': // First Bit
		i = std::stoi(arg);
		_firstBit = bool(i);
		break;
	default:
		std::cout << "writeParameter UNKNOWN param type: " << type << "\n";
		throw std::runtime_error(std::string() + "writeParameter UNKNOWN param type: " + type);
	}
}

void GroovieDisassembler::writeParameterVideoName(std::vector<byte> &bytes, const std::string &arg) {
	size_t s = 0, e = 0;
	while(e < arg.length()) {
		e = getEndArgument(arg, s);
		std::string a = arg.substr(s, e - s);
		switch(a[0]) {
		case '"':
			for(size_t i=1; i < a.length()-1; i++) {
				bytes.push_back(a[i]);
			}
			break;
		case 'm':
			a = a.substr(2);
			a.pop_back();
			bytes.push_back(0x23);
			bytes.push_back(std::stoul(a) + 0x61);
			break;
		case 'M':
			bytes.push_back(0x7C);
			std::string first;
			std::string second;
			splitArrayString(a, first, second);
			writeParameterIndexed(false, false, false, bytes, first);
			writeParameterIndexed(false, false, false, bytes, second);
			break;
		}
		s = e + 2;
	}

	bytes.push_back(0);
}

void GroovieDisassembler::writeParameterIndexed(bool allow7C, bool limitVal, bool limitVar, std::vector<byte> &bytes, const std::string &arg) {
	if (allow7C && arg[0] == 'M') {
		bytes.push_back(0x7C);
		std::string first;
		std::string second;
		splitArrayString(arg, first, second);
		writeParameterIndexed(false, false, false, bytes, first);
		writeParameterIndexed(false, true, true, bytes, second);
	} else if (arg[0] == 'm') {
		bytes.push_back(0x23);
		std::string a = arg.substr(2);
		a.pop_back();
		uint8 data = std::stoul(a);
		bytes.push_back(data + 0x61);
	} else {
		// Immediate value
		uint8 data = std::stoul(arg);
		bytes.push_back(data + 0x30);
	}
}

void GroovieDisassembler::writeParameterArray(std::vector<byte> &bytes, const std::string &arg) {
	size_t s = 0, e = 0;
	while(e < arg.length()) {
		e = getEndArgument(arg, s);
		std::string a = arg.substr(s, e - s);
		writeParameterIndexed(true, true, true, bytes, a);
		s = e + 2;
	}
	// terminate the array by using the first bit
	bytes[bytes.size() - 1] |= 0x80;
}
} // End of namespace Groovie
