#ifndef AFFECTATION_PROCESSOR_HPP
#define AFFECTATION_PROCESSOR_HPP

#include <language/PEGGrammar.hpp>
#include <language/node_processor/INodeProcessor.hpp>
#include <language/utils/ParseError.hpp>
#include <language/utils/SymbolTable.hpp>
#include <utils/Exceptions.hpp>
#include <utils/PugsTraits.hpp>

template <typename Op>
struct AffOp;

template <>
struct AffOp<language::multiplyeq_op>
{
  template <typename A, typename B>
  PUGS_INLINE void
  eval(A& a, const B& b)
  {
    a *= b;
  }
};

template <>
struct AffOp<language::divideeq_op>
{
  template <typename A, typename B>
  PUGS_INLINE void
  eval(A& a, const B& b)
  {
    a /= b;
  }
};

template <>
struct AffOp<language::pluseq_op>
{
  template <typename A, typename B>
  PUGS_INLINE void
  eval(A& a, const B& b)
  {
    a += b;
  }
};

template <>
struct AffOp<language::minuseq_op>
{
  template <typename A, typename B>
  PUGS_INLINE void
  eval(A& a, const B& b)
  {
    a -= b;
  }
};

struct IAffectationExecutor
{
  virtual void affect(ExecutionPolicy& exec_policy, DataVariant&& rhs) = 0;

  IAffectationExecutor(const IAffectationExecutor&) = delete;
  IAffectationExecutor(IAffectationExecutor&&)      = delete;

  IAffectationExecutor() = default;

  virtual ~IAffectationExecutor() = default;
};

template <typename OperatorT, typename ValueT, typename DataT>
class AffectationExecutor final : public IAffectationExecutor
{
 private:
  ValueT& m_lhs;

  static inline const bool m_is_defined{[] {
    if constexpr (std::is_same_v<std::decay_t<ValueT>, bool>) {
      if constexpr (not std::is_same_v<OperatorT, language::eq_op>) {
        return false;
      }
    }
    return true;
  }()};

  template <typename T>
  std::string
  _stringify(const T& value)
  {
    std::ostringstream os;
    os << std::boolalpha << value;
    return os.str();
  }

 public:
  AffectationExecutor(ASTNode& node, ValueT& lhs) : m_lhs(lhs)
  {
    // LCOV_EXCL_START
    if constexpr (not m_is_defined) {
      throw ParseError("unexpected error: invalid operands to affectation expression", std::vector{node.begin()});
    }
    // LCOV_EXCL_STOP
  }

