// Copyright (C) 2018-2024 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//

#pragma once

#include <fstream>
#include <iostream>
#include <type_traits>

#include "openvino/core/extension.hpp"
#include "openvino/frontend/extension/conversion.hpp"
#include "openvino/frontend/node_context.hpp"
#include "openvino/frontend/visibility.hpp"
#include "openvino/opsets/opset.hpp"

namespace ov {
namespace frontend {

/// \brief The helper function to return an instance of OpSet class initialized with
/// operations from provided opset by name.
/// \param opset_name Opset name (opsetN) to initialize OpSet class.
inline const ov::OpSet& get_opset_by_name(const std::string& opset_name) {
    const auto& opsets = ov::get_available_opsets();
    if (opsets.find(opset_name) != opsets.end())
        return opsets.at(opset_name)();
    if (opset_name.empty() || opset_name == "latest") {
        return ov::get_opset15();
    } else {
        FRONT_END_GENERAL_CHECK(false, "Unsupported opset name: ", opset_name);
    }
}

/// \brief The helper function to create an instance of ov::Node class initialized by provided type name.
/// Expected formats:
/// - opsetN::OpName
/// - opsetN.OpName
/// - OpName
/// \param ov_type_name Type name of created ov::Node.
inline std::shared_ptr<ov::Node> create_ov_node_by_name(const std::string& ov_type_name) {
    auto split = [](const std::string& s, const std::string& delimiter) {
        size_t pos_start = 0, pos_end, delim_len = delimiter.length();
        std::string token;
        std::vector<std::string> res;

        while ((pos_end = s.find(delimiter, pos_start)) != std::string::npos) {
            token = s.substr(pos_start, pos_end - pos_start);
            pos_start = pos_end + delim_len;
            res.push_back(token);
        }

        res.push_back(s.substr(pos_start));
        return res;
    };

    std::string opset_name;
    std::string op_name;
    auto cnt_colons = std::count(ov_type_name.begin(), ov_type_name.end(), ':');
    auto cnt_dots = std::count(ov_type_name.begin(), ov_type_name.end(), '.');
    if (cnt_colons == 2 && cnt_dots == 0) {
        auto divided = split(ov_type_name, "::");
        if (divided.size() != 2) {
            FRONT_END_GENERAL_CHECK(false,
                                    "Invalid OpenVINO operation format, one of the next is expected:"
                                    "opsetN::OpName or opsetN.OpName or OpName. Provided operation format: ",
                                    ov_type_name);
        }
        opset_name = divided[0];
        op_name = divided[1];
    } else if (cnt_colons == 0 && cnt_dots == 1) {
        auto divided = split(ov_type_name, ".");
        if (divided.size() != 2) {
            FRONT_END_GENERAL_CHECK(false,
                                    "Invalid OpenVINO operation format, one of the next is expected:"
                                    "opsetN::OpName or opsetN.OpName or OpName. Provided operation format: ",
                                    ov_type_name);
        }
        opset_name = divided[0];
        op_name = divided[1];
    } else if (cnt_colons == 0 && cnt_dots == 0) {
        opset_name = "latest";
        op_name = ov_type_name;
    } else {
        FRONT_END_GENERAL_CHECK(false,
                                "Invalid OpenVINO operation format, one of the next is expected: \n"
                                "opsetN::OpName or opsetN.OpName or OpName. Provided operation format: ",
                                ov_type_name);
    }

    const auto& opset = get_opset_by_name(opset_name);
    if (!opset.contains_type(op_name)) {
        FRONT_END_GENERAL_CHECK(false,
                                "OpenVINO opset doesn't contain operation with "
                                "name ",
                                op_name);
    }
    return std::shared_ptr<ov::Node>(opset.create(op_name));
}

// One-to-one operation mapping for OVOpType != void which means OV type is specified by OVOpType
// See a specialization for OVOptype = void
template <typename BaseConversionType, typename OVOpType = void>
class OpExtensionBase : public BaseConversionType {
public:
    // All attributes come from OVOpType definition, op type in FW and OV match, available for OVOpType != void only
    // Attributes mapping can be modified with optional parameters
    OpExtensionBase(const std::map<std::string, std::string>& attr_names_map = {},
                    const std::map<std::string, ov::Any>& attr_values_map = {})
        : OpExtensionBase(OVOpType::get_type_info_static().name, attr_names_map, attr_values_map) {}

    // Maps op with a given type in FW and OV type given in template parameter
    OpExtensionBase(const std::string& fw_type_name,
                    const std::map<std::string, std::string>& attr_names_map = {},
                    const std::map<std::string, ov::Any>& attr_values_map = {});

