module axi_sim_mem #(
  /// AXI Address Width
  parameter int unsigned AddrWidth = 32'd0,
  /// AXI Data Width
  parameter int unsigned DataWidth = 32'd0,
  /// AXI ID Width
  parameter int unsigned IdWidth = 32'd0,
  /// AXI User Width.
  parameter int unsigned UserWidth = 32'd0,
  /// AXI4 request struct definition
  parameter type req_t = logic,
  /// AXI4 response struct definition
  parameter type rsp_t = logic,
  /// Warn on accesses to uninitialized bytes
  parameter bit WarnUninitialized = 1'b0,
  /// Application delay (measured after rising clock edge)
  parameter time ApplDelay = 0ps,
  /// Acquisition delay (measured after rising clock edge)
  parameter time AcqDelay = 0ps
) (
  /// Rising-edge clock
  input  logic clk_i,
  /// Active-low reset
  input  logic rst_ni,
  /// AXI4 request struct
  input  req_t axi_req_i,
  /// AXI4 response struct
  output rsp_t axi_rsp_o
);

  localparam int unsigned StrbWidth = DataWidth / 8;
  typedef logic [AddrWidth-1:0] addr_t;
  typedef logic [DataWidth-1:0] data_t;
  typedef logic [IdWidth-1:0]   id_t;
  typedef logic [StrbWidth-1:0] strb_t;
  typedef logic [UserWidth-1:0] user_t;
  `AXI_TYPEDEF_AW_CHAN_T(aw_t, addr_t, id_t, user_t)
  `AXI_TYPEDEF_W_CHAN_T(w_t, data_t, strb_t, user_t)
  `AXI_TYPEDEF_B_CHAN_T(b_t, id_t, user_t)
  `AXI_TYPEDEF_AR_CHAN_T(ar_t, addr_t, id_t, user_t)
  `AXI_TYPEDEF_R_CHAN_T(r_t, data_t, id_t, user_t)

  logic [7:0] mem[addr_t];

  initial begin
    automatic ar_t ar_queue[$];
    automatic aw_t aw_queue[$];
    automatic b_t b_queue[$];
    automatic shortint unsigned r_cnt = 0, w_cnt = 0;
    axi_rsp_o = '0;
    wait (rst_ni);
    fork
      // AW
      forever begin
        @(posedge clk_i);
        #(ApplDelay);
        axi_rsp_o.aw_ready = 1'b1;
        #(AcqDelay - ApplDelay);
        if (axi_req_i.aw_valid) begin
          automatic aw_t aw = axi_req_i.aw;
          aw_queue.push_back(aw);
        end
      end
      // W
      forever begin
        @(posedge clk_i);
        #(ApplDelay);
        axi_rsp_o.w_ready = 1'b0;
        if (aw_queue.size() != 0) begin
          axi_rsp_o.w_ready = 1'b1;
          #(AcqDelay - ApplDelay);
          if (axi_req_i.w_valid) begin
            automatic axi_pkg::burst_t burst = aw_queue[0].burst;
            automatic axi_pkg::len_t len = aw_queue[0].len;
            automatic axi_pkg::size_t size = aw_queue[0].size;
            automatic addr_t addr = axi_pkg::beat_addr(aw_queue[0].addr, size, len, burst,
                w_cnt);
            for (shortint unsigned
                i_byte = axi_pkg::beat_lower_byte(addr, size, len, burst, StrbWidth, w_cnt);
                i_byte <= axi_pkg::beat_upper_byte(addr, size, len, burst, StrbWidth, w_cnt);
                i_byte++) begin
              if (axi_req_i.w.strb[i_byte]) begin
                automatic addr_t byte_addr = (addr / StrbWidth) * StrbWidth + i_byte;
                mem[byte_addr] = axi_req_i.w.data[i_byte*8+:8];
              end
            end
            if (w_cnt == aw_queue[0].len) begin
              automatic b_t b_beat = '0;
              assert (axi_req_i.w.last) else $error("Expected last beat of W burst!");
              b_beat.id = aw_queue[0].id;
              b_beat.resp = axi_pkg::RESP_OKAY;
              b_queue.push_back(b_beat);
              w_cnt = 0;
              void'(aw_queue.pop_front());
            end else begin
              assert (!axi_req_i.w.last) else $error("Did not expect last beat of W burst!");
              w_cnt++;
            end
          end
        end
      end
      // B
      forever begin
        @(posedge clk_i);
        #(ApplDelay);
        axi_rsp_o.b_valid = 1'b0;
        if (b_queue.size() != 0) begin
          axi_rsp_o.b = b_queue[0];
          axi_rsp_o.b_valid = 1'b1;
          #(AcqDelay - ApplDelay);
          if (axi_req_i.b_ready) begin
            void'(b_queue.pop_front());
          end
        end
      end
      // AR
      forever begin
        @(posedge clk_i);
        #(ApplDelay);
        axi_rsp_o.ar_ready = 1'b1;
        #(AcqDelay - ApplDelay);
        if (axi_req_i.ar_valid) begin
          automatic ar_t ar = axi_req_i.ar;
          ar_queue.push_back(ar);
        end
      end
      // R
      forever begin
        @(posedge clk_i);
        #(ApplDelay);
        axi_rsp_o.r_valid = 1'b0;
        if (ar_queue.size() != 0) begin
          automatic axi_pkg::burst_t burst = ar_queue[0].burst;
          automatic axi_pkg::len_t len = ar_queue[0].len;
          automatic axi_pkg::size_t size = ar_queue[0].size;
          automatic addr_t addr = axi_pkg::beat_addr(ar_queue[0].addr, size, len, burst, r_cnt);
          automatic r_t r_beat = '0;
          r_beat.data = 'x;
          r_beat.id = ar_queue[0].id;
          r_beat.resp = axi_pkg::RESP_OKAY;
          for (shortint unsigned
              i_byte = axi_pkg::beat_lower_byte(addr, size, len, burst, StrbWidth, r_cnt);
              i_byte <= axi_pkg::beat_upper_byte(addr, size, len, burst, StrbWidth, r_cnt);
              i_byte++) begin
            automatic addr_t byte_addr = (addr / StrbWidth) * StrbWidth + i_byte;
            if (!mem.exists(byte_addr)) begin
              if (WarnUninitialized) begin
                $warning("Access to non-initialized byte at address 0x%016x by ID 0x%x.", byte_addr,
                    r_beat.id);
              end
              r_beat.data[i_byte*8+:8] = 'x;
            end else begin
              r_beat.data[i_byte*8+:8] = mem[byte_addr];
            end
          end
          if (r_cnt == ar_queue[0].len) begin
            r_beat.last = 1'b1;
          end
          axi_rsp_o.r = r_beat;
          axi_rsp_o.r_valid = 1'b1;
          #(AcqDelay - ApplDelay);
          if (axi_req_i.r_ready) begin
            if (r_beat.last) begin
              r_cnt = 0;
              void'(ar_queue.pop_front());
            end else begin
              r_cnt++;
            end
          end
        end
      end
    join
  end

  // Parameter Assertions
  initial begin
    assert (AddrWidth != 0) else $fatal("AddrWidth must be non-zero!", 1);
    assert (DataWidth != 0) else $fatal("DataWidth must be non-zero!", 1);
    assert (IdWidth != 0) else $fatal("IdWidth must be non-zero!", 1);
    assert (UserWidth != 0) else $fatal("UserWidth must be non-zero!", 1);
  end

endmodule