/**
 * \file
 */

#ifndef TABLEAU_HYPER_NUMERIC_HPP
#define TABLEAU_HYPER_NUMERIC_HPP

#include <cstdint>
#include <hyperapi/string_view.hpp>
#include <string>
#include <type_traits>
#include <hyperapi/hyperapi.h>

class BigNumeric_rawValue_Test;
class Numeric_rawValue_Test;

namespace std {
/** Specialization of `std::hash` for `hyper_data128_t`. */
template <>
struct hash<hyper_data128_t> {
   /** Calculates the hash value of the given hyper_data128_t. */
   size_t operator()(hyper_data128_t n) const noexcept {
      return hash<uint64_t>()(n.data[0]) ^ hash<uint64_t>()(n.data[1]);
   }
};
}

namespace hyperapi {
template <unsigned precision_value, unsigned scale_value>
class Numeric;
namespace internal {
template <typename ReturnType>
struct ValueAccess;
struct ValueInserter;
template <unsigned p, unsigned s>
// NOLINTNEXTLINE(readability-redundant-typename)
typename Numeric<p, s>::data_t numericInternalRepresentation(const Numeric<p, s>& numeric) noexcept;
}
/** Equality operator for hyper_data128_t. */
inline bool operator==(const hyper_data128_t& a, const hyper_data128_t& b) noexcept { return (a.data[0] == b.data[0]) && (a.data[1] == b.data[1]); }
/** Greater operator for hyper_data128_t. */
inline bool operator>(const hyper_data128_t& a, const hyper_data128_t& b) noexcept {
   if (a.data[1] != b.data[1]) return (static_cast<int64_t>(a.data[1]) > static_cast<int64_t>(b.data[1]));
   if (a.data[0] != b.data[0]) return (a.data[0] > b.data[0]);
   return false;
}

/**
 * A fixed-point numeric data value with `scale` fraction digits and `precision` digits overall.
 */
template <unsigned precision_value, unsigned scale_value>
class Numeric final {
   public:
   static constexpr unsigned precision = precision_value;
   static constexpr unsigned scale = scale_value;
   static_assert(precision >= scale, "The precision of a numeric must be greater or equal than the scale");
   static_assert(precision <= 38, "Precision must be at most 38");
   // NOLINTNEXTLINE(modernize-type-traits, readability-redundant-typename)
   using data_t = typename std::conditional<precision <= 18, int64_t, hyper_data128_t>::type;

   /**
     * Default constructor.
     */
   Numeric() noexcept = default;

   /**
     * Creates a numeric value from an integer.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   Numeric(short value);
   /**
     * Creates a numeric value from an integer.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   Numeric(unsigned short value);
   /**
     * Creates a numeric value from an integer.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   Numeric(int value);
   /**
     * Creates a numeric value from an integer.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   Numeric(unsigned value);
   /**
     * Creates a numeric value from an integer.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   Numeric(long value);
   /**
     * Creates a numeric value from an integer.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   Numeric(unsigned long value);
   /**
     * Creates a numeric value from an integer.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   Numeric(long long value);
   /**
     * Creates a numeric value from an integer.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   Numeric(unsigned long long value);
   /**
     * Creates a numeric value from a float; may lose accuracy.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   Numeric(float value);
   /**
     * Creates a numeric value from a double; may lose accuracy.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   Numeric(double value);
   /**
     * Creates a numeric value from a double; may lose accuracy.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   Numeric(long double value);

   /**
     * Creates a numeric value from another numeric value with different precision and scale.
     * \throws HyperException  if the value cannot be represented with the specified precision and scale.
     */
   template <unsigned otherPrecision, unsigned otherScale>
   explicit Numeric(hyperapi::Numeric<otherPrecision, otherScale> other);

   /**
     * Creates a numeric value from a string representation.
     * \throws HyperException  if `value` is not a valid numeric representation or overflows.
     */
   explicit Numeric(hyperapi::string_view value);

   /**
     * Gets an exact string representation, which is round-trip compatible with the constructor,
     * i.e., `n == Numeric(n.stringValue())`
     */
   std::string stringValue() const;