  PUGS_INLINE void
  affect(ExecutionPolicy&, DataVariant&& rhs)
  {
    if constexpr (m_is_defined) {
      if constexpr (not std::is_same_v<DataT, ZeroType>) {
        if constexpr (std::is_same_v<ValueT, std::string>) {
          if constexpr (std::is_same_v<OperatorT, language::eq_op>) {
            if constexpr (std::is_same_v<std::string, DataT>) {
              m_lhs = std::get<DataT>(rhs);
            } else {
              m_lhs = std::move(_stringify(std::get<DataT>(rhs)));
            }
          } else {
            if constexpr (std::is_same_v<std::string, DataT>) {
              m_lhs += std::get<std::string>(rhs);
            } else {
              m_lhs += std::move(_stringify(std::get<DataT>(rhs)));
            }
          }
        } else {
          if constexpr (std::is_same_v<OperatorT, language::eq_op>) {
            if constexpr (std::is_convertible_v<DataT, ValueT>) {
              m_lhs = std::get<DataT>(rhs);
            } else if constexpr (std::is_same_v<TinyVector<1>, ValueT>) {
              std::visit(
                [&](auto&& v) {
                  using Vi_T = std::decay_t<decltype(v)>;
                  if constexpr (std::is_convertible_v<Vi_T, double>) {
                    m_lhs = TinyVector<1>(v);
                  } else {
                    // LCOV_EXCL_START
                    throw UnexpectedError("unexpected rhs type in affectation");
                    // LCOV_EXCL_STOP
                  }
                },
                rhs);
            } else if constexpr (std::is_same_v<TinyMatrix<1>, ValueT>) {
              std::visit(
                [&](auto&& v) {
                  using Vi_T = std::decay_t<decltype(v)>;
                  if constexpr (std::is_convertible_v<Vi_T, double>) {
                    m_lhs = TinyMatrix<1>(v);
                  } else {
                    // LCOV_EXCL_START
                    throw UnexpectedError("unexpected rhs type in affectation");
                    // LCOV_EXCL_STOP
                  }
                },
                rhs);
            } else if constexpr (is_std_vector_v<ValueT> and is_std_vector_v<DataT>) {
              using ValueContentT = typename ValueT::value_type;
              using DataContentT  = typename DataT::value_type;

              static_assert(not std::is_same_v<ValueContentT, DataContentT>,
                            "the case should have been treated previously");
              if constexpr (std::is_convertible_v<DataContentT, ValueContentT>) {
                m_lhs.resize(std::get<DataT>(rhs).size());
                std::visit(
                  [&](auto&& v) {
                    using Vi_T = std::decay_t<decltype(v)>;
                    if constexpr (is_std_vector_v<Vi_T>) {
                      if constexpr (std::is_arithmetic_v<typename Vi_T::value_type>) {
                        for (size_t i = 0; i < v.size(); ++i) {
                          m_lhs[i] = v[i];
                        }
                      } else {
                        // LCOV_EXCL_START
                        throw UnexpectedError("unexpected rhs type in affectation");
                        // LCOV_EXCL_STOP
                      }
                    } else {
                      // LCOV_EXCL_START
                      throw UnexpectedError("unexpected rhs type in affectation");
                      // LCOV_EXCL_STOP
                    }
                  },
                  rhs);
              } else if constexpr (std::is_same_v<ValueContentT, std::string>) {
                m_lhs.resize(std::get<DataT>(rhs).size());

                std::visit(
                  [&](auto&& v) {
                    using V_T = std::decay_t<decltype(v)>;
                    if constexpr (is_std_vector_v<V_T>) {
                      for (size_t i = 0; i < v.size(); ++i) {
                        if constexpr (std::is_same_v<typename V_T::value_type, bool>) {
                          // Ugly workaround to allow compilation with libstdc++-9
                          bool v_i = v[i];
                          m_lhs[i] = std::move(_stringify(v_i));
                        } else {
                          m_lhs[i] = std::move(_stringify(v[i]));
                        }
                      }
                    } else {
                      // LCOV_EXCL_START
                      throw UnexpectedError("unexpected rhs type in affectation");
                      // LCOV_EXCL_STOP
                    }
                  },
                  rhs);
              } else if constexpr (is_tiny_vector_v<ValueContentT>) {
                static_assert(not std::is_same_v<DataContentT, ValueContentT>, "should have been treated before");
                static_assert(ValueContentT::Dimension == 1,
                              "conversions are only allowed for dimension 1 TinyVector's");

                m_lhs.resize(std::get<DataT>(rhs).size());

                std::visit(
                  [&](auto&& v) {
                    if constexpr (is_std_vector_v<std::decay_t<decltype(v)>>) {
                      using Vi_T = typename std::decay_t<decltype(v)>::value_type;
                      for (size_t i = 0; i < v.size(); ++i) {
                        if constexpr (std::is_arithmetic_v<Vi_T>) {
                          m_lhs[i][0] = v[i];
                        } else {
                          // LCOV_EXCL_START
                          throw UnexpectedError("unexpected rhs type in affectation");
                          // LCOV_EXCL_STOP
                        }
                      }
                    } else {
                      // LCOV_EXCL_START
                      throw UnexpectedError("unexpected rhs type in affectation");
                      // LCOV_EXCL_STOP
                    }
                  },
                  rhs);

              } else if constexpr (is_tiny_matrix_v<ValueContentT>) {
                static_assert(not std::is_same_v<DataContentT, ValueContentT>, "should have been treated before");
                static_assert(ValueContentT::Dimension == 1, "conversions are only allowed for 1x1 TinyMatrix's");

                m_lhs.resize(std::get<DataT>(rhs).size());

                std::visit(
                  [&](auto&& v) {
                    if constexpr (is_std_vector_v<std::decay_t<decltype(v)>>) {
                      using Vi_T = typename std::decay_t<decltype(v)>::value_type;
                      for (size_t i = 0; i < v.size(); ++i) {
                        if constexpr (std::is_arithmetic_v<Vi_T>) {
                          m_lhs[i](0, 0) = v[i];
                        } else {
                          // LCOV_EXCL_START
                          throw UnexpectedError("unexpected rhs type in affectation");
                          // LCOV_EXCL_STOP
                        }
                      }
                    } else {
                      // LCOV_EXCL_START
                      throw UnexpectedError("unexpected rhs type in affectation");
                      // LCOV_EXCL_STOP
                    }
                  },
                  rhs);

              } else {
                // LCOV_EXCL_START
                throw UnexpectedError("invalid value type");
                // LCOV_EXCL_STOP
              }
            } else if constexpr (is_std_vector_v<ValueT> and std::is_same_v<DataT, AggregateDataVariant>) {
              using ValueContentT                         = typename ValueT::value_type;
              const AggregateDataVariant& children_values = std::get<AggregateDataVariant>(rhs);
              m_lhs.resize(children_values.size());
              auto& tuple_value = m_lhs;
              for (size_t i = 0; i < children_values.size(); ++i) {
                std::visit(
                  [&](auto&& child_value) {
                    using T = std::decay_t<decltype(child_value)>;
                    if constexpr (std::is_same_v<T, ValueContentT>) {
                      tuple_value[i] = child_value;
                    } else if constexpr (std::is_arithmetic_v<ValueContentT> and
                                         std::is_convertible_v<T, ValueContentT>) {
                      tuple_value[i] = static_cast<ValueContentT>(child_value);
                    } else if constexpr (std::is_same_v<std::string, ValueContentT>) {
                      tuple_value[i] = std::move(_stringify(child_value));
                    } else if constexpr (is_tiny_vector_v<ValueContentT>) {
                      if constexpr (std::is_arithmetic_v<T>) {
                        if constexpr (std::is_same_v<ValueContentT, TinyVector<1>>) {
                          tuple_value[i][0] = child_value;
                        } else {
                          // in this case a 0 is given
                          Assert(child_value == 0);
                          tuple_value[i] = ZeroType{};
                        }
                      } else {
                        // LCOV_EXCL_START
                        throw UnexpectedError("unexpected error: unexpected right hand side type in affectation");
                        // LCOV_EXCL_STOP
                      }
                    } else if constexpr (is_tiny_matrix_v<ValueContentT>) {
                      if constexpr (std::is_arithmetic_v<T>) {
                        if constexpr (std::is_same_v<ValueContentT, TinyMatrix<1>>) {
                          tuple_value[i](0, 0) = child_value;
                        } else {
                          // in this case a 0 is given
                          Assert(child_value == 0);
                          tuple_value[i] = ZeroType{};
                        }
                      } else {
                        // LCOV_EXCL_START
                        throw UnexpectedError("unexpected error: unexpected right hand side type in affectation");
                        // LCOV_EXCL_STOP
                      }
                    } else {
                      // LCOV_EXCL_START
                      throw UnexpectedError("unexpected error: unexpected right hand side type in affectation");
                      // LCOV_EXCL_STOP
                    }
                  },
                  children_values[i]);
              }
            } else if constexpr (is_std_vector_v<ValueT>) {
              using ValueContentT = typename ValueT::value_type;
              m_lhs.resize(1);
              auto& tuple_value = m_lhs;
              std::visit(
                [&](auto&& child_value) {
                  using T = std::decay_t<decltype(child_value)>;
                  if constexpr (std::is_same_v<T, ValueContentT>) {
                    tuple_value[0] = child_value;
                  } else if constexpr (std::is_arithmetic_v<ValueContentT> and
                                       std::is_convertible_v<T, ValueContentT>) {
                    tuple_value[0] = static_cast<ValueContentT>(child_value);
                  } else if constexpr (std::is_same_v<std::string, ValueContentT>) {
                    tuple_value[0] = std::move(_stringify(child_value));
                  } else if constexpr (is_tiny_vector_v<ValueContentT>) {
                    if constexpr (std::is_arithmetic_v<T>) {
                      if constexpr (std::is_same_v<ValueContentT, TinyVector<1>>) {
                        tuple_value[0][0] = child_value;
                      } else {
                        // in this case a 0 is given
                        Assert(child_value == 0);
                        tuple_value[0] = ZeroType{};
                      }
                    } else {
                      // LCOV_EXCL_START
                      throw UnexpectedError("unexpected error: unexpected right hand side type in affectation");
                      // LCOV_EXCL_STOP
                    }
                  } else if constexpr (is_tiny_matrix_v<ValueContentT>) {
                    if constexpr (std::is_arithmetic_v<T>) {
                      if constexpr (std::is_same_v<ValueContentT, TinyMatrix<1>>) {
                        tuple_value[0](0, 0) = child_value;
                      } else {
                        // in this case a 0 is given
                        Assert(child_value == 0);
                        tuple_value[0] = ZeroType{};
                      }
                    } else {
                      // LCOV_EXCL_START
                      throw UnexpectedError("unexpected error: unexpected right hand side type in affectation");
                      // LCOV_EXCL_STOP
                    }
                  } else {
                    // LCOV_EXCL_START
                    throw UnexpectedError("unexpected error: unexpected right hand side type in affectation");
                    // LCOV_EXCL_STOP
                  }
                },
                rhs);

            } else {
              // LCOV_EXCL_START
              throw UnexpectedError("invalid value type");
              // LCOV_EXCL_STOP
            }
          } else {
            AffOp<OperatorT>().eval(m_lhs, std::get<DataT>(rhs));
          }
        }
      } else if (std::is_same_v<OperatorT, language::eq_op>) {
        m_lhs = ValueT{zero};
      } else {
        static_assert(std::is_same_v<OperatorT, language::eq_op>, "unexpected operator type");
      }
    }
  }
};

