library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.std_logic_textio.all;

library std; -- for Printing
use std.textio.all;

use work.mem_pkg.all;
use work.op_pkg.all;
use work.core_pkg.all;
use work.tb_util_pkg.all;

entity tb is
end entity;

architecture bench of tb is
	constant CLK_PERIOD : time := 10 ns;

	signal clk : std_logic;
	signal res_n : std_logic := '0';
	signal stop : boolean := false;
	
	file input_file : text;
	file output_ref_file : text;

	type input_t is
	record
		stall        : std_logic;
		flush        : std_logic;
		mem_op       : mem_op_type;
		wbop_in      : wb_op_type;
		pc_new_in    : pc_type;
		pc_old_in    : pc_type;
		aluresult_in : data_type;
		wrdata       : data_type;
		zero         : std_logic;
		mem_in       : mem_in_type;
	end record;

	type output_t is
	record
		mem_busy      : std_logic;
		reg_write     : reg_write_type;
		pc_new_out    : pc_type;
		pcsrc         : std_logic;
		wbop_out      : wb_op_type;
		pc_old_out    : pc_type;
		aluresult_out : data_type;
		memresult     : data_type;
		mem_out       : mem_out_type;
		exc_load      : std_logic;
		exc_store     : std_logic;
	end record;

	signal inp  : input_t := (
		'0',
		'0',
		MEM_NOP,
		WB_NOP,
		(others => '0'),
		(others => '0'),
		(others => '0'),
		(others => '0'),
		'0',
		(busy => '0', rddata => (others => '0'))
	);

	signal outp : output_t;

	impure function read_next_input(file f : text) return input_t is
		variable l : line;
		variable result : input_t;
	begin
		l := get_next_valid_line(f);
		result.stall := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.flush := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.mem_op.branch := str_to_branch(l.all);

		l := get_next_valid_line(f);
		result.mem_op.mem.memread := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.mem_op.mem.memwrite := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.mem_op.mem.memtype := str_to_mem_op(l.all);

		l := get_next_valid_line(f);
		result.wbop_in.rd := bin_to_slv(l.all, inp.wbop_in.rd'length);

		l := get_next_valid_line(f);
		result.wbop_in.write := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.wbop_in.src := str_to_wbs_op(l.all);

		l := get_next_valid_line(f);
		result.pc_old_in := bin_to_slv(l.all, inp.pc_old_in'length);

		l := get_next_valid_line(f);
		result.pc_new_in := bin_to_slv(l.all, inp.pc_new_in'length);

		l := get_next_valid_line(f);
		result.aluresult_in := bin_to_slv(l.all, inp.aluresult_in'length);

		l := get_next_valid_line(f);
		result.wrdata := bin_to_slv(l.all, inp.wrdata'length);

		l := get_next_valid_line(f);
		result.zero := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.mem_in.busy := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.mem_in.rddata := bin_to_slv(l.all, inp.mem_in.rddata'length);

		return result;
	end function;

	impure function read_next_output(file f : text) return output_t is
		variable l : line;
		variable result : output_t;
		variable temp : std_logic_vector(15 downto 0);
	begin
		l := get_next_valid_line(f);
		result.mem_busy := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.reg_write.write := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.reg_write.reg := bin_to_slv(l.all, outp.reg_write.reg'length);

		l := get_next_valid_line(f);
		result.reg_write.data := bin_to_slv(l.all, outp.reg_write.data'length);

		l := get_next_valid_line(f);
		result.pc_new_out := bin_to_slv(l.all, outp.pc_new_out'length);

		l := get_next_valid_line(f);
		result.pc_old_out := bin_to_slv(l.all, outp.pc_old_out'length);

		l := get_next_valid_line(f);
		result.pcsrc := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.wbop_out.rd := bin_to_slv(l.all, outp.wbop_out.rd'length);

		l := get_next_valid_line(f);
		result.wbop_out.write := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.wbop_out.src := str_to_wbs_op(l.all);

		l := get_next_valid_line(f);
		result.aluresult_out := bin_to_slv(l.all, outp.aluresult_out'length);

		l := get_next_valid_line(f);
		result.memresult := bin_to_slv(l.all, outp.memresult'length);

		l := get_next_valid_line(f);
		result.mem_out.address := bin_to_slv(l.all, outp.mem_out.address'length);

		l := get_next_valid_line(f);
		result.mem_out.rd := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.mem_out.wr := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.mem_out.byteena := bin_to_slv(l.all, outp.mem_out.byteena'length);

		l := get_next_valid_line(f);
		result.mem_out.wrdata := bin_to_slv(l.all, outp.mem_out.wrdata'length);

		l := get_next_valid_line(f);
		result.exc_load := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.exc_store := str_to_sl(l(1));

		return result;
	end function;

	procedure check_output(output_ref : output_t) is
		variable passed : boolean;
	begin
		passed := (outp = output_ref);

		if passed then
			report "PASSED" & lf
			& "** stall="             & to_string(inp.stall) & lf
			& "** flush="             & to_string(inp.flush) & lf
			& "** mem_op.branch="     & to_string(inp.mem_op.branch) & lf
			& "** mem_op.memread="    & to_string(inp.mem_op.mem.memread) & lf
			& "** mem_op.memwrite="   & to_string(inp.mem_op.mem.memwrite) & lf
			& "** mem_op.memtype="    & to_string(inp.mem_op.mem.memtype) & lf
			& "** wbop_in.rd="        & to_string(inp.wbop_in.rd) & lf
			& "** wbop_in.write="     & to_string(inp.wbop_in.write) & lf
			& "** wbop_in.src="       & to_string(inp.wbop_in.src) & lf
			& "** pc_old_in="         & to_string(inp.pc_old_in) & lf
			& "** pc_new_in="         & to_string(inp.pc_new_in) & lf
			& "** aluresult="         & to_string(inp.pc_new_in) & lf
			& "** wrdata="            & to_string(inp.pc_new_in) & lf
			& "** zero="              & to_string(inp.pc_new_in) & lf
			& "** mem.busy="          & to_string(inp.pc_new_in) & lf
			& "** mem.rddata="        & to_string(inp.pc_new_in) & lf
			severity note;
		else
			report "FAILED" & lf
			& "** stall="             & to_string(inp.stall) & lf
			& "** flush="             & to_string(inp.flush) & lf
			& "** mem_op.branch="     & to_string(inp.mem_op.branch) & lf
			& "** mem_op.memread="    & to_string(inp.mem_op.mem.memread) & lf
			& "** mem_op.memwrite="   & to_string(inp.mem_op.mem.memwrite) & lf
			& "** mem_op.memtype="    & to_string(inp.mem_op.mem.memtype) & lf
			& "** wbop_in.rd="        & to_string(inp.wbop_in.rd) & lf
			& "** wbop_in.write="     & to_string(inp.wbop_in.write) & lf
			& "** wbop_in.src="       & to_string(inp.wbop_in.src) & lf
			& "** pc_old_in="         & to_string(inp.pc_old_in) & lf
			& "** pc_new_in="         & to_string(inp.pc_new_in) & lf
			& "** aluresult_in="      & to_string(inp.aluresult_in) & lf
			& "** wrdata="            & to_string(inp.wrdata) & lf
			& "** zero="              & to_string(inp.zero) & lf
			& "** mem_in.busy="       & to_string(inp.mem_in.busy) & lf
			& "** mem_in.rddata="     & to_string(inp.mem_in.rddata) & lf;
			report lf
			& "**     expected: mem_busy="        & to_string(output_ref.mem_busy)        & ", actual: " & to_string(outp.mem_busy) & lf
			& "**     expected: reg_write.write=" & to_string(output_ref.reg_write.write) & ", actual: " & to_string(outp.reg_write.write) & lf
			& "**     expected: reg_write.reg="   & to_string(output_ref.reg_write.reg)   & ", actual: " & to_string(outp.reg_write.reg) & lf
			& "**     expected: reg_write.data="  & to_string(output_ref.reg_write.data)  & ", actual: " & to_string(outp.reg_write.data) & lf
			& "**     expected: pc_new_out="      & to_string(output_ref.pc_new_out)      & ", actual: " & to_string(outp.pc_new_out) & lf
			& "**     expected: pc_old_out="      & to_string(output_ref.pc_old_out)      & ", actual: " & to_string(outp.pc_old_out) & lf
			& "**     expected: pcsrc="           & to_string(output_ref.pcsrc)           & ", actual: " & to_string(outp.pcsrc) & lf
			& "**     expected: wbop_out.rd="     & to_string(output_ref.wbop_out.rd)     & ", actual: " & to_string(outp.wbop_out.rd) & lf
			& "**     expected: wbop_out.write="  & to_string(output_ref.wbop_out.write)  & ", actual: " & to_string(outp.wbop_out.write) & lf
			& "**     expected: wbop_out.src="    & to_string(output_ref.wbop_out.src)    & ", actual: " & to_string(outp.wbop_out.src) & lf
			& "**     expected: aluresult_out="   & to_string(output_ref.aluresult_out)   & ", actual: " & to_string(outp.aluresult_out) & lf
			& "**     expected: memresult="       & to_string(output_ref.memresult)       & ", actual: " & to_string(outp.memresult) & lf
			& "**     expected: mem_out.address=" & to_string(output_ref.mem_out.address) & ", actual: " & to_string(outp.mem_out.address) & lf
			& "**     expected: mem_out.rd="      & to_string(output_ref.mem_out.rd)      & ", actual: " & to_string(outp.mem_out.rd) & lf
			& "**     expected: mem_out.wr="      & to_string(output_ref.mem_out.wr)      & ", actual: " & to_string(outp.mem_out.wr) & lf
			& "**     expected: mem_out.byteena=" & to_string(output_ref.mem_out.byteena) & ", actual: " & to_string(outp.mem_out.byteena) & lf
			& "**     expected: mem_out.wrdata="  & to_string(output_ref.mem_out.wrdata)  & ", actual: " & to_string(outp.mem_out.wrdata) & lf
			& "**     expected: exc_load="        & to_string(output_ref.exc_load)        & ", actual: " & to_string(outp.exc_load) & lf
			& "**     expected: exc_store="       & to_string(output_ref.exc_store)       & ", actual: " & to_string(outp.exc_store) & lf
			severity error;
		end if;
	end procedure;
begin

	mem_inst : entity work.mem
	port map (
		clk   => clk,
		res_n => res_n,
		stall => inp.stall,
		flush => inp.flush,

		-- to Ctrl
		mem_busy => outp.mem_busy,

		-- from EXEC
		mem_op       => inp.mem_op,
		wbop_in      => inp.wbop_in,
		pc_new_in    => inp.pc_new_in,
		pc_old_in    => inp.pc_old_in,
		aluresult_in => inp.aluresult_in,
		wrdata       => inp.wrdata,
		zero         => inp.zero,

		-- to EXEC (forwarding)
		reg_write => outp.reg_write,

		-- to FETCH
		pc_new_out => outp.pc_new_out,
		pcsrc      => outp.pcsrc,

		-- to WB
		wbop_out      => outp.wbop_out,
		pc_old_out    => outp.pc_old_out,
		aluresult_out => outp.aluresult_out,
		memresult     => outp.memresult,

		-- memory controller interface
		mem_out => outp.mem_out,
		mem_in  => inp.mem_in,

		-- exceptions
		exc_load  => outp.exc_load,
		exc_store => outp.exc_store
	);

	stimulus : process
		variable fstatus: file_open_status;
	begin
		res_n <= '0';
		wait until rising_edge(clk);
		res_n <= '1';
		
		file_open(fstatus, input_file, "testdata/input.txt", READ_MODE);
		
		timeout(1, CLK_PERIOD);

		while not endfile(input_file) loop
			inp <= read_next_input(input_file);
			timeout(1, CLK_PERIOD);
		end loop;
		
		wait;
	end process;

	output_checker : process
		variable fstatus: file_open_status;
		variable output_ref : output_t;
	begin
		file_open(fstatus, output_ref_file, "testdata/output.txt", READ_MODE);

		wait until res_n = '1';
		timeout(1, CLK_PERIOD);

		while not endfile(output_ref_file) loop
			output_ref := read_next_output(output_ref_file);

			wait until falling_edge(clk);
			check_output(output_ref);
			wait until rising_edge(clk);
		end loop;
		stop <= true;
		
		wait;
	end process;

	generate_clk : process
	begin
		clk_generate(clk, CLK_PERIOD, stop);
		wait;
	end process;

end architecture;