    OpExtensionBase(const std::map<std::string, size_t>& attr_names_map,
                    const std::map<std::string, ov::Any>& attr_values_map = {})
        : OpExtensionBase(OVOpType::get_type_info_static().name, attr_names_map, attr_values_map) {}

    OpExtensionBase(const std::string& fw_type_name,
                    const std::map<std::string, size_t>& attr_names_map,
                    const std::map<std::string, ov::Any>& attr_values_map = {});

    OpExtensionBase(const std::vector<std::string>& in_names_vec,
                    const std::vector<std::string>& out_names_vec,
                    const std::map<std::string, std::string>& attr_names_map = {},
                    const std::map<std::string, ov::Any>& attr_values_map = {})
        : OpExtensionBase(OVOpType::get_type_info_static().name,
                          in_names_vec,
                          out_names_vec,
                          attr_names_map,
                          attr_values_map) {}

    OpExtensionBase(const std::string& fw_type_name,
                    const std::vector<std::string>& in_names_vec,
                    const std::vector<std::string>& out_names_vec,
                    const std::map<std::string, std::string>& attr_names_map = {},
                    const std::map<std::string, ov::Any>& attr_values_map = {});
};

template <typename BaseConversionType>
class OpExtensionBase<BaseConversionType, void> : public BaseConversionType {
public:
    // Default ctor is not available, you need to specify OV type with another ctor
    OpExtensionBase() = delete;

    // Maps op with a given type in FW and matching OV type given in template parameter
    explicit OpExtensionBase(const std::string& fw_ov_type_name,
                             const std::map<std::string, std::string>& attr_names_map = {},
                             const std::map<std::string, ov::Any>& attr_values_map = {})
        : OpExtensionBase(fw_ov_type_name, fw_ov_type_name, attr_names_map, attr_values_map) {}

    // Maps op with a given type in FW and specified OV type given in template parameter
    OpExtensionBase(const std::string& ov_type_name,
                    const std::string& fw_type_name,
                    const std::map<std::string, std::string>& attr_names_map = {},
                    const std::map<std::string, ov::Any>& attr_values_map = {});

    explicit OpExtensionBase(const std::string& fw_ov_type_name,
                             const std::map<std::string, size_t>& attr_names_map,
                             const std::map<std::string, ov::Any>& attr_values_map = {})
        : OpExtensionBase(fw_ov_type_name, fw_ov_type_name, attr_names_map, attr_values_map) {}

    OpExtensionBase(const std::string& ov_type_name,
                    const std::string& fw_type_name,
                    const std::map<std::string, size_t>& attr_names_map,
                    const std::map<std::string, ov::Any>& attr_values_map = {});

    OpExtensionBase(const std::string& fw_ov_type_name,
                    const std::vector<std::string>& in_names_vec,
                    const std::vector<std::string>& out_names_vec,
                    const std::map<std::string, std::string>& attr_names_map = {},
                    const std::map<std::string, ov::Any>& attr_values_map = {})
        : OpExtensionBase(fw_ov_type_name,
                          fw_ov_type_name,
                          in_names_vec,
                          out_names_vec,
                          attr_names_map,
                          attr_values_map) {}

    explicit OpExtensionBase(const std::string& ov_type_name,
                             const std::string& fw_type_name,
                             const std::vector<std::string>& in_names_vec,
                             const std::vector<std::string>& out_names_vec,
                             const std::map<std::string, std::string>& attr_names_map = {},
                             const std::map<std::string, ov::Any>& attr_values_map = {});
};

class FWVisitor : public ov::AttributeVisitor {
public:
    explicit FWVisitor(const NodeContext& context,
                       const std::map<std::string, std::string>& attr_names_map = {},
                       const std::map<std::string, ov::Any>& attr_values_map = {})
        : m_context(context),
          m_attr_names_map(attr_names_map),
          m_attr_values_map(attr_values_map) {}

