#ifndef SYMBOL_TABLE_HPP
#define SYMBOL_TABLE_HPP

#include <language/ast/ASTNodeDataType.hpp>
#include <language/utils/DataVariant.hpp>
#include <language/utils/EmbedderTable.hpp>
#include <language/utils/FunctionTable.hpp>
#include <utils/PugsMacros.hpp>

#include <pegtl/position.hpp>

#include <iostream>

class TypeDescriptor;
class IBuiltinFunctionEmbedder;

class SymbolTable
{
 public:
  class Attributes
  {
   private:
    TAO_PEGTL_NAMESPACE::position m_position;
    int32_t m_context_id;

    bool m_is_initialized{false};

    ASTNodeDataType m_data_type;
    DataVariant m_value;

   public:
    bool
    hasLocalContext() const
    {
      return m_context_id != -1;
    }

    const int32_t&
    contextId() const
    {
      return m_context_id;
    }

    auto&
    value()
    {
      return m_value;
    }

    const auto&
    value() const
    {
      return m_value;
    }

    const bool&
    isInitialized() const
    {
      return m_is_initialized;
    }

    void
    setIsInitialized()
    {
      m_is_initialized = true;
    }

    const ASTNodeDataType&
    dataType() const
    {
      return m_data_type;
    }

    auto
    position() const
    {
      return m_position;
    }

    void
    setDataType(const ASTNodeDataType& data_type)
    {
      Assert(m_data_type == ASTNodeDataType::undefined_t, "data type has already been defined!");
      m_data_type = data_type;
    }

    friend std::ostream&
    operator<<(std::ostream& os, const Attributes& attributes)
    {
      os << rang::fg::green;
      if (attributes.m_data_type == ASTNodeDataType::function_t) {
        os << "function_id";
      } else {
        os << dataTypeName(attributes.dataType());
      }
      os << rang::style::reset << ':' << attributes.m_value;

      return os;
    }

    Attributes(const TAO_PEGTL_NAMESPACE::position& position, int32_t context_id)
      : m_position{position}, m_context_id{context_id}
    {}

    Attributes(const Attributes&) = default;
  };

  class Symbol   // LCOV_EXCL_LINE
  {
   private:
    std::string m_name;
    Attributes m_attributes;

   public:
    PUGS_INLINE
    const std::string&
    name() const
    {
      return m_name;
    }

    PUGS_INLINE
    const Attributes&
    attributes() const
    {
      return m_attributes;
    }

    PUGS_INLINE
    Attributes&
    attributes()
    {
      return m_attributes;
    }

    Symbol(const std::string& name, const Attributes& attributes) : m_name(name), m_attributes(attributes) {}

    Symbol& operator=(Symbol&&) = default;
    Symbol& operator=(const Symbol&) = default;

    Symbol(const Symbol&) = default;
    Symbol(Symbol&&)      = default;
    ~Symbol()             = default;
  };

  class Context
  {
   private:
    inline static int32_t next_context_id{0};

    int32_t m_id;
    size_t m_size{0};

   public:
    PUGS_INLINE
    int32_t
    id() const
    {
      return m_id;
    }

    PUGS_INLINE
    size_t
    size() const
    {
      return m_size;
    }

    PUGS_INLINE
    size_t
    getNextSymbolId()
    {
      return m_size++;
    }

    Context() : m_id{next_context_id++} {}

    Context& operator=(const Context&) = default;   // clazy:exclude=function-args-by-value
    Context& operator=(Context&&) = default;

    Context(const Context&) = default;
    Context(Context&&)      = default;
    ~Context()              = default;
  };

 private:
  std::vector<Symbol> m_symbol_list;

  std::shared_ptr<SymbolTable> m_parent_table;
  std::shared_ptr<Context> m_context;

  std::shared_ptr<FunctionTable> m_function_table;

  std::shared_ptr<EmbedderTable<IBuiltinFunctionEmbedder>> m_builtin_function_embedder_table;
  std::shared_ptr<EmbedderTable<TypeDescriptor>> m_type_embedder_table;

 public:
  bool
  hasContext() const
  {
    return bool{m_context};
  }

  const Context&
  context() const
  {
    Assert(m_context);
    return *m_context;
  }

