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;
	signal start : std_logic := '0';
	
	file input_file : text;
	file output_ref_file : text;

	subtype addr is std_logic_vector(REG_BITS-1 downto 0);
	subtype data is std_logic_vector(DATA_WIDTH-1 downto 0);
	
	type data_arr is array(0 to 400) of MEM_DATA_TYPE;

	type input_t is
	record
		stall         : std_logic;
		flush         : std_logic;
		pc_in         : pc_type;
		op            : exec_op_type;
		memop_in      : mem_op_type;
		wbop_in       : wb_op_type;
		reg_write_mem : reg_write_type;
		reg_write_wr  : reg_write_type;
	end record;

	type output_t is
	record
		pc_old_out : pc_type;
		pc_new_out : pc_type;
		aluresult  : data_type;
		zero       : std_logic;
		wrdata     : data_type;
		exec_op    : exec_op_type;
		memop_out  : mem_op_type;
		wbop_out   : wb_op_type;
	end record;

	signal inp  : input_t := (
		'0',
		'0',
		(others => '0'),
		EXEC_NOP,
		MEM_NOP,
		WB_NOP,
		(write => '0', reg => (others => '0'), data => (others => '0')),
		(write => '0', reg => (others => '0'), data => (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.pc_in := bin_to_slv(l.all, inp.pc_in'LENGTH);

		l := get_next_valid_line(f);
		result.op.aluop := str_to_alu_op(l.all);

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

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

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

		l := get_next_valid_line(f);
		result.op.rs1 := bin_to_slv(l.all, inp.op.rs1'LENGTH);

		l := get_next_valid_line(f);
		result.op.rs2 := bin_to_slv(l.all, inp.op.rs2'LENGTH);

		l := get_next_valid_line(f);
		result.op.readdata1 := bin_to_slv(l.all, inp.op.readdata1'LENGTH);

		l := get_next_valid_line(f);
		result.op.readdata2 := bin_to_slv(l.all, inp.op.readdata2'LENGTH);

		l := get_next_valid_line(f);
		result.op.imm := bin_to_slv(l.all, inp.op.imm'LENGTH);

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

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

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

		l := get_next_valid_line(f);
		result.memop_in.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.reg_write_mem.write := str_to_sl(l(1));

		l := get_next_valid_line(f);
		result.reg_write_mem.reg := bin_to_slv(l.all, inp.reg_write_mem.reg'LENGTH);

		l := get_next_valid_line(f);
		result.reg_write_mem.data := bin_to_slv(l.all, inp.reg_write_mem.data'LENGTH);

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

		l := get_next_valid_line(f);
		result.reg_write_wr.reg := bin_to_slv(l.all, inp.reg_write_wr.reg'LENGTH);

		l := get_next_valid_line(f);
		result.reg_write_wr.data := bin_to_slv(l.all, inp.reg_write_wr.data'LENGTH);

		return result;
	end function;

	impure function read_next_output(file f : text) return output_t is
		variable l : line;
		variable result : output_t;
	begin
		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.pc_new_out := bin_to_slv(l.all, outp.pc_new_out'LENGTH);

		l := get_next_valid_line(f);
		result.aluresult := bin_to_slv(l.all, outp.aluresult'LENGTH);

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

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

		l := get_next_valid_line(f);
		result.exec_op.aluop := str_to_alu_op(l.all);

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

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

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

		l := get_next_valid_line(f);
		result.exec_op.rs1 := bin_to_slv(l.all, outp.exec_op.rs1'LENGTH);

		l := get_next_valid_line(f);
		result.exec_op.rs2 := bin_to_slv(l.all, outp.exec_op.rs2'LENGTH);

		l := get_next_valid_line(f);
		result.exec_op.readdata1 := bin_to_slv(l.all, outp.exec_op.readdata1'LENGTH);

		l := get_next_valid_line(f);
		result.exec_op.readdata2 := bin_to_slv(l.all, outp.exec_op.readdata2'LENGTH);

		l := get_next_valid_line(f);
		result.exec_op.imm := bin_to_slv(l.all, outp.exec_op.imm'LENGTH);

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

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

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

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

		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);

		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
			& "** pc_in="         & to_string(inp.pc_in) & lf
			& "** op.aluop="      & to_string(inp.op.aluop) & lf
			& "** op.alusrc1="    & to_string(inp.op.alusrc1) & lf
			& "** op.alusrc2="    & to_string(inp.op.alusrc2) & lf
			& "** op.alusrc3="    & to_string(inp.op.alusrc3) & lf
			& "** op.rs1="        & to_string(inp.op.rs1) & lf
			& "** op.rs1="        & to_string(inp.op.rs2) & lf
			& "** op.readdata1="  & to_string(inp.op.readdata1) & lf
			& "** op.readdata2="  & to_string(inp.op.readdata2) & lf
			& "** op.imm="        & to_string(inp.op.imm) & lf
			& "** memop_in.branch="     & to_string(inp.memop_in.branch) & lf
			& "** memop_in.memread="    & to_string(inp.memop_in.mem.memread) & lf
			& "** memop_in.memwrite="   & to_string(inp.memop_in.mem.memwrite) & lf
			& "** memop_in.memtype="    & to_string(inp.memop_in.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
			& "** reg_write_mem.write=" & to_string(inp.reg_write_mem.write) & lf
			& "** reg_write_mem.reg="   & to_string(inp.reg_write_mem.reg) & lf
			& "** reg_write_mem.data="  & to_string(inp.reg_write_mem.data) & lf
			& "** reg_write_wr.write="  & to_string(inp.reg_write_wr.write) & lf
			& "** reg_write_wr.reg="    & to_string(inp.reg_write_wr.reg) & lf
			& "** reg_write_wr.data="   & to_string(inp.reg_write_wr.data) & lf
			severity note;
		else
			report "FAILED: " & lf
			& "** stall="         & to_string(inp.stall) & lf
			& "** flush="         & to_string(inp.flush) & lf
			& "** pc_in="         & to_string(inp.pc_in) & lf
			& "** op.aluop="      & to_string(inp.op.aluop) & lf
			& "** op.alusrc1="    & to_string(inp.op.alusrc1) & lf
			& "** op.alusrc2="    & to_string(inp.op.alusrc2) & lf
			& "** op.alusrc3="    & to_string(inp.op.alusrc3) & lf
			& "** op.rs1="        & to_string(inp.op.rs1) & lf
			& "** op.rs1="        & to_string(inp.op.rs2) & lf
			& "** op.readdata1="  & to_string(inp.op.readdata1) & lf
			& "** op.readdata2="  & to_string(inp.op.readdata2) & lf
			& "** op.imm="        & to_string(inp.op.imm) & lf
			& "** memop_in.branch="     & to_string(inp.memop_in.branch) & lf
			& "** memop_in.memread="    & to_string(inp.memop_in.mem.memread) & lf
			& "** memop_in.memwrite="   & to_string(inp.memop_in.mem.memwrite) & lf
			& "** memop_in.memtype="    & to_string(inp.memop_in.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
			& "** reg_write_mem.write=" & to_string(inp.reg_write_mem.write) & lf
			& "** reg_write_mem.reg="   & to_string(inp.reg_write_mem.reg) & lf
			& "** reg_write_mem.data="  & to_string(inp.reg_write_mem.data) & lf
			& "** reg_write_wr.write="  & to_string(inp.reg_write_wr.write) & lf
			& "** reg_write_wr.reg="    & to_string(inp.reg_write_wr.reg) & lf
			& "** reg_write_wr.data="   & to_string(inp.reg_write_wr.data) & lf;
			report lf & "**     expected: pc_old_out="   & to_string(output_ref.pc_old_out) & ", actual: " & to_string(outp.pc_old_out) & lf
			& "**     expected: pc_new_out="             & to_string(output_ref.pc_new_out) & ", actual: " & to_string(outp.pc_new_out) & lf
			& "**     expected: aluresult="              & to_string(output_ref.aluresult) & ", actual: " & to_string(outp.aluresult) & lf
			& "**     expected: zero="                   & to_string(output_ref.zero) & ", actual: " & to_string(outp.zero) & lf
			& "**     expected: wrdata="                 & to_string(output_ref.wrdata) & ", actual: " & to_string(outp.wrdata) & lf
			& "**     expected: exec_op.aluop="          & to_string(output_ref.exec_op.aluop) & ", actual: " & to_string(outp.exec_op.aluop) & lf
			& "**     expected: exec_op.alusrc1="        & to_string(output_ref.exec_op.alusrc1) & ", actual: " & to_string(outp.exec_op.alusrc1) & lf
			& "**     expected: exec_op.alusrc2="        & to_string(output_ref.exec_op.alusrc2) & ", actual: " & to_string(outp.exec_op.alusrc2) & lf
			& "**     expected: exec_op.alusrc3="        & to_string(output_ref.exec_op.alusrc3) & ", actual: " & to_string(outp.exec_op.alusrc3) & lf
			& "**     expected: exec_op.rs1="            & to_string(output_ref.exec_op.rs1) & ", actual: " & to_string(outp.exec_op.rs1) & lf
			& "**     expected: exec_op.rs2="            & to_string(output_ref.exec_op.rs2) & ", actual: " & to_string(outp.exec_op.rs2) & lf
			& "**     expected: exec_op.readdata1="      & to_string(output_ref.exec_op.readdata1) & ", actual: " & to_string(outp.exec_op.readdata1) & lf
			& "**     expected: exec_op.readdata2="      & to_string(output_ref.exec_op.readdata2) & ", actual: " & to_string(outp.exec_op.readdata2) & lf
			& "**     expected: exec_op.imm="            & to_string(output_ref.exec_op.imm) & ", actual: " & to_string(outp.exec_op.imm) & lf
			& "**     expected: memop_out.branch="       & to_string(output_ref.memop_out.branch) & ", actual: " & to_string(outp.memop_out.branch) & lf
			& "**     expected: memop_out.mem.memread="  & to_string(output_ref.memop_out.mem.memread) & ", actual: " & to_string(outp.memop_out.mem.memread) & lf
			& "**     expected: memop_out.mem.memwrite=" & to_string(output_ref.memop_out.mem.memwrite) & ", actual: " & to_string(outp.memop_out.mem.memwrite) & lf
			& "**     expected: memop_out.mem.memtype="  & to_string(output_ref.memop_out.mem.memtype) & ", actual: " & to_string(outp.memop_out.mem.memtype) & 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
			severity error;
		end if;
	end procedure;

begin

	exec_inst : entity work.exec
	port map (
		clk   => clk,
		res_n => res_n,
		stall => inp.stall,
		flush => inp.flush,

		-- from DEC
		op    => inp.op,
		pc_in => inp.pc_in,

		-- to MEM
		pc_old_out => outp.pc_old_out,
		pc_new_out => outp.pc_new_out,
		aluresult  => outp.aluresult,
		wrdata     => outp.wrdata,
		zero       => outp.zero,

		memop_in   => inp.memop_in,
		memop_out  => outp.memop_out,
		wbop_in    => inp.wbop_in,
		wbop_out   => outp.wbop_out,

		-- FWD
		exec_op       => outp.exec_op,
		reg_write_mem => inp.reg_write_mem,
		reg_write_wr  => inp.reg_write_wr
	);

	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);
		
		wait until falling_edge(clk);
		wait for CLK_PERIOD/10;

		while not endfile(input_file) loop
			inp <= read_next_input(input_file);
			start <= '1';
			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';
		wait until start = '1';
		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;
