// Copyright (C) 2026 Kiyotsugu Arai
// SPDX-License-Identifier: LGPL-3.0-or-later

// generic_math.hpp
// Math-constant helpers that work for both standard and multiprecision floating-point (sangi::Float).
// Generalizes the if constexpr branching pattern established in FFT (detail::fft_pi in fft.hpp).
//
//
// Design policy (same as fft_pi):
//   - This header does not include Float.hpp (kept lightweight).
//   - The sangi::Float branch (T::pi(...)) is a dependent expression, so it is resolved at the
//     point of instantiation with T = Float (i.e. when the caller has already included Float.hpp).
//   - Function calls (sin/cos/sqrt, etc.) need no separate helper because Float.hpp injects
//     std::cos(Float) etc. into namespace std. This header only handles "constants" (π, etc.)
//     that cannot be resolved via ADL/std.

#ifndef SANGI_GENERIC_MATH_HPP
#define SANGI_GENERIC_MATH_HPP

#include <cmath>
#include <numbers>
#include <type_traits>
#include <concepts>
#include <cstddef>
#include <cstdint>

namespace sangi {

// "Floating-point type" concept.
// Accepts both built-in floating-point (float/double/long double) and multiprecision floating-point
// (sangi::Float — a class with a defaultPrecision()).
// std::floating_point passes only built-in types and rejects sangi::Float, so this is a relaxed
// constraint that lets geometry types (AngleAxis/EulerAngles/Transform, etc.) use sangi::Float.
// "Exact fields" such as Rational (which lack defaultPrecision()) are excluded — they have no
// transcendental functions (sin/cos, etc.), so the internal geometry implementations would not work.
// (This trait does not include Float.hpp; it decides by the presence of T::defaultPrecision().)
template<typename T>
concept FloatingPointType =
    std::floating_point<T> ||
    requires { { T::defaultPrecision() } -> std::convertible_to<int>; };

namespace detail {

// Pi, π. constants::pi<T> (a constexpr variable template) cannot be instantiated for the non-literal
// type Float, so this is its Float-capable counterpart.
template<typename T>
[[nodiscard]] inline T generic_pi() {
    if constexpr (std::is_floating_point_v<T>) {
        return std::numbers::pi_v<T>;
    } else if constexpr (requires { T::pi(); }) {
        // sangi::Float etc.: multiprecision π at the default precision
        return T::pi();
    } else {
        // fallback for Complex<double> etc. (same precision as constants::pi<T>)
        return static_cast<T>(3.141592653589793238462643383279502884L);
    }
}

// 2π
template<typename T>
[[nodiscard]] inline T generic_two_pi() {
    return T(2) * generic_pi<T>();
}

// π/2
template<typename T>
[[nodiscard]] inline T generic_half_pi() {
    return generic_pi<T>() / T(2);
}

// Napier's constant e
template<typename T>
[[nodiscard]] inline T generic_e() {
    if constexpr (std::is_floating_point_v<T>) {
        return std::numbers::e_v<T>;
    } else if constexpr (requires { T::e(); }) {
        return T::e();
    } else {
        return static_cast<T>(2.718281828459045235360287471352662498L);
    }
}

// atan2(y, x). For multiprecision Float, `using std::atan2; atan2(y,x)` is ambiguous (C2668)
// due to the double definition of the std-namespace bridge (std::atan2(Float,Float)) and
// sangi::atan2(Float,Float). Here standard floating-point uses std::atan2, and
// everything else resolves via ADL only (without a using) (Float → sangi::atan2,
// Fixed → Fixed's atan2), avoiding the ambiguity.
template<typename T>
[[nodiscard]] inline T generic_atan2(const T& y, const T& x) {
    if constexpr (std::is_floating_point_v<T>) {
        return std::atan2(y, x);
    } else {
        return atan2(y, x);
    }
}

// abs(x). As with atan2, for multiprecision Float `using std::abs; abs(x)` is ambiguous (C2668)
// between the std::abs(Float) bridge and sangi::abs(Float)
// (std::abs has integer overloads etc., so ADL is not suppressed).
// Standard floating-point uses std::abs; everything else resolves via ADL without a using.
template<typename T>
[[nodiscard]] inline T generic_abs(const T& x) {
    if constexpr (std::is_floating_point_v<T>) {
        return std::abs(x);
    } else {
        return abs(x);
    }
}

// One-argument transcendental functions. Because Float.hpp injects both the std::X(Float) bridge
// into namespace std and sangi::X(Float) into namespace sangi, `using std::X; X(x)`
// can be ambiguous (C2668) depending on context (depends on MSVC overload resolution and
// is unstable). Here we resolve it reliably with the if constexpr branching established in FFT:
//   - standard floating-point (double, etc.) → std::X
//   - everything else (sangi::Float / Fixed, etc.) → bare ADL without a using
//     (for Float the std bridge is not a candidate, leaving only sangi::X → no ambiguity)
#define SANGI_DEFINE_GENERIC_UNARY(NAME)                       \
    template<typename T>                                       \
    [[nodiscard]] inline T generic_##NAME(const T& x) {        \
        if constexpr (std::is_floating_point_v<T>) {           \
            return std::NAME(x);                               \
        } else {                                               \
            return NAME(x);                                    \
        }                                                      \
    }

SANGI_DEFINE_GENERIC_UNARY(sqrt)
SANGI_DEFINE_GENERIC_UNARY(cbrt)
SANGI_DEFINE_GENERIC_UNARY(sin)
SANGI_DEFINE_GENERIC_UNARY(cos)
SANGI_DEFINE_GENERIC_UNARY(tan)
SANGI_DEFINE_GENERIC_UNARY(asin)
SANGI_DEFINE_GENERIC_UNARY(acos)
SANGI_DEFINE_GENERIC_UNARY(atan)
SANGI_DEFINE_GENERIC_UNARY(exp)
SANGI_DEFINE_GENERIC_UNARY(log)
SANGI_DEFINE_GENERIC_UNARY(sinh)
SANGI_DEFINE_GENERIC_UNARY(cosh)
SANGI_DEFINE_GENERIC_UNARY(tanh)
SANGI_DEFINE_GENERIC_UNARY(asinh)
SANGI_DEFINE_GENERIC_UNARY(acosh)
SANGI_DEFINE_GENERIC_UNARY(atanh)
SANGI_DEFINE_GENERIC_UNARY(floor)
SANGI_DEFINE_GENERIC_UNARY(ceil)
SANGI_DEFINE_GENERIC_UNARY(round)
SANGI_DEFINE_GENERIC_UNARY(trunc)
SANGI_DEFINE_GENERIC_UNARY(exp2)
SANGI_DEFINE_GENERIC_UNARY(expm1)
SANGI_DEFINE_GENERIC_UNARY(log2)
SANGI_DEFINE_GENERIC_UNARY(log10)
SANGI_DEFINE_GENERIC_UNARY(log1p)
SANGI_DEFINE_GENERIC_UNARY(erf)
SANGI_DEFINE_GENERIC_UNARY(erfc)
SANGI_DEFINE_GENERIC_UNARY(tgamma)
SANGI_DEFINE_GENERIC_UNARY(lgamma)

#undef SANGI_DEFINE_GENERIC_UNARY

// Two-argument transcendental function (helper-ized for the same reason as atan2).
#define SANGI_DEFINE_GENERIC_BINARY(NAME)                              \
    template<typename T>                                              \
    [[nodiscard]] inline T generic_##NAME(const T& a, const T& b) {   \
        if constexpr (std::is_floating_point_v<T>) {                 \
            return std::NAME(a, b);                                  \
        } else {                                                     \
            return NAME(a, b);                                       \
        }                                                            \
    }

