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

entity fifo is
   generic(
      sync_depth : natural := 3;
      gray_code  : boolean := true;
      addr_width : natural := 4;
      data_width : natural := 32);
   port(
      rst    : in  std_logic;
      -- write port, only set w_en when w_rdy
      w_clk  : in  std_logic;
      w_rdy  : out std_logic;
      w_en   : in  std_logic;
      w_data : in  std_logic_vector(data_width-1 downto 0);
      -- (pre)alloc port, can be unused
      a_clk  : in  std_logic;
      a_rdy  : out std_logic;
      a_en   : in  std_logic;
      -- read port, only set r_en when r_rdy
      -- data is valid the cycle after r_en raised
      r_clk  : in  std_logic;
      r_rdy  : out std_logic;
      r_en   : in  std_logic;
      r_data : out std_logic_vector(data_width-1 downto 0));
end fifo;

architecture rtl of fifo is
   subtype counter is unsigned(addr_width downto 0);
   type counter_shift is array(sync_depth downto 0) of counter;
   
   signal r_idx_bnry : counter;
   signal r_idx_gray : counter;
   signal w_idx_bnry : counter;
   signal w_idx_gray : counter;
   signal a_idx_bnry : counter;
   signal a_idx_gray : counter;
   
   signal r_idx_shift_w : counter_shift; -- r_idx_gray in w_clk
   signal r_idx_shift_a : counter_shift; -- r_idx_gray in a_clk
   signal w_idx_shift_r : counter_shift; -- w_idx_gray in r_clk
   
   function bin2gray(a : unsigned) return unsigned is
      variable o : unsigned(a'length downto 0);
   begin
      if gray_code then
         o := (a & '0') xor ('0' & a);
      else
         o := (a & '0');
      end if;
      return o(a'length downto 1);
   end bin2gray;
   
   function index(a : counter) return std_logic_vector is
   begin
      return std_logic_vector(a(addr_width-1 downto 0));
   end index;
   
   function empty(a, b : counter) return std_logic is
   begin
      if a = b then
         return '1';
      else
         return '0';
      end if;
   end empty;
   
   function full(a, b : counter) return std_logic is
      variable mask : counter := (others => '0');
   begin
      -- In binary a full FIFO has indexes (a XOR 1000...00) = b
      -- bin2gray is a linear function, thus:
      --   a XOR 1000..00 = b                                iff
      --   bin2gray(a XOR 1000...00) = bin2gray(b)           iff
      --   bin2gray(a) XOR bin2gray(1000...00) = bin2gray(b) iif
      --   bin2gray(a) XOR 1100..00 = bin2gray(b)
      mask(addr_width) := '1';
      mask := bin2gray(mask);
      if (a xor mask) = b then
         return '1';
      else
         return '0';
      end if;
   end full;
begin
   ram : rwp_ram
      generic map(addr_width => addr_width, data_width => data_width)
      port map(w_clk => w_clk, w_en => w_en, w_addr => index(w_idx_bnry), w_data => w_data,
               r_clk => r_clk, r_en => r_en, r_addr => index(r_idx_bnry), r_data => r_data);
   
   read : process(r_clk)
      variable idx : counter;
   begin
      if rising_edge(r_clk) then
         if rst = '1' then
            idx := (others => '0');
         elsif r_en = '1' then
            idx := r_idx_bnry + 1;
         else
            idx := r_idx_bnry;
         end if;
         r_idx_bnry <= idx;
         r_idx_gray <= bin2gray(idx);
         w_idx_shift_r(sync_depth downto 1) <= w_idx_shift_r(sync_depth-1 downto 0);
      end if;
   end process;
   w_idx_shift_r(0) <= w_idx_gray;
   r_rdy <= not empty(r_idx_gray, w_idx_shift_r(sync_depth));
   
   write : process(w_clk)
     variable idx : counter;
   begin
      if rising_edge(w_clk) then
         if rst = '1' then
            idx := (others => '0');
         elsif w_en = '1' then
            idx := w_idx_bnry + 1;
         else
            idx := w_idx_bnry;
         end if;
         w_idx_bnry <= idx;
         w_idx_gray <= bin2gray(idx);
         r_idx_shift_w(sync_depth downto 1) <= r_idx_shift_w(sync_depth-1 downto 0);
      end if;
   end process;
   r_idx_shift_w(0) <= r_idx_gray;
   w_rdy <= not full(w_idx_gray, r_idx_shift_w(sync_depth));

   alloc : process(a_clk)
     variable idx : counter;
   begin
      if rising_edge(a_clk) then
         if rst = '1' then
            idx := (others => '0');
         elsif a_en = '1' then
            idx := a_idx_bnry + 1;
         else
            idx := a_idx_bnry;
         end if;
         a_idx_bnry <= idx;
         a_idx_gray <= bin2gray(idx);
         r_idx_shift_a(sync_depth downto 1) <= r_idx_shift_a(sync_depth-1 downto 0);
      end if;
   end process;
   r_idx_shift_a(0) <= r_idx_gray;
   a_rdy <= not full(a_idx_gray, r_idx_shift_a(sync_depth));
end rtl;
