#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "../utils/defs.h"
#include "../utils/bucketlist.h"
#include "executable.h"

#define MAX_OPS 3

typedef struct {
	Opcode 	opcode;
	int 	offset;
	int 	length;
	union {
		long long int as_int;
		double 		  as_float;
	} operands[MAX_OPS];
} Instruction;

struct xExecutable {
	int refs;
	int headl, bodyl;
	char 		*head;
	Instruction *body;
	Source 		*src;
};

struct xExeBuilder {
	BucketList *data, *code;
	int promc;
};

typedef struct {
	const char  *name;
	int 		 opcount;
	OperandType *optypes;
} InstrInfo;

static const InstrInfo instr_table[] = {

	[OPCODE_NOPE] = {"NOPE", 0, NULL},

	[OPCODE_POS]  = {"POS", 0, NULL},
	[OPCODE_NEG]  = {"NEG", 0, NULL},

	[OPCODE_ADD]  = {"ADD", 0, NULL},
	[OPCODE_SUB]  = {"SUB", 0, NULL},
	[OPCODE_MUL]  = {"MUL", 0, NULL},
	[OPCODE_DIV]  = {"DIV", 0, NULL},

	[OPCODE_PUSHINT] = {"PUSHINT", 1, (OperandType[]) {OPTP_INT}},
	[OPCODE_PUSHFLT] = {"PUSHFLT", 1, (OperandType[]) {OPTP_FLOAT}},
	[OPCODE_PUSHSTR] = {"PUSHSTR", 1, (OperandType[]) {OPTP_STRING}},
	[OPCODE_PUSHVAR] = {"PUSHVAR", 1, (OperandType[]) {OPTP_STRING}},
	[OPCODE_PUSHTRU] = {"PUSHTRU", 0, NULL},
	[OPCODE_PUSHFLS] = {"PUSHFLS", 0, NULL},
	[OPCODE_PUSHNNE] = {"PUSHNNE", 0, NULL},

	[OPCODE_RETURN] = {"RETURN", 0, NULL},

	[OPCODE_JUMPIFNOTANDPOP] = {"JUMPIFNOTANDPOP", 1, (OperandType[]) {OPTP_INT}},
	[OPCODE_JUMP] = {"JUMP", 1, (OperandType[]) {OPTP_INT}},
};

Executable *Executable_Copy(Executable *exe)
{
	assert(exe != NULL);

	exe->refs += 1;
	return exe;
}

void Executable_Free(Executable *exe)
{
	exe->refs -= 1;
	assert(exe->refs >= 0);

	if(exe->refs == 0)
		{
			if(exe->src)
				Source_Free(exe->src);
			free(exe);
		}
}

void Executable_Dump(Executable *exe)
{
	for(int i = 0; i < exe->bodyl; i += 1)
		{
			Opcode opcode;
			Operand ops[MAX_OPS];
			int opc = MAX_OPS;

			(void) Executable_Fetch(exe, i, &opcode, ops, &opc);

			const InstrInfo *info = instr_table + exe->body[i].opcode;

			fprintf(stderr, "%d: %s ", i, info->name);

			for(int j = 0; j < opc; j += 1)
				{
					switch(ops[j].type)
						{
							case OPTP_INT:
							fprintf(stderr, "%lld ", ops[j].as_int);
							break;

							case OPTP_FLOAT:
							fprintf(stderr, "%f ", ops[j].as_float);
							break;

							case OPTP_STRING:
							fprintf(stderr, "[%s] ", ops[j].as_string);
							break;

							case OPTP_PROMISE:
							UNREACHABLE;
							break;
						}
				}

			fprintf(stderr, "\n");
		}
}

_Bool Executable_SetSource(Executable *exe, Source *src)
{
	src = Source_Copy(src);
	
	if(src == NULL)
		return 0;

	exe->src = src;
	return 1;
}

Source *Executable_GetSource(Executable *exe)
{
	return exe->src;
}

_Bool Executable_Fetch(Executable *exe, int index, Opcode *opcode, Operand *ops, int *opc)
{
	assert(index >= 0);

	if(index >= exe->bodyl)
		return 0;

	const Instruction *instr = exe->body + index;
	const InstrInfo *info = instr_table + instr->opcode;

	if(opcode)
		*opcode = instr->opcode;

	int i;

	if(ops && opc)
		{
			int k = MIN(*opc, info->opcount);

			for(i = 0; i < k; i += 1)
				{
					OperandType type = info->optypes[i];
					assert(type != OPTP_PROMISE);

					switch(type) {

						case OPTP_STRING:

						int data_offset = instr->operands[i].as_int;

						assert(data_offset < exe->headl);

						ops[i].type = OPTP_STRING;
						ops[i].as_string = exe->head + data_offset;
						break;

						case OPTP_INT:
						ops[i].type = OPTP_INT;
						ops[i].as_int = instr->operands[i].as_int;
						break;

						case OPTP_FLOAT:
						ops[i].type = OPTP_FLOAT;
						ops[i].as_int = instr->operands[i].as_int;
						break;

						case OPTP_PROMISE:
						UNREACHABLE;
						break;
					}
				}
		
			*opc = MIN(*opc, info->opcount);
		}

	return 1;
}

ExeBuilder *ExeBuilder_New(BPAlloc *alloc)
{
	assert(alloc != NULL);

	ExeBuilder *exeb = BPAlloc_Malloc(alloc, sizeof(ExeBuilder));

	if(exeb == NULL)
		return NULL;

	exeb->promc = 0;
	exeb->data = BucketList_New(alloc);
	exeb->code = BucketList_New(alloc);

	if(exeb->data == NULL || exeb->code == NULL)
		return NULL;
	return exeb;
}

