//*****************************************************************************
// Copyright 2017-2019 Intel Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the 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.
//*****************************************************************************

#pragma once

#include <deque>
#include <functional>
#include <list>
#include <memory>
#include <stack>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>

#include "ngraph/check.hpp"
#include "ngraph/function.hpp"
#include "ngraph/node.hpp"
#include "ngraph/placement.hpp"

namespace ngraph
{
    namespace descriptor
    {
        class Input;
        class Output;
    }

    namespace op
    {
        class Parameter;
    }

    void traverse_nodes(const std::shared_ptr<const Function> p,
                        std::function<void(std::shared_ptr<Node>)> f,
                        bool include_control_deps = false);

    void traverse_nodes(const Function* p,
                        std::function<void(std::shared_ptr<Node>)> f,
                        bool include_control_deps);

    /// \brief Visit each node in a sub-graph of the entire graph
    /// \param subgraph_results The output nodes of the sub-graph
    /// \param f Function to execute at each node in the traversal
    /// \param include_control_deps Whether to include control deps
    ///        while traversing the sub-graph
    /// \param subgraph_params Input nodes of the sub-graph (optional)
    ///
    /// Traverses a sub-graph starting from subgraph_results moving up
    /// towards parameter nodes. Traversal stops if it hits a node in
    /// subgraph_params.
    ///
    /// Most useful for finding parameters of a graph directly from the
    /// result nodes and not from function parameters or extracting a
    /// subgraph relevant to the computation of certain outputs
    void traverse_nodes(const NodeVector& subgraph_results,
                        std::function<void(std::shared_ptr<Node>)> f,
                        bool include_control_deps,
                        const NodeVector& subgraph_params = {});

    inline void traverse_functions(std::shared_ptr<Function> p,
                                   std::function<void(std::shared_ptr<Function>)> f)
        NGRAPH_DEPRECATED("Replace with f(p)")
    {
        f(p);
    };

    /// \brief Replace the node `target` with the node `replacement`, i.e.,
    ///        redirect all users and control dependencies of `target` to
    ///        `replacement`.
    ///
    /// \param target Node to be replaced.
    /// \param replacement Node to replace `target` with.
    ///
    /// This is primarily used in graph-rewriting passes. For example, we
    /// might "fuse" two Concat operations as follows:
    ///
    /// (Step 0: Original graph)
    ///
    ///   A                       B
    ///   |                       |
    ///   v                       v
    /// N0[Concat, concatenation_axis=3]     C
    ///          |                           |
    ///          v                           v
    ///        N1[Concat, concatenation_axis=3]
    ///          |                |
    ///          v                v
    ///       some_user         another_user
    ///
    /// (Step 1: Construct replacement)
    ///
    ///    shared_ptr<Node> new_N1 = make_shared<op::Concat>({A,B,C},3);
    ///
    ///   A----------------------------------------.
    ///   |                                        |
    ///   |                       B----------------)--.
    ///   |                       |                |  |
    ///   v                       v                |  |
    /// N0[Concat, concatenation_axis=3]     C-----)--)--.
    ///          |                           |     |  |  |
    ///          v                           v     v  v  v
    ///        N1[Concat, concatenation_axis=3]   new_N1[Concat, concatenation_axis=3]
    ///          |                |
    ///          v                v
    ///       some_user         another_user
    ///
    /// (Step 2: Replace N1 with new_N1)
    ///
    ///    replace_node(N1, new_N1);
    ///
    ///   A----------------------------------------.
    ///   |                                        |
    ///   |                       B----------------)--.
    ///   |                       |                |  |
    ///   v                       v                |  |
    /// N0[Concat, concatenation_axis=3]     C-----)--)--.
    ///          |                           |     |  |  |
    ///          v                           v     v  v  v
    ///        N1[Concat, concatenation_axis=3]   new_N1[Concat, concatenation_axis=3]
    ///                                                  |                |
    ///                                                  v                v
    ///                                               some_user         another_user
    ///
    /// (Step 3: N0 and N1 are now dead, nodes will be freed)
    ///
    ///    [happens automatically, once all shared_ptrs to N1 are released]
    ///
    ///   A----------------------------------------.
    ///                                            |
    ///                           B----------------)--.
    ///                                            |  |
    ///                                            |  |
    ///                                      C-----)--)--.
    ///                                            |  |  |
    ///                                            v  v  v
    ///                                           new_N1[Concat, concatenation_axis=3]
    ///                                                  |                |
    ///                                                  v                v
    ///                                               some_user         another_user
    ///
    /// NOTE 1: replace_node is not type-safe (the graph is not revalidated).
    /// For example, the following is allowed, even if node `some_user`
    /// requires an input of shape 2x2:
    ///
    /// (Before)
    ///      A(shape=2x2)  B(shape=3x3)
    ///      |
    ///      v
    ///   some_user(requires 2x2 input)
    ///
    /// (After -- graph is now invalid)
    ///
    ///      replace_node(A, B);
    ///
    ///      A(shape=2x2)  B(shape=3x3)
    ///                    |
    ///                    v
    ///                 some_user(requires 2x2 input)
    ///
    /// NOTE 2: it is possible to insert a cycle into the graph with
    /// replace_node, resulting in an invalid graph. Care must be taken to
    /// avoid this. One common example is when you are attempting to insert a
    /// new node `M` "after"` a node `N`. For example, you might expect this
    /// to work:
    ///
    ///    shared_ptr<Node> M = make_shared<SomeUnaryOp>(N);
    ///    replace_node(M, N);
    ///
    /// The problem is that at replacement time, N itself is a user of M. So
    /// we end up introducing a cycle as follows:
    ///
    ///       N
    ///       |
    ///       v
    ///  other users...
    ///
    ///      |||
    ///      vvv
    ///
    ///       N------------>M
    ///       |
    ///       v
    ///  other users...
    ///
    ///      |||
    ///      vvv
    ///
    ///               .----.
    ///              |      |
    ///              |      |
    ///       N      `----->M
    ///                     |
    ///                     v
    ///                other users...
    ///
    /// To avoid the cycle, a valid way to perform the above desired insertion would be,
    ///
    ///        auto new_N = N->copy_with_new_args(N->get_arguments());
    ///        shared_ptr<Node> M = make_shared<SomeUnaryOp>(new_N);
    ///        replace_node(N, M);
    void replace_node(std::shared_ptr<Node> target,
                      std::shared_ptr<Node> replacement,
                      bool disable_prov_tag_prop = false);

