// Runtime palette staging: one CLOCK2_50 write port and one design-clock
// copy port. The host fills all 256 entries, then flips commit once. The
// design domain copies the stable bank into clut_stub and returns a sum32.
`timescale 1ns/1ps

module clut_stage_cdc (
    input  logic        bclk,
    input  logic        breset_n,
    input  logic        b_we,
    input  logic [7:0]  b_waddr,
    input  logic [31:0] b_wdata,
    input  logic        b_commit_tgl,
    input  logic [31:0] b_expected_crc,

    input  logic        dclk,
    input  logic        dreset_n,
    output logic        clut_wr_en,
    output logic [7:0]  clut_wr_idx,
    output logic [31:0] clut_wr_data,
    output logic        busy,
    output logic        done_tgl,
    output logic [31:0] crc
);

    // 256 x 32 fits in one M20K and remains a true dual-clock staging bank
    // on the board instead of becoming a wide register array.
    (* ramstyle = "M20K" *) logic [31:0] stage_mem [0:255];
    logic [7:0]  rd_addr;
    logic [31:0] rd_data_q;

    always_ff @(posedge bclk) begin
        if (b_we)
            stage_mem[b_waddr] <= b_wdata;
    end

    // This is the read side of the small dual-clock staging RAM. The host
    // must finish writes before commit; commit is the coherency boundary.
    always_ff @(posedge dclk) begin
        rd_data_q <= stage_mem[rd_addr];
    end

    (* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED" *)
    logic [2:0] commit_sync;
    logic       commit_seen;
    logic [31:0] expected_crc_q;
    logic [7:0] copy_idx;
    logic [31:0] crc_acc;
    typedef enum logic [1:0] { S_IDLE, S_PRIME, S_COPY } state_t;
    state_t state;

    always_ff @(posedge dclk or negedge dreset_n) begin
        if (!dreset_n) begin
            commit_sync    <= 3'b000;
            commit_seen    <= 1'b0;
            expected_crc_q <= 32'd0;
            rd_addr        <= 8'd0;
            copy_idx       <= 8'd0;
            crc_acc        <= 32'd0;
            crc            <= 32'd0;
            clut_wr_en     <= 1'b0;
            clut_wr_idx    <= 8'd0;
            clut_wr_data   <= 32'd0;
            busy            <= 1'b0;
            done_tgl        <= 1'b0;
            state           <= S_IDLE;
        end else begin
            commit_sync <= {commit_sync[1:0], b_commit_tgl};
            clut_wr_en  <= 1'b0;

            // Consume any commit edge observed while a copy is live. A host
            // retry belongs after done, never behind the current transaction.
            if (busy && (commit_sync[2] != commit_seen))
                commit_seen <= commit_sync[2];

            case (state)
                S_IDLE: begin
                    // A commit that arrives during a copy is deliberately not
                    // consumed. The host polls busy/done before reusing bank.
                    if (commit_sync[2] != commit_seen) begin
                        commit_seen    <= commit_sync[2];
                        expected_crc_q <= b_expected_crc;
                        rd_addr        <= 8'd0;
                        copy_idx       <= 8'd0;
                        crc_acc        <= 32'd0;
                        busy           <= 1'b1;
                        state          <= S_PRIME;
                    end
                end
                S_PRIME: begin
                    // Prime the synchronous read before writing entry zero.
                    rd_addr <= 8'd1;
                    state   <= S_COPY;
                end
                S_COPY: begin
                    clut_wr_en   <= 1'b1;
                    clut_wr_idx  <= copy_idx;
                    clut_wr_data <= rd_data_q;
                    crc_acc      <= crc_acc + rd_data_q;
                    if (copy_idx == 8'hFF) begin
                        crc      <= crc_acc + rd_data_q;
                        busy     <= 1'b0;
                        done_tgl <= ~done_tgl;
                        state    <= S_IDLE;
                    end else begin
                        copy_idx <= copy_idx + 8'd1;
                        rd_addr  <= rd_addr + 8'd1;
                    end
                end
                default: state <= S_IDLE;
            endcase
        end
    end

    // Keep expected_crc_q live in synthesis as an explicit documentation of
    // the host/design transaction contract. The bridge returns `crc` on done;
    // software compares it against its expected sum before GO.
    wire _unused_expected_crc = ^expected_crc_q;

endmodule : clut_stage_cdc
