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

entity fancy_spi_master is
	generic(
		TX_WIDTH_MAX      : natural := 8; -- In bits
		RX_WIDTH_MAX      : natural := 8; -- In bits
		RX_IGNORE_MAX     : natural := 8; -- In bits
		SPI_CPOL          : std_logic;  -- SPI clock polarity. '0': Idle low, '1': Idle high
		SPI_CPHA          : std_logic;  -- SPI clock phase. '0': Sample on first edge, '1': Sample on second edge
		CKDIV             : natural := 8; -- Clock divider for SPI clock, minimum 2
		CS_INACTIVE_DELAY : natural := 1 -- In system clock cycles
	);
	port(
		-- System Signals
		clk       : in  std_logic;      -- System clock
		rst       : in  std_logic;      -- Asynchronous reset
		clr       : in  std_logic;      -- Synchronous reset

		-- TX
		tx_width  : in  integer range 0 to TX_WIDTH_MAX; -- Width of the next word in bits, sampled when asserting tx_next and at the beginning
		tx_enable : in  std_logic;      -- Enable transmission, taking this low for at least one clock cycle will trigger a CS inactive sequence
		tx_data   : in  std_logic_vector(TX_WIDTH_MAX - 1 downto 0); -- Data to be transmitted, MSB first, aligned to the left when tx_width /= TX_WIDTH_MAX
		tx_next   : out std_logic;      -- Strobe to request new data or to deassert tx_enable

		-- RX
		rx_width  : in  integer range 0 to RX_WIDTH_MAX; -- Width of the next word in bits, sampled when asserting rx_valid and at the beginning
		rx_valid  : out std_logic;      -- rx_data is valid, a new word has been received.
		rx_data   : out std_logic_vector(RX_WIDTH_MAX - 1 downto 0); -- Received data, MSB first, aligned to the right when rx_width /= RX_WIDTH_MAX
		rx_ignore : in  integer range 0 to RX_IGNORE_MAX; -- Bits to be ignored at the beginning of the transaction

		-- SPI HW Signals
		spi_clk   : out std_logic;      -- SPI clock output
		spi_cs_n  : out std_logic;      -- SPI chip select (low active)
		spi_mosi  : out std_logic;      -- SPI MOSI output
		spi_miso  : in  std_logic       -- SPI MISO input
	);
end entity fancy_spi_master;

architecture RTL of fancy_spi_master is
	-- CKDIV
	signal spi_clk_en      : std_logic;
	signal spi_clk_en_last : std_logic;
	signal spi_clk_i       : std_logic;
	signal ckdiv_cnt       : integer range 0 to CKDIV;
	signal spi_clk_event   : boolean;

	-- MAIN FSM
	type state_t is (IDLE, ACTIVE, STOP);
	signal state          : state_t;
	signal tx_width_i     : integer range 0 to TX_WIDTH_MAX;
	signal shiftreg_out   : std_logic_vector(TX_WIDTH_MAX - 1 downto 0);
	signal shiftreg_in    : std_logic_vector(RX_WIDTH_MAX - 1 downto 0);
	signal bit_cnt_tx     : integer range 0 to CS_INACTIVE_DELAY - 1;
	signal bit_cnt_rx     : integer range 0 to RX_IGNORE_MAX; -- TODO: or RX_WIDTH_MAX or CS_INACTIVE_DELAY if bigger
	signal rx_past_ignore : boolean;
	signal rx_width_i     : integer range 0 to RX_WIDTH_MAX;
	signal active_cycles  : boolean;