    void on_adapter(const std::string& name, ValueAccessor<void>& adapter) override {
        auto p_value = m_attr_values_map.find(name);

        if (p_value != m_attr_values_map.end()) {
            adapter.set_as_any(p_value->second);
        } else {
            auto p_name = m_attr_names_map.find(name);
            const std::string& target_name = p_name != m_attr_names_map.end() ? p_name->second : name;
            try {
                adapter.set_as_any(m_context.get_attribute_as_any(target_name));
            } catch (::ov::AssertFailure& ex) {
                OPENVINO_ASSERT(false,
                                ex.what(),
                                "\nValue for attribute \"",
                                target_name,
                                "\" is not set or mapping between "
                                "framework and openvino node attributes is incorrect.");
            }
        }
    }

private:
    const NodeContext& m_context;
    const std::map<std::string, std::string>& m_attr_names_map;
    const std::map<std::string, ov::Any>& m_attr_values_map;
};

class OpConversionFunction {
public:
    explicit OpConversionFunction(const std::function<std::shared_ptr<ov::Node>()>& op_creator,
                                  const std::map<std::string, std::string>& attr_names_map = {},
                                  const std::map<std::string, ov::Any>& attr_values_map = {})
        : m_op_creator(op_creator),
          m_attr_names_map(attr_names_map),
          m_attr_values_map(attr_values_map) {}

    ov::OutputVector operator()(const NodeContext& context) {
        auto node = m_op_creator();

        std::vector<Output<Node>> inputs;
        for (size_t i = 0; i < context.get_input_size(); ++i) {
            inputs.push_back(context.get_input(static_cast<int>(i)));
        }
        node->set_arguments(inputs);
        FWVisitor fw_visitor(context, m_attr_names_map, m_attr_values_map);
        node->visit_attributes(fw_visitor);
        node->validate_and_infer_types();
        return node->outputs();
    }

private:
    std::function<std::shared_ptr<ov::Node>()> m_op_creator;
    std::map<std::string, std::string> m_attr_names_map;
    std::map<std::string, ov::Any> m_attr_values_map;
};

class OpConversionFunctionNamed {
public:
    explicit OpConversionFunctionNamed(const std::function<std::shared_ptr<ov::Node>()>& op_creator,
                                       const std::vector<std::string>& in_names_vec,
                                       const std::vector<std::string>& out_names_vec,
                                       const std::map<std::string, std::string>& attr_names_map = {},
                                       const std::map<std::string, ov::Any>& attr_values_map = {})
        : m_op_creator(op_creator),
          m_in_names_vec(in_names_vec),
          m_out_names_vec(out_names_vec),
          m_attr_names_map(attr_names_map),
          m_attr_values_map(attr_values_map) {}

    std::map<std::string, OutputVector> operator()(const NodeContext& context) {
        auto node = m_op_creator();

        std::vector<Output<Node>> inputs;
        for (const auto& name : m_in_names_vec) {
            for (size_t i = 0; i < context.get_input_size(name); ++i) {
                inputs.push_back(context.get_input(name, static_cast<int>(i)));
            }
        }

        node->set_arguments(inputs);
        FWVisitor fw_visitor(context, m_attr_names_map, m_attr_values_map);
        node->visit_attributes(fw_visitor);
        node->validate_and_infer_types();
        std::map<std::string, OutputVector> out;
        OPENVINO_ASSERT(m_out_names_vec.size() == node->get_output_size(),
                        "each output should has a name, names number: ",
                        m_out_names_vec.size(),
                        ", output size: ",
                        node->get_output_size());
        int i = 0;
        for (const auto& name : m_out_names_vec) {
            out[name].emplace_back(node->output(i++));
        }
        return out;
    }

private:
    std::function<std::shared_ptr<ov::Node>()> m_op_creator;
    std::vector<std::string> m_in_names_vec;
    std::vector<std::string> m_out_names_vec;
    std::map<std::string, std::string> m_attr_names_map;
    std::map<std::string, ov::Any> m_attr_values_map;
};

class FWVisitorInputAttributes : public ov::AttributeVisitor {
public:
    explicit FWVisitorInputAttributes(const NodeContext& context,
                                      const std::map<std::string, size_t>& attr_names_map = {},
                                      const std::map<std::string, ov::Any>& attr_values_map = {})
        : m_context(context),
          m_attr_names_map(attr_names_map),
          m_attr_values_map(attr_values_map) {}