SANGI_DEFINE_GENERIC_BINARY(pow)
SANGI_DEFINE_GENERIC_BINARY(hypot)
SANGI_DEFINE_GENERIC_BINARY(fmod)

#undef SANGI_DEFINE_GENERIC_BINARY

// Integer power x^n. The general pow (exp(n·log x)) breaks for negative bases, so this is dedicated.
// For Float, sangi::pow(Float,int) is resolved via ADL (no collision with the std bridge without a using).
template<typename T>
[[nodiscard]] inline T generic_powi(const T& x, int n) {
    if constexpr (std::is_floating_point_v<T>) {
        return std::pow(x, n);
    } else {
        return pow(x, n);
    }
}

// ---- conversion from type T to a built-in type ----
// Multiprecision Float has no static_cast conversion operators to double/int; use .toDouble()/
// .toInt64()/.toSizet(). These helpers let you write conversions that work whether T is double or Float.
//
//   - to_double: ★loses precision (Float→double). Use only where double precision suffices,
//     such as deciding logs / thresholds / scaling exponents. Do not use for the actual numerics.
//   - to_int64 / to_sizet: for index computation. Float converts directly to an integer without going through double.

template<typename T>
[[nodiscard]] inline double to_double(const T& x) {
    if constexpr (std::is_arithmetic_v<T>) return static_cast<double>(x);
    else return x.toDouble();
}

template<typename T>
[[nodiscard]] inline std::int64_t to_int64(const T& x) {
    if constexpr (std::is_arithmetic_v<T>) return static_cast<std::int64_t>(x);
    else return x.toInt64();
}

template<typename T>
[[nodiscard]] inline std::size_t to_sizet(const T& x) {
    if constexpr (std::is_arithmetic_v<T>) return static_cast<std::size_t>(x);
    else return x.toSizet();
}

} // namespace detail
} // namespace sangi

#endif // SANGI_GENERIC_MATH_HPP
