#ifndef BUILTIN_FUNCTION_EMBEDDER_HPP
#define BUILTIN_FUNCTION_EMBEDDER_HPP

#include <language/ast/ASTNodeDataType.hpp>
#include <language/utils/ASTNodeDataTypeTraits.hpp>
#include <language/utils/DataHandler.hpp>
#include <language/utils/DataVariant.hpp>
#include <language/utils/FunctionTable.hpp>
#include <utils/Demangle.hpp>
#include <utils/Exceptions.hpp>
#include <utils/PugsTraits.hpp>

#include <functional>
#include <memory>
#include <vector>

class IBuiltinFunctionEmbedder
{
 public:
  virtual size_t numberOfParameters() const = 0;

  virtual ASTNodeDataType getReturnDataType() const = 0;

  virtual std::vector<ASTNodeDataType> getParameterDataTypes() const = 0;

  virtual DataVariant apply(const std::vector<DataVariant>&) const = 0;

  IBuiltinFunctionEmbedder() = default;

  IBuiltinFunctionEmbedder(const IBuiltinFunctionEmbedder&) = delete;
  IBuiltinFunctionEmbedder(IBuiltinFunctionEmbedder&&)      = delete;

  virtual ~IBuiltinFunctionEmbedder() = default;
};

template <typename FX, typename... Args>
class BuiltinFunctionEmbedder
{
  static_assert(std::is_class_v<BuiltinFunctionEmbedder<FX, Args...>>,
                "wrong template parameters do not use <FX, Args...>, use <FX(Args...)>");
};

template <typename T>
inline constexpr bool is_const_ref_or_non_ref = (std::is_const_v<T> and std::is_lvalue_reference_v<T>) or
                                                (not std::is_lvalue_reference_v<T>);

template <typename FX, typename... Args>
class BuiltinFunctionEmbedder<FX(Args...)> : public IBuiltinFunctionEmbedder
{
 private:
  std::function<FX(Args...)> m_f;
  using ArgsTuple = std::tuple<std::decay_t<Args>...>;

  template <size_t I>
  PUGS_INLINE void
  _copyValue(ArgsTuple& t, const std::vector<DataVariant>& v) const
  {
    std::visit(
      [&](auto&& v_i) {
        using Ti_Type = std::decay_t<decltype(std::get<I>(t))>;
        using Vi_Type = std::decay_t<decltype(v_i)>;

        if constexpr ((std::is_same_v<Vi_Type, Ti_Type>)) {
          std::get<I>(t) = v_i;
        } else if constexpr ((std::is_arithmetic_v<Vi_Type>)and(std::is_arithmetic_v<Ti_Type> or
                                                                std::is_same_v<Ti_Type, std::string>)) {
          std::get<I>(t) = v_i;
        } else if constexpr (is_shared_ptr_v<Ti_Type>) {
          if constexpr (std::is_same_v<Vi_Type, EmbeddedData>) {
            using Ti_handeled_type = typename Ti_Type::element_type;
            try {
              auto& data_handler = dynamic_cast<const DataHandler<Ti_handeled_type>&>(v_i.get());
              std::get<I>(t)     = data_handler.data_ptr();
            }
            catch (std::bad_cast&) {
              throw UnexpectedError("unexpected argument types while casting: invalid EmbeddedData type, expecting " +
                                    demangle<DataHandler<Ti_handeled_type>>());
            }
          } else {
            throw UnexpectedError("unexpected argument types while casting: expecting EmbeddedData");
          }
        } else if constexpr (std::is_same_v<Vi_Type, std::vector<EmbeddedData>>) {
          if constexpr (is_vector_v<Ti_Type>) {
            using Ti_value_type = typename Ti_Type::value_type;
            if constexpr (is_shared_ptr_v<Ti_value_type>) {
              static_assert(is_shared_ptr_v<Ti_value_type>, "expecting shared_ptr");

              using Ti_handeled_type = typename Ti_value_type::element_type;
              std::get<I>(t).resize(v_i.size());
              for (size_t j = 0; j < v_i.size(); ++j) {
                try {
                  auto& data_handler = dynamic_cast<const DataHandler<Ti_handeled_type>&>(v_i[j].get());
                  std::get<I>(t)[j]  = data_handler.data_ptr();
                }
                catch (std::bad_cast&) {
                  throw UnexpectedError(
                    "unexpected argument types while casting: invalid EmbeddedData type, expecting " +
                    demangle<DataHandler<Ti_handeled_type>>());
                }
              }
            } else {
              throw UnexpectedError("unexpected argument types while casting \"" + demangle<Vi_Type>() + "\" to \"" +
                                    demangle<Ti_Type>() + '"');
            }
          } else {
            throw UnexpectedError("unexpected argument types while casting \"" + demangle<Vi_Type>() + "\" to \"" +
                                  demangle<Ti_Type>() + '"');
          }
        } else {
          throw UnexpectedError("unexpected argument types while casting \"" + demangle<Vi_Type>() + "\" to \"" +
                                demangle<Ti_Type>() + '"');
        }
      },
      v[I]);
  }

  template <size_t... I>
  PUGS_INLINE void
  _copyFromVector(ArgsTuple& t, const std::vector<DataVariant>& v, std::index_sequence<I...>) const
  {
    Assert(sizeof...(Args) == v.size());
    (_copyValue<I>(t, v), ...);
  }

  template <size_t I>
  PUGS_INLINE ASTNodeDataType
  _getOneParameterDataType(ArgsTuple& t) const
  {
    using ArgN_T = std::decay_t<decltype(std::get<I>(t))>;
    if constexpr (is_data_variant_v<ArgN_T>) {
      return ast_node_data_type_from<ArgN_T>;
    } else if constexpr (std::is_same_v<void, ArgN_T>) {
      return ASTNodeDataType::void_t;
    } else {
      return ASTNodeDataType::type_id_t;
    }
  }

