#include "tb.h"

#include <fstream>
#include <iostream>

mem_io_state::mem_io_state(const tb_cli_args &args) : uart(args) {
	mtime = 0;
	mtimecmp[0] = 0;
	mtimecmp[1] = 0;
	exit_req = false;
	exit_code = 0;
	monitor_enabled = false;
	soft_irq_state = 0;
	irq_state = 0;
	for (int i = 0; i < N_RESERVATIONS; ++i) {
		reservation_valid[i] = false;
		reservation_addr[i] = 0;
	}
	poison_addr = -4u;
	mem = new uint8_t[MEM_SIZE];
	for (size_t i = 0; i < MEM_SIZE; ++i)
		mem[i] = 0;

	if (args.load_bin) {
		std::ifstream fd(args.bin_path, std::ios::binary | std::ios::ate);
		if (!fd){
			std::cerr << "Failed to open \"" << args.bin_path << "\"\n";
			exit(-1);
		}
		std::streamsize bin_size = fd.tellg();
		if (bin_size > MEM_SIZE) {
			std::cerr << "Binary file (" << bin_size << " bytes) is larger than memory (" << MEM_SIZE << " bytes)\n";
			exit(-1);
		}
		fd.seekg(0, std::ios::beg);
		fd.read((char*)mem, bin_size);
	}
}

