#pragma once

#include <llvm/Support/Casting.h>

#include <string>
#include <type_traits>

#include "common/macros.h"
#include "execution/sql/sql.h"

namespace terrier::execution::sql {

/**
 * Base class for algebraic SQL types.
 */
class SqlType {
 public:
  virtual ~SqlType() = default;

  /** @return ID of this SQL type. */
  SqlTypeId GetId() const { return id_; }

  /** @return True if this SQL type is nullable. */
  bool IsNullable() const { return nullable_; }

  /** @return Non-nullable version of this SQL type. */
  virtual const SqlType &GetNonNullableVersion() const = 0;

  /** @return Nullable version of this SQL type. */
  virtual const SqlType &GetNullableVersion() const = 0;

  /** @return Primitive type ID for this SQL type. */
  virtual TypeId GetPrimitiveTypeId() const = 0;

  /** @return Name of this SQL type. */
  virtual std::string GetName() const = 0;

  /** @return True if this SQL type is an integer type. */
  virtual bool IsIntegral() const = 0;

  /** @return True if this SQL type is a floating-point type. */
  virtual bool IsFloatingPoint() const = 0;

  /** @return True if this SQL type is a type that supports arithmetic. */
  virtual bool IsArithmetic() const = 0;

  /** @return True if this SQL type equals that SQL type. */
  virtual bool Equals(const SqlType &that) const = 0;

  /** @return True if this SQL type == that SQL type. */
  bool operator==(const SqlType &that) const noexcept { return Equals(that); }

  /** @return True if this SQL type != that SQL type. */
  bool operator!=(const SqlType &that) const noexcept { return !(*this == that); }

  // -------------------------------------------------------
  // Type-checking
  // -------------------------------------------------------

  /** @return True if this SQL type is the specified templated type. */
  template <typename T>
  bool Is() const {
    return llvm::isa<T>(this);
  }

  /** @return This SQL type casted to the specified templated type. */
  template <typename T>
  T *As() {
    return llvm::cast<T>(this);
  }

  /** @return This SQL type casted to the specified const templated type. */
  template <typename T>
  const T *As() const {
    return llvm::cast<const T>(this);
  }

  /** @return True if this SQL type can be casted to the specified templated type. */
  template <typename T>
  T *SafeAs() {
    return llvm::dyn_cast<T>(this);
  }

  /** @return True if this SQL type can be const casted to the specified templated type. */
  template <typename T>
  const T *SafeAs() const {
    return llvm::dyn_cast<const T>(this);
  }

 protected:
  /**
   * Construct the specified SQL type.
   * @param sql_type_id The type ID of the SQL type to be constructed.
   * @param nullable True if the specified SQL type is nullable, false otherwise.
   */
  explicit SqlType(SqlTypeId sql_type_id, bool nullable) : id_(sql_type_id), nullable_(nullable) {}

 private:
  /** The SQL type ID. */
  SqlTypeId id_;
  /** Flag indicating if the type is nullable. */
  bool nullable_;
};

/**
 * A SQL boolean type.
 */
class BooleanType : public SqlType {
 public:
  /** @return A non-nullable version of the boolean type. */
  static const BooleanType &InstanceNonNullable();

  /** @return A nullable version of the boolean type. */
  static const BooleanType &InstanceNullable();

  /** @return The boolean type with the requested nullability. */
  static const BooleanType &Instance(bool nullable) { return (nullable ? InstanceNullable() : InstanceNonNullable()); }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(); }

  TypeId GetPrimitiveTypeId() const override;

  std::string GetName() const override;

  bool IsIntegral() const override;

  bool IsFloatingPoint() const override;

  bool IsArithmetic() const override;

  bool Equals(const SqlType &that) const override;

  /** @return True if the input type is a Boolean type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::Boolean; }  // NOLINT

 private:
  explicit BooleanType(bool nullable);
};

/**
 * Base for all types that are stored as primitive C/C++ numbers. This includes
 * the regular SQL numbers (smallint, int, decimal), but also dates and
 * timestamps.
 * @tparam CppType The primitive type
 */
template <typename CppType>
class NumberBaseType : public SqlType {
 public:
  /** @return Primitive type ID for this type. */
  TypeId GetPrimitiveTypeId() const override { return GetTypeId<CppType>(); }

  /** @return True if this type is an integer type. */
  bool IsIntegral() const override { return std::is_integral_v<CppType>; }

  /** @return True if this type is a floating-point type. */
  bool IsFloatingPoint() const override { return std::is_floating_point_v<CppType>; }

  /** @return True if this type is a type that supports arithmetic. */
  bool IsArithmetic() const override { return true; }

 protected:
  /** Construct the number type with the specified type ID and nullability. */
  NumberBaseType(SqlTypeId type_id, bool nullable) : SqlType(type_id, nullable) {}
};

/**
 * A SQL tiny-int type..
 */
class TinyIntType : public NumberBaseType<int8_t> {
 public:
  /** @return A non-nullable version of the tiny integer type. */
  static const TinyIntType &InstanceNonNullable();

