// FIFO buffer implemented with synchronous dual-port block ram
// Reference: 
//   https://embeddedthoughts.com/2016/07/13/fifo-buffer-using-block-ram-on-a-xilinx-spartan-3-fpga/
module fifo
  #( parameter ADDRESS_WIDTH = 4, // number of words in ram
                 DATA_WIDTH    =  4  // number of bits in word
   )
   
  // IO ports
  (
    input wire clk, reset,
    input wire read, write,
    input wire [DATA_WIDTH-1:0] write_data,
    output wire empty, full,
    output wire [DATA_WIDTH-1:0] read_data
  );
  
  // internal signal declarations
  reg [ADDRESS_WIDTH-1:0] write_address_reg, write_address_next, write_address_after;
  reg [ADDRESS_WIDTH-1:0] read_address_reg, read_address_next, read_address_after;
  reg full_reg, empty_reg, full_next, empty_next;
  wire write_en;
  
  // write enable is asserted when write input is asserted and FIFO isn't full
  assign write_en = write & ~full_reg;
  
  // instantiate synchronous block ram
  sync_dual_port_ram #(.ADDRESS_WIDTH(ADDRESS_WIDTH), .DATA_WIDTH(DATA_WIDTH)) ram
                       (.clk(clk), .write_en(write_en), .write_address(write_address_reg),
      .read_address(read_address_reg), .write_data_in(write_data),
      .write_data_out(), .read_data_out(read_data));
  
  // register for address pointers, full/empty status
  always @(posedge clk, posedge reset)
    if (reset)
        begin
                    write_address_reg <= 0;
                    read_address_reg  <= 0;
                    full_reg          <= 1'b0;
                    empty_reg         <= 1'b1;
        end
    else
        begin
                    write_address_reg <= write_address_next;
                    read_address_reg  <= read_address_next;
                    full_reg          <= full_next;
                    empty_reg         <= empty_next;
        end
      
  // next-state logic for address index values after read/write operations
  always @*
    begin
    write_address_after = write_address_reg + 1;
    read_address_after  = read_address_reg + 1;
    end
    
  // next-state logic for address pointers
  always @*
    begin
    // defaults
    write_address_next = write_address_reg;
    read_address_next  = read_address_reg;
    full_next          = full_reg;
    empty_next         = empty_reg;
    
    // if read input asserted and FIFO isn't empty
    if(read && ~empty_reg && ~write)
      begin
      read_address_next = read_address_after;       // read address moves forward
      full_next = 1'b0;                             // FIFO isn't full if a read occured
      
      if (read_address_after == write_address_reg)  // if read address caught up with write address,
        empty_next = 1'b1;                        // FIFO is empty
      end
    
    // if write input asserted and FIFO isn't full
    else if(write && ~full_reg && ~read)
      begin
      write_address_next = write_address_after;     // write address moves forward
      empty_next = 1'b0;                            // FIFO isn't empty if write occured
      
      if (write_address_after == read_address_reg)    // if write address caught up with read address
        full_next = 1'b1;                         // FIFO is full
                        end
    
    // if write and read are asserted
                else if(write && read)
      begin
      write_address_next = write_address_after;     // write address moves forward
      read_address_next  = read_address_after;      // read address moves forward
                        end
    end 
  
   // assign full/empty status to output ports
   assign full  = full_reg;
   assign empty = empty_reg;
endmodule