bus_response tb_mem_access(tb_top &tb, mem_io_state &memio, bus_request req) {
	bus_response resp;

	// Global monitor. When monitor is not enabled, HEXOKAY is tied high
	if (memio.monitor_enabled) {
		if (req.excl) {
			// Always set reservation on read. Always clear reservation on
			// write. On successful write, clear others' matching reservations.
			if (req.write) {
				resp.exokay = memio.reservation_valid[req.reservation_id] &&
					memio.reservation_addr[req.reservation_id] == (req.addr & RESERVATION_ADDR_MASK);
				memio.reservation_valid[req.reservation_id] = false;
				if (resp.exokay) {
					for (int i = 0; i < N_RESERVATIONS; ++i) {
						if (i == req.reservation_id)
							continue;
						if (memio.reservation_addr[i] == (req.addr & RESERVATION_ADDR_MASK))
							memio.reservation_valid[i] = false;
					}
				}
			} else {
				resp.exokay = true;
				memio.reservation_valid[req.reservation_id] = true;
				memio.reservation_addr[req.reservation_id] = req.addr & RESERVATION_ADDR_MASK;
			}
		} else {
			resp.exokay = false;
			// Non-exclusive write still clears others' reservations
			if (req.write) {
				for (int i = 0; i < N_RESERVATIONS; ++i) {
					if (i == req.reservation_id)
						continue;
					if (memio.reservation_addr[i] == (req.addr & RESERVATION_ADDR_MASK))
						memio.reservation_valid[i] = false;
				}
			}
		}
	}


	if (req.write) {
		if (memio.monitor_enabled && req.excl && !resp.exokay) {
			// Failed exclusive write; do nothing
		} else if ((req.addr & -4u) == memio.poison_addr) {
			resp.err = true;
		} else if (req.addr >= MEM_BASE && req.addr <= MEM_BASE + MEM_SIZE - (1u << (int)req.size)) {
			unsigned int n_bytes = 1u << (int)req.size;
			// Note we are relying on hazard3's byte lane replication
			for (unsigned int i = 0; i < n_bytes; ++i) {
				memio.mem[req.addr + i - MEM_BASE] = req.wdata >> (8 * i) & 0xffu;
			}
		} else if (req.addr == IO_BASE + IO_PRINT_CHAR) {
			const uint8_t ch = (uint8_t)(req.wdata & 0xffu);
			fprintf(tb.logfile, "%c", (char)ch);
			memio.uart.write_data(0, ch);
		} else if (req.addr == IO_BASE + IO_PRINT_U32) {
			fprintf(tb.logfile, "%08x\n", req.wdata);
		} else if (req.addr == IO_BASE + IO_EXIT) {
			if (!memio.exit_req) {
				memio.exit_req = true;
				memio.exit_code = req.wdata;
			}
		} else if (req.addr == IO_BASE + IO_SET_SOFTIRQ) {
			memio.soft_irq_state |= req.wdata;
			tb.set_soft_irq(memio.soft_irq_state);
		} else if (req.addr == IO_BASE + IO_CLR_SOFTIRQ) {
			memio.soft_irq_state &= ~req.wdata;
			tb.set_soft_irq(memio.soft_irq_state);
		} else if (req.addr == IO_BASE + IO_GLOBMON_EN) {
			memio.monitor_enabled = req.wdata;
		} else if (req.addr == IO_BASE + IO_POISON_ADDR) {
			memio.poison_addr = req.wdata & -4u;
		} else if (req.addr == IO_BASE + IO_SET_IRQ) {
			memio.irq_state |= req.wdata;
			tb.set_irq(memio.irq_state);
		} else if (req.addr == IO_BASE + IO_CLR_IRQ) {
			memio.irq_state &= ~req.wdata;
			tb.set_irq(memio.irq_state);
		} else if (req.addr == IO_BASE + IO_MTIME) {
			memio.mtime = (memio.mtime & 0xffffffff00000000u) | req.wdata;
		} else if (req.addr == IO_BASE + IO_MTIMEH) {
			memio.mtime = (memio.mtime & 0x00000000ffffffffu) | ((uint64_t)req.wdata << 32);
		} else if (req.addr == IO_BASE + IO_MTIMECMP0) {
			memio.mtimecmp[0] = (memio.mtimecmp[0] & 0xffffffff00000000u) | req.wdata;
		} else if (req.addr == IO_BASE + IO_MTIMECMP0H) {
			memio.mtimecmp[0] = (memio.mtimecmp[0] & 0x00000000ffffffffu) | ((uint64_t)req.wdata << 32);
		} else if (req.addr == IO_BASE + IO_MTIMECMP1) {
			memio.mtimecmp[1] = (memio.mtimecmp[1] & 0xffffffff00000000u) | req.wdata;
		} else if (req.addr == IO_BASE + IO_MTIMECMP1H) {
			memio.mtimecmp[1] = (memio.mtimecmp[1] & 0x00000000ffffffffu) | ((uint64_t)req.wdata << 32);
		} else if (req.addr >= IO_BASE + IO_UART_BASE && req.addr < IO_BASE + IO_UART_BASE + IO_UART_N * IO_UART_STRIDE) {
			const uint32_t rel = req.addr - (IO_BASE + IO_UART_BASE);
			const uint32_t uart_idx = rel / IO_UART_STRIDE;
			const uint32_t reg_off = rel % IO_UART_STRIDE;
			if (reg_off == IO_UART_DATA) {
				const uint8_t ch = (uint8_t)(req.wdata & 0xffu);
				if (uart_idx == 0)
					fprintf(tb.logfile, "%c", (char)ch);
				memio.uart.write_data(uart_idx, ch);
			} else if (reg_off == IO_UART_CTRL) {
				memio.uart.write_ctrl(uart_idx, req.wdata);
			} else {
				resp.err = true;
			}
		} else {
			resp.err = true;
		}
	} else {
		if (req.addr == (memio.poison_addr & -4u)) {
			resp.err = true;
		} else if (req.addr >= MEM_BASE && req.addr <= MEM_BASE + MEM_SIZE - (1u << (int)req.size)) {
			req.addr &= ~0x3u;
			req.addr -= MEM_BASE;
			resp.rdata =
				(uint32_t)memio.mem[req.addr] |
				memio.mem[req.addr + 1] << 8 |
				memio.mem[req.addr + 2] << 16 |
				memio.mem[req.addr + 3] << 24;
		} else if (req.addr >= IO_BASE + IO_UART_BASE && req.addr < IO_BASE + IO_UART_BASE + IO_UART_N * IO_UART_STRIDE) {
			const uint32_t rel = req.addr - (IO_BASE + IO_UART_BASE);
			const uint32_t uart_idx = rel / IO_UART_STRIDE;
			const uint32_t reg_off = rel % IO_UART_STRIDE;
			if (reg_off == IO_UART_STATUS) {
				resp.rdata = memio.uart.read_status(uart_idx);
			} else if (reg_off == IO_UART_DATA) {
				resp.rdata = memio.uart.read_data(uart_idx);
			} else {
				resp.err = true;
			}
		} else if (req.addr == IO_BASE + IO_SET_SOFTIRQ || req.addr == IO_BASE + IO_CLR_SOFTIRQ) {
			resp.rdata = memio.soft_irq_state;
		} else if (req.addr == IO_BASE + IO_SET_IRQ || req.addr == IO_BASE + IO_CLR_IRQ) {
			resp.rdata = memio.irq_state;
		} else if (req.addr == IO_BASE + IO_MTIME) {
			resp.rdata = memio.mtime;
		} else if (req.addr == IO_BASE + IO_MTIMEH) {
			resp.rdata = memio.mtime >> 32;
		} else if (req.addr == IO_BASE + IO_MTIMECMP0) {
			resp.rdata = memio.mtimecmp[0];
		} else if (req.addr == IO_BASE + IO_MTIMECMP0H) {
			resp.rdata = memio.mtimecmp[0] >> 32;
		} else if (req.addr == IO_BASE + IO_MTIMECMP1) {
			resp.rdata = memio.mtimecmp[1];
		} else if (req.addr == IO_BASE + IO_MTIMECMP1H) {
			resp.rdata = memio.mtimecmp[1] >> 32;
		} else {
			resp.err = true;
		}
	}
	if (resp.err) {
		resp.exokay = false;
	}
	return resp;
}

void mem_io_state::step(tb_top &tb) {
	// Default update logic for mtime, mtimecmp
	++mtime;
	tb.set_timer_irq((uint8_t)((mtime >= mtimecmp[0]) | (mtime >= mtimecmp[1]) << 1));
	uart.step();
}