    NodeVector find_common_args(std::shared_ptr<Node> target, std::shared_ptr<Node> replacement);

    /// Topological sort of nodes needed to compute root_nodes
    template <typename T>
    std::list<std::shared_ptr<Node>> topological_sort(T root_nodes,
                                                      bool include_control_deps = false)
    {
        std::stack<Node*, std::vector<Node*>> nodes_to_do;
        std::unordered_set<Node*> nodes_done;
        std::list<std::shared_ptr<Node>> result;

        for (auto& node : root_nodes)
        {
            nodes_to_do.push(node.get());
        }
        while (nodes_to_do.size() > 0)
        {
            Node* node = nodes_to_do.top();
            if (nodes_done.count(node) == 0)
            {
                bool can_add = true;
                size_t arg_count = node->get_input_size();
                for (size_t i = 0; i < arg_count; ++i)
                {
                    Node* dep = node->input(arg_count - i - 1).get_source_output().get_node();
                    if (nodes_done.count(dep) == 0)
                    {
                        can_add = false;
                        nodes_to_do.push(dep);
                    }
                }
                if (include_control_deps)
                {
                    for (auto& depptr : node->get_control_dependencies())
                    {
                        Node* dep = depptr.get();
                        if (nodes_done.count(dep) == 0)
                        {
                            can_add = false;
                            nodes_to_do.push(dep);
                        }
                    }
                }
                if (can_add)
                {
                    result.push_back(node->shared_from_this());
                    nodes_to_do.pop();
                    nodes_done.insert(node);
                }
            }
            else
            {
                nodes_to_do.pop();
            }
        }
        return result;
    }

    /// Topological sort of just nodes
    template <typename T>
    std::list<std::shared_ptr<Node>> subgraph_topological_sort(T nodes,
                                                               bool include_control_deps = false)
    {
        std::stack<Node*, std::vector<Node*>> nodes_to_do;
        std::unordered_set<Node*> nodes_done;
        std::unordered_set<Node*> nodes_to_emit;
        std::list<std::shared_ptr<Node>> result;

        for (auto& node : nodes)
        {
            nodes_to_emit.insert(node.get());
            nodes_to_do.push(node.get());
        }
        // NB: Some centos versions implement std::list::size() by counting elements
        size_t nodes_remaining = nodes_to_emit.size();
        while (nodes_to_do.size() > 0 && nodes_remaining > 0)
        {
            Node* node = nodes_to_do.top();
            if (nodes_done.count(node) == 0)
            {
                bool can_add = true;
                size_t arg_count = node->get_input_size();
                for (size_t i = 0; i < arg_count; ++i)
                {
                    Node* dep = node->input(arg_count - i - 1).get_source_output().get_node();
                    if (nodes_done.count(dep) == 0)
                    {
                        can_add = false;
                        nodes_to_do.push(dep);
                    }
                }
                if (include_control_deps)
                {
                    for (auto& depptr : node->get_control_dependencies())
                    {
                        Node* dep = depptr.get();
                        if (nodes_done.count(dep) == 0)
                        {
                            can_add = false;
                            nodes_to_do.push(dep);
                        }
                    }
                }
                if (can_add)
                {
                    if (nodes_to_emit.count(node) != 0)
                    {
                        result.push_back(node->shared_from_this());
                        nodes_remaining--;
                    }
                    nodes_to_do.pop();
                    nodes_done.insert(node);
                }
            }

            else
            {
                nodes_to_do.pop();
            }
        }
        return result;
    }

