//
// Copyright 2015 Ettus Research LLC
//
`ifndef INCLUDED_SIM_CVITA_LIB
`define INCLUDED_SIM_CVITA_LIB

`include "sim_axis_lib.svh"

typedef logic [63:0] cvita_payload_t[$];

// CVITA packet types
typedef enum logic [1:0] {
  DATA=2'b00, FC=2'b01, CMD=2'b10, RESP=2'b11
} cvita_pkt_type_t;

// CVITA Header
typedef struct packed {
  cvita_pkt_type_t  pkt_type;
  logic             has_time;
  logic             eob;
  logic [11:0]      seqnum;
  logic [15:0]      length;
  logic [15:0]      src_sid;
  logic [15:0]      dst_sid;
  logic [63:0]      timestamp;
} cvita_hdr_t;

// CVITA Packet, Header + Payload
typedef struct {
  cvita_hdr_t     hdr;
  cvita_payload_t payload;
} cvita_pkt_t;

// CVITA packet metadata
typedef struct {
  logic         eob = 1'b0;
  logic         has_time = 1'b0;
  logic [63:0]  timestamp = 'd0;
} cvita_metadata_t;

function logic[63:0] flatten_chdr_no_ts(input cvita_hdr_t hdr);
  return {hdr.pkt_type, 1'b0 /* No timestamp */, hdr.eob, hdr.seqnum, hdr.length, hdr.src_sid, hdr.dst_sid};
endfunction

//TODO: This should be a function but it segfaults XSIM.
task automatic unflatten_chdr_no_ts;
  input logic[63:0] hdr_bits;
  output cvita_hdr_t hdr;
  begin
    hdr = '{
      pkt_type:cvita_pkt_type_t'(hdr_bits[63:62]), has_time:hdr_bits[61], eob:hdr_bits[60],
      seqnum:hdr_bits[59:48], length:hdr_bits[47:32], src_sid:hdr_bits[31:16], dst_sid:hdr_bits[15:0], timestamp:0  //Default timestamp
    };
  end
endtask

// Extracts header from CVITA packets.
// Args:
// - pkt: CVITA packet
// - hdr: CVITA header
task automatic extract_chdr(ref logic [63:0] pkt[$], ref cvita_hdr_t hdr);
  begin
    unflatten_chdr_no_ts(pkt[0],hdr);
    if (hdr.has_time) begin
      hdr.timestamp = pkt[1];
      // Delete both header and time stamp
      pkt = pkt[2:pkt.size-1];
    end else begin
      // Delete header
      pkt = pkt[1:pkt.size-1];
    end
  end
endtask

// Drops header from CVITA packets leaving only payload data.
// Args:
// - pkt: CVITA packet
task automatic drop_chdr(ref logic [63:0] pkt[$]);
  begin
    automatic cvita_hdr_t hdr;
    extract_chdr(pkt,hdr);
  end
endtask

task automatic unflatten_chdr;
  input logic[63:0] hdr_bits;
  input logic[63:0] timestamp;
  output cvita_hdr_t hdr;
  begin
    hdr = '{
      pkt_type:cvita_pkt_type_t'(hdr_bits[63:62]), has_time:hdr_bits[61], eob:hdr_bits[60],
      seqnum:hdr_bits[59:48], length:hdr_bits[47:32], src_sid:hdr_bits[31:16], dst_sid:hdr_bits[15:0], timestamp:timestamp
    };
  end
endtask

function logic chdr_compare(input cvita_hdr_t a, input cvita_hdr_t b);
  return ((a.pkt_type == b.pkt_type) && (a.has_time == b.has_time) && (a.eob == b.eob) &&
          (a.seqnum == b.seqnum) && (a.length == b.length) && (a.src_sid == b.src_sid) && (a.dst_sid == b.dst_sid));
endfunction

typedef struct packed {
  logic [31:0]  count;
  logic [63:0]  sum;
  logic [63:0]  min;
  logic [63:0]  max;
  logic [63:0]  crc;
} cvita_stats_t;

// Interface and tasks to send CVITA packets over a master AXI-Stream bus
interface cvita_master #(parameter DWIDTH = 64, parameter NUM_STREAMS = 1)(input clk);
  axis_t #(.DWIDTH(DWIDTH), .NUM_STREAMS(NUM_STREAMS)) axis(.clk(clk));

  // Check that stream is actually in use
  function void check_stream(int stream);
    assert (stream < NUM_STREAMS) else
      $error("cvita_master::check_stream(): Tried to perform operation on unused stream %0d", stream);
  endfunction

  // Reset signals / properties used by this interface
  task automatic reset;
    begin
      axis.tvalid = 0;
      axis.tlast  = 0;
      axis.tdata  = 0;
    end
  endtask

  // Push a word onto the AXI-Stream bus and wait for it to transfer
  // Args:
  // - word: The data to push onto the bus
  // - eop: End of packet (asserts tlast) (Optional)
  // - stream: Stream to use (Optional)
  task automatic push_word (
    input logic [DWIDTH-1:0] word,
    input logic eop = 1'b0,
    input int stream = 0);
    begin
      check_stream(stream);
      if (clk) @(negedge clk);                    // Align with negative edge
      axis.tvalid[stream] = 1;
      axis.tlast[stream] = eop;
      axis.tdata[DWIDTH*stream +: DWIDTH] = word;
      @(posedge clk);                             // Put sample on data bus
      while(~axis.tready[stream]) @(posedge clk); // Wait until receiver ready
      @(negedge clk);                             // Put sample on data bus
      axis.tvalid[stream] = 0;
      axis.tlast[stream] = 0;
    end
  endtask

  // Push a bubble cycle onto the AXI-Stream bus
  // Args:
  // - stream: Stream to use (Optional)
  task automatic push_bubble (input int stream = 0);
    begin
      check_stream(stream);
      axis.tvalid[stream] = 0;
      @(negedge clk);
    end
  endtask

  // Push a CVITA header into the stream
  // Args:
  // - hdr: The header to push
  // - stream: Stream to use (Optional)
  task automatic push_hdr (
    input cvita_hdr_t hdr,
    input int stream = 0);
    push_word(flatten_chdr_no_ts(hdr), 0, stream);
  endtask

  // Push a packet with random data onto to the AXI Stream bus
  // Args:
  // - num_samps: Packet size.
  // - hdr: Header to attach to packet (length will be ignored)
  // - stream: Stream to use (Optional)
  task automatic push_rand_pkt (
    input integer num_samps,
    input cvita_hdr_t hdr,
    input int stream = 0);
    begin
      cvita_hdr_t tmp_hdr = hdr;
      tmp_hdr.length = (num_samps * 8) + (hdr.has_time ? 16 : 8);
      @(negedge clk);
      push_hdr(tmp_hdr);
      if (hdr.has_time) push_word(hdr.timestamp, 0, stream);
      repeat(num_samps-1) begin
        push_word({$random,$random}, 0, stream);
      end
      push_word({$random,$random}, 1, stream);
    end
  endtask

  // Push a packet with a ramp on to the AXI Stream bus
  // Args:
  // - num_samps: Packet size.
  // - ramp_start: Start value for the ramp
  // - ramp_inc: Increment per clock cycle
  // - hdr: Header to attach to packet (length will be ignored)
  // - stream: Stream to use (Optional)
  task automatic push_ramp_pkt (
    input integer num_samps,
    input logic [63:0] ramp_start,
    input logic [63:0] ramp_inc,
    input cvita_hdr_t hdr,
    input int stream = 0);
    begin
      automatic integer counter = 0;
      cvita_hdr_t tmp_hdr = hdr;
      tmp_hdr.length = (num_samps * 8) + (hdr.has_time ? 16 : 8);
      @(negedge clk);
      push_hdr(tmp_hdr);
      if (hdr.has_time) push_word(hdr.timestamp, 0, stream);
      repeat(num_samps-1) begin
        push_word(ramp_start+(counter*ramp_inc), 0, stream);
        counter = counter + 1;
      end
      push_word(ramp_start+(counter*ramp_inc), 1, stream);
    end
  endtask

  // Push a packet on to the AXI Stream bus.
  // Args:
  // - pkt: Packet data (queue)
  // - stream: Stream to use (Optional)
  task automatic push_pkt (
    input cvita_pkt_t pkt,
    input int stream = 0);
    begin
      // Use $ceil() to match packets with partial lines
      int pkt_size;
      // Vivado XSIM workaround
      cvita_payload_t payload = pkt.payload;
      pkt_size = (pkt.hdr.has_time ? payload.size() + 2 : payload.size() + 1);
      assert (pkt.hdr.length == 8*pkt_size || pkt.hdr.length == 8*pkt_size-4)
        else $error("cvita_master::push_pkt(): Packet size does not match packet length in header! Header: %0d, Actual: %0d",
                    pkt.hdr.length, 8*pkt_size);
      if (pkt.hdr.length == 8) begin
        push_word(pkt.hdr[127:64], 1, stream);
      end else begin
        push_word(pkt.hdr[127:64], 0, stream);
        if (pkt.hdr.has_time) begin
          push_word(pkt.hdr[63:0], (pkt.hdr.length == 16), stream);
        end
        for (int i = 0; i < pkt.payload.size()-1; i = i + 1) begin
          push_word(pkt.payload[i], 0, stream);
        end
        push_word(pkt.payload[pkt.payload.size()-1], 1, stream);
      end
    end
  endtask

endinterface


// Interface and tasks to receive CVITA packets over a slave AXI-Stream bus
interface cvita_slave #(parameter DWIDTH = 64, parameter NUM_STREAMS = 1)(input clk);
  axis_t #(.DWIDTH(DWIDTH), .NUM_STREAMS(NUM_STREAMS)) axis(.clk(clk));

  // Check that stream is actually in use
  function void check_stream(int stream);
    assert (stream < NUM_STREAMS) else
      $error("cvita_slave::check_stream(): Tried to perform operation on unused stream %0d", stream);
  endfunction

  // Reset signals / properties used by this interface
  task automatic reset;
    begin
      axis.tready = 0;
    end
  endtask

  // Accept a sample on the AXI Stream bus and
  // return the data and last
  // Args:
  // - word: The data pulled from the bus
  // - eop: End of packet (tlast)
  // - stream: Stream to use (Optional)
  task automatic pull_word (
    output logic [DWIDTH-1:0] word,
    output logic eop,
    input int stream = 0);
    begin
      check_stream(stream);
      if (clk) @(negedge clk);
      axis.tready[stream] = 1;
      while(~axis.tvalid[stream]) @(posedge clk);
      word = axis.tdata[DWIDTH*stream +: DWIDTH];
      eop = axis.tlast[stream];
      @(negedge clk);
      axis.tready[stream] = 0;
    end
  endtask

  // Wait for a sample to be transferred on the AXI Stream
  // bus and return the data and last. Note, this task only
  // observes the bus and does not affect the AXI control
  // signals.
  // Args:
  // - word: The data pulled from the bus
  // - eop: End of packet (tlast)
  // - stream: Stream to use (Optional)
  task automatic copy_word (
    output logic [DWIDTH-1:0] word,
    output logic eop,
    input int stream = 0);
    begin
      check_stream(stream);
      while(~(axis.tready[stream]&axis.tvalid[stream])) @(posedge clk);  // Wait until sample is transferred
      word = axis.tdata[DWIDTH*stream +: DWIDTH];
      eop = axis.tlast[stream];
      @(negedge clk);
    end
  endtask

  // Wait for a bubble cycle on the AXI Stream bus
  // Args:
  // - stream: Stream to use (Optional)
  task automatic wait_for_bubble (
    input int stream = 0);
    begin
      check_stream(stream);
      while(axis.tready[stream]&axis.tvalid[stream]) @(posedge clk);
      @(negedge clk);
    end
  endtask

  // Wait for a packet to finish on the bus
  // Args:
  // - stream: Stream to use (Optional)
  task automatic wait_for_pkt (
    input int stream = 0);
    begin
      check_stream(stream);
      while(~(axis.tready[stream]&axis.tvalid[stream]&axis.tlast[stream])) @(posedge clk);
      @(negedge clk);
    end
  endtask

  // Pull a packet from the AXI Stream bus.
  // Args:
  // - pkt: Packet data (queue)
  // - stream: Stream to use (Optional)
  task automatic pull_pkt (
    output cvita_pkt_t pkt,
    input int stream = 0);
    begin
      cvita_payload_t payload;  // Vivado XSIM work around
      logic [63:0] word;
      int i = 0;
      logic eop = 0;
      int pkt_size;

      while(~eop) begin
        pull_word(word, eop, stream);
        if (i == 0) begin
          pkt.hdr[127:64] = word;
        end else if ((i == 1) && pkt.hdr.has_time) begin
          pkt.hdr[63:0] = word;
        end else begin
          payload.push_back(word);
        end
        i++;
      end
      pkt.payload = payload;
      pkt_size = (pkt.hdr.has_time ? payload.size() + 2 : payload.size() + 1);
      assert (pkt.hdr.length == 8*pkt_size || pkt.hdr.length == 8*pkt_size-4)
        else $error("cvita_slave::pull_pkt(): Packet size does not match packet length in header! Header: %0d, Actual: %0d",
                    pkt.hdr.length, pkt_size*8);
    end
  endtask

  // Pull a packet from the AXI Stream bus and
  // drop it instead of passing data to the user.
  // Args:
  // - stream: Stream to use (Optional)
  task automatic drop_pkt (
    input int stream = 0);
    begin
      cvita_pkt_t pkt;
      pull_pkt(pkt, stream);
    end
  endtask

  // Vivado XSIM workaround, cannot be made into a task (with ref inputs) due to segfault
  `define WAIT_FOR_PKT_GET_INFO__UPDATE \
    stats.count = stats.count + 1; \
    stats.sum   = stats.sum + axis.tdata[DWIDTH*stream +: DWIDTH]; \
    stats.crc   = stats.crc ^ axis.tdata[DWIDTH*stream +: DWIDTH]; \
    if (axis.tdata < stats.min) stats.min = axis.tdata[DWIDTH*stream +: DWIDTH]; \
    if (axis.tdata > stats.max) stats.max = axis.tdata[DWIDTH*stream +: DWIDTH];

  // Wait for a packet to finish on the bus
  task automatic wait_for_pkt_get_info (
    output cvita_hdr_t hdr,
    output cvita_stats_t stats,
    input int stream = 0);
    begin
      automatic logic is_hdr  = 1;
      automatic logic is_time = 0;
      stats.count = 32'h0;
      stats.sum   = 64'h0;
      stats.min   = 64'h7FFFFFFFFFFFFFFF;
      stats.max   = 64'h0;
      stats.crc   = 64'h0;
      check_stream(stream);
      @(posedge clk);
      //Corner case. We are already looking at the end
      //of a packet i.e. its just a header
      if (axis.tready[stream]&axis.tvalid[stream]&axis.tlast[stream]) begin
        unflatten_chdr_no_ts(axis.tdata[DWIDTH*stream +: DWIDTH], hdr);
        @(negedge clk);
      end else begin
        while(~(axis.tready[stream]&axis.tvalid[stream]&axis.tlast[stream])) begin
          if (axis.tready[stream]&axis.tvalid[stream]) begin
            if (is_hdr) begin
              unflatten_chdr_no_ts(axis.tdata[DWIDTH*stream +: DWIDTH], hdr);
              is_time = hdr.has_time;
              is_hdr = 0;
            end else if (is_time) begin
              hdr.timestamp = axis.tdata[DWIDTH*stream +: DWIDTH];
              is_time = 0;
            end else begin
              `WAIT_FOR_PKT_GET_INFO__UPDATE
            end
          end
          @(posedge clk);
        end
        `WAIT_FOR_PKT_GET_INFO__UPDATE
        @(negedge clk);
      end
    end
  endtask

  `undef WAIT_FOR_PKT_GET_INFO__UPDATE

endinterface

`endif