template <typename OperatorT, typename ValueT, typename DataT>
class AffectationProcessor final : public INodeProcessor
{
 private:
  ASTNode& m_rhs_node;

  std::unique_ptr<IAffectationExecutor> m_affectation_executor;

  std::unique_ptr<IAffectationExecutor>
  _buildAffectationExecutor(ASTNode& lhs_node)
  {
    if (lhs_node.is_type<language::name>()) {
      const std::string& symbol = lhs_node.string();
      auto [i_symbol, found]    = lhs_node.m_symbol_table->find(symbol, lhs_node.begin());
      Assert(found);
      DataVariant& value = i_symbol->attributes().value();

      if (not std::holds_alternative<ValueT>(value)) {
        value = ValueT{};
      }

      using AffectationExecutorT = AffectationExecutor<OperatorT, ValueT, DataT>;
      return std::make_unique<AffectationExecutorT>(lhs_node, std::get<ValueT>(value));
    } else {
      // LCOV_EXCL_START
      throw ParseError("unexpected error: invalid lhs", std::vector{lhs_node.begin()});
      // LCOV_EXCL_STOP
    }
  }

 public:
  DataVariant
  execute(ExecutionPolicy& exec_policy)
  {
    m_affectation_executor->affect(exec_policy, m_rhs_node.execute(exec_policy));

    return {};
  }

