// Copyright 2018 ETH Zurich and University of Bologna.
// Copyright and related rights are licensed under the Solderpad Hardware
// License, Version 0.51 (the "License"); you may not use this file except in
// compliance with the License. You may obtain a copy of the License at
// http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law
// or agreed to in writing, software, hardware and materials distributed under
// this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.

// Igor Loi <igor.loi@unibo.it>

module generic_fifo_adv 
#( 
   parameter int unsigned          DATA_WIDTH = 32,
   parameter int unsigned          DATA_DEPTH = 8
   )
   (
    input  logic                                    clk,
    input  logic                                    rst_n,
    input  logic                                    clear_i,
    
    //PUSH SIDE
    input  logic [DATA_WIDTH-1:0]                   data_i,
    input  logic                                    valid_i,
    output logic                                    grant_o,
    
    //POP SIDE
    output logic [DATA_WIDTH-1:0]                   data_o,
    output logic                                    valid_o,
    input  logic                                    grant_i,
    
    input  logic                                    test_mode_i
    );
   
   
   // Local Parameter
   localparam  int 				    unsigned ADDR_DEPTH = $clog2(DATA_DEPTH);
   enum 					    logic [1:0] { EMPTY, FULL, MIDDLE } CS, NS;
   // Internal Signals
   
   logic 					    gate_clock;
   logic 					    clk_gated;
   
   logic [ADDR_DEPTH-1:0] 			    Pop_Pointer_CS,  Pop_Pointer_NS;
   logic [ADDR_DEPTH-1:0] 			    Push_Pointer_CS, Push_Pointer_NS;
   logic [DATA_WIDTH-1:0] 			    FIFO_REGISTERS[DATA_DEPTH-1:0];
   int 						    unsigned                    i;
   
   // Parameter Check
   // synopsys translate_off
   initial
   begin : parameter_check
      integer param_err_flg;
      param_err_flg = 0;
      
      if (DATA_WIDTH < 1)
	begin
           param_err_flg = 1;
           $display("ERROR: %m :\n  Invalid value (%d) for parameter DATA_WIDTH (legal range: greater than 1)", DATA_WIDTH );
	end
      
      if (DATA_DEPTH < 1)
	begin
           param_err_flg = 1;
           $display("ERROR: %m :\n  Invalid value (%d) for parameter DATA_DEPTH (legal range: greater than 1)", DATA_DEPTH );
	end                   
   end
   // synopsys translate_on
   
`ifndef PULP_FPGA_EMUL
   cluster_clock_gating cg_cell
     (
      .clk_i     ( clk         ),
      .en_i      (~gate_clock  ),
      .test_en_i ( test_mode_i ),
      .clk_o     ( clk_gated   )
      );
`else
   assign clk_gated = clk;
