#ifndef TABLEAU_HYPER_IMPL_TABLEDEFINITION_IMPL_HPP
#define TABLEAU_HYPER_IMPL_TABLEDEFINITION_IMPL_HPP

#include <algorithm>
#include <hyperapi/HyperException.hpp>
#include <string>
#include <hyperapi/hyperapi.h>

namespace hyperapi {

namespace internal {
class HyperTableDefinition final {
   /// The underlying pointer
   hyper_table_definition_t* tableDefinition_ = nullptr;

   public:
   /// Constructor
   HyperTableDefinition() noexcept = default;
   /// Constructor
   HyperTableDefinition(const TableDefinition& t);
   /// Destructor
   ~HyperTableDefinition();

   /// Move constructor
   HyperTableDefinition(HyperTableDefinition&&);
   /// Move assignment
   HyperTableDefinition& operator=(HyperTableDefinition&&);

   /// Copy forbidden
   HyperTableDefinition(const HyperTableDefinition&) = delete;
   HyperTableDefinition& operator=(const HyperTableDefinition&) = delete;

   /**
     * Returns the underlying `hyper_table_definition_t*`.
     */
   hyper_table_definition_t* get() { return tableDefinition_; }
};

inline HyperTableDefinition::HyperTableDefinition(const TableDefinition& tableDefinition) {
   const TableName& t = tableDefinition.getTableName();
   const char* tableName = t.getName().getUnescaped().c_str();
   const char* schemaName = t.getSchemaName() ? t.getSchemaName()->getName().getUnescaped().c_str() : "";
   const char* databaseName = t.getDatabaseName() ? t.getDatabaseName()->getName().getUnescaped().c_str() : "";

   hyper_table_persistence_t tablePersistence;
   switch (tableDefinition.getPersistence()) {
      case Persistence::Permanent:
         tablePersistence = hyper_table_persistence_t::HYPER_PERMANENT;
         break;
      case Persistence::Temporary:
         tablePersistence = hyper_table_persistence_t::HYPER_TEMPORARY;
         break;
   }

   this->tableDefinition_ = hyper_create_table_definition(databaseName, schemaName, tableName, tablePersistence, false);

   for (const TableDefinition::Column& c : tableDefinition.columns_) {
      const SqlType& cType = c.getType();
      const char* collation = c.getCollation().empty() ? nullptr : c.getCollation().c_str();
      if (hyper_error_t* error = hyper_table_definition_add_column(this->tableDefinition_, c.getName().getUnescaped().c_str(), static_cast<hyper_type_tag_t>(cType.tag_), cType.modifier_, collation, c.getNullability() == Nullability::Nullable)) {
         throw internal::makeHyperException(error);
      }
   }
}

inline HyperTableDefinition::~HyperTableDefinition() {
   if (tableDefinition_) {
      hyper_destroy_table_definition(tableDefinition_);
   }
}

inline HyperTableDefinition::HyperTableDefinition(HyperTableDefinition&& other)
   : tableDefinition_(other.tableDefinition_) {
   other.tableDefinition_ = nullptr;
}

inline HyperTableDefinition& HyperTableDefinition::operator=(HyperTableDefinition&& other) {
   if (this != &other) {
      if (tableDefinition_) {
         hyper_destroy_table_definition(tableDefinition_);
      }
      tableDefinition_ = other.tableDefinition_;
      other.tableDefinition_ = nullptr;
   }
   return *this;
}

template <typename T>
struct NullabilityGetter {
   Nullability get() noexcept { return Nullability::NotNullable; }
};

template <typename T>
struct NullabilityGetter<optional<T>> {
   inline Nullability get() noexcept { return Nullability::Nullable; }
};
template <typename T>
inline Nullability getNullability() noexcept {
   return NullabilityGetter<T>().get();
}
}

inline std::ostream& operator<<(std::ostream& os, Nullability nullability) {
   if (nullability == Nullability::Nullable) {
      os << "Nullable";
   } else if (nullability == Nullability::NotNullable) {
      os << "NotNullable";
   }
   return os;
}

inline TableDefinition::Column::Column(Name name, SqlType type, Nullability nullability)
   : name_(std::move(name)), type_(type), nullability_(nullability) {
}

inline TableDefinition::Column::Column(
   Name name, SqlType type, std::string collation, Nullability nullability)
   : name_(std::move(name)), type_(type), nullability_(nullability), collation_(std::move(collation)) {
}

inline TableDefinition::TableDefinition(TableName name, Persistence persistence)
   : name_(std::move(name)), persistence_(persistence) {
}

inline TableDefinition::TableDefinition(TableName name, std::vector<Column> columns, Persistence persistence)
   : name_(std::move(name)), persistence_(persistence), columns_(std::move(columns)) {
}

inline const TableDefinition::Column& TableDefinition::getColumn(hyper_field_index_t columnIndex) const {
   HYPER_PRECONDITION(columnIndex < getColumnCount());
   return columns_[columnIndex];
}

inline const TableDefinition::Column* TableDefinition::getColumnByName(const Name& s) const noexcept {
   optional<hyper_field_index_t> columnIndex = getColumnPositionByName(s);
   return (!columnIndex.has_value()) ? nullptr : &columns_[*columnIndex];
}

inline optional<hyper_field_index_t> TableDefinition::getColumnPositionByName(const Name& s) const noexcept {
   for (hyper_field_index_t columnIndex = 0; columnIndex < columns_.size(); ++columnIndex) {
      if (columns_[columnIndex].getName() == s) {
         return columnIndex;
      }
   }
   return {};
}

inline TableDefinition& TableDefinition::addColumn(Column&& c) noexcept {
   columns_.push_back(std::move(c));
   return *this;
}

inline TableDefinition& TableDefinition::addColumn(const Column& c) noexcept {
   columns_.emplace_back(c);
   return *this;
}

inline TableDefinition& TableDefinition::setTableName(TableName n) noexcept {
   name_ = std::move(n);
   return *this;
}

inline TableDefinition& TableDefinition::setPersistence(Persistence p) noexcept {
   persistence_ = p;
   return *this;
}
}
#endif