  const FunctionTable&
  functionTable() const
  {
    Assert(m_function_table);
    return *m_function_table;
  }

  FunctionTable&
  functionTable()
  {
    Assert(m_function_table);
    return *m_function_table;
  }

  const auto&
  builtinFunctionEmbedderTable() const
  {
    Assert(m_builtin_function_embedder_table);
    return *m_builtin_function_embedder_table;
  }

  auto&
  builtinFunctionEmbedderTable()
  {
    Assert(m_builtin_function_embedder_table);
    return *m_builtin_function_embedder_table;
  }

  auto&
  typeEmbedderTable()
  {
    Assert(m_type_embedder_table);
    return *m_type_embedder_table;
  }

  const auto&
  typeEmbedderTable() const
  {
    Assert(m_type_embedder_table);
    return *m_type_embedder_table;
  }

  friend std::ostream&
  operator<<(std::ostream& os, const SymbolTable& symbol_table)
  {
    os << "-- Symbol table state -- parent : " << symbol_table.m_parent_table.get() << "\n";
    for (auto i_symbol : symbol_table.m_symbol_list) {
      if (i_symbol.attributes().dataType() != ASTNodeDataType::builtin_function_t) {
        os << ' ' << i_symbol.name() << ": " << std::boolalpha << i_symbol.attributes() << '\n';
      }
    }
    os << "------------------------\n";
    return os;
  }

  void
  clearValues()
  {
    for (auto& symbol : m_symbol_list) {
      std::visit(
        [](auto&& value) {
          using T = std::decay_t<decltype(value)>;
          value   = T{};
        },
        symbol.attributes().value());
    }
  }

  auto
  find(const std::string& symbol, const TAO_PEGTL_NAMESPACE::position& use_position)
  {
    auto i_symbol = m_symbol_list.end();

    for (auto i_stored_symbol = m_symbol_list.begin(); i_stored_symbol != m_symbol_list.end(); ++i_stored_symbol) {
      if (i_stored_symbol->name() == symbol) {
        i_symbol = i_stored_symbol;
        break;
      }
    }

    if (i_symbol != m_symbol_list.end() and (use_position.byte >= i_symbol->attributes().position().byte)) {
      return std::make_pair(i_symbol, true);
    } else {
      if (m_parent_table) {
        return m_parent_table->find(symbol, use_position);
      } else {
        return std::make_pair(i_symbol, false);
      }
    }
  }

  auto
  add(const std::string& symbol_name, const TAO_PEGTL_NAMESPACE::position& symbol_position)
  {
    for (auto i_stored_symbol = m_symbol_list.begin(); i_stored_symbol != m_symbol_list.end(); ++i_stored_symbol) {
      if (i_stored_symbol->name() == symbol_name) {
        return std::make_pair(i_stored_symbol, false);
      }
    }

    int32_t context_id = this->hasContext() ? m_context->id() : -1;

    auto i_symbol =
      m_symbol_list.emplace(m_symbol_list.end(), Symbol{symbol_name, Attributes{symbol_position, context_id}});

    if (this->hasContext()) {
      i_symbol->attributes().value() = m_context->getNextSymbolId();
    }

    return std::make_pair(i_symbol, true);
  }

  SymbolTable(const std::shared_ptr<SymbolTable>& parent_table, const std::shared_ptr<Context>& context)
    : m_parent_table{parent_table},
      m_context{context},
      m_function_table{parent_table->m_function_table},
      m_builtin_function_embedder_table{parent_table->m_builtin_function_embedder_table},
      m_type_embedder_table{parent_table->m_type_embedder_table}
  {
    ;
  }

  SymbolTable(const std::shared_ptr<SymbolTable>& parent_table) : SymbolTable{parent_table, parent_table->m_context}
  {
    ;
  }

  SymbolTable()
    : m_parent_table{nullptr},
      m_context{nullptr},
      m_function_table{std::make_shared<FunctionTable>()},
      m_builtin_function_embedder_table{std::make_shared<EmbedderTable<IBuiltinFunctionEmbedder>>()},
      m_type_embedder_table{std::make_shared<EmbedderTable<TypeDescriptor>>()}
  {
    ;
  }
};

#endif   // SYMBOL_TABLE_HPP