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

use work.mem_pkg.all;
use work.cache_pkg.all;

entity cache is
	generic (
		SETS_LD   : natural          := SETS_LD;
		WAYS_LD   : natural          := WAYS_LD;
		ADDR_MASK : mem_address_type := (others => '1')
	);
	port (
		clk   : in std_logic;
		res_n : in std_logic;

		mem_out_cpu : in  mem_out_type;
		mem_in_cpu  : out mem_in_type;
		mem_out_mem : out mem_out_type;
		mem_in_mem  : in  mem_in_type
	);
end entity;

architecture bypass of cache is --bypass cache for exIII and testing
	alias cpu_to_cache : mem_out_type is mem_out_cpu; 
	alias cache_to_cpu : mem_in_type is mem_in_cpu;   
	alias cache_to_mem : mem_out_type is mem_out_mem; 
	alias mem_to_cache : mem_in_type is mem_in_mem;   
begin
	cache_to_mem <= cpu_to_cache; 
	cache_to_cpu <= mem_to_cache; 
end architecture;

architecture impl of cache is
	alias cpu_index : c_index_type is mem_out_cpu.address(SETS_LD-1 downto 0);
	alias cpu_tag   : c_tag_type is mem_out_cpu.address(ADDR_WIDTH-1 downto SETS_LD);

	type data_st_type is record
		wr       : std_logic;
		rd       : std_logic;
		index    : c_index_type;
		byteena  : mem_byteena_type;
		data_in  : mem_data_type;
		data_out : mem_data_type;
	end record;
	-- aux signal for data_st
	signal data : data_st_type;

	type mgmt_st_type is record
		index     : c_index_type;
		wr        : std_logic;
		rd        : std_logic;
		dirty_in  : std_logic;
		tag_in    : c_tag_type;
		valid_out : std_logic;
		dirty_out : std_logic;
		tag_out   : c_tag_type;
		hit_out   : std_logic;
	end record;
	-- aux signal for mgmt_st
	signal mgmt : mgmt_st_type;
	
	type fsm_state_type is (
		IDLE,
		READ_CACHE, READ_MEM_START, READ_MEM,
		WRITE_BACK_START, WRITE_BACK
	);
	type internal_type is record
		state   : fsm_state_type;
		tag_in  : c_tag_type;
		tag_out : c_tag_type;
		index   : c_index_type;
		dirty   : std_logic;
		valid   : std_logic;
	end record;

	constant INITIAL_INTERNAL : internal_type  := (
		state    => IDLE,
		tag_in   => (others => '0'),
		tag_out  => (others => '0'),
		index    => (others => '0'),
		dirty    => '0',
		valid    => '0'
	);

	signal internal, internal_next : internal_type;
	signal data_out_next, data_out : mem_data_type;
