/* 
 * common.sv
 *
 * Copyright (C) 2025 Sathsara Geeth
 *
 */

/*
 * Version 1.0
 *
 * Version History
 *
 * Version | Description
 * --------+-----------------------------------------
 * 1.0     | Initial implementation
 */

/*
 * Comments:
 * 1. Helper modules
 */

module register #(
    parameter WIDTH = 8
)(
    input  logic                  i_clk,
    input  logic                  i_rst_n,
    input  logic                  i_clr_n,
    input  logic                  i_en,
    input  logic [WIDTH-1:0]      i_data,
    output logic [WIDTH-1:0]      o_data
);
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            o_data <= '0;
        end else if (!i_clr_n) begin
            o_data <= '0;
        end else if (i_en) begin
            o_data <= i_data;
        end
    end
endmodule

module mux2 #(
    parameter WIDTH = 8
    )(
    input  logic                    i_sel,
    input  logic [WIDTH-1:0]        i_data0,
    input  logic [WIDTH-1:0]        i_data1,
    output logic [WIDTH-1:0]        o_data
);
    assign o_data = i_sel ? i_data1 : i_data0;
endmodule

module delayed_pipe #(
    parameter WIDTH = 8,
    parameter DEPTH = 0
)(
    input  logic              i_clk,
    input  logic              i_rst_n,
    input  logic              i_en,
    input  logic              i_clr_n,
    input  logic [WIDTH-1:0]  i_in,
    output logic [WIDTH-1:0]  o_out
);
generate
    if (DEPTH == 0) begin
        assign o_out = i_in;
    end else begin
        logic [WIDTH-1:0] r_d [DEPTH:0];
        assign r_d[0] = i_in;
        genvar k;
        for (k = 0; k < DEPTH; k++) begin : DELAY
            register #(WIDTH) u_sr (
                .i_clk     (i_clk),
                .i_rst_n   (i_rst_n),
                .i_en      (i_en),
                .i_clr_n   (i_clr_n),
                .i_data    (r_d[k]),
                .o_data    (r_d[k+1])
            );
        end
        assign o_out = r_d[DEPTH];
    end
endgenerate
endmodule

module mul_lat1 #(
    parameter IN_WIDTH = 8
)(
    input  logic                  i_clk,
    input  logic                  i_rst_n,
    input  logic                  i_clr_n,
    input  logic                  i_en,
    input  logic [IN_WIDTH-1:0]   i_data0,
    input  logic [IN_WIDTH-1:0]   i_data1,
    output logic [2*IN_WIDTH-1:0] o_data
);
    (* use_dsp = "yes" *)
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            o_data <= '0;
        end else if (!i_clr_n) begin
            o_data <= '0;
        end else if (i_en) begin
            o_data <= $signed(i_data0) * $signed(i_data1);
        end
    end
endmodule

module add_lat1 #(
    parameter IN_WIDTH = 8
)(
    input  logic                  i_clk,
    input  logic                  i_rst_n,
    input  logic                  i_clr_n,
    input  logic                  i_en,
    input  logic [IN_WIDTH-1:0]   i_data0,
    input  logic [IN_WIDTH-1:0]   i_data1,
    output logic [2*IN_WIDTH-1:0] o_data
);
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            o_data <= '0;
        end else if (!i_clr_n) begin
            o_data <= '0;
        end else if (i_en) begin
            o_data <= $signed(i_data0) + $signed(i_data1);
        end
    end
endmodule

module delayed_pipev #(
    parameter WIDTH = 8,
    parameter DEPTH = 0,
    parameter M     = 4
)(
    input  logic                   i_clk,
    input  logic                   i_rst_n,
    input  logic                   i_en,
    input  logic                   i_clr_n,
    input  logic [WIDTH-1:0]       i_in,
    input  logic [$clog2(M+1)-1:0] i_ptr,
    output logic [WIDTH-1:0]       o_out
);
    generate
        if (DEPTH == 0) begin
            assign o_out = i_in;
        end else begin : gen_pipe

            logic [WIDTH-1:0]       r_d [DEPTH+1];
            logic [$clog2(M+1)-1:0] r_ptr;
            logic [DEPTH:0]         r_sel_oh;

            always_ff @(posedge i_clk or negedge i_rst_n) begin
                if (!i_rst_n) r_ptr <= '0;
                else if (i_en) r_ptr <= i_ptr;
            end

            always_ff @(posedge i_clk or negedge i_rst_n) begin
                if (!i_rst_n) begin
                    r_sel_oh <= (DEPTH+1)'(1);
                end else if (i_en) begin
                    r_sel_oh <= '0;
                    r_sel_oh[r_ptr] <= 1'b1;
                end
            end

            assign r_d[0] = i_in;
            genvar k;
            for (k = 0; k < DEPTH; k++) begin : DELAY
                register #(.WIDTH(WIDTH)) u_sr (
                    .i_clk   (i_clk),
                    .i_rst_n (i_rst_n),
                    .i_en    (i_en),
                    .i_clr_n (i_clr_n),
                    .i_data  (r_d[k]),
                    .o_data  (r_d[k+1])
                );
            end

            logic [WIDTH-1:0] w_out;
            always_comb begin
                w_out = '0;
                for (int i = 0; i <= DEPTH; i++)
                    w_out |= {WIDTH{r_sel_oh[i]}} & r_d[i];
            end
            assign o_out = w_out;

        end
    endgenerate