  template <size_t... I>
  PUGS_INLINE std::vector<ASTNodeDataType>
  _getParameterDataTypes(ArgsTuple t, std::index_sequence<I...>) const
  {
    std::vector<ASTNodeDataType> parameter_type_list;
    (parameter_type_list.push_back(this->_getOneParameterDataType<I>(t)), ...);
    return parameter_type_list;
  }

  template <typename T>
  PUGS_INLINE std::shared_ptr<IDataHandler>
  _createHandler(std::shared_ptr<T> data) const
  {
    return std::make_shared<DataHandler<T>>(data);
  }

  template <size_t I>
  PUGS_INLINE void constexpr _check_arg() const
  {
    using ArgN_T = std::tuple_element_t<I, std::tuple<Args...>>;
    if constexpr (std::is_lvalue_reference_v<ArgN_T>) {
      static_assert(std::is_const_v<std::remove_reference_t<ArgN_T>>,
                    "builtin function arguments are non mutable use 'const' when passing references");
    }

    if constexpr (is_std_ptr_v<ArgN_T>) {
      static_assert(std::is_const_v<typename ArgN_T::element_type>,
                    "builtin function arguments are non mutable. For instance use std::shared_ptr<const T>");
    }
  }

  template <size_t... I>
  PUGS_INLINE void constexpr _check_arg_list(std::index_sequence<I...>) const
  {
    (_check_arg<I>(), ...);
  }

 public:
  PUGS_INLINE ASTNodeDataType
  getReturnDataType() const final
  {
    if constexpr (is_data_variant_v<FX>) {
      return ast_node_data_type_from<FX>;
    } else if constexpr (std::is_same_v<void, FX>) {
      return ASTNodeDataType::void_t;
    } else {
      return ASTNodeDataType::type_id_t;
    }
  }

  PUGS_INLINE std::vector<ASTNodeDataType>
  getParameterDataTypes() const final
  {
    constexpr size_t N = std::tuple_size_v<ArgsTuple>;
    ArgsTuple t;
    using IndexSequence = std::make_index_sequence<N>;

    return this->_getParameterDataTypes(t, IndexSequence{});
  }

  PUGS_INLINE size_t
  numberOfParameters() const final
  {
    return sizeof...(Args);
  }

 public:
  PUGS_INLINE
  DataVariant
  apply(const std::vector<DataVariant>& x) const final
  {
    constexpr size_t N = std::tuple_size_v<ArgsTuple>;
    ArgsTuple t;
    using IndexSequence = std::make_index_sequence<N>;

    this->_copyFromVector(t, x, IndexSequence{});
    if constexpr (is_data_variant_v<FX>) {
      return {std::apply(m_f, t)};
    } else if constexpr (std::is_same_v<FX, void>) {
      std::apply(m_f, t);
      return {};
    } else {
      return EmbeddedData(_createHandler(std::apply(m_f, t)));
    }
  }

  BuiltinFunctionEmbedder(std::function<FX(Args...)> f) : m_f{f}
  {
    using IndexSequence = std::make_index_sequence<std::tuple_size_v<ArgsTuple>>;
    this->_check_arg_list(IndexSequence{});

    static_assert((std::is_same_v<Args, Args> and ...));
  }

  BuiltinFunctionEmbedder(const BuiltinFunctionEmbedder&) = delete;
  BuiltinFunctionEmbedder(BuiltinFunctionEmbedder&&)      = delete;

  ~BuiltinFunctionEmbedder() = default;
};

template <typename FX>
class BuiltinFunctionEmbedder<FX, void>
{
  static_assert(std::is_class_v<BuiltinFunctionEmbedder<FX, void>>,
                "wrong template parameters do not use <FX, void>, use <FX(void)>");
};

template <typename FX>
class BuiltinFunctionEmbedder<FX(void)> : public IBuiltinFunctionEmbedder
{
 private:
  std::function<FX(void)> m_f;

  template <typename T>
  PUGS_INLINE std::shared_ptr<IDataHandler>
  _createHandler(std::shared_ptr<T> data) const
  {
    return std::make_shared<DataHandler<T>>(data);
  }

 public:
  PUGS_INLINE ASTNodeDataType
  getReturnDataType() const final
  {
    if constexpr (is_data_variant_v<FX>) {
      return ast_node_data_type_from<FX>;
    } else if constexpr (std::is_same_v<void, FX>) {
      return ASTNodeDataType::void_t;
    } else {
      return ASTNodeDataType::type_id_t;
    }
  }

  PUGS_INLINE std::vector<ASTNodeDataType>
  getParameterDataTypes() const final
  {
    return {};
  }

  PUGS_INLINE size_t
  numberOfParameters() const final
  {
    return 0;
  }

  PUGS_INLINE
  DataVariant
  apply(const std::vector<DataVariant>&) const final
  {
    if constexpr (is_data_variant_v<FX>) {
      return {m_f()};
    } else if constexpr (std::is_same_v<FX, void>) {
      m_f();
      return {};
    } else {
      return EmbeddedData(_createHandler(m_f()));
    }
  }

  BuiltinFunctionEmbedder(std::function<FX(void)> f) : m_f{f} {}

  BuiltinFunctionEmbedder(const BuiltinFunctionEmbedder&) = delete;
  BuiltinFunctionEmbedder(BuiltinFunctionEmbedder&&)      = delete;

  ~BuiltinFunctionEmbedder() = default;
};

#endif   //  BUILTIN_FUNCTION_EMBEDDER_HPP