// Copyright 2018 ETH Zurich and University of Bologna.
// Copyright and related rights are licensed under the Solderpad Hardware
// License, Version 0.51 (the "License"); you may not use this file except in
// compliance with the License. You may obtain a copy of the License at
// http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law
// or agreed to in writing, software, hardware and materials distributed under
// this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.

// Stream fork: Connects the input stream (ready-valid) handshake to *all* of `N_OUP` output stream
// handshakes. For each input stream handshake, every output stream handshakes exactly once. The
// input stream only handshakes when all output streams have handshaked, but the output streams do
// not have to handshake simultaneously.
//
// This module has no data ports because stream data does not need to be forked: the data of the
// input stream can just be applied at all output streams.

module stream_fork #(
    parameter int unsigned N_OUP = 0    // Synopsys DC requires a default value for parameters.
) (
    input  logic                clk_i,
    input  logic                rst_ni,
    input  logic                valid_i,
    output logic                ready_o,
    output logic [N_OUP-1:0]    valid_o,
    input  logic [N_OUP-1:0]    ready_i
);

    typedef enum logic {READY, WAIT} state_t;

    logic [N_OUP-1:0]   oup_ready,
                        all_ones;

    state_t inp_state_d, inp_state_q;

    // Input control FSM
    always_comb begin
        // ready_o     = 1'b0;
        inp_state_d = inp_state_q;

        unique case (inp_state_q)
            READY: begin
                if (valid_i) begin
                    if (valid_o == all_ones && ready_i == all_ones) begin
                        // If handshake on all outputs, handshake on input.
                        ready_o = 1'b1;
                    end else begin
                        ready_o = 1'b0;
                        // Otherwise, wait for inputs that did not handshake yet.
                        inp_state_d = WAIT;
                    end
                end else begin
                    ready_o = 1'b0;
                end
            end
            WAIT: begin
                if (valid_i && oup_ready == all_ones) begin
                    ready_o = 1'b1;
                    inp_state_d = READY;
                end else begin
                    ready_o = 1'b0;
                end
            end
            default: begin
                inp_state_d = READY;
                ready_o = 1'b0;
            end
        endcase
    end

    always_ff @(posedge clk_i, negedge rst_ni) begin
        if (!rst_ni) begin
            inp_state_q <= READY;
        end else begin
            inp_state_q <= inp_state_d;
        end
    end

    // Output control FSM
    for (genvar i = 0; i < N_OUP; i++) begin: gen_oup_state
        state_t oup_state_d, oup_state_q;

        always_comb begin
            oup_ready[i]    = 1'b1;
            valid_o[i]      = 1'b0;
            oup_state_d     = oup_state_q;

            unique case (oup_state_q)
                READY: begin
                    if (valid_i) begin
                        valid_o[i] = 1'b1;
                        if (ready_i[i]) begin   // Output handshake
                            if (!ready_o) begin     // No input handshake yet
                                oup_state_d = WAIT;
                            end
                        end else begin          // No output handshake
                            oup_ready[i] = 1'b0;
                        end
                    end
                end
                WAIT: begin
                    if (valid_i && ready_o) begin   // Input handshake
                        oup_state_d = READY;
                    end
                end
                default: begin
                    oup_state_d = READY;
                end
            endcase
        end

        always_ff @(posedge clk_i, negedge rst_ni) begin
            if (!rst_ni) begin
                oup_state_q <= READY;
            end else begin
                oup_state_q <= oup_state_d;
            end
        end
    end

    assign all_ones = '1;   // Synthesis fix for Vivado, which does not correctly compute the width
                            // of the '1 literal when assigned to a port of parametrized width.

// pragma translate_off
`ifndef VERILATOR
    initial begin: p_assertions
        assert (N_OUP >= 1) else $fatal(1, "Number of outputs must be at least 1!");
    end
`endif
// pragma translate_on

endmodule