#include "Instruction.hpp"

#include "Error.hpp"
#include "String.hpp"

namespace SoftWire
{
	Instruction::Instruction(const Syntax &instruction) : syntax(instruction)
	{
		extractOperands(instruction.operands);

		if(secondOperand == Operand::OPERAND_IMM8)
		{
			if(Operand::isSubtypeOf(firstOperand, Operand::OPERAND_R_M16) ||
			   Operand::isSubtypeOf(firstOperand, Operand::OPERAND_R_M32))
			{
				secondOperand = Operand::OPERAND_EXT8;
			}
		}

		flags = instruction.flags;
		next = 0;
	}

	Instruction::~Instruction()
	{
		// Avoid call stack overflow
		while(next)
		{
			Instruction *nextNext = next->next;
			next->next = 0;
			delete next;
			next = nextNext;
		}
		next = 0;
	}

	void Instruction::extractOperands(const char *syntax)
	{
		if(!syntax)
		{
			throw INTERNAL_ERROR;
		}

		specifier = Specifier::TYPE_UNKNOWN;
		firstOperand = Operand::OPERAND_VOID;
		secondOperand = Operand::OPERAND_VOID;
		thirdOperand = Operand::OPERAND_VOID;

		char *string = strdup(syntax);
		const char *token = strtok(string, " ,");

		if(!token)
		{
			delete[] string;
			return;
		}

		specifier = Specifier::scan(token);

		if(specifier != Specifier::TYPE_UNKNOWN)
		{
			token = strtok(0, " ,");

			if(!token)
			{
				delete[] string;
				return;
			}
		}

		firstOperand = Operand::scanSyntax(token);

		if(firstOperand != Operand::OPERAND_UNKNOWN)
		{
			token = strtok(0, " ,");

			if(token == 0)
			{
				delete[] string;
				return;
			}
		}

		secondOperand = Operand::scanSyntax(token);

		if(secondOperand != Operand::OPERAND_UNKNOWN)
		{
			token = strtok(0, " ,");

			if(token == 0)
			{
				delete[] string;
				return;
			}
		}

		thirdOperand = Operand::scanSyntax(token);

		if(thirdOperand != Operand::OPERAND_UNKNOWN)
		{
			token = strtok(0, " ,");

			if(token == 0)
			{
				delete[] string;
				return;
			}
		}

		delete[] string;

		if(token == 0)
		{
			return;
		}
		else
		{
			throw Error("Invalid operand encoding '%s'", syntax);
		}
	}

	void Instruction::attach(Instruction *instruction)
	{
		if(!next)
		{
			next = instruction;
		}
		else
		{
			next->attach(instruction);
		}
	}

	void Instruction::attachNew(const Syntax &instruction)
	{
		if(!next)
		{
			next = new Instruction(instruction);
		}
		else
		{
			next->attachNew(instruction);
		}
	}

	Instruction *Instruction::getNext() const
	{
		return next;
	}

	const char *Instruction::getMnemonic() const
	{
		return syntax.mnemonic;
	}

	Operand::Type Instruction::getFirstOperand() const
	{
		return firstOperand;
	}

	Operand::Type Instruction::getSecondOperand() const
	{
		return secondOperand;
	}

	Operand::Type Instruction::getThirdOperand() const
	{
		return thirdOperand;
	}

	const char *Instruction::getOperandSyntax() const
	{
		return syntax.operands;
	}

	const char *Instruction::getEncoding() const
	{
		return syntax.encoding;
	}

	bool Instruction::is32Bit() const
	{
		return (flags & CPU_386) == CPU_386;
	}

	int Instruction::approximateSize() const
	{
		const char *format = syntax.encoding;

		if(!format)
		{
			throw INTERNAL_ERROR;
		}

		int size = 0;

		while(*format)
		{
			switch((format[0] << 8) | format[1])
			{
			case LOCK_PRE:
			case CONST_PRE:
			case REPNE_PRE:
			case REP_PRE:
				size += 1;
				break;
			case OFF_PRE:
				if(!is32Bit())
				{
					size += 1;
				}
				break;
			case ADDR_PRE:
				if(!is32Bit())
				{
					size += 1;
				}
				break;
			case ADD_REG:
				break;
			case EFF_ADDR:
			case MOD_RM_0:
			case MOD_RM_1:
			case MOD_RM_2:
			case MOD_RM_3:
			case MOD_RM_4:
			case MOD_RM_5:
			case MOD_RM_6:
			case MOD_RM_7:
				size += 1;
				break;
			case DWORD_IMM:
			case DWORD_REL:
				size += 4;
				break;
			case WORD_IMM:
				size += 2;
				break;
			case BYTE_IMM:
			case BYTE_REL:
				size += 1;
				break;
			default:
				size += 1;
			}

			format += 2;

			if(*format == ' ')	
			{
				format++;
			}
			else if(*format == '\0')
			{
				break;
			}
			else
			{
				throw INTERNAL_ERROR;
			}
		}

		return size;
	}
}
