//
// Copyright 2020 Ettus Research, A National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: eth_ipv4_add_udp.sv
// Description: Add a UDP header onto an incoming CHDR stream
//
// Parameters:
//   - PREAMBLE_BYTES: Number of bytes of Preamble expected
//   - MAX_PACKET_BYTES: Maximum expected packet size
//   Constant fields in the added packet header
//   - ETH_TYPE: EthType in Ethernet Frame header
//   - MISC_IP: IP version / IP header Length / DSCP and ECN in IpV4 header
//   - FLAG_FRAG: Identification in IPv4 header
//   - TTL_PROT: Time to Live / Protocol in IPv4 header

module eth_ipv4_add_udp #(
  int          PREAMBLE_BYTES   = 6,
  int          MAX_PACKET_BYTES = 2**16,
  logic [15:0] ETH_TYPE = 16'h0800,  // IPv4
  logic [15:0] MISC_IP = { 4'd4 /* IPv4 */, 4'd5 /* IP HDR Len */, 8'h00 /* DSCP and ECN */},
  logic [15:0] IDENT = 16'h0,
  logic [15:0] FLAG_FRAG = { 3'b010 /* don't fragment */, 13'h0 },
  logic [15:0] TTL_PROT = { 8'h10 /* TTL */, 8'h11 /* UDP */ }
)(
  // Device addresses
  input  logic [47:0] mac_src,
  input  logic [31:0] ip_src,
  input  logic [15:0] udp_src,

  input  logic [47:0] mac_dst,
  input  logic [31:0] ip_dst,
  input  logic [15:0] udp_dst,

  // Ethernet Stream - CHDR ONLY
  AxiStreamIf.slave  i, // tUser = {*not used*}
  AxiStreamIf.master o  // tUser = {1'b0,trailing bytes};

);

  localparam ENET_USER_W = $clog2(i.DATA_WIDTH/8)+1;
  //---------------------------------------
  // Include for byte positions
  //---------------------------------------
  `include "eth_constants.vh"
  `include "../../axi4s_sv/axi4s.vh"

  //   tUser = {1'b0,trailing bytes(always full)}
  AxiStreamPacketIf #(.DATA_WIDTH(i.DATA_WIDTH),.USER_WIDTH(ENET_USER_W),.TKEEP(0),
                .MAX_PACKET_BYTES(MAX_PACKET_BYTES))
     s0(i.clk,i.rst);
  //   tUser = {1'b0,trailing bytes(after add)}
  AxiStreamPacketIf #(.DATA_WIDTH(i.DATA_WIDTH),.USER_WIDTH(ENET_USER_W),.TKEEP(0),
                .MAX_PACKET_BYTES(MAX_PACKET_BYTES))
     s1(i.clk,i.rst);
  //   tUser = {HeaderInfo,trailing bytes(after add)}
  AxiStreamPacketIf #(.DATA_WIDTH(i.DATA_WIDTH),.USER_WIDTH(ENET_USER_W+96+48),.TKEEP(0),
                .MAX_PACKET_BYTES(MAX_PACKET_BYTES))
     s2(i.clk,i.rst);
  //   tUser = {1'b0,trailing bytes}
  AxiStreamPacketIf #(.DATA_WIDTH(i.DATA_WIDTH),.USER_WIDTH(ENET_USER_W+96+48),.TKEEP(0),
                .MAX_PACKET_BYTES(MAX_PACKET_BYTES))
     s3(i.clk,i.rst);
  //   tUser = {1'b0,trailing bytes}
  AxiStreamPacketIf #(.DATA_WIDTH(i.DATA_WIDTH),.USER_WIDTH(ENET_USER_W),.TKEEP(0),
                .MAX_PACKET_BYTES(MAX_PACKET_BYTES))
     s4(i.clk,i.rst);

  //---------------------------------------
  // Ethernet Framer
  //---------------------------------------
  logic [15:0] chdr_len_new, chdr_len_old, chdr_len_reg;

  logic [15:0] ip_len, ip_len_reg;
  logic [15:0] udp_len, udp_len_reg;
  //delayed one clock cycle to pipeline
  logic [47:0] mac_dst_reg;
  logic [31:0] ip_dst_reg;
  logic [15:0] udp_dst_reg;
  logic [ENET_USER_W-1:0] trailing_bytes;

  always_comb begin : s0_assign
    `AXI4S_ASSIGN(s0,i)
    s0.tuser  = 0; // all full words going in
  end

  localparam BYTES_TO_ADD = UDP_END+1+PREAMBLE_BYTES;
  axi4s_add_bytes #(.ADD_START(0),.ADD_BYTES(BYTES_TO_ADD)
  ) add_header (
   .i(s0), .o(s1)
  );

  // try to calculate what tuser should be on the final word
  logic [15:0] total_len;
  logic [15:0] rem_bytes;

  always_ff @(posedge i.clk) begin : chdr_len_ff
    if (i.rst) begin
      chdr_len_old  <= '0;
    end else begin
      if (s0.tvalid && s0.tready) begin
         chdr_len_old  <= chdr_len_new;
      end
    end
  end

  always_comb begin : calc_length_fields
    // exract fields
    chdr_len_new  = s0.get_packet_field16(chdr_len_old,CHDR_LENGTH_BYTE);
    ip_len  = (16'd28 + chdr_len_new);  // 20 for IP, 8 for UDP
    udp_len = (16'd8 + chdr_len_new);
  end

  logic [15:0] iphdr_checksum;

  always_comb begin : pack_unpack_header_info
    `AXI4S_ASSIGN(s2,s1)
    s2.tuser  = { udp_len, ip_len, chdr_len_new, udp_dst, ip_dst, mac_dst, s1.tuser};
    { udp_len_reg, ip_len_reg, chdr_len_reg, udp_dst_reg, ip_dst_reg,
      mac_dst_reg,trailing_bytes} = s3.tuser;
  end

  axi4s_fifo #(
    .SIZE(1)
  ) s3_pipline_reg_ (
    .clear(1'b0),.space(),.occupied(),
    .i(s2),.o(s3)
  );

  logic chdr_end_early;

  // track when we cut off the outgoing packet
  always_ff @(posedge i.clk) begin : chdr_end_early_ff
    if (i.rst) begin
      chdr_end_early  <= 0;
    end else begin
      if (s3.tvalid && s3.tready && s3.tlast)
        chdr_end_early  <= 0;
      else if (s4.tvalid && s4.tready &&s4.tlast)
        chdr_end_early  <= 1;
    end
  end

  localparam [15:0] udp_checksum = 16'h0;

  ip_hdr_checksum  #(
    .LATENCY(1)
  ) ip_hdr_checksum_i (
    .clk(i.clk),
    .in({MISC_IP, ip_len, IDENT, FLAG_FRAG, TTL_PROT, 16'd0, ip_src, ip_dst}),
    .clken(s2.tready && s2.tvalid),
    .out(iphdr_checksum)
  );

  always_comb begin : calc_rem_bytes
    total_len = (BYTES_TO_ADD + chdr_len_reg);
    rem_bytes = total_len - s4.word_count*i.DATA_WIDTH/8;
  end

  // zero value fields are commented out because the fill goes to zero
  // if those fields become non zero uncomment them
  always_comb begin : set_header_fields
    // assign bus
    s4.tvalid = s3.tvalid && !chdr_end_early;
    s3.tready = s4.tready || chdr_end_early;
    if (rem_bytes > i.DATA_WIDTH/8 || chdr_end_early) begin
      s4.tlast = s3.tvalid && s3.tlast; // s0metimes packets end early
      s4.tuser = 0;
    end else if (rem_bytes == i.DATA_WIDTH/8) begin
      s4.tlast = s3.tvalid;
      s4.tuser = 0;
    end else begin
      s4.tlast = s3.tvalid;
      s4.tuser = rem_bytes;
    end
    s4.tdata = s3.tdata;
    s4.put_packet_field48(mac_dst_reg,      PREAMBLE_BYTES+DST_MAC_BYTE,     .NETWORK_ORDER(1));
    s4.put_packet_field48(mac_src,          PREAMBLE_BYTES+SRC_MAC_BYTE,     .NETWORK_ORDER(1));
    s4.put_packet_field16(ETH_TYPE,         PREAMBLE_BYTES+ETH_TYPE_BYTE,    .NETWORK_ORDER(1));
    s4.put_packet_field16(MISC_IP,          PREAMBLE_BYTES+IP_VERSION_BYTE,  .NETWORK_ORDER(1));
    s4.put_packet_field16(ip_len_reg,       PREAMBLE_BYTES+IP_LENGTH_BYTE,   .NETWORK_ORDER(1));
//  s4.put_packet_field16(IDENT,            PREAMBLE_BYTES+IP_ID_BYTE,       .NETWORK_ORDER(1));
    s4.put_packet_field16(FLAG_FRAG,        PREAMBLE_BYTES+IP_FRAG_BYTE,     .NETWORK_ORDER(1));
    s4.put_packet_field16(TTL_PROT,         PREAMBLE_BYTES+IP_TTL_BYTE,      .NETWORK_ORDER(1));
    s4.put_packet_field16(iphdr_checksum,   PREAMBLE_BYTES+IP_CHECKSUM_BYTE, .NETWORK_ORDER(1));
    s4.put_packet_field32(ip_src,           PREAMBLE_BYTES+SRC_IP_BYTE,      .NETWORK_ORDER(1));
    s4.put_packet_field32(ip_dst_reg,       PREAMBLE_BYTES+DST_IP_BYTE,      .NETWORK_ORDER(1));
    s4.put_packet_field16(udp_src,          PREAMBLE_BYTES+SRC_PORT_BYTE,    .NETWORK_ORDER(1));
    s4.put_packet_field16(udp_dst_reg,      PREAMBLE_BYTES+DST_PORT_BYTE,    .NETWORK_ORDER(1));
    s4.put_packet_field16(udp_len_reg,      PREAMBLE_BYTES+UDP_LENGTH_BYTE,  .NETWORK_ORDER(1));
//  s4.put_packet_field16(udp_checksum,     PREAMBLE_BYTES+UDP_CHECKSUM_BYTE,.NETWORK_ORDER(1));

  end

  always_comb begin : assign_output
    `AXI4S_ASSIGN(o,s4)
  end

endmodule // eth_ipv4_add_udp
