module axi_serializer #(
  /// Maximum number of in flight read transactions.
  parameter int unsigned MaxReadTxns  = 32'd0,
  /// Maximum number of in flight write transactions.
  parameter int unsigned MaxWriteTxns = 32'd0,
  /// AXI4+ATOP ID width.
  parameter int unsigned AxiIdWidth   = 32'd0,
  /// AXI4+ATOP request struct definition.
  parameter type         req_t        = logic,
  /// AXI4+ATOP response struct definition.
  parameter type         resp_t       = logic
) (
  /// Clock
  input  logic  clk_i,
  /// Asynchronous reset, active low
  input  logic  rst_ni,
  /// Slave port request
  input  req_t  slv_req_i,
  /// Slave port response
  output resp_t slv_resp_o,
  /// Master port request
  output req_t  mst_req_o,
  /// Master port response
  input  resp_t mst_resp_i
);

  typedef logic [AxiIdWidth-1:0] id_t;
  typedef enum logic [1:0] {
    AtopIdle    = 2'b00,
    AtopDrain   = 2'b01,
    AtopExecute = 2'b10
  } state_e;

  logic   rd_fifo_full, rd_fifo_empty, rd_fifo_push, rd_fifo_pop,
          wr_fifo_full, wr_fifo_empty, wr_fifo_push, wr_fifo_pop;
  id_t    b_id,
          r_id,         ar_id;
  state_e state_q,      state_d;

  always_comb begin
    // Default assignments
    state_d      = state_q;
    rd_fifo_push = 1'b0;
    wr_fifo_push = 1'b0;

    // Default, connect the channels
    mst_req_o  = slv_req_i;
    slv_resp_o = mst_resp_i;

    // Serialize transactions -> tie downstream IDs to zero.
    mst_req_o.aw.id = '0;
    mst_req_o.ar.id = '0;

    // Reflect upstream ID in response.
    ar_id           = slv_req_i.ar.id;
    slv_resp_o.b.id = b_id;
    slv_resp_o.r.id = r_id;

    // Default, cut the AW/AR handshaking
    mst_req_o.ar_valid  = 1'b0;
    slv_resp_o.ar_ready = 1'b0;
    mst_req_o.aw_valid  = 1'b0;
    slv_resp_o.aw_ready = 1'b0;

    unique case (state_q)
      AtopIdle, AtopExecute: begin

        // Wait until the ATOP response(s) have been sent back upstream.
        if (state_q == AtopExecute) begin
          if ((wr_fifo_empty && rd_fifo_empty) || (wr_fifo_pop && rd_fifo_pop) ||
              (wr_fifo_empty && rd_fifo_pop)   || (wr_fifo_pop && rd_fifo_empty)) begin
            state_d = AtopIdle;
          end
        end

        // This part lets new Transactions through, if no ATOP is underway or the last ATOP
        // response has been transmitted.
        if ((state_q == AtopIdle) || (state_d == AtopIdle)) begin
          // Gate AR handshake with ready output of Read FIFO.
          mst_req_o.ar_valid  = slv_req_i.ar_valid  & ~rd_fifo_full;
          slv_resp_o.ar_ready = mst_resp_i.ar_ready & ~rd_fifo_full;
          rd_fifo_push        = mst_req_o.ar_valid  & mst_resp_i.ar_ready;
          if (slv_req_i.aw_valid) begin
            if (slv_req_i.aw.atop[5:4] == axi_pkg::ATOP_NONE) begin
              // Normal operation
              // Gate AW handshake with ready output of Write FIFO.
              mst_req_o.aw_valid  = ~wr_fifo_full;
              slv_resp_o.aw_ready = mst_resp_i.aw_ready & ~wr_fifo_full;
              wr_fifo_push        = mst_req_o.aw_valid  & mst_resp_i.aw_ready;
            end else begin
              // Atomic Operation received, go to drain state, when both channels are ready
              // Wait for finished or no AR beat
              if (!mst_req_o.ar_valid || (mst_req_o.ar_valid && mst_resp_i.ar_ready)) begin
                state_d = AtopDrain;
              end
            end
          end
        end
      end
      AtopDrain: begin
        // Send the ATOP AW when the last open transaction terminates
        if (wr_fifo_empty && rd_fifo_empty) begin
          mst_req_o.aw_valid  = 1'b1;
          slv_resp_o.aw_ready = mst_resp_i.aw_ready;
          wr_fifo_push        = mst_resp_i.aw_ready;
          if (slv_req_i.aw.atop[axi_pkg::ATOP_R_RESP]) begin
            // Overwrite the read ID with the one from AW
            ar_id        = slv_req_i.aw.id;
            rd_fifo_push = mst_resp_i.aw_ready;
          end
          if (mst_resp_i.aw_ready) begin
            state_d = AtopExecute;
          end
        end
      end
      default : /* do nothing */;
    endcase

    // Gate B handshake with empty flag output of Write FIFO.
    slv_resp_o.b_valid = mst_resp_i.b_valid & ~wr_fifo_empty;
    mst_req_o.b_ready  = slv_req_i.b_ready  & ~wr_fifo_empty;

    // Gate R handshake with empty flag output of Read FIFO.
    slv_resp_o.r_valid = mst_resp_i.r_valid & ~rd_fifo_empty;
    mst_req_o.r_ready  = slv_req_i.r_ready  & ~rd_fifo_empty;
  end

  fifo_v3 #(
    .FALL_THROUGH ( 1'b0        ), // No fall-through as response has to come a cycle later anyway
    .DEPTH        ( MaxReadTxns ),
    .dtype        ( id_t        )
  ) i_rd_id_fifo (
    .clk_i,
    .rst_ni,
    .flush_i    ( 1'b0          ),
    .testmode_i ( 1'b0          ),
    .data_i     ( ar_id         ),
    .push_i     ( rd_fifo_push  ),
    .full_o     ( rd_fifo_full  ),
    .data_o     ( r_id          ),
    .empty_o    ( rd_fifo_empty ),
    .pop_i      ( rd_fifo_pop   ),
    .usage_o    ( /*not used*/  )
  );
  // Assign as this condition is needed in FSM
  assign rd_fifo_pop = slv_resp_o.r_valid & slv_req_i.r_ready & slv_resp_o.r.last;

  fifo_v3 #(
    .FALL_THROUGH ( 1'b0         ),
    .DEPTH        ( MaxWriteTxns ),
    .dtype        ( id_t         )
  ) i_wr_id_fifo (
    .clk_i,
    .rst_ni,
    .flush_i    ( 1'b0            ),
    .testmode_i ( 1'b0            ),
    .data_i     ( slv_req_i.aw.id ),
    .push_i     ( wr_fifo_push    ),
    .full_o     ( wr_fifo_full    ),
    .data_o     ( b_id            ),
    .empty_o    ( wr_fifo_empty   ),
    .pop_i      ( wr_fifo_pop     ),
    .usage_o    ( /*not used*/    )
  );
  // Assign as this condition is needed in FSM
  assign wr_fifo_pop = slv_resp_o.b_valid & slv_req_i.b_ready;

  `FFARN(state_q, state_d, AtopIdle, clk_i, rst_ni)

// pragma translate_off
`ifndef VERILATOR
  initial begin: p_assertions
    assert (AxiIdWidth    >= 1) else $fatal(1, "AXI ID width must be at least 1!");
    assert (MaxReadTxns   >= 1)
      else $fatal(1, "Maximum number of read transactions must be >= 1!");
    assert (MaxWriteTxns  >= 1)
      else $fatal(1, "Maximum number of write transactions must be >= 1!");
  end
  default disable iff (~rst_ni);
  aw_lost : assert property( @(posedge clk_i)
      (slv_req_i.aw_valid & slv_resp_o.aw_ready |-> mst_req_o.aw_valid & mst_resp_i.aw_ready))
    else $error("AW beat lost.");
  w_lost  : assert property( @(posedge clk_i)
      (slv_req_i.w_valid & slv_resp_o.w_ready |-> mst_req_o.w_valid & mst_resp_i.w_ready))
    else $error("W beat lost.");
  b_lost  : assert property( @(posedge clk_i)
      (mst_resp_i.b_valid & mst_req_o.b_ready |-> slv_resp_o.b_valid & slv_req_i.b_ready))
    else $error("B beat lost.");
  ar_lost : assert property( @(posedge clk_i)
      (slv_req_i.ar_valid & slv_resp_o.ar_ready |-> mst_req_o.ar_valid & mst_resp_i.ar_ready))
    else $error("AR beat lost.");
  r_lost :  assert property( @(posedge clk_i)
      (mst_resp_i.r_valid & mst_req_o.r_ready |-> slv_resp_o.r_valid & slv_req_i.r_ready))
    else $error("R beat lost.");
`endif
// pragma translate_on
endmodule