#ifndef CFUNCTION_EMBEDDER_HPP
#define CFUNCTION_EMBEDDER_HPP

#include <PugsAssert.hpp>
#include <PugsMacros.hpp>

#include <ASTNodeDataType.hpp>
#include <ASTNodeDataVariant.hpp>

#include <cmath>
#include <functional>
#include <iostream>
#include <tuple>
#include <vector>

#include <type_traits>

class ICFunctionEmbedder
{
 public:
  virtual size_t numberOfArguments() const = 0;

  virtual ASTNodeDataType getReturnDataType() const = 0;

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

  virtual void apply(const std::vector<ASTNodeDataVariant>& x, ASTNodeDataVariant& f_x) const = 0;

  virtual ~ICFunctionEmbedder() = default;
};

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

  template <size_t I>
  PUGS_INLINE void
  _copy_value(ArgsTuple& t, const std::vector<ASTNodeDataVariant>& v) const
  {
    std::visit(
      [&](auto v_i) {
        if constexpr (std::is_arithmetic_v<decltype(v_i)>) {
          std::get<I>(t) = v_i;
        } else {
          throw std::runtime_error("unexpected argument type!");
        }
      },
      v[I]);
  }

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

  template <size_t I>
  PUGS_INLINE ASTNodeDataType
  _getOneArgumentDataType(ArgsTuple& t) const
  {
    return ast_node_data_type_from_pod<std::decay_t<decltype(std::get<I>(t))>>;
  }

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

 public:
  PUGS_INLINE ASTNodeDataType
  getReturnDataType() const final
  {
    return ast_node_data_type_from_pod<FX>;
  }

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

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

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

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

    this->_copy_from_vector(t, x, IndexSequence{});
    f_x = std::apply(m_f, t);
  }

  // @note This is written in a template fashion to ensure that function type
  // is correct. If one uses simply CFunctionEmbedder(std::function<FX(Args...)>&&),
  // types seem unchecked
  template <typename FX2, typename... Args2>
  CFunctionEmbedder(std::function<FX2(Args2...)> f) : m_f(f)
  {
    static_assert(std::is_same_v<FX, FX2>, "incorrect return type");
    static_assert(sizeof...(Args) == sizeof...(Args2), "invalid number of arguments");
    using Args2Tuple = std::tuple<Args2...>;
    static_assert(std::is_same_v<ArgsTuple, Args2Tuple>, "invalid arguments type");
  }
};

#endif   // CFUNCTION_EMBEDDER_HPP