`include <params.svh>
`include <routing.svh>

// IMPORTANT: interfaces are supposed to keep track of their own packet states
module hub(input logic                         rst,
           input logic                         sys_clk,
           input logic [7:0]                   rx_byte [INTERFACE_CNT],
           input logic                         rx_valid [INTERFACE_CNT],
           output logic                        rx_ready [INTERFACE_CNT],
           input logic [PACKET_ADDR_LEN - 1:0] rx_pkt_addr [INTERFACE_CNT],
           output logic [7:0]                  tx_byte [INTERFACE_CNT],
           output logic                        tx_valid [INTERFACE_CNT],
           input logic                         tx_ready [INTERFACE_CNT],
           input logic [PACKET_ADDR_LEN - 1:0] tx_pkt_addr [INTERFACE_CNT],
           input logic [QUEUE_ADDR_LEN - 1:0]  tx_queue_addr [INTERFACE_CNT],
           output logic [QUEUE_ADDR_LEN - 1:0] tx_new_queue[INTERFACE_CNT],
           output logic                        tx_new_queue_valid [INTERFACE_CNT],
           input logic                         tx_new_queue_ready [INTERFACE_CNT],
           output logic                        hub_ready);
    timeunit 1ns;
    timeprecision 1ps;

    logic [INTERFACE_ADDR_LEN - 1:0] curr;
    logic [INTERFACE_ADDR_LEN - 1:0] dest_buff [INTERFACE_CNT];
    logic                            reuse_queue_slot [INTERFACE_CNT];
    logic                            last_served_read;
    
    logic                            request_new_slot;
    logic [QUEUE_ADDR_LEN - 1:0]     new_slot_addr;
    logic [QUEUE_ADDR_LEN - 1:0]     empty_slot_addr;
    logic                            empty_slot_enqueue;
    logic                            free_queue_empty;
    
    free_queue fqueue(.sys_clk(sys_clk),
                      .rst(rst),
                      .request_new_slot(request_new_slot),
                      .empty_slot_addr(empty_slot_addr),
                      .empty_slot_enqueue(empty_slot_enqueue),
                      .new_slot_addr(new_slot_addr),
                      .queue_empty(free_queue_empty),
                      .queue_ready(hub_ready));
    
    logic [QUEUE_ADDR_LEN - 1:0]       rx_queue_addr [INTERFACE_CNT];
    logic [MEMORY_POOL_ADDR_LEN - 1:0] mem_read_addr;
    logic [7:0]                        mem_read_byte;
    logic [MEMORY_POOL_ADDR_LEN - 1:0] mem_write_addr;
    logic                              mem_write_enable;
    logic [7:0]                        mem_write_byte;
    shortint                           new_slot_cooldown [INTERFACE_CNT];

    memory_pool mpool(.sys_clk(sys_clk),
                      .rst(rst),
                      .read_addr(mem_read_addr),
                      .write_addr(mem_write_addr),
                      .write_byte(mem_write_byte),
                      .write_enable(mem_write_enable),
                      .read_byte(mem_read_byte));

    always_comb begin
        foreach (rx_ready[i]) begin
            assign rx_ready[i] = !free_queue_empty || reuse_queue_slot[i];
        end
    end
    
    always_ff @ (posedge sys_clk or rst) begin
        if (rst) begin
            curr <= '0;
            for (int i = 0; i < INTERFACE_CNT; i++) begin
                tx_new_queue[i] <= '0;
                tx_new_queue_valid[i] <= 0;
                tx_byte[i] <= '0;
                tx_valid[i] <= 0;
                rx_ready[i] <= 0;
                rx_queue_addr[i] <= '0;
                dest_buff[i] <= '0;
                reuse_queue_slot[i] <= 0;
                new_slot_cooldown[i] <= 0;
            end
            mem_read_addr <= '0;
            mem_write_addr <= '0;
            mem_write_enable <= 0;
            mem_write_byte <= '0;
        end else begin
            // NOTE: signaled the servicing interface in the last cycle
            tx_new_queue_valid[dest_buff[curr - 1]] <= 0;

            // IMPORTANT: interfaces should send the byte no matter what, rx_ready is to prevent starting a new packet
            if (rx_valid[curr]) begin
                // IMPORTANT: memory_write_addr is ready on the next cycle
                if (rx_pkt_addr[curr] == 0 && !reuse_queue_slot[curr] &&
                    !(|new_slot_cooldown[curr])) begin
                    request_new_slot <= 1;
                    rx_queue_addr[curr] <= new_slot_addr;
                    mem_write_addr <= {new_slot_addr, rx_pkt_addr[curr]};
                    new_slot_cooldown[curr] <= NEW_SLOT_COOLDOWN;
                end else begin // if (rx_new_packet[curr])
                    reuse_queue_slot[curr] <= 0;
                    mem_write_addr <= {rx_queue_addr[curr], rx_pkt_addr[curr]};
                    request_new_slot <= 0;
                    if (rx_pkt_addr[curr] == ROSE_DEST_INDEX) begin
                        dest_buff[curr] <= next_hop(rx_byte[curr]);
                    end
                end // else: !if(rx_new_packet[curr])
                if (|new_slot_cooldown[curr]) begin
                    new_slot_cooldown[curr] <= new_slot_cooldown[curr] - 1;
                end
                mem_write_byte <= rx_byte[curr];
                mem_write_enable <= 1;
                if (&rx_pkt_addr[curr]) begin // packet complete
                    if (tx_new_queue_ready[dest_buff[curr]]) begin
                        tx_new_queue[dest_buff[curr]] <= rx_queue_addr[curr];
                        tx_new_queue_valid[dest_buff[curr]] <= 1;
                    end else begin
                        reuse_queue_slot[curr] <= 1;
                    end
                end
            end else begin // if (rx_valid[curr])
                mem_write_enable <= 0;
            end // else: !if(rx_valid[curr])

            // IMPORTANT: tx_ready is only signaled when tx_pkt_addr is valid
            if (tx_ready[curr]) begin
                last_served_read <= 1;
                mem_read_addr <= {tx_queue_addr[curr], tx_pkt_addr[curr]};
                if (&tx_pkt_addr[curr]) begin
                    empty_slot_addr <= tx_queue_addr[curr];
                    empty_slot_enqueue <= 1;
                end else begin
                    empty_slot_enqueue <= 0;
                end
            end else begin
                empty_slot_enqueue <= 0;
                last_served_read <= 0;
            end // else: !if(tx_ready[curr])
            if (last_served_read) begin
                tx_byte[curr - 1] <= mem_read_byte;
                tx_valid[curr - 1] <= 1;
            end else begin
                tx_valid[curr - 1] <= 0;
            end
        end
    end