`endif
   
   // UPDATE THE STATE
   always_ff @(posedge clk, negedge rst_n)
     begin
	if(rst_n == 1'b0)
	  begin
             CS              <= EMPTY;
             Pop_Pointer_CS  <= {ADDR_DEPTH {1'b0}};
             Push_Pointer_CS <= {ADDR_DEPTH {1'b0}};
	  end
	else
	  begin
             if(clear_i)
               begin
		  CS              <= EMPTY;
		  Pop_Pointer_CS  <= {ADDR_DEPTH {1'b0}};
		  Push_Pointer_CS <= {ADDR_DEPTH {1'b0}};
               end
             else
               begin
		  CS              <= NS;
		  Pop_Pointer_CS  <= Pop_Pointer_NS;
		  Push_Pointer_CS <= Push_Pointer_NS;
               end
	  end
     end
   
   
   // Compute Next State
   always_comb
     begin
	gate_clock      = 1'b0;
	
	case(CS)
	  
	  EMPTY:
	    begin
               grant_o = 1'b1;
               valid_o = 1'b0;
	       
               case(valid_i)
		 1'b0 : 
		   begin 
                      NS              = EMPTY;
                      Push_Pointer_NS = Push_Pointer_CS;
                      Pop_Pointer_NS  = Pop_Pointer_CS;
                      gate_clock      = 1'b1;
		   end
		 
		 1'b1: 
		   begin 
                      NS              = MIDDLE;
                      Push_Pointer_NS = Push_Pointer_CS + 1'b1;
                      Pop_Pointer_NS  = Pop_Pointer_CS;
		   end
		 
               endcase
	    end//~EMPTY
	  
	  MIDDLE:
	    begin
               grant_o = 1'b1;
               valid_o = 1'b1;
	       
               case({valid_i,grant_i})
		 
		 2'b01:
		   begin
                      gate_clock      = 1'b1;
		      
                      if((Pop_Pointer_CS == Push_Pointer_CS -1 ) || ((Pop_Pointer_CS == DATA_DEPTH-1) && (Push_Pointer_CS == 0) ))
                        NS              = EMPTY;
                      else
                        NS              = MIDDLE;
		      
                      Push_Pointer_NS = Push_Pointer_CS;
		      
                      if(Pop_Pointer_CS == DATA_DEPTH-1)
                        Pop_Pointer_NS  = 0;
                      else
                        Pop_Pointer_NS  = Pop_Pointer_CS + 1'b1;
		   end
		 
		 2'b00 : 
		   begin
                      gate_clock      = 1'b1; 
                      NS              = MIDDLE;
                      Push_Pointer_NS = Push_Pointer_CS;
                      Pop_Pointer_NS  = Pop_Pointer_CS;
		   end
		 
		 2'b11: 
		   begin
                      NS              = MIDDLE;
		      
                      if(Push_Pointer_CS == DATA_DEPTH-1)
                        Push_Pointer_NS = 0;
                      else
                        Push_Pointer_NS = Push_Pointer_CS + 1'b1;
		      
                      if(Pop_Pointer_CS == DATA_DEPTH-1)
                        Pop_Pointer_NS  = 0;
                      else
                        Pop_Pointer_NS  = Pop_Pointer_CS  + 1'b1;
		   end
		 
		 2'b10:
		   begin 
                      if(( Push_Pointer_CS == Pop_Pointer_CS - 1) || ( (Push_Pointer_CS == DATA_DEPTH-1) && (Pop_Pointer_CS == 0) ))
                        NS              = FULL;
                      else
                        NS        = MIDDLE;
		      
                      if(Push_Pointer_CS == DATA_DEPTH - 1)
                        Push_Pointer_NS = 0;
                      else
                        Push_Pointer_NS = Push_Pointer_CS + 1'b1;
		      
                      Pop_Pointer_NS  = Pop_Pointer_CS;
		   end
		 
               endcase                     
	    end
	  
	  FULL:
	    begin
               grant_o     = 1'b0;
               valid_o     = 1'b1;
               gate_clock  = 1'b1;
	       
               case(grant_i)
		 1'b1: 
		   begin 
                      NS              = MIDDLE;
		      
                      Push_Pointer_NS = Push_Pointer_CS;
		      
                      if(Pop_Pointer_CS == DATA_DEPTH-1)
                        Pop_Pointer_NS  = 0;
                      else
                        Pop_Pointer_NS  = Pop_Pointer_CS  + 1'b1;
		   end
		 
		 1'b0:
		   begin 
                      NS              = FULL;
                      Push_Pointer_NS = Push_Pointer_CS;
                      Pop_Pointer_NS  = Pop_Pointer_CS;
		   end
               endcase                 
	       
	    end // end of FULL
	  
	  default :
	    begin
               gate_clock      = 1'b1;
               grant_o         = 1'b0;
               valid_o         = 1'b0;
               NS              = EMPTY;
               Pop_Pointer_NS  = 0;
               Push_Pointer_NS = 0;
	    end
	  
	endcase
     end
   
   always_ff @(posedge clk_gated, negedge rst_n)
     begin
	if(rst_n == 1'b0)
	  begin
	     for (i=0; i< DATA_DEPTH; i++)
               FIFO_REGISTERS[i] <= {DATA_WIDTH {1'b0}};
	  end
	else
	  begin
             if((grant_o == 1'b1) && (valid_i == 1'b1))
               FIFO_REGISTERS[Push_Pointer_CS] <= data_i;
	  end
     end
   
   assign data_o = FIFO_REGISTERS[Pop_Pointer_CS];
   
endmodule // generic_fifo