  /** @return A nullable version of the tiny integer type. */
  static const TinyIntType &InstanceNullable();

  /** @return The tiny integer type with the requested nullability. */
  static const TinyIntType &Instance(bool nullable) { return (nullable ? InstanceNullable() : InstanceNonNullable()); }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(); }

  std::string GetName() const override;

  bool Equals(const SqlType &that) const override;

  /** @return True if the input type is a TinyInt type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::TinyInt; }  // NOLINT

 private:
  explicit TinyIntType(bool nullable);
};

/**
 * A SQL small-int type..
 */
class SmallIntType : public NumberBaseType<int16_t> {
 public:
  /** @return A non-nullable version of the small integer type. */
  static const SmallIntType &InstanceNonNullable();

  /** @return A nullable version of the small integer type. */
  static const SmallIntType &InstanceNullable();

  /** @return The small integer type with the requested nullability. */
  static const SmallIntType &Instance(bool nullable) { return (nullable ? InstanceNullable() : InstanceNonNullable()); }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(); }

  std::string GetName() const override;

  bool Equals(const SqlType &that) const override;

  /** @return True if the input type is a SmallInt type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::SmallInt; }  // NOLINT

 private:
  explicit SmallIntType(bool nullable);
};

/**
 * A SQL integer type.
 */
class IntegerType : public NumberBaseType<int32_t> {
 public:
  /** @return A non-nullable version of the integer type. */
  static const IntegerType &InstanceNonNullable();

  /** @return A nullable version of the integer type. */
  static const IntegerType &InstanceNullable();

  /** @return The integer type with the requested nullability. */
  static const IntegerType &Instance(bool nullable) { return (nullable ? InstanceNullable() : InstanceNonNullable()); }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(); }

  std::string GetName() const override;

  bool Equals(const SqlType &that) const override;

  /** @return True if the input type is a Integer type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::Integer; }  // NOLINT

 private:
  explicit IntegerType(bool nullable);
};

/**
 * A SQL bigint type.
 */
class BigIntType : public NumberBaseType<int64_t> {
 public:
  /** @return A non-nullable version of the big integer type. */
  static const BigIntType &InstanceNonNullable();

  /** @return A nullable version of the big integer type. */
  static const BigIntType &InstanceNullable();

  /** @return The big integer type with the requested nullability. */
  static const BigIntType &Instance(bool nullable) { return (nullable ? InstanceNullable() : InstanceNonNullable()); }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(); }

  std::string GetName() const override;

  bool Equals(const SqlType &that) const override;

  /** @return True if the input type is a BigInt type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::BigInt; }  // NOLINT

 private:
  explicit BigIntType(bool nullable);
};

/**
 * A SQL real type, i.e., a 4-byte floating point type.
 */
class RealType : public NumberBaseType<float> {
 public:
  /** @return A non-nullable version of the real type. */
  static const RealType &InstanceNonNullable();

  /** @return A nullable version of the real type. */
  static const RealType &InstanceNullable();

  /** @return The real type with the requested nullability. */
  static const RealType &Instance(bool nullable) { return (nullable ? InstanceNullable() : InstanceNonNullable()); }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(); }

  std::string GetName() const override;

  bool Equals(const SqlType &that) const override;

  /** @return True if the input type is a Real type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::Real; }  // NOLINT

 private:
  explicit RealType(bool nullable);
};

/**
 * A SQL double type, i.e., an 8-byte floating point type.
 */
class DoubleType : public NumberBaseType<double> {
 public:
  /** @return A non-nullable version of the double type. */
  static const DoubleType &InstanceNonNullable();

  /** @return A nullable version of the double type. */
  static const DoubleType &InstanceNullable();

  /** @return The double type with the requested nullability. */
  static const DoubleType &Instance(bool nullable) { return (nullable ? InstanceNullable() : InstanceNonNullable()); }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(); }

  std::string GetName() const override;

  bool Equals(const SqlType &that) const override;

  /** @return True if the input type is a Double type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::Double; }  // NOLINT

 private:
  explicit DoubleType(bool nullable);
};

/**
 * A SQL decimal type.
 */
class DecimalType : public SqlType {
 public:
  /** @return A non-nullable version of the decimal type. */
  static const DecimalType &InstanceNonNullable(uint32_t precision, uint32_t scale);

  /** @return A nullable version of the decimal type. */
  static const DecimalType &InstanceNullable(uint32_t precision, uint32_t scale);

  /** @return The decimal type with the requested nullability, precision, and scale. */
  static const DecimalType &Instance(bool nullable, uint32_t precision, uint32_t scale) {
    return (nullable ? InstanceNullable(precision, scale) : InstanceNonNullable(precision, scale));
  }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(Precision(), Scale()); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(Precision(), Scale()); }

  TypeId GetPrimitiveTypeId() const override;

  std::string GetName() const override;

  bool Equals(const SqlType &that) const override;

  bool IsIntegral() const override;

