`timescale 1ns/1ps
module round_robin_arbiter #(
    parameter int REQUESTERS = 4
) (
    input  logic                  clk,
    input  logic                  rst_n,
    input  logic [REQUESTERS-1:0] request,
    input  logic                  grant_ready,
    output logic [REQUESTERS-1:0] grant,
    output logic                  grant_valid
);
    localparam int INDEX_WIDTH = REQUESTERS > 1 ? $clog2(REQUESTERS) : 1;
    localparam logic [INDEX_WIDTH-1:0] LAST = INDEX_WIDTH'(REQUESTERS - 1);

    logic [INDEX_WIDTH-1:0] next_priority, held_index, selected;
    logic held, found;

    // A requester must retain its request and payload until accepted.
    // Once presented, the grant stays fixed while the downstream is stalled.
    always @* begin
        int candidate;
        candidate = 0;
        selected = held_index;
        found = held;
        for (int offset = 0; offset < REQUESTERS; offset++) begin
            candidate = int'(next_priority) + offset;
            if (candidate >= REQUESTERS)
                candidate = candidate - REQUESTERS;
            if (!found && request[candidate]) begin
                selected = INDEX_WIDTH'(candidate);
                found = 1'b1;
            end
        end

        grant_valid = rst_n && found;
        grant = '0;
        if (grant_valid)
            grant[selected] = 1'b1;
    end

    always_ff @(posedge clk) begin
        if (!rst_n) begin
            next_priority <= '0;
            held_index <= '0;
            held <= 1'b0;
        end else if (grant_valid) begin
            if (grant_ready) begin
                next_priority <= selected == LAST ? '0 : selected + 1'b1;
                held <= 1'b0;
            end else if (!held) begin
                held_index <= selected;
                held <= 1'b1;
            end
        end
    end
endmodule