    void on_adapter(const std::string& name, ValueAccessor<void>& adapter) override {
        auto p_value = m_attr_values_map.find(name);
        auto p_name = m_attr_names_map.find(name);
        bool is_value_found = p_value != m_attr_values_map.end();
        bool is_name_mapping_found = p_name != m_attr_names_map.end();
        OPENVINO_ASSERT(!(is_value_found && is_name_mapping_found),
                        "For attribute " + name +
                            " both name mapping and value mapping are provided."
                            " The behavior for this case is undefined. Please leave only one mapping.");

        if (is_value_found) {
            adapter.set_as_any(p_value->second);
        } else if (is_name_mapping_found) {
            auto a = m_context.get_values_from_const_input(static_cast<int>(p_name->second));
            adapter.set_as_any(a);
        } else {
            OPENVINO_ASSERT(false,
                            "\nValue for attribute \"",
                            name,
                            "\" is not set or mapping between "
                            "framework and openvino node attributes is incorrect.");
        }
    }

private:
    const NodeContext& m_context;
    const std::map<std::string, size_t>& m_attr_names_map;
    const std::map<std::string, ov::Any>& m_attr_values_map;
};

class OpConversionFunctionInputAttributes {
public:
    explicit OpConversionFunctionInputAttributes(const std::function<std::shared_ptr<ov::Node>()>& op_creator,
                                                 const std::map<std::string, size_t>& attr_names_map = {},
                                                 const std::map<std::string, ov::Any>& attr_values_map = {})
        : m_op_creator(op_creator),
          m_attr_names_map(attr_names_map),
          m_attr_values_map(attr_values_map) {
        first_attr_index = std::numeric_limits<size_t>::max();
        for (const auto& it : m_attr_names_map) {
            first_attr_index = std::min(first_attr_index, it.second);
        }
    }