  AffectationProcessor(ASTNode& lhs_node, ASTNode& rhs_node)
    : m_rhs_node{rhs_node}, m_affectation_executor{this->_buildAffectationExecutor(lhs_node)}
  {}
};

class AffectationToDataVariantProcessorBase : public INodeProcessor
{
 protected:
  DataVariant* m_lhs;

 public:
  AffectationToDataVariantProcessorBase(ASTNode& lhs_node)
  {
    const std::string& symbol = lhs_node.string();
    auto [i_symbol, found]    = lhs_node.m_symbol_table->find(symbol, lhs_node.begin());
    Assert(found);

    m_lhs = &i_symbol->attributes().value();
  }

  virtual ~AffectationToDataVariantProcessorBase() = default;
};

template <typename ValueT>
class AffectationToTupleProcessor final : public AffectationToDataVariantProcessorBase
{
 private:
  ASTNode& m_rhs_node;

  template <typename T>
  std::string
  _stringify(const T& value)
  {
    std::ostringstream os;
    os << std::boolalpha << value;
    return os.str();
  }

 public:
  DataVariant
  execute(ExecutionPolicy& exec_policy)
  {
    DataVariant value = m_rhs_node.execute(exec_policy);

    std::visit(
      [&](auto&& v) {
        using T = std::decay_t<decltype(v)>;
        if constexpr (std::is_same_v<T, ValueT>) {
          *m_lhs = std::vector{std::move(v)};
        } else if constexpr (std::is_arithmetic_v<ValueT> and std::is_convertible_v<T, ValueT>) {
          *m_lhs = std::vector{std::move(static_cast<ValueT>(v))};
        } else if constexpr (std::is_same_v<std::string, ValueT>) {
          *m_lhs = std::vector{std::move(_stringify(v))};
        } else if constexpr (is_tiny_vector_v<ValueT> or is_tiny_matrix_v<ValueT>) {
          if constexpr (std::is_same_v<ValueT, TinyVector<1>> and std::is_arithmetic_v<T>) {
            *m_lhs = std::vector<TinyVector<1>>{TinyVector<1>{static_cast<double>(v)}};
          } else if constexpr (std::is_same_v<ValueT, TinyMatrix<1>> and std::is_arithmetic_v<T>) {
            *m_lhs = std::vector<TinyMatrix<1>>{TinyMatrix<1>{static_cast<double>(v)}};
          } else if constexpr (std::is_same_v<T, int64_t>) {
            Assert(v == 0);
            *m_lhs = std::vector<ValueT>{ValueT{zero}};
          } else {
            // LCOV_EXCL_START
            throw ParseError("unexpected error: unexpected right hand side type in affectation", m_rhs_node.begin());
            // LCOV_EXCL_STOP
          }
        } else {
          // LCOV_EXCL_START
          throw ParseError("unexpected error: unexpected right hand side type in affectation", m_rhs_node.begin());
          // LCOV_EXCL_STOP
        }
      },
      value);

    return {};
  }

