//! Traits and operations related to bounds of a function.

use std::fmt;
use std::ops::Bound;

use libm::support::Int;

use crate::{BaseName, Float, FloatExt, Identifier};

/// Representation of a single dimension of a function's domain.
#[derive(Clone, Debug)]
pub struct Domain<T> {
    /// Start of the region for which a function is defined (ignoring poles).
    pub start: Bound<T>,
    /// Endof the region for which a function is defined (ignoring poles).
    pub end: Bound<T>,
    /// Additional points to check closer around. These can be e.g. undefined asymptotes or
    /// inflection points.
    pub check_points: Option<fn() -> BoxIter<T>>,
}

type BoxIter<T> = Box<dyn Iterator<Item = T>>;

impl<F: FloatExt> Domain<F> {
    /// The start of this domain, saturating at negative infinity.
    pub fn range_start(&self) -> F {
        match self.start {
            Bound::Included(v) => v,
            Bound::Excluded(v) => v.next_up(),
            Bound::Unbounded => F::NEG_INFINITY,
        }
    }

    /// The end of this domain, saturating at infinity.
    pub fn range_end(&self) -> F {
        match self.end {
            Bound::Included(v) => v,
            Bound::Excluded(v) => v.next_down(),
            Bound::Unbounded => F::INFINITY,
        }
    }
}

/// A value that may be any float type or any integer type.
#[derive(Clone, Debug)]
pub enum EitherPrim<F, I> {
    Float(F),
    Int(I),
}

impl<F: fmt::Debug, I: fmt::Debug> EitherPrim<F, I> {
    pub fn unwrap_float(self) -> F {
        match self {
            EitherPrim::Float(f) => f,
            EitherPrim::Int(_) => panic!("expected float; got {self:?}"),
        }
    }

    pub fn unwrap_int(self) -> I {
        match self {
            EitherPrim::Float(_) => panic!("expected int; got {self:?}"),
            EitherPrim::Int(i) => i,
        }
    }
}

/// Convenience 1-dimensional float domains.
impl<F: Float> Domain<F> {
    /// x ∈ ℝ
    const UNBOUNDED: Self = Self {
        start: Bound::Unbounded,
        end: Bound::Unbounded,
        check_points: None,
    };

    /// x ∈ ℝ >= 0
    const POSITIVE: Self = Self {
        start: Bound::Included(F::ZERO),
        end: Bound::Unbounded,
        check_points: None,
    };

    /// x ∈ ℝ > 0
    const STRICTLY_POSITIVE: Self = Self {
        start: Bound::Excluded(F::ZERO),
        end: Bound::Unbounded,
        check_points: None,
    };

    /// Wrap in the float variant of [`EitherPrim`].
    const fn into_prim_float<I>(self) -> EitherPrim<Self, Domain<I>> {
        EitherPrim::Float(self)
    }
}

/// Convenience 1-dimensional integer domains.
impl<I: Int> Domain<I> {
    /// x ∈ ℝ
    const UNBOUNDED_INT: Self = Self {
        start: Bound::Unbounded,
        end: Bound::Unbounded,
        check_points: None,
    };

    /// Wrap in the int variant of [`EitherPrim`].
    const fn into_prim_int<F>(self) -> EitherPrim<Domain<F>, Self> {
        EitherPrim::Int(self)
    }
}

/// Multidimensional domains, represented as an array of 1-D domains.
impl<F: Float, I: Int> EitherPrim<Domain<F>, Domain<I>> {
    /// x ∈ ℝ
    const UNBOUNDED1: [Self; 1] = [Domain {
        start: Bound::Unbounded,
        end: Bound::Unbounded,
        check_points: None,
    }
    .into_prim_float()];

    /// {x1, x2} ∈ ℝ
    const UNBOUNDED2: [Self; 2] = [
        Domain::UNBOUNDED.into_prim_float(),
        Domain::UNBOUNDED.into_prim_float(),
    ];

    /// {x1, x2, x3} ∈ ℝ
    const UNBOUNDED3: [Self; 3] = [
        Domain::UNBOUNDED.into_prim_float(),
        Domain::UNBOUNDED.into_prim_float(),
        Domain::UNBOUNDED.into_prim_float(),
    ];