   /**
     * Gets an integer representation of this value; if the value has fraction digits, these will be truncated.
     * \throws HyperException  if the value cannot be represented in 64-bit.
     */
   int64_t intValue() const;
   /**
     * Explicit conversion to int64_t
     * \throws HyperException  if the value cannot be represented in 64-bit.
     */
   operator int64_t() const { return intValue(); }

   /**
     * Gets a float representation of this value; may lose accuracy
     */
   float floatValue() const noexcept;
   /**
     * Explicit conversion to float
     */
   operator float() const noexcept { return floatValue(); }

   /**
     * Gets a double representation of this value; may lose accuracy
     */
   double doubleValue() const noexcept;
   /**
     * Explicit conversion to double
     */
   operator double() const noexcept { return doubleValue(); }

   /**
     * Gets a string representation for debugging.
     * Currently, the result is equivalent to calling stringValue(), but this is not guaranteed; it might change in future versions.
     */
   std::string toString() const;

   /**
     * Equality operator.
     */
   friend bool operator==(const Numeric& a, const Numeric& b) noexcept { return a.value_ == b.value_; }
   /**
     * Greater operator.
     */
   friend bool operator>(const Numeric& a, const Numeric& b) noexcept { return a.value_ > b.value_; }
   /**
     * Not equal operator.
     */
   friend bool operator!=(const Numeric& a, const Numeric& b) noexcept { return !(a == b); }
   /**
     * Less than operator.
     */
   friend bool operator<(const Numeric& a, const Numeric& b) noexcept { return (b > a); }
   /**
     * Less than or equal operator.
     */
   friend bool operator<=(const Numeric& a, const Numeric& b) noexcept { return !(a > b); }
   /**
     * Greater or equal operator.
     */
   friend bool operator>=(const Numeric& a, const Numeric& b) noexcept { return !(a < b); }

   /** Stream output operator */
   friend std::ostream& operator<<(std::ostream& os, const Numeric& obj) { return os << obj.toString(); }

   private:
   friend class ::Numeric_rawValue_Test;
   friend class ::BigNumeric_rawValue_Test;
   template <unsigned otherPrecision, unsigned otherScale>
   friend class Numeric;
   friend class Inserter;
   template <typename ReturnType>
   friend struct internal::ValueAccess;
   friend struct internal::ValueInserter;
   template <typename T>
   friend struct std::hash;
   // NOLINTNEXTLINE(readability-redundant-typename)
   friend typename Numeric<precision, scale>::data_t internal::numericInternalRepresentation(const Numeric<precision, scale>& numeric) noexcept;

   struct raw_t {};

   /**
     * Creates a Numeric value from the raw bit representation
     *
     * \param rawNumeric The raw numeric value.
     */
   explicit Numeric(data_t rawNumeric, raw_t) noexcept
      : value_(rawNumeric) {
   }

   /**
     * Creates a Numeric value from the raw bit representation and
     * dynamic precision and scale parameters that may differ from the current precision and scale.
     *
     * \param rawNumeric The raw numeric value.
     * \param otherPrecision The precision of the raw value.
     * \param otherScale The scale of the raw value.
     */
   explicit Numeric(int64_t rawNumeric, unsigned otherPrecision, unsigned otherScale, raw_t);

   /**
     * Creates a Numeric value from the raw bit representation and
     * dynamic precision and scale parameters that may differ from the current precision and scale.
     *
     * \param rawNumeric The raw numeric value.
     * \param otherPrecision The precision of the raw value.
     * \param otherScale The scale of the raw value.
     */
   explicit Numeric(hyper_data128_t rawNumeric, unsigned otherPrecision, unsigned otherScale, raw_t);

   /**
     * The encoding of this numeric
     */
   data_t value_{};
};
}

namespace std {
/** Specialization of `std::hash` for `hyperapi::Numeric`. */
template <unsigned p, unsigned s>
struct hash<hyperapi::Numeric<p, s>> {
   /** Calculates the hash value of the given numeric. */
   size_t operator()(hyperapi::Numeric<p, s> n) const noexcept {
      return std::hash<typename hyperapi::Numeric<p, s>::data_t>()(n.value_);
   }
};
}

#include <hyperapi/impl/Numeric.impl.hpp> // IWYU pragma: export

#endif
