#include <iostream>
#include <fstream>
#include <cstdint>
#include <string>
#include <stdio.h>

#include "tb.h"
#include "tb_cli.h"
#include "tb_jtag.h"

// Device-under-test model generated by CXXRTL:
#include "dut.cpp"
#include <cxxrtl/cxxrtl_vcd.h>
#ifdef CXXRTL_DEBUG_AGENT
#include <cxxrtl/cxxrtl_server.h>
using namespace cxxrtl::time_literals;
#endif

#ifdef CXXRTL_DEBUG_AGENT
#define STEP() agent.step();
#else
#define STEP() top.step();
#endif

class tb_cxxrtl_top: public tb_top {
	cxxrtl_design::p_tb top;
	cxxrtl::vcd_writer vcd;
	std::ofstream waves_fd;

	// Loop-carried address-phase requests
	bus_request req_i;
	bus_request req_d;
	bool req_i_vld = false;
	bool req_d_vld = false;
	int64_t cycle = 0;

#ifdef CXXRTL_DEBUG_AGENT
	cxxrtl::agent<cxxrtl_design::p_tb> agent(cxxrtl::spool("spool.bin"), top);
#endif

public:
	tb_cxxrtl_top(const tb_cli_args &args);
	void step(const tb_cli_args &args, mem_io_state &memio) override;

	void set_trst_n(bool trst_n)     override {top.p_trst__n.set<bool>(trst_n);}
	void set_tck(bool tck)           override {top.p_tck.set<bool>(tck);}
	void set_tdi(bool tdi)           override {top.p_tdi.set<bool>(tdi);}
	void set_tms(bool tms)           override {top.p_tms.set<bool>(tms);}
	bool get_tdo()                   override {return top.p_tdo.get<bool>();}
	void set_irq(uint32_t mask)      override {top.p_irq.set<uint32_t>(mask);}
	void set_soft_irq(uint8_t mask)  override {top.p_soft__irq.set<uint8_t>(mask);}
	void set_timer_irq(uint8_t mask) override {top.p_timer__irq.set<uint8_t>(mask);}
};

tb_cxxrtl_top::tb_cxxrtl_top(const tb_cli_args &args): tb_top {args} {
	req_i_vld = false;
	req_d_vld = false;
	req_i.reservation_id = 0;
	req_d.reservation_id = 1;

	if (args.dump_waves) {
		waves_fd.open(args.waves_path);
		cxxrtl::debug_items all_debug_items;
		top.debug_info(&all_debug_items, /*scopes=*/nullptr, "");
		vcd.timescale(1, "us");
		vcd.add(all_debug_items);
	}
#ifdef CXXRTL_DEBUG_AGENT
	if (args.run_agent) {
		std::cerr << "Waiting for debugger on " << agent.start_debugging() << std::endl;
	}
#endif

	// Set bus interfaces to generate good IDLE responses at first
	top.p_i__hready.set<bool>(true);
	top.p_d__hready.set<bool>(true);

	// Reset + initial clock pulse
	STEP();
	top.p_clk.set<bool>(true);
	top.p_tck.set<bool>(true);
	STEP();
	top.p_clk.set<bool>(false);
	top.p_tck.set<bool>(false);
	top.p_trst__n.set<bool>(true);
	top.p_rst__n.set<bool>(true);
	STEP();
	STEP(); // workaround for github.com/YosysHQ/yosys/issues/2780
}