  AffectationToTupleProcessor(ASTNode& lhs_node, ASTNode& rhs_node)
    : AffectationToDataVariantProcessorBase(lhs_node), m_rhs_node{rhs_node}
  {}
};

template <typename ValueT>
class AffectationToTupleFromListProcessor final : public AffectationToDataVariantProcessorBase
{
 private:
  ASTNode& m_rhs_node;

  template <typename T>
  std::string
  _stringify(const T& value)
  {
    std::ostringstream os;
    os << std::boolalpha << value;
    return os.str();
  }

  void
  _copyAggregateDataVariant(const AggregateDataVariant& children_values)
  {
    std::vector<ValueT> tuple_value(children_values.size());
    for (size_t i = 0; i < children_values.size(); ++i) {
      std::visit(
        [&](auto&& child_value) {
          using T = std::decay_t<decltype(child_value)>;
          if constexpr (std::is_same_v<T, ValueT>) {
            tuple_value[i] = child_value;
          } else if constexpr (std::is_arithmetic_v<ValueT> and std::is_convertible_v<T, ValueT>) {
            tuple_value[i] = static_cast<ValueT>(child_value);
          } else if constexpr (std::is_same_v<std::string, ValueT>) {
            tuple_value[i] = std::move(_stringify(child_value));
          } else if constexpr (is_tiny_vector_v<ValueT>) {
            if constexpr (std::is_arithmetic_v<T>) {
              if constexpr (std::is_same_v<ValueT, TinyVector<1>>) {
                tuple_value[i][0] = child_value;
              } else {
                // in this case a 0 is given
                Assert(child_value == 0);
                tuple_value[i] = ZeroType{};
              }
            } else {
              // LCOV_EXCL_START
              throw ParseError("unexpected error: unexpected right hand side type in affectation",
                               m_rhs_node.children[i]->begin());
              // LCOV_EXCL_STOP
            }
          } else if constexpr (is_tiny_matrix_v<ValueT>) {
            if constexpr (std::is_arithmetic_v<T>) {
              if constexpr (std::is_same_v<ValueT, TinyMatrix<1>>) {
                tuple_value[i](0, 0) = child_value;
              } else {
                // in this case a 0 is given
                Assert(child_value == 0);
                tuple_value[i] = ZeroType{};
              }
            } else {
              // LCOV_EXCL_START
              throw ParseError("unexpected error: unexpected right hand side type in affectation",
                               m_rhs_node.children[i]->begin());
              // LCOV_EXCL_STOP
            }
          } else {
            // LCOV_EXCL_START
            throw ParseError("unexpected error: unexpected right hand side type in affectation",
                             m_rhs_node.children[i]->begin());
            // LCOV_EXCL_STOP
          }
        },
        children_values[i]);
    }
    *m_lhs = std::move(tuple_value);
  }