endmodule // hub

// IMPORTANT: the current queue_addr is always valid unless queue_empty
// REQUIRES: hub does not request a new slot when the queue is empty
module free_queue(input logic                         sys_clk,
                  input logic                         rst,
                  input logic                         request_new_slot,
                  input logic [QUEUE_ADDR_LEN - 1:0]  empty_slot_addr,
                  input logic                         empty_slot_enqueue,
                  output logic [QUEUE_ADDR_LEN - 1:0] new_slot_addr,
                  output logic                        queue_empty,
                  output logic                        queue_ready);
    timeunit 1ns;
    timeprecision 1ps;

    logic [QUEUE_ADDR_LEN - 1:0] fqueue [QUEUE_SIZE - 1:0];
    logic [QUEUE_ADDR_LEN - 1:0] head;
    logic [QUEUE_ADDR_LEN - 1:0] tail;
    shortint                     queue_size;

    enum logic [1:0] {VOID, INIT, READY} state = VOID;

    assign queue_empty = queue_size == 0;

    always_ff @ (posedge sys_clk) begin
        new_slot_addr <= fqueue[head];
        
        if (state == VOID) begin
            queue_ready <= '0;
            head <= '0;
            tail <= '0;
            queue_size <= 0;
            state <= INIT;
        end else if (state == INIT) begin
            if (tail != {QUEUE_ADDR_LEN{1'b1}}) begin
                fqueue[tail] <= queue_size[QUEUE_ADDR_LEN - 1:0];
                tail <= tail + 1;
                queue_size <= queue_size + 1;
            end else begin
                state <= READY;
                queue_ready <= 1;
            end
        end else begin
            if (request_new_slot) begin
                head <= head + 1;
                queue_size <= queue_size - 1;
            end
            if (empty_slot_enqueue) begin
                fqueue[tail] <= empty_slot_addr;
                tail <= tail + 1;
                queue_size <= queue_size + 1;
            end
        end // else: !if(fqueue_state_t == INIT)
    end
    
endmodule // free_queue

module memory_pool(input logic                              sys_clk,
                   input logic                              rst,
                   input logic [MEMORY_POOL_ADDR_LEN - 1:0] read_addr,
                   input logic [MEMORY_POOL_ADDR_LEN - 1:0] write_addr,
                   input logic [7:0]                        write_byte,
                   input logic                              write_enable,
                   output logic [7:0]                       read_byte);
    timeunit 1ns;
    timeprecision 1ps;

    logic [7:0] mem_pool[MEMORY_POOL_SIZE - 1:0];

    always_ff @ (posedge sys_clk or rst) begin
        if (rst) begin
            read_byte <= 8'hFF;
        end else begin
            if (write_enable)
                mem_pool[write_addr] <= write_byte;
            read_byte <= mem_pool[read_addr];
        end
    end
endmodule // memory_pool
