module axi_rate_change #(
  parameter WIDTH                   = 32, // Input bit width, must be a power of 2 and greater than or equal to 8.
  parameter MAX_N                   = 2**16,
  parameter MAX_M                   = 2**16,
  parameter MAXIMIZE_OUTPUT_PKT_LEN = 1,
  // Settings registers
  parameter SR_N_ADDR               = 0,
  parameter SR_M_ADDR               = 1,
  parameter SR_CONFIG_ADDR          = 2,
  parameter DEFAULT_N               = 1,
  parameter DEFAULT_M               = 1
)(
  input clk, input reset, input clear,
  output clear_user,  // Strobed after end of burst. Throttles input. Useful for resetting user code between bursts.
  input [15:0] src_sid, input [15:0] dst_sid,
  input set_stb, input [7:0] set_addr, input [31:0] set_data,
  input [WIDTH-1:0] i_tdata, input i_tlast, input i_tvalid, output i_tready, input [127:0] i_tuser,
  output [WIDTH-1:0] o_tdata, output o_tlast, output o_tvalid, input o_tready, output [127:0] o_tuser,
  output [WIDTH-1:0] m_axis_data_tdata, output m_axis_data_tlast, output m_axis_data_tvalid, input m_axis_data_tready,
  input [WIDTH-1:0] s_axis_data_tdata, input s_axis_data_tlast, input s_axis_data_tvalid, output s_axis_data_tready,
  // Debugging signals:
  // - Warnings indicate there may be an issue with user code.
  // - Errors mean the user code has violated a rule.
  // - Signals latch once set and block must be reset to clear.
  output reg warning_long_throttle,     // In the throttle state for a "long" time.
  output reg error_extra_outputs,       // User code generated extra outputs, i.e. received more than the expected M outputs.
  output reg error_drop_pkt_lockup      // Drop partial packet module is not accepting data even though user code is ready.
);

  reg [$clog2(MAX_N+1)-1:0] n = DEFAULT_N;
  reg [$clog2(MAX_M+1)-1:0] m = DEFAULT_M;

  wire [WIDTH-1:0] i_reg_tdata;
  wire i_reg_tvalid, i_reg_tready, i_reg_tlast;
  wire i_reg_tvalid_int, i_reg_tready_int, i_reg_tlast_int;

  reg throttle = 1'b1, first_header, partial_first_word;
  reg [15:0] word_cnt_div_n;
  reg [$clog2(MAX_N+1)-1:0] word_cnt_div_n_frac = 1;
  reg [$clog2(MAX_N+1)-1:0] in_pkt_cnt = 1;

  reg send_done;
  reg rate_changed;

  /********************************************************
  ** Settings Registers
  ********************************************************/
  wire [$clog2(MAX_N+1)-1:0] sr_n;
  wire n_changed;
  setting_reg #(.my_addr(SR_N_ADDR), .width($clog2(MAX_N+1)), .at_reset(DEFAULT_N)) set_n (
    .clk(clk), .rst(reset), .strobe(set_stb), .addr(set_addr), .in(set_data),
    .out(sr_n), .changed(n_changed));

  wire [$clog2(MAX_M+1)-1:0] sr_m;
  wire m_changed;
  setting_reg #(.my_addr(SR_M_ADDR), .width($clog2(MAX_M+1)), .at_reset(DEFAULT_M)) set_m (
    .clk(clk), .rst(reset), .strobe(set_stb), .addr(set_addr), .in(set_data),
    .out(sr_m), .changed(m_changed));

  wire sr_config;
  wire enable_clear_user; // Enable strobing clear_user between bursts.
  setting_reg #(.my_addr(SR_CONFIG_ADDR), .width(1), .at_reset(1'b1)) set_config (
    .clk(clk), .rst(reset), .strobe(set_stb), .addr(set_addr), .in(set_data),
    .out(sr_config), .changed());
  assign enable_clear_user = sr_config;

  /********************************************************
  ** Header, word count FIFOs
  ** - Header provides VITA Time and payload length for
  **   output packets
  ** - Word count provides a normalized count for the
  **   output state machine to know when it has consumed
  **   the final input sample in a burst.
  ********************************************************/
  // Decode input header
  wire [127:0] i_reg_tuser;
  wire has_time_in, eob_in, eob_in_header;
  wire [15:0] payload_length_in;
  reg [15:0] payload_length_out;
  wire [63:0] vita_time_in;
  cvita_hdr_decoder cvita_hdr_decoder_in_header (
    .header(i_reg_tuser), .pkt_type(), .eob(eob_in_header),
    .has_time(has_time_in), .seqnum(), .length(), .payload_length(payload_length_in),
    .src_sid(), .dst_sid(), .vita_time(vita_time_in));

  assign eob_in = eob_in_header | rate_changed;

  reg [15:0] word_cnt_div_n_tdata;
  wire [15:0] word_cnt_div_n_fifo_tdata;
  reg word_cnt_div_n_tvalid;
  wire word_cnt_div_n_tready, word_cnt_div_n_fifo_tvalid, word_cnt_div_n_fifo_tready;
  axi_fifo #(.WIDTH(16), .SIZE(0)) axi_fifo_word_cnt (
    .clk(clk), .reset(reset), .clear(clear),
    .i_tdata(word_cnt_div_n_tdata), .i_tvalid(word_cnt_div_n_tvalid), .i_tready(word_cnt_div_n_tready),
    .o_tdata(word_cnt_div_n_fifo_tdata), .o_tvalid(word_cnt_div_n_fifo_tvalid), .o_tready(word_cnt_div_n_fifo_tready),
    .space(), .occupied());

  /********************************************************
  ** Register input stream
  ** - Upsteam will be throttled when clearing user logic
  ********************************************************/
  // Input register with header
  axi_fifo_flop #(.WIDTH(WIDTH+1+128)) axi_fifo_flop_input (
    .clk(clk), .reset(reset), .clear(clear),
    .i_tdata({i_tlast,i_tdata,i_tuser}), .i_tvalid(i_tvalid), .i_tready(i_tready),
    .o_tdata({i_reg_tlast,i_reg_tdata,i_reg_tuser}), .o_tvalid(i_reg_tvalid_int), .o_tready(i_reg_tready),
    .space(), .occupied());

  assign i_reg_tready     = i_reg_tready_int & word_cnt_div_n_tready & ~throttle;
  assign i_reg_tvalid     = i_reg_tvalid_int & word_cnt_div_n_tready & ~throttle;
  // Assert AXI Drop Partial Packet's i_tlast every N samples, which is used to detect and drop
  // partial output samples.
  assign i_reg_tlast_int  = (word_cnt_div_n_frac == n) | (eob_in & i_reg_tlast);

  /********************************************************
  ** Input state machine
  ********************************************************/
  reg [1:0] input_state;
  localparam RECV_INIT                = 0;
  localparam RECV                     = 1;
  localparam RECV_WAIT_FOR_SEND_DONE  = 2;

  always @(posedge clk) begin
    if (reset | clear) begin
      n                     <= DEFAULT_N;
      m                     <= DEFAULT_M;
      rate_changed          <= 1'b0;
      first_header          <= 1'b1;
      partial_first_word    <= 1'b1;
      payload_length_out    <= 'd0;
      word_cnt_div_n        <= 0;
      word_cnt_div_n_frac   <= 1;
      throttle              <= 1'b1;
      word_cnt_div_n_tvalid <= 1'b0;
      word_cnt_div_n_tdata  <= 'd0;
      input_state           <= RECV_INIT;
    end else begin
      if (word_cnt_div_n_tvalid & word_cnt_div_n_tready) begin
        word_cnt_div_n_tvalid <= 1'b0;
      end
      // Input state machine
      case (input_state)
        RECV_INIT : begin
          n                      <= sr_n;
          m                      <= sr_m;
          rate_changed           <= 1'b0;
          first_header           <= 1'b1;
          partial_first_word     <= 1'b1;
          payload_length_out     <= 'd0;
          word_cnt_div_n         <= 0;
          word_cnt_div_n_frac    <= 1;
          if (i_reg_tvalid_int & word_cnt_div_n_tready) begin
            throttle             <= 1'b0;
            input_state          <= RECV;
          end
        end
        // Logic used by the RECV state to track several variables:
        // word_cnt_div_n:      Number of words received divided by n.
        //                      Needed for tracking final sample in a burst.
        // word_cnt_div_n_frac: Used to increment word_cnt_div_n. Can be
        //                      thought of as the fractional part of
        //                      word_cnt_div_n.
        // in_pkt_cnt:          Similar to in_word_cnt, but for packets. Used
        //                      to determine when a group of N packets has been
        //                      received to store the next header.
        // first_header:        We only use the header from the first packet in
        //                      a group of N packets (this greatly reduces
        //                      the header FIFO size).
        RECV : begin
          // If rate changed, force a EOB.
          if (m_changed) begin
            rate_changed           <= 1'b1;
          end
          if (i_reg_tvalid & i_reg_tready) begin
            // Track the number of words sent to the user divided by N.
            // At the end of a burst, this value is forwarded to the output
            // state machine and used to determine when the final sample has
            // arrived from the user code.
            if (word_cnt_div_n_frac == n) begin
              word_cnt_div_n       <= word_cnt_div_n + 1;
              word_cnt_div_n_frac  <= 1;
            end else begin
              word_cnt_div_n_frac  <= word_cnt_div_n_frac + 1;
            end
            // Use payload length from first packet
            first_header           <= 1'b0;
            if (first_header) begin
              payload_length_out   <= payload_length_in;
            end else if (MAXIMIZE_OUTPUT_PKT_LEN) begin
              if (payload_length_out < payload_length_in) begin
                payload_length_out <= payload_length_in;
              end
            end
            // Track when at least N input samples have been received in this burst
            if (partial_first_word & (word_cnt_div_n_frac == n)) begin
              partial_first_word   <= 1'b0;
            end
            // Burst ended before we received enough samples to form
            // at least one full output sample.
            // Note: axi_drop_partial_packet automatically handles
            //       dropping the partial sample.
            if (i_reg_tlast & eob_in & partial_first_word) begin
              input_state          <= RECV_INIT;
            end else begin
              if (i_reg_tlast) begin
                // At the end of a burst, forward the number of words divided by N to
                // the output state machine via a FIFO. This allows the output state
                // machine to know when it has received the final output word.
                // We use a FIFO in case the bursts are very small and we
                // need to store several of these values.
                if (eob_in) begin
                  word_cnt_div_n_tdata  <= word_cnt_div_n + (word_cnt_div_n_frac == n);
                  word_cnt_div_n_tvalid <= 1'b1;
                  throttle              <= 1'b1;
                  if (enable_clear_user) begin
                    input_state         <= RECV_WAIT_FOR_SEND_DONE;
                  end else begin
                    input_state         <= RECV_INIT;
                  end
                end
              end
            end
          end
        end
        // Wait until last sample has been output and user logic is cleared
        // WARNING: This can be a huge bubble state! However, since it only happens with
        //          EOBs, it should be infrequent.
        RECV_WAIT_FOR_SEND_DONE : begin
          if (send_done) begin
            input_state <= RECV_INIT;
          end
        end
        default : begin
          input_state   <= RECV_INIT;
        end
      endcase
    end
  end

  assign clear_user = send_done & enable_clear_user;

  /********************************************************
  ** AXI Drop Partial Packet (to user)
  ** - Enforces sending integer multiple of N samples
  **   to user
  ********************************************************/
  axi_drop_partial_packet #(
    .WIDTH(WIDTH+1),
    .HOLD_LAST_WORD(1),
    .MAX_PKT_SIZE(MAX_N),
    .SR_PKT_SIZE_ADDR(SR_N_ADDR),
    .DEFAULT_PKT_SIZE(DEFAULT_N))
  axi_drop_partial_packet (
    .clk(clk), .reset(reset), .clear(clear | send_done),
    .flush(word_cnt_div_n_tvalid & word_cnt_div_n_tready),  // Flush on EOB
    .set_stb(set_stb), .set_addr(set_addr), .set_data(set_data),
    .i_tdata({i_reg_tlast,i_reg_tdata}), .i_tvalid(i_reg_tvalid), .i_tlast(i_reg_tlast_int), .i_tready(i_reg_tready_int),
    .o_tdata({m_axis_data_tlast,m_axis_data_tdata}), .o_tvalid(m_axis_data_tvalid), .o_tlast(/* Unused */), .o_tready(m_axis_data_tready));

  /********************************************************
  ** Output state machine
  ********************************************************/
  reg [1:0] output_state;
  localparam SEND_INIT = 0;
  localparam SEND      = 1;

  wire [WIDTH-1:0] o_reg_tdata;
  wire [127:0] o_reg_tuser;
  wire o_reg_tvalid, o_reg_tready, o_reg_tlast, o_reg_tlast_int;

  reg [15:0] out_payload_cnt = (WIDTH/8);
  reg [15:0] word_cnt_div_m;
  reg [$clog2(MAX_M+1)-1:0] word_cnt_div_m_frac = 1;
  reg [$clog2(MAX_M+1)-1:0] out_pkt_cnt = 1;

  // End of burst tracking. Compare the number of words sent to the user divided by N
  // to the number of words received from the user divided by M. When they equal each other
  // then we have received the last word from the user in this burst.
  // Note: Using word_cnt_div_n_fifo_tdata to make sure the last word is identified before
  //       it has been consumed.
  wire last_word_in_burst = word_cnt_div_n_fifo_tvalid &
                            (word_cnt_div_m == word_cnt_div_n_fifo_tdata) &
                            (word_cnt_div_m_frac == m);

  always @(posedge clk) begin
    if (reset | clear) begin
      word_cnt_div_m      <= 1;
      word_cnt_div_m_frac <= 1;
      out_payload_cnt     <= (WIDTH/8);
      send_done           <= 1'b0;
      output_state        <= SEND_INIT;
    end else begin
      // Track
      case (output_state)
        SEND_INIT : begin
          word_cnt_div_m      <= 1;
          word_cnt_div_m_frac <= 1;
          out_payload_cnt     <= (WIDTH/8);
          send_done           <= 1'b0;
          output_state        <= SEND;
        end
        SEND : begin
          if (o_reg_tvalid & o_reg_tready) begin
            if (o_reg_tlast) begin
              // Track number of samples from user to set tlast
              out_payload_cnt <= (WIDTH/8);
            end else begin
              out_payload_cnt <= out_payload_cnt + (WIDTH/8);
            end
            // Track number of words consumed divided by M. This is used
            // in conjunction with word_cnt_div_n to determine when we have received
            // the last word in a burst from the user.
            if (word_cnt_div_m_frac == m) begin
              word_cnt_div_m      <= word_cnt_div_m + 1;
              word_cnt_div_m_frac <= 1;
            end else begin
              word_cnt_div_m_frac <= word_cnt_div_m_frac + 1;
            end
            if (last_word_in_burst) begin
              send_done    <= 1'b1;
              output_state <= SEND_INIT;
            end
          end
        end
        default : begin
          output_state <= SEND_INIT;
        end
      endcase
    end
  end

  // Only pop this FIFO at EOB.
  assign word_cnt_div_n_fifo_tready = o_reg_tvalid & o_reg_tready & last_word_in_burst;

  /********************************************************
  ** Adjust VITA time
  ********************************************************/
  localparam VT_INIT       = 0;
  localparam VT_INCREMENT  = 1;
  reg vt_state;

  reg has_time_out, has_time_clear;
  reg [63:0] vita_time_out, vita_time_accum;

  always @(posedge clk) begin
    if (reset | clear) begin
      vt_state               <= VT_INIT;
    end else begin
      case (vt_state)
        VT_INIT : begin
          has_time_clear       <= 1'b0;
          if (i_reg_tvalid & i_reg_tready & first_header) begin
            vita_time_out      <= vita_time_in;
            vita_time_accum    <= vita_time_in;
            has_time_out       <= has_time_in;
            vt_state           <= VT_INCREMENT;
          end
        end
        VT_INCREMENT : begin
          // Stop sending vita time if user does not send vita time
          if (i_reg_tvalid & ~has_time_in) begin
            has_time_clear     <= 1'b1;
          end
          if (o_reg_tvalid & o_reg_tready) begin
            if (o_reg_tlast) begin
              if (has_time_clear) begin
                has_time_out   <= 1'b0;
              end
              vita_time_out    <= vita_time_accum + n;
            end
            vita_time_accum    <= vita_time_accum + n;
            if (last_word_in_burst) begin
              vt_state         <= VT_INIT;
            end
          end
        end
        default : begin
          vt_state             <= VT_INIT;
        end
      endcase
    end
  end

  // Create output header
  cvita_hdr_encoder cvita_hdr_encoder (
    .pkt_type(2'd0), .eob(last_word_in_burst), .has_time(has_time_out),
    .seqnum(12'd0), .payload_length(16'd0), // Not needed, handled by AXI Wrapper
    .src_sid(src_sid), .dst_sid(dst_sid),
    .vita_time(vita_time_out),
    .header(o_reg_tuser));

  /********************************************************
  ** Register input stream from user and output stream
  ********************************************************/
  assign o_reg_tlast      = o_reg_tlast_int |
                            // End of packet
                            (out_payload_cnt == payload_length_out) |
                            // EOB, could be a partial packet
                            last_word_in_burst;

  axi_fifo_flop #(.WIDTH(WIDTH+1)) axi_fifo_flop_from_user_0 (
    .clk(clk), .reset(reset), .clear(clear),
    // FIXME: If user asserts tlast at the wrong time, it likely causes a deadlock. For now ignore tlast.
    //.i_tdata({s_axis_data_tlast,s_axis_data_tdata}), .i_tvalid(s_axis_data_tvalid), .i_tready(s_axis_data_tready),
    .i_tdata({1'b0,s_axis_data_tdata}), .i_tvalid(s_axis_data_tvalid), .i_tready(s_axis_data_tready),
    .o_tdata({o_reg_tlast_int,o_reg_tdata}), .o_tvalid(o_reg_tvalid), .o_tready(o_reg_tready),
    .space(), .occupied());

  // Output register with header
  axi_fifo_flop #(.WIDTH(WIDTH+1+128)) axi_fifo_flop_output (
    .clk(clk), .reset(reset), .clear(clear),
    .i_tdata({o_reg_tlast,o_reg_tdata,o_reg_tuser}), .i_tvalid(o_reg_tvalid), .i_tready(o_reg_tready),
    .o_tdata({o_tlast,o_tdata,o_tuser}), .o_tvalid(o_tvalid), .o_tready(o_tready),
    .space(), .occupied());

  /********************************************************
  ** Error / warning signals
  ********************************************************/
  reg [23:0] counter_header_fifo_full, counter_throttle, counter_drop_pkt_lockup;
  reg [2:0] counter_header_fifo_empty;
  always @(posedge clk) begin
    if (reset) begin
      warning_long_throttle       <= 1'b0;
      error_extra_outputs         <= 1'b0;
      error_drop_pkt_lockup       <= 1'b0;
      counter_throttle            <= 0;
      counter_header_fifo_full    <= 0;
      counter_drop_pkt_lockup     <= 0;
      counter_header_fifo_empty   <= 0;
    end else begin
      // In throttle state for a "long" time
      if (throttle) begin
        counter_throttle          <= counter_throttle + 1;
        if (counter_throttle == 2**24-1) begin
          warning_long_throttle   <= 1'b1;
        end
      end else begin
        counter_throttle          <= 0;
      end
      // More than M outputs per N inputs
      if (word_cnt_div_n_fifo_tvalid & (word_cnt_div_m > word_cnt_div_n_fifo_tdata)) begin
        error_extra_outputs         <= 1'b1;
      end
      // Bad internal state. AXI drop partial packet is in a lockup condition.
      if (~i_reg_tready_int & m_axis_data_tready) begin
        counter_drop_pkt_lockup     <= counter_drop_pkt_lockup + 1;
        if (counter_drop_pkt_lockup == 2**24-1) begin
          error_drop_pkt_lockup     <= 1'b1;
        end
      end else begin
        counter_drop_pkt_lockup     <= 0;
      end
    end
  end

endmodule