  bool IsFloatingPoint() const override;

  bool IsArithmetic() const override;

  /** @return The precision of this decimal type. */
  uint32_t Precision() const;

  /** @return The scale of this decimal type. */
  uint32_t Scale() const;

  /** @return True if the input type is a Decimal type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::Decimal; }  // NOLINT

 private:
  DecimalType(bool nullable, uint32_t precision, uint32_t scale);

  template <bool nullable>
  static const DecimalType &InstanceInternal(uint32_t precision, uint32_t scale);

 private:
  uint32_t precision_;
  uint32_t scale_;
};

/**
 * A SQL date type.
 */
class DateType : public SqlType {
 public:
  /** @return A non-nullable version of the date type. */
  static const DateType &InstanceNonNullable();

  /** @return A nullable version of the date type. */
  static const DateType &InstanceNullable();

  /** @return The date type with the requested nullability. */
  static const DateType &Instance(bool nullable) { return (nullable ? InstanceNullable() : InstanceNonNullable()); }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(); }

  TypeId GetPrimitiveTypeId() const override;

  std::string GetName() const override;

  bool Equals(const SqlType &that) const override;

  bool IsIntegral() const override { return false; }

  bool IsFloatingPoint() const override { return false; }

  bool IsArithmetic() const override { return false; }

  /** @return True if the input type is a Date type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::Date; }  // NOLINT

 private:
  explicit DateType(bool nullable);
};

/**
 * A SQL timestamp type.
 */
class TimestampType : public SqlType {
 public:
  /** @return A non-nullable version of the timestamp type. */
  static const TimestampType &InstanceNonNullable();

  /** @return A nullable version of the timestamp type. */
  static const TimestampType &InstanceNullable();

  /** @return The timestamp type with the requested nullability. */
  static const TimestampType &Instance(bool nullable) {
    return (nullable ? InstanceNullable() : InstanceNonNullable());
  }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(); }

  TypeId GetPrimitiveTypeId() const override;

  std::string GetName() const override;

  bool Equals(const SqlType &that) const override;

  bool IsIntegral() const override { return false; }

  bool IsFloatingPoint() const override { return false; }

  bool IsArithmetic() const override { return false; }

  /** @return True if the input type is a Timestamp type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::Timestamp; }  // NOLINT

 private:
  explicit TimestampType(bool nullable);
};

/**
 * A SQL char type.
 */
class CharType : public SqlType {
 public:
  /** @return A non-nullable version of the char type. */
  static const CharType &InstanceNonNullable(uint32_t len);

  /** @return A nullable version of the char type. */
  static const CharType &InstanceNullable(uint32_t len);

  /** @return The char type with the requested nullability and length. */
  static const CharType &Instance(bool nullable, uint32_t len) {
    return (nullable ? InstanceNullable(len) : InstanceNonNullable(len));
  }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(Length()); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(Length()); }

  TypeId GetPrimitiveTypeId() const override;

  std::string GetName() const override;

  bool Equals(const SqlType &that) const override;

  bool IsIntegral() const override { return false; }

  bool IsFloatingPoint() const override { return false; }

  bool IsArithmetic() const override { return false; }

  /** @return The length of this char type. */
  uint32_t Length() const;

  /** @return True if the input type is a Char type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::Char; }  // NOLINT

 private:
  explicit CharType(bool nullable, uint32_t length);

  template <bool nullable>
  static const CharType &InstanceInternal(uint32_t length);

 private:
  uint32_t length_;
};

/**
 * A SQL varchar type.
 */
class VarcharType : public SqlType {
 public:
  /** @return A non-nullable version of the varchar type. */
  static const VarcharType &InstanceNonNullable(uint32_t max_len);

  /** @return A nullable version of the varchar type. */
  static const VarcharType &InstanceNullable(uint32_t max_len);

  /** @return The varchar type with the requested nullability. */
  static const VarcharType &Instance(bool nullable, uint32_t max_len) {
    return (nullable ? InstanceNullable(max_len) : InstanceNonNullable(max_len));
  }

  const SqlType &GetNonNullableVersion() const override { return InstanceNonNullable(MaxLength()); }

  const SqlType &GetNullableVersion() const override { return InstanceNullable(MaxLength()); }

  TypeId GetPrimitiveTypeId() const override;

  std::string GetName() const override;

  bool Equals(const SqlType &that) const override;

  bool IsIntegral() const override { return false; }

  bool IsFloatingPoint() const override { return false; }

  bool IsArithmetic() const override { return false; }

  /** @return The maximum length of this varchar type. */
  uint32_t MaxLength() const;

  /** @return True if the input type is a Varchar type. */
  static bool classof(const SqlType *type) { return type->GetId() == SqlTypeId::Varchar; }  // NOLINT

 private:
  explicit VarcharType(bool nullable, uint32_t max_len);

  template <bool nullable>
  static const VarcharType &InstanceInternal(uint32_t length);

 private:
  uint32_t max_len_;
};

}  // namespace terrier::execution::sql