begin
	clk_gen : process(rst, clk) is
		procedure default_state is
		begin
			null;
		end procedure default_state;
		procedure reset_state is
		begin
			default_state;
			ckdiv_cnt       <= 0;
			spi_clk_i       <= SPI_CPOL;
			spi_clk_en_last <= '0';
		end procedure reset_state;
	begin
		if (rst = '1') then
			reset_state;
		elsif (rising_edge(clk)) then
			default_state;
			if (clr = '1') then
				reset_state;
			else
				if (ckdiv_cnt = 0) then
					spi_clk_en_last <= spi_clk_en;
					if (spi_clk_en_last /= spi_clk_en) then
						ckdiv_cnt <= CKDIV / 4;
					else
						if (spi_clk_i /= SPI_CPOL or spi_clk_en = '1') then
							spi_clk_i <= not spi_clk_i;
						end if;
						if (spi_clk_en = '1' or spi_clk_i = SPI_CPOL) then
							ckdiv_cnt <= CKDIV / 4;
						end if;
					end if;
				else
					ckdiv_cnt <= ckdiv_cnt - 1;
				end if;
			end if;
		end if;
	end process clk_gen;
	spi_clk       <= spi_clk_i;
	spi_clk_event <= true when ckdiv_cnt = 0 and (spi_clk_en_last = spi_clk_en) else false;

	fancy_spi : process(rst, clk) is
		variable tx_break     : boolean;
		variable tx_word_done : boolean;
		variable rx_word_done : boolean;

		procedure default_state is
		begin
			tx_next      <= '0';
			tx_word_done := false;
			rx_word_done := false;
			rx_valid     <= '0';
		end procedure default_state;
		procedure reset_state is
		begin
			default_state;
			state          <= IDLE;
			tx_break       := true;
			bit_cnt_tx     <= 0;
			shiftreg_out   <= (others => '0');
			shiftreg_in    <= (others => '0');
			spi_cs_n       <= '1';
			rx_past_ignore <= false;
			rx_width_i     <= 0;
			tx_width_i     <= 0;
			bit_cnt_rx     <= 0;
			active_cycles  <= false;
			spi_clk_en     <= '0';
		end procedure reset_state;

		procedure load_new_tx_word is
		begin
			tx_width_i   <= tx_width;
			shiftreg_out <= tx_data;
			bit_cnt_tx   <= 0;
			tx_next      <= '1';
		end procedure load_new_tx_word;

		procedure shift_out is
		begin
			shiftreg_out <= shiftreg_out(shiftreg_out'high - 1 downto 0) & '0';
			if (bit_cnt_tx = tx_width_i - 1) then
				bit_cnt_tx   <= 0;
				tx_word_done := true;
			else
				bit_cnt_tx <= bit_cnt_tx + 1;
			end if;
		end procedure shift_out;

		procedure shift_in is
		begin
			if (rx_past_ignore) then
				shiftreg_in <= shiftreg_in(shiftreg_in'high - 1 downto 0) & spi_miso;
				if (bit_cnt_rx = rx_width_i - 1) then
					bit_cnt_rx   <= 0;
					rx_word_done := true;
				else
					bit_cnt_rx <= bit_cnt_rx + 1;
				end if;
			else
				if (bit_cnt_rx = rx_ignore - 1) then
					bit_cnt_rx     <= 0;
					rx_past_ignore <= true;
				else
					bit_cnt_rx <= bit_cnt_rx + 1;
				end if;
			end if;

		end procedure shift_in;

	begin
		if (rst = '1') then
			reset_state;
		elsif (rising_edge(clk)) then
			default_state;
			if (clr = '1') then
				reset_state;
			else
				if (tx_enable = '0') then
					tx_break := true;
				end if;
				case state is
					when IDLE =>
						if (tx_enable = '1') then
							if (tx_break) then
								rx_width_i <= rx_width;
							end if;
							spi_clk_en <= '1';
							state      <= ACTIVE;
							tx_break   := false;
							load_new_tx_word;
							spi_cs_n <= '0';
						end if;
					when ACTIVE =>
						if (spi_clk_event) then
							active_cycles <= true;
						end if;
						if (active_cycles or SPI_CPHA = '0') then
							if (spi_clk_event and spi_clk_i /= SPI_CPOL) then
								shift_out;
								if (tx_word_done) then
									if (tx_break) then
										spi_clk_en <= '0';
									else
										load_new_tx_word;
									end if;
								end if;
							elsif (spi_clk_event and spi_clk_i = SPI_CPOL) then
								if (spi_clk_en = '1') then
									shift_in;
									if (rx_word_done) then
										rx_width_i <= rx_width;
										rx_valid   <= '1';
									end if;
								else
									bit_cnt_tx <= 0;
									state      <= STOP;
									spi_cs_n   <= '1';
								end if;
							end if;
						end if;
					when STOP =>
						if (bit_cnt_tx = CS_INACTIVE_DELAY - 1) then
							state          <= IDLE;
							rx_past_ignore <= false;
							tx_break       := true;
							active_cycles  <= false;
							bit_cnt_rx     <= 0;
						else
							bit_cnt_tx <= bit_cnt_tx + 1;
						end if;
				end case;
			end if;
		end if;
	end process fancy_spi;
	spi_mosi <= shiftreg_out(shiftreg_out'high);
	rx_data  <= shiftreg_in;

end architecture RTL;