    /// {x1, x2} ∈ ℝ, one float and one int
    const UNBOUNDED_F_I: [Self; 2] = [
        Domain::UNBOUNDED.into_prim_float(),
        Domain::UNBOUNDED_INT.into_prim_int(),
    ];

    /// x ∈ ℝ >= 0
    const POSITIVE: [Self; 1] = [Domain::POSITIVE.into_prim_float()];

    /// x ∈ ℝ > 0
    const STRICTLY_POSITIVE: [Self; 1] = [Domain::STRICTLY_POSITIVE.into_prim_float()];

    /// Used for versions of `asin` and `acos`.
    const INVERSE_TRIG_PERIODIC: [Self; 1] = [Domain {
        start: Bound::Included(F::NEG_ONE),
        end: Bound::Included(F::ONE),
        check_points: None,
    }
    .into_prim_float()];

    /// Domain for `acosh`
    const ACOSH: [Self; 1] = [Domain {
        start: Bound::Included(F::ONE),
        end: Bound::Unbounded,
        check_points: None,
    }
    .into_prim_float()];

    /// Domain for `atanh`
    const ATANH: [Self; 1] = [Domain {
        start: Bound::Excluded(F::NEG_ONE),
        end: Bound::Excluded(F::ONE),
        check_points: None,
    }
    .into_prim_float()];

    /// Domain for `sin`, `cos`, and `tan`
    const TRIG: [Self; 1] = [Domain {
        // Trig functions have special behavior at fractions of π.
        check_points: Some(|| Box::new([-F::PI, -F::FRAC_PI_2, F::FRAC_PI_2, F::PI].into_iter())),
        ..Domain::UNBOUNDED
    }
    .into_prim_float()];

    /// Domain for `log` in various bases
    const LOG: [Self; 1] = Self::STRICTLY_POSITIVE;

    /// Domain for `log1p` i.e. `log(1 + x)`
    const LOG1P: [Self; 1] = [Domain {
        start: Bound::Excluded(F::NEG_ONE),
        end: Bound::Unbounded,
        check_points: None,
    }
    .into_prim_float()];

    /// Domain for `sqrt`
    const SQRT: [Self; 1] = Self::POSITIVE;

    /// Domain for `gamma`
    const GAMMA: [Self; 1] = [Domain {
        check_points: Some(|| {
            // Negative integers are asymptotes
            Box::new((0..u8::MAX).map(|scale| {
                let mut base = F::ZERO;
                for _ in 0..scale {
                    base = base - F::ONE;
                }
                base
            }))
        }),
        // Whether or not gamma is defined for negative numbers is implementation dependent
        ..Domain::UNBOUNDED
    }
    .into_prim_float()];

    /// Domain for `loggamma`
    const LGAMMA: [Self; 1] = Self::UNBOUNDED1;

    /// Domain for `jn` and `yn`.
    // FIXME: the domain should provide some sort of "reasonable range" so we don't actually test
    // the entire system unbounded.
    const BESSEL_N: [Self; 2] = [
        Domain::UNBOUNDED_INT.into_prim_int(),
        Domain::UNBOUNDED.into_prim_float(),
    ];
}

