library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.math_pkg.all;
use work.nes_controller_pkg.all;

entity nes_controller is
	generic (
		-- actual clock frequency of the `clk` signal given in Hz
		CLK_FREQ : integer := 50e6;
		-- desired clock frequency that should be generated for the `nes_clk` signal in Hz
		-- don't use frequencies higher than 1 MHz
		CLK_OUT_FREQ : integer := 1e6;
		-- timeout in `clk` cycles the controller should wait in between button readouts
		-- set this generic to the equivalent of 8 ms:
		--   x*T = 8 ms => x = 8 ms / T = 8 ms * f = 8e-3 * f
		REFRESH_TIMEOUT : integer := 8 / 1e3 * 50e6
	);
	port (
		-- global clock signal
		clk   : in std_logic;
		-- global reset signal (low-active, not internally synchronized)
		res_n : in std_logic;

		-- data from the shift register in the NES controller
		nes_data     : in std_logic;
		-- clock signal for the shift register in the NES controller
		nes_clk      : out std_logic;
		-- latch signal used to load the shift register in the NES controller with the current state of all buttons
		nes_latch    : out std_logic;
		-- record type defined in the `nes_controller_pkg` package
		button_state : out nes_buttons_t
	);
end entity;

-- a 3 process method fsm
architecture fsm_arch of nes_controller is
	type states_t is (WAIT_TIMEOUT, LATCH, LATCH_WAIT, CLK_LOW, SAMPLE, CLK_HIGH);
	-- initial state constant
	constant INITIAL  : states_t := WAIT_TIMEOUT;
	-- duration in `clk` cycles for transmission of a single bit of `nes_data`
	constant BIT_TIME : integer := CLK_FREQ / CLK_OUT_FREQ;

	signal state      : states_t := INITIAL;
	signal state_next : states_t;

	signal button_state_next : nes_buttons_t;

	-- necessary for `nes_data`
	signal shift_reg      : std_logic_vector(7 downto 0) := (others => '0');
	signal shift_reg_next : std_logic_vector(7 downto 0) := (others => '0');

	signal clk_cnt      : integer range 0 to max(BIT_TIME/2, REFRESH_TIMEOUT) := 0;
	signal clk_cnt_next : integer range 0 to max(BIT_TIME/2, REFRESH_TIMEOUT) := 0;
	signal bit_cnt      : integer range 0 to 7 := 0;
	signal bit_cnt_next : integer range 0 to 7 := 0;

begin

	sync : process(clk, res_n)
	begin
		-- flip flops with async reset
		if res_n = '0' then
			state <= INITIAL;

			button_state <= (others => '0');
			--button_state_next <= (others => '0');

			bit_cnt <= 0;
			clk_cnt <= 0;

			shift_reg <= (others => '0');
			--shift_reg_next <= (others => '0');
		elsif rising_edge(clk) then
			state <= state_next;
			button_state <= button_state_next;

			bit_cnt <= bit_cnt_next;
			clk_cnt <= clk_cnt_next;

			shift_reg <= shift_reg_next;
		end if;
	end process;

	next_logic : process(all)
	begin
		-- defaults
		state_next <= state;
		bit_cnt_next <= bit_cnt;
		clk_cnt_next <= clk_cnt;

		case state is
			when WAIT_TIMEOUT =>
				if clk_cnt = REFRESH_TIMEOUT then
					state_next <= LATCH;
					clk_cnt_next <= 0;
				else
					clk_cnt_next <= clk_cnt + 1;
				end if;

			when LATCH =>
				if clk_cnt = BIT_TIME / 2 then
					state_next <= LATCH_WAIT;
					clk_cnt_next <= 0;
				else
					clk_cnt_next <= clk_cnt + 1;
				end if;

			when LATCH_WAIT =>
				if clk_cnt = BIT_TIME / 2 then
					state_next <= CLK_LOW;
					clk_cnt_next <= 0;
				else
					clk_cnt_next <= clk_cnt + 1;
				end if;

			when CLK_LOW =>
				if clk_cnt = BIT_TIME / 2 - 1 then
					state_next <= SAMPLE;
					clk_cnt_next <= 0;
				else
					clk_cnt_next <= clk_cnt + 1;
				end if;

			when SAMPLE =>
				state_next <= CLK_HIGH;

			when CLK_HIGH =>
				if not(bit_cnt = 7) and clk_cnt = BIT_TIME / 2 then
					state_next <= CLK_LOW;
					bit_cnt_next <= bit_cnt + 1;
					clk_cnt_next <= 0;
				elsif bit_cnt = 7 and clk_cnt = BIT_TIME / 2 then
					state_next <= WAIT_TIMEOUT;
					bit_cnt_next <= 0;
					clk_cnt_next <= 0;
				else
					clk_cnt_next <= clk_cnt + 1;
				end if;

			-- should be unreachable
			when others =>
				null;
		end case;
	end process;

	out_logic : process(all)
	begin
		-- defaults
		shift_reg_next <= shift_reg;
		button_state_next <= button_state;

		case state is
			when WAIT_TIMEOUT =>
				nes_latch <= '0';
				nes_clk <= '1';
				button_state_next.btn_a <= shift_reg(7);
				button_state_next.btn_b <= shift_reg(6);
				button_state_next.btn_select <= shift_reg(5);
				button_state_next.btn_start <= shift_reg(4);
				button_state_next.btn_up <= shift_reg(3);
				button_state_next.btn_down <= shift_reg(2);
				button_state_next.btn_left <= shift_reg(1);
				button_state_next.btn_right <= shift_reg(0);

			when LATCH =>
				nes_latch <= '1';
				nes_clk <= '1';

			when LATCH_WAIT =>
				nes_latch <= '0';
				nes_clk <= '1';

			when CLK_LOW =>
				nes_latch <= '0';
				nes_clk <= '0';

			when SAMPLE =>
				nes_latch <= '0';
				nes_clk <= '0';
				shift_reg_next <= shift_reg(6 downto 0) & not nes_data;

			when CLK_HIGH =>
				nes_latch <= '0';
				nes_clk <= '1';

			-- should be unreachable
			when others =>
				null;
		end case;
	end process;

end architecture;