  template <typename DataType>
  void
  _copyVector(const std::vector<DataType>& values)
  {
    std::vector<ValueT> v(values.size());
    if constexpr (std::is_same_v<ValueT, DataType>) {
      for (size_t i = 0; i < values.size(); ++i) {
        v[i] = values[i];
      }
    } else if constexpr (std::is_arithmetic_v<ValueT> and std::is_convertible_v<DataType, ValueT>) {
      for (size_t i = 0; i < values.size(); ++i) {
        v[i] = static_cast<DataType>(values[i]);
      }
    } else if constexpr (std::is_same_v<ValueT, std::string>) {
      for (size_t i = 0; i < values.size(); ++i) {
        v[i] = std::move(_stringify(values[i]));
      }
    } else {
      // LCOV_EXCL_START
      throw ParseError("unexpected error: unexpected right hand side type in tuple affectation", m_rhs_node.begin());
      // LCOV_EXCL_STOP
    }

    *m_lhs = std::move(v);
  }

 public:
  DataVariant
  execute(ExecutionPolicy& exec_policy)
  {
    std::visit(
      [&](auto&& value_list) {
        using ValueListT = std::decay_t<decltype(value_list)>;
        if constexpr (std::is_same_v<AggregateDataVariant, ValueListT>) {
          this->_copyAggregateDataVariant(value_list);
        } else if constexpr (is_std_vector_v<ValueListT>) {
          this->_copyVector(value_list);
        } else {
          // LCOV_EXCL_START
          throw ParseError("unexpected error: invalid lhs (expecting list or tuple)", std::vector{m_rhs_node.begin()});
          // LCOV_EXCL_STOP
        }
      },
      m_rhs_node.execute(exec_policy));

    return {};
  }

  AffectationToTupleFromListProcessor(ASTNode& lhs_node, ASTNode& rhs_node)
    : AffectationToDataVariantProcessorBase(lhs_node), m_rhs_node{rhs_node}
  {}
};

template <typename ValueT>
class AffectationFromZeroProcessor final : public AffectationToDataVariantProcessorBase
{
 public:
  DataVariant
  execute(ExecutionPolicy&)
  {
    *m_lhs = ValueT{zero};
    return {};
  }

  AffectationFromZeroProcessor(ASTNode& lhs_node) : AffectationToDataVariantProcessorBase(lhs_node) {}
};

template <typename OperatorT>
class ListAffectationProcessor final : public INodeProcessor
{
 private:
  ASTNode& m_node;

  std::vector<std::unique_ptr<IAffectationExecutor>> m_affectation_executor_list;

 public:
  template <typename ValueT, typename DataT>
  void
  add(ASTNode& lhs_node)
  {
    using AffectationExecutorT = AffectationExecutor<OperatorT, ValueT, DataT>;

    if (lhs_node.is_type<language::name>()) {
      const std::string& symbol = lhs_node.string();
      auto [i_symbol, found]    = m_node.m_symbol_table->find(symbol, m_node.children[0]->end());
      Assert(found);
      DataVariant& value = i_symbol->attributes().value();

      if (not std::holds_alternative<ValueT>(value)) {
        value = ValueT{};
      }

      m_affectation_executor_list.emplace_back(std::make_unique<AffectationExecutorT>(m_node, std::get<ValueT>(value)));
    } else {
      // LCOV_EXCL_START
      throw ParseError("unexpected error: invalid left hand side", std::vector{lhs_node.begin()});
      // LCOV_EXCL_STOP
    }
  }

  DataVariant
  execute(ExecutionPolicy& exec_policy)
  {
    AggregateDataVariant children_values = std::get<AggregateDataVariant>(m_node.children[1]->execute(exec_policy));
    Assert(m_affectation_executor_list.size() == children_values.size());

    for (size_t i = 0; i < m_affectation_executor_list.size(); ++i) {
      m_affectation_executor_list[i]->affect(exec_policy, std::move(children_values[i]));
    }

    return {};
  }

  ListAffectationProcessor(ASTNode& node) : m_node{node} {}
};

#endif   // AFFECTATION_PROCESSOR_HPP