#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>

uint32_t create_mask(uint8_t from, uint8_t to)
{
	uint32_t r = 0;

	for (uint8_t i = from; i <= to; i++)
		r |= 1 << i;

	return r;
}

uint8_t *allocate_uint_bytes(uint32_t uint)
{
	uint8_t *uints = malloc(4);

	for (size_t i = 0; i < 4; i++)
		uints[3 - i] = (uint & create_mask(i * 8, (i + 1) * 8)) >> (i * 8);
	
	return uints;
}

// little endian
// incoherently unreadable, but does get the job done
char *uint_to_binary_string(uint32_t uint, size_t bytes)
{
	char *buf = malloc(33);
	uint8_t *uints = allocate_uint_bytes(uint);

	for (size_t i = 0; i < bytes; i++)
		for (size_t j = 0; j < 8; j++)
			buf[i * 8 + j] = (((1 << (7 - j)) & uints[i + 4 - bytes]) > 0) ? '1' : '0';
	buf[bytes * 8] = '\0';

	//printf("%u\n%x %x %x %x\n%s\n", uint, uints[0], uints[1], uints[2], uints[3], buf);
	free(uints);

	return buf;
}

char *binary_string_n_bits(char *str, size_t n)
{
	size_t i;
	for (i = 0; i < n; i++)
		str[i] = str[strlen(str) - n + i];
	str[i] = '\0';
	return str;
}

typedef enum {
	MEM_B = 0, MEM_BU, MEM_H, MEM_HU, MEM_W, MEMTYPE_SIZE
} memtype;
char *memtypes[MEMTYPE_SIZE] = {
	"MEM_B", "MEM_BU", "MEM_H", "MEM_HU", "MEM_W"
};

typedef enum {
	BR_NOP = 0, BR_BR, BR_CND, BR_CNDI, BRANCHTYPE_SIZE
} branchtype;
char *branchtypes[BRANCHTYPE_SIZE] = {
	"BR_NOP", "BR_BR", "BR_CND", "BR_CNDI"
};

typedef enum {
	WBS_ALU = 0, WBS_MEM, WBS_OPC, WBTYPE_SIZE
} wbtype;
char *wbtypes[WBTYPE_SIZE] = {
	"WBS_ALU", "WBS_MEM", "WBS_OPC"
};

typedef struct {
	// mem op
	branchtype branch;
	bool memread;
	bool memwrite;
	memtype memtype;
} mem_op_t;

typedef struct {
	// wb op
	uint8_t rd;
	bool write;
	wbtype src;
} wb_op_t;

typedef struct {
	bool write;
	uint8_t reg;
	uint32_t data;
} reg_write_t;

typedef struct {
	bool busy;
	uint32_t rddata;
} mem_in_t;

typedef struct {
	// 14 bits address!
	uint16_t address;
	bool rd;
	bool wr;
	// 4 bits byteena!
	uint8_t byteena;
	uint32_t wrdata;
} mem_out_t;

typedef struct {
	bool stall;
	bool flush;
	// mem op
	mem_op_t mem_op;
	// wb op
	wb_op_t wb_op;
	uint16_t pc_old;
	uint16_t pc_new;
	uint32_t aluresult;
	uint32_t wrdata;
	bool zero;
	mem_in_t mem;
} input_t;

typedef struct {
	bool mem_busy;
	reg_write_t reg_write;
	uint16_t pc_new;
	uint16_t pc_old;
	bool pcsrc;
	// wb op
	wb_op_t wb_op;
	uint32_t aluresult;
	uint32_t memresult;
	mem_out_t mem;
	bool exc_load;
	bool exc_store;
} output_t;

