// NOTE: The first byte is used for syncing due to using different clock domains
`include <params.svh>
module spi_interface(input logic                          rst,
                     input logic                          sys_clk,
                     input logic                          sclk,
                     input logic                          cs_n,
                     input logic                          mosi,
                     output logic                         miso,
                     output logic [7:0]                   rx_byte,
                     output logic                         rx_valid,
                     input logic                          rx_ready,
                     output logic [PACKET_ADDR_LEN - 1:0] rx_pkt_addr,
                     output logic                         tx_active,
                     input logic [7:0]                    tx_byte,
                     input logic                          tx_valid,
                     output logic                         tx_ready,
                     output logic [PACKET_ADDR_LEN - 1:0] tx_pkt_addr,
                     output logic [QUEUE_ADDR_LEN - 1:0]  tx_queue_addr,
                     input logic [QUEUE_ADDR_LEN - 1:0]   tx_new_queue,
                     input logic                          tx_new_queue_valid,
                     output logic                         tx_new_queue_ready);
    
    timeunit 1ns;
    timeprecision 1ps;

    // SPI logic
    logic sclk_rising_edge;
    logic sclk_falling_edge;
    
    async_get_clk_edges sync(.rst(rst),
                             .ext_clk(sclk),
                             .sys_clk(sys_clk),
                             .clk_rising_edge(sclk_rising_edge),
                             .clk_falling_edge(sclk_falling_edge));
    logic [7:0] rx_buff;
    shortint    bit_cnt;
    logic       rx_byte_ready;
    logic [7:0] tx_buff;
    logic       tx_buff_valid;
    logic       tx_loaded;

    spi spi_module(.rst(rst),
                   .sclk_rising_edge(sclk_rising_edge),
                   .sclk_falling_edge(sclk_falling_edge),
                   .cs_n(cs_n),
                   .mosi(mosi),
                   .miso(miso),
                   .bit_cnt(bit_cnt),
                   .rx_buff(rx_buff),
                   .rx_byte_ready(rx_byte_ready),
                   .tx_buff(tx_buff),
                   .tx_buff_valid(tx_buff_valid),
                   .tx_loaded(tx_loaded));

    rx_hub rx_module(.rst(rst),
                     .sys_clk(sys_clk),
                     .rx_buff(rx_buff),
                     .rx_byte_ready(rx_byte_ready),
                     .rx_byte(rx_byte),
                     .rx_valid(rx_valid),
                     .rx_ready(rx_ready),
                     .rx_pkt_addr(rx_pkt_addr));

    assign tx_buff = tx_byte;

    tx_hub tx_module(.rst(rst),
                     .sys_clk(sys_clk),
                     .tx_active(tx_active),
                     .tx_loaded(tx_loaded),
                     .tx_byte(tx_byte),
                     .tx_valid(tx_valid),
                     .tx_ready(tx_ready),
                     .tx_pkt_addr(tx_pkt_addr),
                     .tx_queue_addr(tx_queue_addr),
                     .tx_new_queue(tx_new_queue),
                     .tx_new_queue_valid(tx_new_queue_valid),
                     .tx_new_queue_ready(tx_new_queue_ready));    

endmodule // spi_interface

module spi(input logic        rst,
           input logic        sclk_rising_edge,
           input logic        sclk_falling_edge,
           input logic        cs_n,
           input logic        mosi,
           output logic       miso,
           output shortint    bit_cnt,
           output logic [7:0] rx_buff,
           output logic       rx_byte_ready,
           input logic        tx_buff_valid,
           input logic [7:0]  tx_buff,
           output logic       tx_loaded);
    timeunit 1ns;
    timeprecision 1ps;
    
    logic [7:0] rx_shift;
    logic [7:0] tx_shift;
    assign miso = tx_shift[7];

    always_ff @ (posedge sclk_rising_edge or posedge rst) begin
        if (rst) begin
            rx_shift <= '0;
            rx_buff <= '0;
            bit_cnt <= '0;
            rx_byte_ready <= 0;
        end
        else begin
            if (cs_n) begin
                rx_shift <= 0;
                rx_buff <= 0;
                bit_cnt <= 0;
            end else begin
                rx_shift <= {rx_shift[6:0], mosi};
                bit_cnt <= bit_cnt + 1;
                
                if (bit_cnt == 7) begin
                    bit_cnt <= 0;
                    rx_buff <= {rx_shift[6:0], mosi};
                    rx_byte_ready <= 1;
                end else begin
                    rx_byte_ready <= 0;
                end
            end // else: !if(cs_n)
        end // else: !if(rst)
    end // always_ff @ (posedge sclk)

    always_ff @ (posedge sclk_falling_edge or rst) begin
        if (rst) begin
            tx_shift <= TX_DEFAULT;
            tx_loaded <= 0;
        end else begin
            if (cs_n) begin
                tx_shift <= TX_DEFAULT;
            end else begin
                if (bit_cnt == 0) begin
                    if (tx_buff_valid) begin
                        tx_shift <= tx_buff;
                        tx_loaded <= 1;
                    end else begin
                        tx_shift <= '0;
                        tx_loaded <= 1;
                    end
                end else begin
                    tx_shift <= {tx_shift[6:0], 1'b0};
                    tx_loaded <= 0;
                end
            end
        end // else: !if(rst)
        $display("last bit sent: %b", miso);
        $display("[%0d] current tx_shift: %b", $time, tx_shift);
        $display("-----------------------------------------");
    end // always_ff @ (negedge sclk)

endmodule // spi

module rx_hub(input logic                          rst,
              input logic                          sys_clk,
              input logic [7:0]                    rx_buff,
              input logic                          rx_byte_ready,
              output logic [7:0]                   rx_byte,
              output logic                         rx_valid,
              input logic                          rx_ready,
              output logic [PACKET_ADDR_LEN - 1:0] rx_pkt_addr);
    timeunit 1ns;
    timeprecision 1ps;
    
    shortint rx_load_cooldown;
    logic    rx_drained;
    logic    rx_dropping;

    always_ff @ (posedge sys_clk or rst) begin
        if (rst) begin
            rx_drained <= 0;
            rx_pkt_addr <= '1;
            rx_byte <= '0;
            rx_valid <= 0;
            rx_load_cooldown <= 0;
            rx_dropping <= 0;
        end else begin
            if (!rx_drained && rx_byte_ready) begin
                if (rx_ready && !rx_dropping) begin
                    rx_byte <= rx_buff;
                    rx_valid <= 1;
                    rx_load_cooldown <= RX_LOAD_COOLDOWN;
                    rx_drained <= 1;
                    rx_pkt_addr <= rx_pkt_addr + 1;
                end else begin
                    rx_dropping <= 1;
                end
                if (rx_ready && &rx_pkt_addr) begin
                    rx_dropping <= 0;
                end
            end else if (!rx_byte_ready) begin
                rx_drained <= 0;
                if (rx_load_cooldown == 0) begin
                    rx_valid <= 0;
                end else begin
                    rx_load_cooldown <= rx_load_cooldown - 1;
                end
            end
        end
    end // always_ff @ (posedge sys_clk or rst)
endmodule // rx_hub

module tx_hub(input logic                          rst,
              input logic                          sys_clk,
              output logic                         tx_active,
              input logic                          tx_loaded,
              input logic [7:0]                    tx_byte,
              input logic                          tx_valid,
              output logic                         tx_ready,
              output logic [PACKET_ADDR_LEN - 1:0] tx_pkt_addr,
              output logic [QUEUE_ADDR_LEN - 1:0]  tx_queue_addr,
              input logic [QUEUE_ADDR_LEN - 1:0]   tx_new_queue,
              input logic                          tx_new_queue_valid,
              output logic                         tx_new_queue_ready);
    timeunit 1ns;
    timeprecision 1ps;
    
    shortint tx_load_cooldown;
    assign tx_new_queue_ready = !pkt_queue_full;

    logic queue_active;
    assign tx_active = queue_active || queue_addr_valid;

    always_ff @ (posedge sys_clk or rst) begin
        if (rst) begin
            tx_ready <= 0;
            tx_pkt_addr <= '0;
            enqueue_addr <= 0;
            new_queue_addr <= '0;
            request_new_pkt <= 0;
            queue_active <= 0;
            tx_pkt_addr <= '0;
            tx_load_cooldown <= 0;
        end else begin // if (rst)
            if (tx_new_queue_valid && tx_new_queue_ready) begin
                enqueue_addr <= 1;
                new_queue_addr <= tx_new_queue;
            end else begin
                enqueue_addr <= 0;
            end
            
            if (!queue_active && queue_addr_valid) begin
                request_new_pkt <= 1;
                queue_active <= 1;
                tx_pkt_addr <= '1;
            end else begin
                request_new_pkt <= 0;
            end

            if (queue_active) begin
                if (tx_loaded && tx_load_cooldown == 0) begin
                    tx_ready <= 1;
                    tx_load_cooldown <= TX_LOAD_COOLDOWN;
                    tx_pkt_addr <= tx_pkt_addr + 1;
                    if (&tx_pkt_addr) begin
                        queue_active <= 0;
                    end
                end else begin
                    tx_ready <= 0;
                    if (tx_load_cooldown != 0) begin
                        tx_load_cooldown <= tx_load_cooldown - 1;
                    end
                end
            end else begin // if (queue_active)
                tx_pkt_addr <= '1;
                if (tx_load_cooldown != 0) begin
                    tx_load_cooldown <= tx_load_cooldown - 1;
                end
            end
        end // else: !if(rst)
    end // always_ff @ (posedge sys_clk or rst)

    logic                        request_new_pkt;
    logic [QUEUE_ADDR_LEN - 1:0] new_queue_addr;
    logic                        enqueue_addr;
    logic                        queue_addr_valid;
    logic                        pkt_queue_full;

    // IMPORTANT: tx_queue_addr is directly linked
    packet_queue pkt_q (.rst(rst),
                        .sys_clk(sys_clk),
                        .request_new_packet(request_new_pkt),
                        .new_queue_addr(new_queue_addr),
                        .enqueue_addr(enqueue_addr),
                        .next_queue_addr(tx_queue_addr),
                        .next_queue_addr_valid(queue_addr_valid),
                        .queue_full(pkt_queue_full));

endmodule // tx_hub

module async_get_clk_edges(input logic  rst,
                           input logic  ext_clk,
                           input logic  sys_clk,
                           output logic clk_rising_edge,
                           output logic clk_falling_edge);
    timeunit 1ns;
    timeprecision 1ps;

    logic sync_0 = 0;
    logic sync_1 = 0;

    always_ff @ (posedge sys_clk) begin
        if (rst) begin
            sync_0 <= 0;
            sync_1 <= 0;
        end else begin
            sync_0 <= ext_clk;
            sync_1 <= sync_0;
        end
    end

    assign clk_rising_edge = sync_0 & ~sync_1;
    assign clk_falling_edge = ~sync_0 & sync_1;
endmodule // async_get_clk_edges

// IMPORTANT: new_queue_addr will always be ready when queue_size is greater than 1
module packet_queue(input logic                         rst,
                    input logic                         sys_clk,
                    input logic                         request_new_packet,
                    input logic [QUEUE_ADDR_LEN - 1:0]  new_queue_addr,
                    input logic                         enqueue_addr,
                    output logic [QUEUE_ADDR_LEN - 1:0] next_queue_addr,
                    output logic                        next_queue_addr_valid,
                    output logic                        queue_full);
    timeunit 1ns;
    timeprecision 1ps;

    logic [QUEUE_ADDR_LEN - 1:0] mem [INTERFACE_QUEUE_SIZE];
    logic [INTERFACE_QUEUE_ADDR_LEN - 1:0] head;
    logic [INTERFACE_QUEUE_ADDR_LEN - 1:0] tail;
    shortint                               queue_size;
    assign queue_full = queue_size == INTERFACE_QUEUE_SIZE[31:16];
    assign next_queue_addr_valid = !(queue_size == 0);

    always_ff @ (posedge sys_clk or rst) begin
        if (rst) begin
            head <= '0;
            tail <= '0;
            queue_size <= 0;
            next_queue_addr <= '0;
        end else begin
            if (request_new_packet) begin
                head <= head + 1;
                queue_size <= queue_size - 1;
            end
            next_queue_addr <= mem[head];

            if (enqueue_addr) begin
                mem[tail] <= new_queue_addr;
                tail <= tail + 1;
                queue_size <= queue_size + 1;
            end
        end
    end
    
endmodule // packet_queue