void tb_cxxrtl_top::step(const tb_cli_args &args, mem_io_state &memio) {
	top.p_clk.set<bool>(false);
	STEP();
	if (args.dump_waves)
		vcd.sample(cycle * 2);
#ifdef CXXRTL_DEBUG_AGENT
	agent.advance(1_us);
#endif
	top.p_clk.set<bool>(true);
	STEP();
	STEP(); // workaround for github.com/YosysHQ/yosys/issues/2780

	// The two bus ports are handled identically. This enables swapping out of
	// various `tb.v` hardware integration files containing:
	//
	// - A single, dual-ported processor (instruction fetch, load/store ports)
	// - A single, single-ported processor (instruction fetch + load/store muxed internally)
	// - A pair of single-ported processors, for dual-core debug tests

	if (top.p_d__hready.get<bool>()) {
		// Clear bus error by default
		top.p_d__hresp.set<bool>(false);

		// Handle current data phase
		req_d.wdata = top.p_d__hwdata.get<uint32_t>();
		bus_response resp;
		if (req_d_vld)
			resp = mem_callback_d(*this, memio, req_d);
		else
			resp.exokay = !memio.monitor_enabled;
		if (resp.err) {
			// Phase 1 of error response
			top.p_d__hready.set<bool>(false);
			top.p_d__hresp.set<bool>(true);
		}
		if (req_d_vld && !req_d.write) {
			top.p_d__hrdata.set<uint32_t>(resp.rdata);
		} else {
			top.p_d__hrdata.set<uint32_t>(rand());
		}
		top.p_d__hexokay.set<bool>(resp.exokay);
	} else {
		// hready=0. Currently this only happens when we're in the first
		// phase of an error response, so go to phase 2.
		top.p_d__hready.set<bool>(true);
	}

	req_d_vld = false;
	if (top.p_d__hready.get<bool>()) {
		// Progress current address phase to data phase
		req_d_vld = top.p_d__htrans.get<uint8_t>() >> 1;
		req_d.write = top.p_d__hwrite.get<bool>();
		req_d.size = (bus_size_t)top.p_d__hsize.get<uint8_t>();
		req_d.addr = top.p_d__haddr.get<uint32_t>();
		req_d.excl = top.p_d__hexcl.get<bool>();
	}

	if (top.p_i__hready.get<bool>()) {
		top.p_i__hresp.set<bool>(false);

		req_i.wdata = top.p_i__hwdata.get<uint32_t>();
		bus_response resp;
		if (req_i_vld)
			resp = mem_callback_i(*this, memio, req_i);
		else
			resp.exokay = !memio.monitor_enabled;
		if (resp.err) {
			// Phase 1 of error response
			top.p_i__hready.set<bool>(false);
			top.p_i__hresp.set<bool>(true);
		}
		if (req_i_vld && !req_i.write) {
			top.p_i__hrdata.set<uint32_t>(resp.rdata);
		} else {
			top.p_i__hrdata.set<uint32_t>(rand());
		}
		top.p_i__hexokay.set<bool>(resp.exokay);
	} else {
		// hready=0. Currently this only happens when we're in the first
		// phase of an error response, so go to phase 2.
		top.p_i__hready.set<bool>(true);
	}

	req_i_vld = false;
	if (top.p_i__hready.get<bool>()) {
		// Progress current address phase to data phase
		req_i_vld = top.p_i__htrans.get<uint8_t>() >> 1;
		req_i.write = top.p_i__hwrite.get<bool>();
		req_i.size = (bus_size_t)top.p_i__hsize.get<uint8_t>();
		req_i.addr = top.p_i__haddr.get<uint32_t>();
		req_i.excl = top.p_i__hexcl.get<bool>();
	}

	if (args.dump_waves) {
		// The extra step() is just here to get the bus responses to line up nicely
		// in the VCD (hopefully is a quick update)
		STEP();
		vcd.sample(cycle * 2 + 1);
#ifdef CXXRTL_DEBUG_AGENT
		agent.advance(1_us);
#endif
		waves_fd << vcd.buffer;
		vcd.buffer.clear();
	}

	++cycle;
}

int main(int argc, char **argv) {
	tb_cli_args args;
	tb_parse_args(argc, argv, args);

	tb_jtag_state jtag(args);
	mem_io_state memio(args);
	tb_cxxrtl_top tb(args);

	bool timed_out = false;
	for (int64_t cycle = 0; cycle < args.max_cycles || args.max_cycles == 0; ++cycle) {
		bool jtag_exit_cmd = jtag.step(tb);
		memio.step(tb);
		tb.step(args, memio);

		if (memio.exit_req) {
			fprintf(tb.logfile, "CPU requested halt. Exit code %d\n", memio.exit_code);
			fprintf(tb.logfile, "Ran for " I64_FMT " cycles\n", cycle + 1);
			break;
		}
		if (cycle + 1 == args.max_cycles) {
			fprintf(tb.logfile, "Max cycles reached\n");
			timed_out = true;
		}
		if (jtag_exit_cmd)
			break;
	}

	jtag.close();

	for (auto r : args.dump_ranges) {
		fprintf(tb.logfile, "Dumping memory from %08x to %08x:\n", r.first, r.second);
		for (int i = 0; i < r.second - r.first; ++i)
			fprintf(tb.logfile, "%02x%c", memio.mem[r.first + i - MEM_BASE], i % 16 == 15 ? '\n' : ' ');
		fprintf(tb.logfile, "\n");
	}

	if (args.sig_path != "") {
		FILE *sigfile = fopen(args.sig_path.c_str(), "wb");
		for (auto r : args.dump_ranges) {
			for (uint32_t i = 0; i < r.second - r.first; i += 4) {
				fprintf(
					sigfile,
					"%02x%02x%02x%02x\n",
					memio.mem[r.first + i + 3 - MEM_BASE],
					memio.mem[r.first + i + 2 - MEM_BASE],
					memio.mem[r.first + i + 1 - MEM_BASE],
					memio.mem[r.first + i + 0 - MEM_BASE]
				);
			}
		}
		fclose(sigfile);
	}

	if (args.propagate_return_code && timed_out) {
		return -1;
	} else if (args.propagate_return_code && memio.exit_req) {
		return memio.exit_code;
	} else {
		return 0;
	}
}