    ov::OutputVector operator()(const NodeContext& context) {
        auto node = m_op_creator();

        std::vector<Output<Node>> inputs;
        for (size_t i = 0; i < context.get_input_size(); ++i) {
            if (i < first_attr_index) {
                inputs.push_back(context.get_input(static_cast<int>(i)));
            }
        }
        node->set_arguments(inputs);
        FWVisitorInputAttributes fw_visitor(context, m_attr_names_map, m_attr_values_map);
        node->visit_attributes(fw_visitor);
        node->validate_and_infer_types();
        return node->outputs();
    }

private:
    std::function<std::shared_ptr<ov::Node>()> m_op_creator;
    std::map<std::string, size_t> m_attr_names_map;
    std::map<std::string, ov::Any> m_attr_values_map;
    size_t first_attr_index;
};

template <typename BaseConversionType>
OpExtensionBase<BaseConversionType, void>::OpExtensionBase(const std::string& ov_type_name,
                                                           const std::string& fw_type_name,
                                                           const std::map<std::string, std::string>& attr_names_map,
                                                           const std::map<std::string, ov::Any>& attr_values_map)
    : BaseConversionType(fw_type_name,
                         OpConversionFunction(
                             [ov_type_name]() {
                                 return create_ov_node_by_name(ov_type_name);
                             },
                             attr_names_map,
                             attr_values_map)) {}

template <typename BaseConversionType>
OpExtensionBase<BaseConversionType, void>::OpExtensionBase(const std::string& ov_type_name,
                                                           const std::string& fw_type_name,
                                                           const std::map<std::string, size_t>& attr_names_map,
                                                           const std::map<std::string, ov::Any>& attr_values_map)
    : BaseConversionType(fw_type_name,
                         OpConversionFunctionInputAttributes(
                             [ov_type_name]() {
                                 return create_ov_node_by_name(ov_type_name);
                             },
                             attr_names_map,
                             attr_values_map)) {}

template <typename BaseConversionType>
OpExtensionBase<BaseConversionType, void>::OpExtensionBase(const std::string& ov_type_name,
                                                           const std::string& fw_type_name,
                                                           const std::vector<std::string>& in_names_vec,
                                                           const std::vector<std::string>& out_names_vec,
                                                           const std::map<std::string, std::string>& attr_names_map,
                                                           const std::map<std::string, ov::Any>& attr_values_map)
    : BaseConversionType(fw_type_name,
                         OpConversionFunctionNamed(
                             [ov_type_name]() -> std::shared_ptr<ov::Node> {
                                 return create_ov_node_by_name(ov_type_name);
                             },
                             in_names_vec,
                             out_names_vec,
                             attr_names_map,
                             attr_values_map)) {}

template <typename BaseConversionType, typename OVOpType>
OpExtensionBase<BaseConversionType, OVOpType>::OpExtensionBase(const std::string& fw_type_name,
                                                               const std::map<std::string, std::string>& attr_names_map,
                                                               const std::map<std::string, ov::Any>& attr_values_map)
    : BaseConversionType(fw_type_name,
                         OpConversionFunction(
                             []() {
                                 return std::make_shared<OVOpType>();
                             },
                             attr_names_map,
                             attr_values_map)) {}

template <typename BaseConversionType, typename OVOpType>
OpExtensionBase<BaseConversionType, OVOpType>::OpExtensionBase(const std::string& fw_type_name,
                                                               const std::map<std::string, size_t>& attr_names_map,
                                                               const std::map<std::string, ov::Any>& attr_values_map)
    : BaseConversionType(fw_type_name,
                         OpConversionFunctionInputAttributes(
                             []() {
                                 return std::make_shared<OVOpType>();
                             },
                             attr_names_map,
                             attr_values_map)) {}

template <typename BaseConversionType, typename OVOpType>
OpExtensionBase<BaseConversionType, OVOpType>::OpExtensionBase(const std::string& fw_type_name,
                                                               const std::vector<std::string>& in_names_vec,
                                                               const std::vector<std::string>& out_names_vec,
                                                               const std::map<std::string, std::string>& attr_names_map,
                                                               const std::map<std::string, ov::Any>& attr_values_map)
    : BaseConversionType(fw_type_name,
                         OpConversionFunctionNamed(
                             []() {
                                 return std::make_shared<OVOpType>();
                             },
                             in_names_vec,
                             out_names_vec,
                             attr_names_map,
                             attr_values_map)) {}

template <typename OVOpType = void>
using OpExtension = ov::frontend::OpExtensionBase<ov::frontend::ConversionExtension, OVOpType>;

#define FRONTEND_EXPAND(X)  X
#define FRONTEND_CAT_(x, y) x##y
#define FRONTEND_CAT(x, y)  FRONTEND_CAT_(x, y)
// extract common attribute and values
#define GEN_VAR_COMMON(...)                     \
    auto params = make_spec_tuple(__VA_ARGS__); \
    const auto& name = std::get<0>(params);     \
    const auto& attr_mp = std::get<1>(params);  \
    const auto& val_mp = std::get<2>(params);
// extract paddle specific param + common values
#define GEN_VAR_PADDLE(in_names, out_names, ...)             \
    const std::vector<std::string> in_names_vec(in_names);   \
    const std::vector<std::string> out_names_vec(out_names); \
    GEN_VAR_COMMON(__VA_ARGS__)
// make common except paddle OpExtension
#define MAKE_MAP_COMMON(FRAMEWORK, ...)                                                          \
    GEN_VAR_COMMON(__VA_ARGS__)                                                                  \
    if (!name.empty())                                                                           \
        return std::make_shared<ov::frontend::FRAMEWORK::OpExtension<T>>(name, attr_mp, val_mp); \
    return std::make_shared<ov::frontend::FRAMEWORK::OpExtension<T>>(attr_mp, val_mp);
#define MAKE_MAP_onnx(...)            MAKE_MAP_COMMON(onnx, __VA_ARGS__)
#define MAKE_MAP_tensorflow(...)      MAKE_MAP_COMMON(tensorflow, __VA_ARGS__)
#define MAKE_MAP_tensorflow_lite(...) MAKE_MAP_COMMON(tensorflow_lite, __VA_ARGS__)
#define MAKE_MAP_pytorch(...)         MAKE_MAP_COMMON(pytorch, __VA_ARGS__)
// make paddle OpExtension
#define MAKE_MAP_paddle(...)                                                         \
    FRONTEND_EXPAND(GEN_VAR_PADDLE(__VA_ARGS__))                                     \
    if (!name.empty())                                                               \
        return std::make_shared<ov::frontend::paddle::OpExtension<T>>(name,          \
                                                                      in_names_vec,  \
                                                                      out_names_vec, \
                                                                      attr_mp,       \
                                                                      val_mp);       \
    return std::make_shared<ov::frontend::paddle::OpExtension<T>>(in_names_vec, out_names_vec, attr_mp, val_mp);

// Per each FRAMEWORK this macro can be used once in one operation class definition
// It defines a member inline function that creates required extension.
#define OPENVINO_FRAMEWORK_MAP(FRAMEWORK, ...)                                                \
    template <typename T>                                                                     \
    struct __openvino_framework_map_helper_##FRAMEWORK {                                      \
        static auto get() -> std::shared_ptr<ov::frontend::FRAMEWORK::OpExtension<T>> {       \
            auto make_spec_tuple = [](const std::string& s = "",                              \
                                      const std::map<std::string, std::string>& attr_mp = {}, \
                                      const std::map<std::string, ov::Any>& val_mp = {}) {    \
                return std::make_tuple(s, attr_mp, val_mp);                                   \
            };                                                                                \
            FRONTEND_CAT(MAKE_MAP_, FRAMEWORK)(__VA_ARGS__)                                   \
        }                                                                                     \
    };
}  // namespace frontend
}  // namespace ov