Executable *ExeBuilder_Finalize(ExeBuilder *exeb, Error *error)
{
	assert(exeb != NULL);

	if(exeb->promc > 0)
		{
			Error_Report(error, 0, "There are still %d unfulfilled promises", exeb->promc);
			return 0;
		}

	Executable *exe;
	{
		int data_size = BucketList_Size(exeb->data);
		int code_size = BucketList_Size(exeb->code);

		assert(code_size % sizeof(Instruction) == 0);

		void *temp = malloc(sizeof(Executable) + data_size + code_size);

		if(temp == NULL)
			{
				Error_Report(error, 1, "No memory");
				return NULL;
			}

		exe = temp;
		exe->headl = data_size;
		exe->bodyl = code_size / sizeof(Instruction);
		exe->body = (Instruction*) (exe + 1);
		exe->head = (char*) (exe->body + exe->bodyl);
		exe->refs = 1;
		exe->src = NULL;
		
	}

	BucketList_Copy(exeb->data, exe->head, -1);
	BucketList_Copy(exeb->code, exe->body, -1);
	return exe;
}

static void promise_callback(void *userp)
{
	assert(userp != NULL);

	ExeBuilder *exeb = userp;

	exeb->promc -= 1;
	assert(exeb->promc >= 0);
}

_Bool ExeBuilder_Append(ExeBuilder *exeb, Error *error, Opcode opcode, Operand *opv, int opc, int off, int len)
{
	assert(exeb != NULL);
	assert(opc >= 0);

	static const char *operand_type_names[] = {
		[OPTP_INT] = "int",
		[OPTP_FLOAT] = "float",
		[OPTP_STRING] = "string",
	};

	static const char *operand_type_arts[] = {
		[OPTP_INT] = "an",
		[OPTP_FLOAT] = "a",
		[OPTP_STRING] = "a",
	};

	static const unsigned int operand_type_sizes[] = {
		[OPTP_INT] = membersizeof(Operand, as_int),
		[OPTP_FLOAT] = membersizeof(Operand, as_float),
		[OPTP_STRING] = membersizeof(Operand, as_string),
	};

	const InstrInfo *info = instr_table + opcode;

	if(opc != info->opcount)
		{
			// ERROR: Too many operands were provided.
			Error_Report(error, 0, 
				"Instruction %s expects %d operands, but %d were provided", 
				info->name, opc, info->opcount);
			return 0;
		}

	assert(opc <= MAX_OPS);

	{
		Instruction *instr = BucketList_Append2(exeb->code, NULL, sizeof(Instruction));

		if(instr == NULL)
			{
				Error_Report(error, 1, "No memory");
				return 0;
			}

		instr->opcode = opcode;
		instr->offset = off;
		instr->length = len;

		for(int i = 0; i < opc; i += 1)
			{
				// Check that the expected type and the provided one
				// match, or that the provided type is a promise with
				// the same size of the expected type.
				{
					OperandType expected_type = info->optypes[i];
					OperandType provided_type = opv[i].type;

					assert(expected_type != OPTP_PROMISE);

					if(provided_type == OPTP_PROMISE)
						{
							assert(opv[i].as_promise != NULL);

							if(expected_type == OPTP_STRING)
								{
									Error_Report(error, 0, "Promise values can't be provided as string operands");
									return 0;
								}
							
							if(Promise_Size(opv[i].as_promise) != operand_type_sizes[expected_type])
								{
									Error_Report(error, 0, 
										"Provided promise has a value size of %d, "
										"but since %s %s was expected, the promise "
										"size must be %d",
										Promise_Size(opv[i].as_promise),
										operand_type_arts[expected_type],
										operand_type_names[expected_type],
										operand_type_sizes[expected_type]);
									return 0;
								}
						}
					else if(expected_type != provided_type)
						{
							// ERROR: Wrong operand type provided.
							Error_Report(error, 0, 
								"Instruction %s expects %s %s as operand %d, but %s %s was provided instead", 
								info->name, 
								operand_type_arts[expected_type], 
								operand_type_names[expected_type], 
								i, 
								operand_type_arts[provided_type], 
								operand_type_names[provided_type]
							);
							return 0;
						}
				}

				// Do the copying of the operands.
				switch(opv[i].type)
					{
						case OPTP_STRING:

						instr->operands[i].as_int = BucketList_Size(exeb->data);
						
						if(!BucketList_Append(exeb->data, opv[i].as_string, strlen(opv[i].as_string)+1))
							{
								Error_Report(error, 1, "No memory");
								return 0;
							}
						break;

						case OPTP_PROMISE:

						assert(info->optypes[i] != OPTP_STRING);

						// This must be incremented before subscribing
						// since the counter-decrementing callback may 
						// be called immediately if the promise was 
						// already fulfilled.
						exeb->promc += 1;

						if(!Promise_Subscribe2(opv[i].as_promise, &instr->operands[i], exeb, promise_callback))
							{
								Error_Report(error, 1, "No memory");
								return 0;
							}
						break;

						case OPTP_INT:
						instr->operands[i].as_int = opv[i].as_int;
						break;

						case OPTP_FLOAT:
						instr->operands[i].as_float = opv[i].as_float;
						break;

					}
			}
	}

	return 1;
}

BPAlloc *ExeBuilder_GetAlloc(ExeBuilder *exeb)
{
	return BucketList_GetAlloc(exeb->code);
}

int ExeBuilder_InstrCount(ExeBuilder *exeb)
{
	int raw_size = BucketList_Size(exeb->code);

	assert(raw_size % sizeof(Instruction) == 0);

	return raw_size / sizeof(Instruction);
}