/// Get the domain for a given function.
pub fn get_domain<F: Float, I: Int>(
    id: Identifier,
    argnum: usize,
) -> EitherPrim<Domain<F>, Domain<I>> {
    let x = match id.base_name() {
        BaseName::Acos => &EitherPrim::INVERSE_TRIG_PERIODIC[..],
        BaseName::Acosh => &EitherPrim::ACOSH[..],
        BaseName::Asin => &EitherPrim::INVERSE_TRIG_PERIODIC[..],
        BaseName::Asinh => &EitherPrim::UNBOUNDED1[..],
        BaseName::Atan => &EitherPrim::UNBOUNDED1[..],
        BaseName::Atan2 => &EitherPrim::UNBOUNDED2[..],
        BaseName::Cbrt => &EitherPrim::UNBOUNDED1[..],
        BaseName::Atanh => &EitherPrim::ATANH[..],
        BaseName::Ceil => &EitherPrim::UNBOUNDED1[..],
        BaseName::Cosh => &EitherPrim::UNBOUNDED1[..],
        BaseName::Copysign => &EitherPrim::UNBOUNDED2[..],
        BaseName::Cos => &EitherPrim::TRIG[..],
        BaseName::Exp => &EitherPrim::UNBOUNDED1[..],
        BaseName::Erf => &EitherPrim::UNBOUNDED1[..],
        BaseName::Erfc => &EitherPrim::UNBOUNDED1[..],
        BaseName::Expm1 => &EitherPrim::UNBOUNDED1[..],
        BaseName::Exp10 => &EitherPrim::UNBOUNDED1[..],
        BaseName::Exp2 => &EitherPrim::UNBOUNDED1[..],
        BaseName::Frexp => &EitherPrim::UNBOUNDED1[..],
        BaseName::Fabs => &EitherPrim::UNBOUNDED1[..],
        BaseName::Fdim => &EitherPrim::UNBOUNDED2[..],
        BaseName::Floor => &EitherPrim::UNBOUNDED1[..],
        BaseName::Fma => &EitherPrim::UNBOUNDED3[..],
        BaseName::Fmax => &EitherPrim::UNBOUNDED2[..],
        BaseName::Fmaximum => &EitherPrim::UNBOUNDED2[..],
        BaseName::FmaximumNum => &EitherPrim::UNBOUNDED2[..],
        BaseName::Fmin => &EitherPrim::UNBOUNDED2[..],
        BaseName::Fminimum => &EitherPrim::UNBOUNDED2[..],
        BaseName::FminimumNum => &EitherPrim::UNBOUNDED2[..],
        BaseName::Fmod => &EitherPrim::UNBOUNDED2[..],
        BaseName::Hypot => &EitherPrim::UNBOUNDED2[..],
        BaseName::Ilogb => &EitherPrim::UNBOUNDED1[..],
        BaseName::J0 => &EitherPrim::UNBOUNDED1[..],
        BaseName::J1 => &EitherPrim::UNBOUNDED1[..],
        BaseName::Jn => &EitherPrim::BESSEL_N[..],
        BaseName::Ldexp => &EitherPrim::UNBOUNDED_F_I[..],
        BaseName::Lgamma => &EitherPrim::LGAMMA[..],
        BaseName::LgammaR => &EitherPrim::LGAMMA[..],
        BaseName::Log => &EitherPrim::LOG[..],
        BaseName::Log10 => &EitherPrim::LOG[..],
        BaseName::Log1p => &EitherPrim::LOG1P[..],
        BaseName::Log2 => &EitherPrim::LOG[..],
        BaseName::Modf => &EitherPrim::UNBOUNDED1[..],
        BaseName::Nextafter => &EitherPrim::UNBOUNDED2[..],
        BaseName::Pow => &EitherPrim::UNBOUNDED2[..],
        BaseName::Remainder => &EitherPrim::UNBOUNDED2[..],
        BaseName::Remquo => &EitherPrim::UNBOUNDED2[..],
        BaseName::Rint => &EitherPrim::UNBOUNDED1[..],
        BaseName::Round => &EitherPrim::UNBOUNDED1[..],
        BaseName::Roundeven => &EitherPrim::UNBOUNDED1[..],
        BaseName::Scalbn => &EitherPrim::UNBOUNDED_F_I[..],
        BaseName::Sin => &EitherPrim::TRIG[..],
        BaseName::Sincos => &EitherPrim::TRIG[..],
        BaseName::Sinh => &EitherPrim::UNBOUNDED1[..],
        BaseName::Sqrt => &EitherPrim::SQRT[..],
        BaseName::Tan => &EitherPrim::TRIG[..],
        BaseName::Tanh => &EitherPrim::UNBOUNDED1[..],
        BaseName::Tgamma => &EitherPrim::GAMMA[..],
        BaseName::Trunc => &EitherPrim::UNBOUNDED1[..],
        BaseName::Y0 => &EitherPrim::UNBOUNDED1[..],
        BaseName::Y1 => &EitherPrim::UNBOUNDED1[..],
        BaseName::Yn => &EitherPrim::BESSEL_N[..],
    };

    x[argnum].clone()
}