    template <typename T>
    void validate_nodes_and_infer_types(const T& nodes)
    {
        for (auto& node : subgraph_topological_sort(nodes))
        {
            node->revalidate_and_infer_types();
        }
    }

    // Check if all paths from X to a result go through Y
    bool is_post_dominated(Node* X, Node* Y);

    bool is_equal_to_const_value(std::string const_value, const Output<Node>& reduce_constant);

    // input nodes are cloned and returned
    // NodeMap input may contain default node mapping i.e. pre-cloned nodes
    // NodeMap output (by reference) fully maps input and cloned nodes
    std::list<std::shared_ptr<ngraph::Node>>
        clone_nodes(const std::list<std::shared_ptr<ngraph::Node>>& nodes, NodeMap& node_map);

    // input function is cloned and returned
    // NodeMap input may contain default node mapping i.e. pre-cloned nodes
    // NodeMap output (by reference) fully maps input and cloned function ops
    std::shared_ptr<ngraph::Function> clone_function(const ngraph::Function& func,
                                                     NodeMap& node_map);

    // input function is cloned and returned
    std::shared_ptr<ngraph::Function> clone_function(const ngraph::Function& func);

    // Assert that nodes in the function is colocated and return that placement
    Placement get_colocated_function_placement(std::shared_ptr<Function> func);

    std::pair<std::shared_ptr<op::Result>, std::shared_ptr<op::Parameter>>
        insert_result_parameter_split(const std::shared_ptr<Node>& src_node,
                                      const std::shared_ptr<Node>& dst_node);

    void insert_new_node_between(const std::shared_ptr<Node>& src_node,
                                 const std::shared_ptr<Node>& dst_node,
                                 const std::shared_ptr<Node>& new_node);

    std::shared_ptr<Node> make_zero(const element::Type& element_type, const Shape& shape);

    std::shared_ptr<Node> make_constant_from_string(std::string val,
                                                    const element::Type& element_type,
                                                    const Shape& shape);

    bool is_zero(const Output<Node>& reduce_constant);

    NodeVector get_subgraph_outputs(const NodeVector& nodes,
                                    const NodeVector& exclusions,
                                    bool ignore_unused = false,
                                    bool ignore_output_duplicates = true);

    // Extract sub-graph computing the `results`. Stops backward traversal at either a Parameter node
    // or a node that belongs to args
    NodeVector extract_subgraph(const NodeVector& results, const NodeVector& args);

    bool is_one(std::shared_ptr<Node> reduce_constant);

    bool compare_constants(const std::shared_ptr<Node>& n1, const std::shared_ptr<Node>& n2);

    // Returns true if `node` is live in the graph i.e. a result op
    // transitively uses this `node`
    bool is_used(Node* node);

    // Returns count of `node` users that are still live in the graph
    size_t get_user_count(Node* node);

    // Return true if a node's user could potentially overwrite
    // the output of this node with in-place kernels
    bool possibly_overwritten(Node* node);

    bool is_strided(const Strides& strides);

    bool is_valid_rank(const std::shared_ptr<Node>& node, std::vector<size_t> valid_ranks);

    void plot_graph(
        std::shared_ptr<Function> f,
        const std::string& filename,
        std::function<void(const Node& node, std::vector<std::string>& attributes)> = nullptr);

    /// \return A vector containing handles for each input of dst that is connected to an output
    ///         of `src`.
    std::vector<Input<Node>> get_inputs_from(Node& src, Node& dst);
    /// \return A vector containing a handle for each output of src that is connected to an input
    ///         of `dst`.
    std::vector<Output<Node>> get_outputs_to(Node& src, Node& dst);
}