endmodule

module buff_mem #(
    parameter DEPTH = 8,
    parameter WIDTH = 8
) (
    input  logic                     i_clk,
    input  logic                     i_we,
    input  logic [$clog2(DEPTH)-1:0] i_wr_addr,
    input  logic [WIDTH-1:0]         i_din,
    input  logic [$clog2(DEPTH)-1:0] i_rd_addr,
    output logic [WIDTH-1:0]         o_dout
);
    // bram
    (* ram_style = "block" *) logic [WIDTH-1:0] mem [0:DEPTH-1];

    always_ff @(posedge i_clk) begin
        if (i_we) begin
            mem[i_wr_addr] <= i_din;
        end
    end

    always_ff @(posedge i_clk) begin
        o_dout <= mem[i_rd_addr];
    end
endmodule

module circular_buffer #(
    parameter  DEPTH            = 8,
    parameter  WIDTH            = 8
) (
    input   logic                           i_clk,
    input   logic                           i_rst_n,
    output  logic   [WIDTH-1:0]             o_head_data,
    output  logic                           o_deq_ready,
    input   logic                           i_deq_valid,    
    input   logic   [WIDTH-1:0]             i_enq_tail_data,
    input   logic                           i_enq_valid,
    output  logic                           o_enq_ready,
    output  logic                           o_full,
    output  logic                           o_empty,
    output  logic   [$clog2(DEPTH+1)-1:0]   o_level,
    input   logic                           i_clr_n
);  

    logic   [$clog2(DEPTH)-1:0]         r_head_ptr, r_tail_ptr;
    logic   [$clog2(DEPTH+1)-1:0]       r_count;
    
    logic   [$clog2(DEPTH)-1:0]         next_head_ptr;
    logic   [$clog2(DEPTH+1)-1:0]       next_count;
    logic                               will_enq, will_deq;

    logic   [WIDTH-1:0]                 w_buff_mem_out;
    logic                               r_bypass_en;
    logic   [WIDTH-1:0]                 r_bypass_data;

    assign will_enq = o_enq_ready & i_enq_valid;
    assign will_deq = o_deq_ready & i_deq_valid;

    always_comb begin
        o_full      = (r_count == DEPTH);
        o_empty     = (r_count == '0);
        o_level     = r_count;
        o_enq_ready = !o_full;
    end

    always_comb begin
        if (will_deq) begin
            next_head_ptr = r_head_ptr + 1'b1;
        end else begin
            next_head_ptr = r_head_ptr;
        end
    end

    buff_mem #(
        .DEPTH(DEPTH),
        .WIDTH(WIDTH)
    ) u_buff_mem (
        .i_clk    (i_clk),
        .i_we     (will_enq),
        .i_wr_addr(r_tail_ptr),
        .i_din    (i_enq_tail_data),
        .i_rd_addr(next_head_ptr),
        .o_dout   (w_buff_mem_out)
    );

    always_ff @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_bypass_en   <= 1'b0;
            r_bypass_data <= '0;
        end else if (!i_clr_n) begin
            r_bypass_en   <= 1'b0;
            r_bypass_data <= '0;
        end else begin
            r_bypass_en   <= will_enq && (r_tail_ptr == next_head_ptr);
            r_bypass_data <= i_enq_tail_data;
        end
    end

    assign o_head_data = r_bypass_en ? r_bypass_data : w_buff_mem_out;

    always_comb begin
        case ({will_enq, will_deq})
            2'b01:   next_count = r_count - 1'b1;
            2'b10:   next_count = r_count + 1'b1;
            default: next_count = r_count; 
        endcase
    end

    always_ff @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_head_ptr  <= '0;
            r_tail_ptr  <= '0;
            r_count     <= '0;
            o_deq_ready <= 1'b0;
        end else if (!i_clr_n) begin
            r_head_ptr  <= '0;
            r_tail_ptr  <= '0;
            r_count     <= '0;
            o_deq_ready <= 1'b0;
        end else begin
            r_count     <= next_count;
            o_deq_ready <= (next_count != '0); 

            case ({will_enq, will_deq})
                2'b01:   r_head_ptr <= r_head_ptr + 1'b1;
                2'b10:   r_tail_ptr <= r_tail_ptr + 1'b1;
                2'b11: begin
                    r_head_ptr <= r_head_ptr + 1'b1;
                    r_tail_ptr <= r_tail_ptr + 1'b1;
                end
                default: begin end
            endcase
        end
    end

endmodule


module ctr #(
    parameter int WIDTH  = 8
)(
    input  logic             i_clk,
    input  logic             i_rst_n,
    input  logic             i_start,
    input  logic             i_en,
    input  logic [WIDTH-1:0] i_target,
    output logic             o_done
    
);
    logic [WIDTH-1:0] r_count;
    
    assign o_done = (r_count == i_target);
    
    always_ff @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_count <= '0;
        end else if (i_start) begin
            r_count <= '0;
        end else if (i_en && !o_done) begin
            r_count <= r_count + 1'b1;
        end
    end
endmodule