#ifndef TABLEAU_HYPER_IMPL_NUMERIC_IMPL_HPP
#define TABLEAU_HYPER_IMPL_NUMERIC_IMPL_HPP

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

#include <cassert>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <sstream>
#include <utility>

// NOLINTNEXTLINE We can't use nested namespaces as it has to remain C++11 compatible
namespace hyperapi {

namespace internal {

/// The maximum length of the string representation for a numeric value (i.e.: minus, leading zero, a comma, 19 digits, and null character)
constexpr size_t maxNumericStringLength = 23;

/// The maximum length of the string representation for a big numeric value (i.e.: minus, leading zero, a comma, 39 digits, and null character)
constexpr size_t maxBigNumericStringLength = 43;

inline void throwOverflow() {
   throw internal::makeHyperException("numeric overflow", "", ContextId(0x4b4091a3u));
}

inline hyper_data128_t smallToBigNumeric(int64_t value) {
   return {static_cast<uint64_t>(value), (value < 0) ? ~0ull : 0ull};
}

inline int64_t bigToSmallNumeric(hyper_data128_t value) {
   if (static_cast<int64_t>(value.data[1]) != (static_cast<int64_t>(value.data[0]) >> 63)) {
      throwOverflow();
   }
   return value.data[0];
}

inline void scaleNumeric(int64_t value, unsigned scale, unsigned newScale, int64_t& result) {
   assert(scale <= 18);
   assert(newScale <= 18);
   hyper_error_t* error = hyper_numeric_change_scale(value, scale, newScale, &result);
   if (error) {
      throw internal::makeHyperException(error);
   }
}

inline void scaleNumeric(hyper_data128_t value, unsigned scale, unsigned newScale, hyper_data128_t& result) {
   assert(scale <= 38);
   assert(newScale <= 38);
   hyper_error_t* error = hyper_big_numeric_change_scale(value, scale, newScale, &result);
   if (error) {
      throw internal::makeHyperException(error);
   }
}

inline void scaleNumeric(int64_t value, unsigned scale, unsigned newScale, hyper_data128_t& result) {
   assert(scale <= 18);
   assert(newScale <= 38);
   hyper_data128_t bigValue = smallToBigNumeric(value);
   scaleNumeric(bigValue, scale, newScale, result);
}

inline void scaleNumeric(hyper_data128_t value, unsigned scale, unsigned newScale, int64_t& result) {
   assert(scale <= 38);
   assert(newScale <= 18);
   hyper_data128_t bigResult;
   hyper_error_t* error = hyper_big_numeric_change_scale(value, scale, newScale, &bigResult);
   if (error) {
      throw internal::makeHyperException(error);
   }
   result = bigToSmallNumeric(bigResult);
}

inline void numericFromInteger(int64_t value, unsigned scale, int64_t& result) {
   assert(scale <= 18);
   internal::scaleNumeric(value, 0, scale, result);
}

inline void numericFromInteger(int64_t value, unsigned scale, hyper_data128_t& result) {
   assert(scale <= 38);
   internal::scaleNumeric(value, 0, scale, result);
}

inline int64_t numericToInteger(int64_t value, unsigned scale) {
   assert(scale <= 18);
   return hyper_numeric_round_to_int64(value, scale);
}

inline int64_t numericToInteger(hyper_data128_t value, unsigned scale) {
   assert(scale <= 38);
   int64_t result;
   hyper_error_t* error = hyper_big_numeric_round_to_int64(value, scale, &result);
   if (error) {
      throw internal::makeHyperException(error);
   }
   return result;
}

inline void numericFromFloat(float value, unsigned scale, int64_t& result) {
   assert(scale <= 18);
   hyper_error_t* error = hyper_numeric_from_float(value, scale, &result);
   if (error) {
      throw internal::makeHyperException(error);
   }
}

inline void numericFromFloat(float value, unsigned scale, hyper_data128_t& result) {
   assert(scale <= 38);
   hyper_error_t* error = hyper_big_numeric_from_float(value, scale, &result);
   if (error) {
      throw internal::makeHyperException(error);
   }
}

inline float numericToFloat(int64_t value, unsigned scale) {
   assert(scale <= 18);
   return hyper_numeric_to_float(value, scale);
}

inline float numericToFloat(hyper_data128_t value, unsigned scale) {
   assert(scale <= 38);
   return hyper_big_numeric_to_float(value, scale);
}

inline void numericFromDouble(double value, unsigned scale, int64_t& result) {
   assert(scale <= 18);
   hyper_error_t* error = hyper_numeric_from_double(value, scale, &result);
   if (error) {
      throw internal::makeHyperException(error);
   }
}

inline void numericFromDouble(double value, unsigned scale, hyper_data128_t& result) {
   assert(scale <= 38);
   hyper_error_t* error = hyper_big_numeric_from_double(value, scale, &result);
   if (error) {
      throw internal::makeHyperException(error);
   }
}

inline double numericToDouble(int64_t value, unsigned scale) {
   assert(scale <= 18);
   return hyper_numeric_to_double(value, scale);
}

inline double numericToDouble(hyper_data128_t value, unsigned scale) {
   assert(scale <= 38);
   return hyper_big_numeric_to_double(value, scale);
}

inline void numericFromString(const hyperapi::string_view value, unsigned precision, unsigned scale, int64_t& result) {
   assert(precision <= 18);
   assert(scale <= precision);
   const char* begin = value.data();
   const char* end = begin + value.size();
   hyper_error_t* error = hyper_numeric_from_string(&begin, end, precision, scale, &result);
   if (error) {
      throw internal::makeHyperException(error);
   }
}

inline void numericFromString(const hyperapi::string_view value, unsigned precision, unsigned scale, hyper_data128_t& result) {
   assert((precision > 18) && (precision <= 38));
   assert(scale <= precision);
   const char* begin = value.data();
   const char* end = begin + value.size();
   hyper_error_t* error = hyper_big_numeric_from_string(&begin, end, precision, scale, &result);
   if (error) {
      throw internal::makeHyperException(error);
   }
}

inline std::string numericToString(int64_t value, unsigned scale) {
   assert(scale <= 18);
   char buffer[maxNumericStringLength];
   char* start = hyper_numeric_to_string(buffer, sizeof(buffer), value, scale);
   return std::string{start, static_cast<size_t>((buffer + sizeof(buffer) - 1) - start)};
}

inline std::string numericToString(hyper_data128_t value, unsigned scale) {
   assert(scale <= 38);
   char buffer[maxBigNumericStringLength];
   char* start = hyper_big_numeric_to_string(buffer, sizeof(buffer), value, scale);
   return std::string{start, static_cast<size_t>((buffer + sizeof(buffer) - 1) - start)};
}

template <unsigned p, unsigned s>
typename Numeric<p, s>::data_t numericInternalRepresentation(const Numeric<p, s>& numeric) noexcept {
   return numeric.value_;
}
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(short value) {
   internal::numericFromInteger(value, scale, value_);
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(unsigned short value) {
   internal::numericFromInteger(value, scale, value_);
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(int value) {
   internal::numericFromInteger(value, scale, value_);
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(unsigned int value) {
   internal::numericFromInteger(value, scale, value_);
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(long value) {
   internal::numericFromInteger(value, scale, value_);
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(unsigned long value) {
   if (value > std::numeric_limits<int64_t>::max()) {
      internal::throwOverflow();
   }
   internal::numericFromInteger(value, scale, value_);
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(long long value) {
   internal::numericFromInteger(value, scale, value_);
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(unsigned long long value) {
   if (value > std::numeric_limits<int64_t>::max()) {
      internal::throwOverflow();
   }
   internal::numericFromInteger(value, scale, value_);
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(float value) {
   internal::numericFromFloat(value, scale, value_);
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(double value) {
   internal::numericFromDouble(value, scale, value_);
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(long double value)
   : Numeric(static_cast<double>(value)) {
}

template <unsigned precision, unsigned scale>
Numeric<precision, scale>::Numeric(const hyperapi::string_view value) {
   internal::numericFromString(value, precision, scale, value_);
}

template <unsigned precision, unsigned scale>
float Numeric<precision, scale>::floatValue() const noexcept {
   return internal::numericToFloat(value_, scale);
}

template <unsigned precision, unsigned scale>
double Numeric<precision, scale>::doubleValue() const noexcept {
   return internal::numericToDouble(value_, scale);
}

template <unsigned precision, unsigned scale>
int64_t Numeric<precision, scale>::intValue() const {
   return internal::numericToInteger(value_, scale);
}

template <unsigned precision, unsigned scale>
template <unsigned otherPrecision, unsigned otherScale>
Numeric<precision, scale>::Numeric(hyperapi::Numeric<otherPrecision, otherScale> other)
   : Numeric(other.value_, otherPrecision, otherScale, raw_t()) {}

template <unsigned precision, unsigned scale>
inline Numeric<precision, scale>::Numeric(int64_t value, unsigned /*otherPrecision*/, unsigned otherScale, raw_t) {
   internal::scaleNumeric(value, otherScale, scale, value_);
}

template <unsigned precision, unsigned scale>
inline Numeric<precision, scale>::Numeric(hyper_data128_t value, unsigned /*otherPrecision*/, unsigned otherScale, raw_t) {
   internal::scaleNumeric(value, otherScale, scale, value_);
}

template <unsigned precision, unsigned scale>
std::string Numeric<precision, scale>::stringValue() const {
   return internal::numericToString(value_, scale);
}

template <unsigned precision, unsigned scale>
std::string Numeric<precision, scale>::toString() const {
   return stringValue();
}
}

#endif