begin

	-- structural
	mgmt_st_inst : entity work.mgmt_st
	generic map (
		SETS_LD => SETS_LD,
		WAYS_LD => WAYS_LD
	)
	port map (
		clk   => clk,
		res_n => res_n,

		index => mgmt.index,
		wr    => mgmt.wr,
		rd    => mgmt.rd,

		valid_in   => '1',
		dirty_in   => mgmt.dirty_in,
		tag_in     => mgmt.tag_in,
		way_out    => open,
		valid_out  => mgmt.valid_out,
		dirty_out  => mgmt.dirty_out,
		tag_out    => mgmt.tag_out,
		hit_out    => mgmt.hit_out
	);

	data_st_inst : entity work.data_st
	generic map (
		SETS_LD => SETS_LD,
		WAYS_LD => WAYS_LD
	)
	port map (
		clk      => clk,

		we       => data.wr,
		rd       => data.rd,
		way      => (others => '0'),
		index    => data.index,
		byteena  => data.byteena,

		data_in  => data.data_in,
		data_out => data.data_out
	);

	-- concurrent
	data.byteena <= mem_out_cpu.byteena when internal.state = IDLE else (others => '1');
	data.index <= cpu_index when internal.state = IDLE else internal.index;
	mgmt.tag_in <= cpu_tag when internal.state = IDLE else internal.tag_in;
	mgmt.index <= cpu_index when internal.state = IDLE else internal.index;
	mgmt.rd <= '1' when internal.state = IDLE else '0';

	-- sequential
	sync : process(clk, res_n)
	begin
		if res_n = '0' then
			-- do the resets
			internal <= INITIAL_INTERNAL;
			data_out <= (others => '0');
		elsif rising_edge(clk) then
			-- do the saving
			internal <= internal_next;
			data_out <= data_out_next;
		end if;
	end process;
	
	async : process(all) 
	begin
		-- default assignments
		-- internal signals
		internal_next <= internal;
		data_out_next <= data_out;

		mgmt.dirty_in <= '0';
		mgmt.wr  <= '0';
		data.rd  <= '0';
		data.wr  <= '0';
		data.data_in  <= (others => '0');

		mem_out_mem.rd <= '0';
		mem_out_mem.wr <= '0';
		mem_out_mem.byteena <= (others => '1');
		mem_out_mem.wrdata  <= (others => '0');
		mem_out_mem.address <= (others => '0');

		mem_in_cpu.rddata <= data_out;
		mem_in_cpu.busy <= '0';

		case internal.state is
			when IDLE =>
				internal_next.index    <= cpu_index;
				internal_next.tag_in   <= cpu_tag;
				internal_next.tag_out  <= mgmt.tag_out;
				internal_next.dirty    <= mgmt.dirty_out;
				internal_next.valid    <= mgmt.valid_out;
				-- bypass if bit outside ADDR_MASK is set
				if or_reduce(mem_out_cpu.address and not(ADDR_MASK)) = '1' then
					-- bypass
					mem_out_mem.address <= mem_out_cpu.address;
					mem_out_mem.rd <= mem_out_cpu.rd;
					mem_out_mem.wr <= mem_out_cpu.wr;
					mem_out_mem.byteena <= mem_out_cpu.byteena;
					mem_out_mem.wrdata <= mem_out_cpu.wrdata;
					mem_in_cpu.busy <= mem_in_mem.busy;
					if mem_out_cpu.wr = '0' then
						mem_in_cpu.rddata <= mem_in_mem.rddata;
					end if;
				-- else do work as usual
				else
					-- don't bypass
					-- init read
					if mem_out_cpu.rd = '1' then
						mem_in_cpu.busy <= '1';
						if mgmt.hit_out = '1' and mgmt.valid_out = '1' then
							-- prepare read
							internal_next.state <= READ_CACHE;
							data.rd <= '1';
						else
							internal_next.state <= READ_MEM_START;
						end if;
					-- init write
					elsif mem_out_cpu.wr = '1' then
						if mgmt.hit_out = '1' and mgmt.valid_out = '1' then
							mgmt.wr <= '1';
							data.wr <= '1';
							data.data_in <= mem_out_cpu.wrdata;
							-- notes:
							--   byteena already forwarded
							--   index set concurrently
							mgmt.dirty_in <= '1';
						else
							-- if invalid or not a hit, bypass
							mem_out_mem <= mem_out_cpu;
						end if;
					end if;
				end if;

			when READ_CACHE =>
				mem_in_cpu.busy <= '1';
				-- next state
				internal_next.state <= IDLE;
				data_out_next <= data.data_out;
				-- deliver data
				-- mem_in_cpu.rddata set concurrently	
			
			when READ_MEM_START =>
				mem_in_cpu.busy <= '1';
				-- query dirty/valid bit to enter writeback
				if (internal.dirty and internal.valid) = '1' then
					data.rd <= '1';
					internal_next.state <= WRITE_BACK_START;
				-- not dirty or not invalid
				else
					internal_next.state <= READ_MEM;
					mem_out_mem.address <= internal.tag_in & internal.index;
					mem_out_mem.rd <= '1';
				end if;

			when READ_MEM =>
				mem_in_cpu.busy <= '1';
				if mem_in_mem.busy = '0' then
					-- next state
					internal_next.state <= READ_CACHE;
					-- perform read and write
					-- set data
					data.wr <= '1';
					data.data_in <= mem_in_mem.rddata;
					-- set mgmt
					-- index, mgmt.tag_in, mgmt.rd set concurrently
					mgmt.wr <= '1';
					mgmt.dirty_in <= '0';
					data.rd <= '1';
				end if;

			when WRITE_BACK_START =>
				-- next state
				internal_next.state <= WRITE_BACK;
				-- handle writeback
				mem_in_cpu.busy <= '1';
				mem_out_mem.wr <= '1';
				mem_out_mem.wrdata <= data.data_out;
				mem_out_mem.address <= internal.tag_out & internal.index;

			when WRITE_BACK =>
				-- next state
				internal_next.state <= READ_MEM;
				-- handle read after write back
				mem_in_cpu.busy <= '1';
				mem_out_mem.address <= internal.tag_in & internal.index;
				mem_out_mem.rd <= '1';
		
		end case;

	end process;
	
end architecture;