int main(void) {
	FILE *file_output;
	FILE *file_input;
	file_output = fopen("./testdata/output.txt", "w");
	file_input = fopen("./testdata/input.txt", "w");
	srand(time(NULL));

	int n = 54;
	for (size_t i = 0; i < n; i++) {
		// create random input
		input_t input;
		input.stall = false;
		input.flush = false;
		input.mem_op.branch = rand() % BRANCHTYPE_SIZE;
		input.mem_op.memread = rand() % 2 ? true : false;
		input.mem_op.memwrite = !input.mem_op.memread;
		input.mem_op.memtype = rand() % MEMTYPE_SIZE;
		input.wb_op.rd = rand();
		input.wb_op.write = rand() % 2 ? true : false;
		input.wb_op.src = rand() % WBTYPE_SIZE;
		input.pc_old = rand();
		input.pc_new = rand();
		input.aluresult = rand();
		input.wrdata = rand();
		input.zero = rand() % 2 ? true : false;
		input.mem.busy = rand() % 2 ? true : false;
		input.mem.rddata = rand();

		// pre determined output
	   	output_t output;
		// constant reg_write
		output.reg_write.write = input.wb_op.write;
		output.reg_write.reg = input.wb_op.rd;
		output.pc_new = input.pc_new;
		output.pc_old = input.pc_old;
		// branch if branch or branch cnd with zero set
		output.pcsrc = (
			input.mem_op.branch == BR_BR ||
			(input.mem_op.branch == BR_CND && input.zero) ||
			(input.mem_op.branch = BR_CNDI && input.zero)
		);
		output.wb_op = input.wb_op;
		output.aluresult = input.aluresult;
		uint8_t r_arr[4];
		uint8_t d_arr[4];
		// don't care entries need to be set correctly before printing
		char m_arr_string[4][9];
		// init all to don't care (for m_arr)
		for (size_t i = 0; i < 4; i++) {
			for (size_t j = 0; j < 8; j++)
				m_arr_string[i][j] = '-';
			m_arr_string[i][8] = '\0';
		}
		// extract rddata bytes
		d_arr[0] = (input.mem.rddata & 0x000000FF);
		d_arr[1] = (input.mem.rddata & 0x0000FF00) >> 8;
		d_arr[2] = (input.mem.rddata & 0x00FF0000) >> 16;
		d_arr[3] = (input.mem.rddata & 0xFF000000) >> 24;
		// mask A to get 1 downto 0
		uint8_t A_byte = input.aluresult & 0b11;
		uint8_t not_A_byte = ((~input.aluresult) & 0b11);
		switch (input.mem_op.memtype) { // very even more confuse but looks cool right :3
			case MEM_B: {
				r_arr[0] = d_arr[not_A_byte];
				// sign-extend rest of r_arr
				bool res = d_arr[not_A_byte] & 0b10000000;
				r_arr[1] = r_arr[2] = r_arr[3] = res ? 0xFF : 0x00;
				// set relevant value
				char *mem_b_str = uint_to_binary_string((input.wrdata & 0x000000FF), 1);
				for (size_t i = 0; i < 8; i++)
					m_arr_string[3 - not_A_byte][i] = mem_b_str[i];
				output.mem.byteena = 0b1;
				output.mem.byteena = output.mem.byteena << (not_A_byte);
				break;
			}
			case MEM_BU: {
				r_arr[0] = d_arr[not_A_byte];
				// unsigned
				r_arr[1] = r_arr[2] = r_arr[3] = 0;
				// set relevant value
				char *mem_bu_str = uint_to_binary_string((input.wrdata & 0x000000FF), 1);
				for (size_t i = 0; i < 8; i++)
					m_arr_string[3 - not_A_byte][i] = mem_bu_str[i];
				output.mem.byteena = 0b1;
				output.mem.byteena = output.mem.byteena << (not_A_byte);
				break;
			}
			case MEM_H: {
				char *mem_h_str0 = uint_to_binary_string((input.wrdata & 0x000000FF), 1);
				char *mem_h_str1 = uint_to_binary_string(((input.wrdata & 0x0000FF00) >> 8), 1);
				if ((~input.aluresult) & 0b10) {
					r_arr[0] = d_arr[3];
					r_arr[1] = d_arr[2];
					// sign-extended
					bool res = (d_arr[2] & 0b10000000) == 0b10000000;
					r_arr[2] = r_arr[3] = res ? 0xFF : 0x00;
					for (size_t i = 0; i < 8; i++) {
						m_arr_string[1][i] = mem_h_str1[i];
						m_arr_string[0][i] = mem_h_str0[i];
					}
					output.mem.byteena = 0b1100;
				} else {
					r_arr[0] = d_arr[1];
					r_arr[1] = d_arr[0];
					// sign-extended
					bool res = (d_arr[0] & 0b10000000) == 0b10000000;
					r_arr[2] = r_arr[3] = res ? 0xFF : 0x00;
					for (size_t i = 0; i < 8; i++) {
						m_arr_string[3][i] = mem_h_str1[i];
						m_arr_string[2][i] = mem_h_str0[i];
					}
					output.mem.byteena = 0b0011;
				}
				break;
			}
			case MEM_HU: {
				char *mem_hu_str0 = uint_to_binary_string((input.wrdata & 0x000000FF), 1);
				char *mem_hu_str1 = uint_to_binary_string(((input.wrdata & 0x0000FF00) >> 8), 1);
				if ((~input.aluresult) & 0b10) {
					r_arr[0] = d_arr[3];
					r_arr[1] = d_arr[2];
					// non sign-extended
					r_arr[2] = r_arr[3] = 0;
					for (size_t i = 0; i < 8; i++) {
						m_arr_string[1][i] = mem_hu_str1[i];
						m_arr_string[0][i] = mem_hu_str0[i];
					}
					output.mem.byteena = 0b1100;
				} else {
					r_arr[0] = d_arr[1];
					r_arr[1] = d_arr[0];
					// non sign-extended
					r_arr[2] = r_arr[3] = 0;
					for (size_t i = 0; i < 8; i++) {
						m_arr_string[3][i] = mem_hu_str1[i];
						m_arr_string[2][i] = mem_hu_str0[i];
					}
					output.mem.byteena = 0b0011;
				}
				break;
			}
			case MEM_W: {
				r_arr[0] = d_arr[3];
				r_arr[1] = d_arr[2];
				r_arr[2] = d_arr[1];
				r_arr[3] = d_arr[0];
				char *mem_w_str0 = uint_to_binary_string((input.wrdata & 0x000000FF), 1);
				char *mem_w_str1 = uint_to_binary_string(((input.wrdata & 0x0000FF00) >> 8), 1);
				char *mem_w_str2 = uint_to_binary_string(((input.wrdata & 0x00FF0000) >> 16), 1);
				char *mem_w_str3 = uint_to_binary_string(((input.wrdata & 0xFF000000) >> 24), 1);
				for (size_t i = 0; i < 8; i++) {
					m_arr_string[0][i] = mem_w_str0[i];
					m_arr_string[1][i] = mem_w_str1[i];
					m_arr_string[2][i] = mem_w_str2[i];
					m_arr_string[3][i] = mem_w_str3[i];
				}
				output.mem.byteena = 0b1111;
				break;
			}
			default:
				break;
		}
		output.memresult = 0;
		output.memresult += (((uint32_t)r_arr[0]) << 0);
		output.memresult += (((uint32_t)r_arr[1]) << 8);
		output.memresult += (((uint32_t)r_arr[2]) << 16);
		output.memresult += (((uint32_t)r_arr[3]) << 24);
		output.mem.address = (((uint16_t)(input.aluresult & 0x0000FFFF)) >> 2);
		bool exception_load = input.mem_op.memread && (
			((input.mem_op.memtype == MEM_H || input.mem_op.memtype == MEM_HU) && (input.aluresult & 0b1)) ||
			(input.mem_op.memtype == MEM_W && A_byte != 0)
		);
		output.mem.rd = !exception_load ? input.mem_op.memread : false;
		bool exception_store = input.mem_op.memwrite && (
			((input.mem_op.memtype == MEM_H || input.mem_op.memtype == MEM_HU) && (input.aluresult & 0b1)) ||
			(input.mem_op.memtype == MEM_W && A_byte != 0)
		);
		output.mem.wr = !exception_store ? input.mem_op.memwrite : false;
		output.mem_busy = input.mem.busy || (input.mem_op.memread && !exception_load);
		// output.mem.wrdata has m_arr_string
		//output.mem.wrdata = 0;
		char m_arr_full_string[33];
		for (size_t i = 0; i < 4; i++)
			for (size_t j = 0; j < 8; j++)
				m_arr_full_string[i * 8 + j] = m_arr_string[i][j];
		m_arr_full_string[32] = '\0';
		output.exc_load = exception_load;
		output.exc_store = exception_store;
		if (input.wb_op.src == WBS_ALU) {
			output.reg_write.data = input.aluresult;
		} else if (input.wb_op.src == WBS_MEM) {
			output.reg_write.data = output.memresult;
		} else {
			output.reg_write.data = (uint32_t) input.pc_old + 4;
		}

		// output results
		fprintf(
			file_input,
"# test case %zu\n\
# stall\n\
%d\n\
# flush\n\
%d\n\
# branchtype\n\
%s\n\
# memread\n\
%d\n\
# memwrite\n\
%d\n\
# memtype\n\
%s\n\
# wb rd\n\
%s\n\
# wb write\n\
%d\n\
# wb src\n\
%s\n\
# pc_old\n\
%s\n\
# pc_new\n\
%s\n\
# aluresult\n\
%s\n\
# wrdata\n\
%s\n\
# zero\n\
%d\n\
# mem busy\n\
%d\n\
# mem rddata\n\
%s\n",
			i,
			input.stall ? 1 : 0,
			input.flush ? 1 : 0,
			branchtypes[input.mem_op.branch],
			input.mem_op.memread ? 1 : 0,
			input.mem_op.memwrite ? 1 : 0,
			memtypes[input.mem_op.memtype],
			binary_string_n_bits(uint_to_binary_string(input.wb_op.rd & 0b11111, 1), 5),
			input.wb_op.write ? 1 : 0,
			wbtypes[input.wb_op.src],
			uint_to_binary_string(input.pc_old, 2),
			uint_to_binary_string(input.pc_new, 2),
			uint_to_binary_string(input.aluresult, 4),
			uint_to_binary_string(input.wrdata, 4),
			input.zero ? 1 : 0,
			input.mem.busy ? 1 : 0,
			uint_to_binary_string(input.mem.rddata, 4)
		);
		if (i < (size_t) n - 1)
			fprintf(file_input,"\n");

		fprintf(
			file_output,
"# test case %zu\n\
# mem_busy\n\
%d\n\
# reg_write write\n\
%d\n\
# reg_write reg\n\
%s\n\
# reg_write data\n\
%s\n\
# pc_new\n\
%s\n\
# pc_old\n\
%s\n\
# pcsrc\n\
%d\n\
# wb_op rd\n\
%s\n\
# wb_op write\n\
%d\n\
# wb_op src\n\
%s\n\
# aluresult\n\
%s\n\
# memresult\n\
%s\n\
# mem_out address\n\
%s\n\
# mem_out rd\n\
%d\n\
# mem_out wr\n\
%d\n\
# mem_out byteena\n\
%s\n\
# mem_out wrdata\n\
%s\n\
# exc_load\n\
%d\n\
# exc_store\n\
%d\n",
			i,
			output.mem_busy ? 1 : 0,
			output.reg_write.write ? 1 : 0,
			binary_string_n_bits(uint_to_binary_string(output.reg_write.reg & 0b11111, 1), 5),
			uint_to_binary_string(output.reg_write.data, 4),
			uint_to_binary_string(output.pc_new, 2),
			uint_to_binary_string(output.pc_old, 2),
			output.pcsrc ? 1 : 0,
			binary_string_n_bits(uint_to_binary_string(output.wb_op.rd & 0b11111, 1), 5),
			output.wb_op.write ? 1 : 0,
			wbtypes[output.wb_op.src],
			uint_to_binary_string(output.aluresult, 4),
			uint_to_binary_string(output.memresult, 4),
			binary_string_n_bits(uint_to_binary_string(output.mem.address, 2), 14),
			output.mem.rd ? 1 : 0,
			output.mem.wr ? 1 : 0,
			binary_string_n_bits(uint_to_binary_string(output.mem.byteena, 1), 4),
			m_arr_full_string,
			output.exc_load ? 1 : 0,
			output.exc_store ? 1 : 0
		);
		if (i < (size_t) n - 1)
			fprintf(file_output, "\n");
	}
	
	fclose(file_output);
	fclose(file_input);

	return EXIT_SUCCESS;
}
