//! Configuration for skipping or changing the result for individual test cases (inputs) rather
//! than ignoring entire tests.

use BaseName as Bn;
use CheckBasis::{Mpfr, Musl};
use Identifier as Id;
use libm::support::CastFrom;

use crate::{BaseName, CheckBasis, CheckCtx, Float, Identifier, Int, TestResult};

/// Type implementing [`IgnoreCase`].
pub struct SpecialCase;

/// ULP allowed to differ from the results returned by a test basis.
#[allow(clippy::single_match)]
pub fn default_ulp(ctx: &CheckCtx) -> u32 {
    // ULP compared to the infinite (MPFR) result.
    let mut ulp = match ctx.base_name {
        // Operations that require exact results. This list should correlate with what we
        // have documented at <https://doc.rust-lang.org/std/primitive.f32.html>.
        Bn::Ceil
        | Bn::Copysign
        | Bn::Fabs
        | Bn::Fdim
        | Bn::Floor
        | Bn::Fma
        | Bn::Fmax
        | Bn::Fmaximum
        | Bn::FmaximumNum
        | Bn::Fmin
        | Bn::Fminimum
        | Bn::FminimumNum
        | Bn::Fmod
        | Bn::Frexp
        | Bn::Ilogb
        | Bn::Ldexp
        | Bn::Modf
        | Bn::Nextafter
        | Bn::Remainder
        | Bn::Remquo
        | Bn::Rint
        | Bn::Round
        | Bn::Roundeven
        | Bn::Scalbn
        | Bn::Sqrt
        | Bn::Trunc => 0,

        // Operations that aren't required to be exact, but our implementations are.
        Bn::Cbrt => 0,

        // Bessel functions have large inaccuracies.
        Bn::J0 | Bn::J1 | Bn::Y0 | Bn::Y1 | Bn::Jn | Bn::Yn => 8_000_000,

        // For all other operations, specify our implementation's worst case precision.
        Bn::Acos => 1,
        Bn::Acosh => 4,
        Bn::Asin => 1,
        Bn::Asinh => 2,
        Bn::Atan => 1,
        Bn::Atan2 => 2,
        Bn::Atanh => 2,
        Bn::Cos => 1,
        Bn::Cosh => 1,
        Bn::Erf => 1,
        Bn::Erfc => 4,
        Bn::Exp => 1,
        Bn::Exp10 => 6,
        Bn::Exp2 => 1,
        Bn::Expm1 => 1,
        Bn::Hypot => 1,
        Bn::Lgamma | Bn::LgammaR => 4,
        Bn::Log => 1,
        Bn::Log10 => 1,
        Bn::Log1p => 1,
        Bn::Log2 => 1,
        Bn::Pow => 1,
        Bn::Sin => 1,
        Bn::Sincos => 1,
        Bn::Sinh => 2,
        Bn::Tan => 1,
        Bn::Tanh => 2,
        // tgammaf has higher accuracy than tgamma.
        Bn::Tgamma if ctx.fn_ident != Id::Tgamma => 1,
        Bn::Tgamma => 20,
    };

    // These have a separate implementation on i586
    if cfg!(x86_no_sse) {
        match ctx.fn_ident {
            Id::Exp => ulp = 1,
            Id::Exp2 => ulp = 1,
            Id::Exp10 => ulp = 1,
            Id::Expf => ulp = 0,
            Id::Exp2f => ulp = 0,
            Id::Exp10f => ulp = 0,
            _ => (),
        }
    }

    // There are some cases where musl's approximation is less accurate than ours. For these
    // cases, increase the ULP.
    if ctx.basis == Musl {
        match ctx.base_name {
            Bn::Cosh => ulp = 2,
            Bn::Exp10 if usize::BITS < 64 => ulp = 4,
            Bn::Tanh => ulp = 4,
            _ => (),
        }

        match ctx.fn_ident {
            Id::Cbrt => ulp = 2,
            // FIXME(#401): musl has an incorrect result here.
            Id::Fdim => ulp = 2,
            Id::Exp2f => ulp = 1,
            Id::Expf => ulp = 1,
            Id::Sincosf => ulp = 500,
            Id::Tgamma => ulp = 20,
            _ => (),
        }
    }

    if cfg!(target_arch = "x86") {
        match ctx.fn_ident {
            // Input `fma(0.999999999999999, 1.0000000000000013, 0.0) = 1.0000000000000002` is
            // incorrect on i586 and i686.
            Id::Fma => ulp = 1,
            _ => (),
        }
    }

    // In some cases, our implementation is less accurate than musl on i586.
    if cfg!(x86_no_sse) {
        match ctx.fn_ident {
            // FIXME(#401): these need to be correctly rounded but are not.
            Id::Fmaf => ulp = 1,
            Id::Fdim => ulp = 1,
            Id::Round => ulp = 1,

            Id::Asinh => ulp = 3,
            Id::Asinhf => ulp = 3,
            Id::Cbrt => ulp = 1,
            Id::Log1p | Id::Log1pf => ulp = 2,
            Id::Tan => ulp = 2,
            _ => (),
        }
    }

    ulp
}

/// Result of checking for possible overrides.
#[derive(Debug, Default)]
pub enum CheckAction {
    /// The check should pass. Default case.
    #[default]
    AssertSuccess,

    /// Override the ULP for this check.
    AssertWithUlp(u32),

    /// Failure is expected, ensure this is the case (xfail). Takes a contxt string to help trace
    /// back exactly why we expect this to fail.
    AssertFailure(&'static str),

    /// The override somehow validated the result, here it is.
    Custom(TestResult),

    /// Disregard the output.
    Skip,
}

/// Don't run further validation on this test case.
const SKIP: CheckAction = CheckAction::Skip;

/// Return this to skip checks on a test that currently fails but shouldn't. Takes a description
/// of context.
const XFAIL: fn(&'static str) -> CheckAction = CheckAction::AssertFailure;

/// Indicates that we expect a test to fail but we aren't asserting that it does (e.g. some results
/// within a range do actually pass).
///
/// Same as `SKIP`, just indicates we have something to eventually fix.
const XFAIL_NOCHECK: CheckAction = CheckAction::Skip;

/// By default, all tests should pass.
const DEFAULT: CheckAction = CheckAction::AssertSuccess;

/// Allow overriding the outputs of specific test cases.
///
/// There are some cases where we want to xfail specific cases or handle certain inputs
/// differently than the rest of calls to `validate`. This provides a hook to do that.
///
/// If `None` is returned, checks will proceed as usual. If `Some(result)` is returned, checks
/// are skipped and the provided result is returned instead.
///
/// This gets implemented once per input type, then the functions provide further filtering
/// based on function name and values.
///
/// `ulp` can also be set to adjust the ULP for that specific test, even if `None` is still
/// returned.
pub trait MaybeOverride<Input> {
    fn check_float<F: Float>(
        _input: Input,
        _actual: F,
        _expected: F,
        _ctx: &CheckCtx,
    ) -> CheckAction {
        DEFAULT
    }

    fn check_int<I: Int>(_input: Input, _actual: I, _expected: I, _ctx: &CheckCtx) -> CheckAction {
        DEFAULT
    }
}

#[cfg(f16_enabled)]
impl MaybeOverride<(f16,)> for SpecialCase {}

impl MaybeOverride<(f32,)> for SpecialCase {
    fn check_float<F: Float>(input: (f32,), actual: F, expected: F, ctx: &CheckCtx) -> CheckAction {
        if ctx.base_name == BaseName::J0 && input.0 < -1e34 {
            // Errors get huge close to -inf
            return XFAIL_NOCHECK;
        }

        // the testing infrastructure doesn't account for allowed ulp in the case of overflow
        if matches!(ctx.base_name, BaseName::Lgamma | BaseName::LgammaR)
            && input.0 == 4.0850034e36
            && expected.is_infinite()
            && actual == F::MAX
        {
            return XFAIL_NOCHECK;
        }

        // FIXME(correctness): lgammaf has high relative inaccuracy near its zeroes
        if matches!(ctx.base_name, BaseName::Lgamma | BaseName::LgammaR)
            && input.0 > -13.0625
            && input.0 < -2.0
            && (expected.abs() < F::ONE || (input.0 - input.0.round()).abs() < 0.02)
        {
            return XFAIL_NOCHECK;
        }

        unop_common(input, actual, expected, ctx)
    }

    fn check_int<I: Int>(input: (f32,), actual: I, expected: I, ctx: &CheckCtx) -> CheckAction {
        // On MPFR for lgammaf_r, we set -1 as the integer result for negative infinity but MPFR
        // sets +1
        if ctx.basis == CheckBasis::Mpfr
            && ctx.base_name == BaseName::LgammaR
            && input.0 == f32::NEG_INFINITY
            && actual.abs() == expected.abs()
        {
            return XFAIL("lgammar integer result");
        }

        DEFAULT
    }
}

impl MaybeOverride<(f64,)> for SpecialCase {
    fn check_float<F: Float>(input: (f64,), actual: F, expected: F, ctx: &CheckCtx) -> CheckAction {
        if cfg!(x86_no_sse)
            && (ctx.base_name == BaseName::Rint || ctx.base_name == BaseName::Roundeven)
            && (expected - actual).abs() <= F::ONE
            && (expected - actual).abs() > F::ZERO
        {
            // Our rounding mode is incorrect.
            return XFAIL("i586 rint rounding mode");
        }

        if ctx.base_name == BaseName::J0 && input.0 < -1e300 {
            // Errors get huge close to -inf
            return XFAIL_NOCHECK;
        }

        if ctx.base_name == BaseName::Acosh
            && input.0 < 1.0
            && actual.is_nan()
            && ctx.basis == CheckBasis::Musl
        {
            // Musl sometimes evaluates acosh(negative) to a numeric value
            return XFAIL_NOCHECK;
        }

        // FIXME(correctness): lgamma has high relative inaccuracy near its zeroes
        if matches!(ctx.base_name, BaseName::Lgamma | BaseName::LgammaR)
            && input.0 > -32.0
            && input.0 < -2.0
            && (expected.abs() < F::ONE || (input.0 - input.0.round()).abs() < 0.02)
        {
            return XFAIL_NOCHECK;
        }

        // maybe_check_nan_bits(actual, expected, ctx)
        unop_common(input, actual, expected, ctx)
    }

    fn check_int<I: Int>(input: (f64,), actual: I, expected: I, ctx: &CheckCtx) -> CheckAction {
        // On MPFR for lgamma_r, we set -1 as the integer result for negative infinity but MPFR
        // sets +1
        if ctx.basis == CheckBasis::Mpfr
            && ctx.base_name == BaseName::LgammaR
            && input.0 == f64::NEG_INFINITY
            && actual.abs() == expected.abs()
        {
            return XFAIL("lgammar integer result");
        }

        DEFAULT
    }
}

#[cfg(f128_enabled)]
impl MaybeOverride<(f128,)> for SpecialCase {}

// F1 and F2 are always the same type, this is just to please generics
fn unop_common<F1: Float, F2: Float>(
    input: (F1,),
    actual: F2,
    expected: F2,
    ctx: &CheckCtx,
) -> CheckAction {
    // fabs and copysign must leave NaNs untouched.
    if ctx.base_name == BaseName::Fabs && input.0.is_nan() {
        // LLVM currently uses x87 instructions which quieten signalling NaNs to handle the i686
        // `extern "C"` `f32`/`f64` return ABI.
        // LLVM issue <https://github.com/llvm/llvm-project/issues/66803>
        // Rust issue <https://github.com/rust-lang/rust/issues/115567>
        if cfg!(target_arch = "x86") && ctx.basis == CheckBasis::Musl && actual.is_nan() {
            return XFAIL_NOCHECK;
        }

        // MPFR only has one NaN bitpattern; allow the default `.is_nan()` checks to validate.
        if ctx.basis == CheckBasis::Mpfr {
            return DEFAULT;
        }

        // abs and copysign require signaling NaNs to be propagated, so verify bit equality.
        if actual.biteq(expected) {
            return CheckAction::Custom(Ok(()));
        } else {
            return CheckAction::Custom(Err(anyhow::anyhow!("NaNs have different bitpatterns")));
        }
    }

    DEFAULT
}

#[cfg(f16_enabled)]
impl MaybeOverride<(f16, f16)> for SpecialCase {
    fn check_float<F: Float>(
        input: (f16, f16),
        actual: F,
        expected: F,
        ctx: &CheckCtx,
    ) -> CheckAction {
        binop_common(input, actual, expected, ctx)
    }
}

impl MaybeOverride<(f32, f32)> for SpecialCase {
    fn check_float<F: Float>(
        input: (f32, f32),
        actual: F,
        expected: F,
        ctx: &CheckCtx,
    ) -> CheckAction {
        binop_common(input, actual, expected, ctx)
    }
}

impl MaybeOverride<(f64, f64)> for SpecialCase {
    fn check_float<F: Float>(
        input: (f64, f64),
        actual: F,
        expected: F,
        ctx: &CheckCtx,
    ) -> CheckAction {
        binop_common(input, actual, expected, ctx)
    }
}

#[cfg(f128_enabled)]
impl MaybeOverride<(f128, f128)> for SpecialCase {
    fn check_float<F: Float>(
        input: (f128, f128),
        actual: F,
        expected: F,
        ctx: &CheckCtx,
    ) -> CheckAction {
        binop_common(input, actual, expected, ctx)
    }
}

// F1 and F2 are always the same type, this is just to please generics
fn binop_common<F1: Float, F2: Float>(
    input: (F1, F1),
    actual: F2,
    expected: F2,
    ctx: &CheckCtx,
) -> CheckAction {
    // MPFR only has one NaN bitpattern; skip tests in cases where the first argument would take
    // the sign of a NaN second argument. The default NaN checks cover other cases.
    if ctx.base_name == BaseName::Copysign && ctx.basis == CheckBasis::Mpfr && input.1.is_nan() {
        return SKIP;
    }

    /* FIXME(#439): our fmin and fmax do not compare signed zeros */

    if ctx.base_name == BaseName::Fmin
        && input.0.biteq(F1::NEG_ZERO)
        && input.1.biteq(F1::ZERO)
        && expected.biteq(F2::NEG_ZERO)
        && actual.biteq(F2::ZERO)
    {
        return XFAIL("fmin signed zeroes");
    }

    if ctx.base_name == BaseName::Fmax
        && input.0.biteq(F1::NEG_ZERO)
        && input.1.biteq(F1::ZERO)
        && expected.biteq(F2::ZERO)
        && actual.biteq(F2::NEG_ZERO)
    {
        return XFAIL("fmax signed zeroes");
    }

    // Musl propagates NaNs if one is provided as the input, but we return the other input.
    if (ctx.base_name == BaseName::Fmax || ctx.base_name == BaseName::Fmin)
        && ctx.basis == Musl
        && (input.0.is_nan() ^ input.1.is_nan())
        && expected.is_nan()
    {
        return XFAIL("fmax/fmin musl NaN");
    }

    DEFAULT
}

impl MaybeOverride<(i32, f32)> for SpecialCase {
    fn check_float<F: Float>(
        input: (i32, f32),
        actual: F,
        expected: F,
        ctx: &CheckCtx,
    ) -> CheckAction {
        // `ynf(213, 109.15641) = -inf` with our library, should be finite.
        if ctx.basis == Mpfr
            && ctx.base_name == BaseName::Yn
            && input.0 > 200
            && !expected.is_infinite()
            && actual.is_infinite()
        {
            return XFAIL("ynf infinity mismatch");
        }

        int_float_common(input, actual, expected, ctx)
    }
}

impl MaybeOverride<(i32, f64)> for SpecialCase {
    fn check_float<F: Float>(
        input: (i32, f64),
        actual: F,
        expected: F,
        ctx: &CheckCtx,
    ) -> CheckAction {
        int_float_common(input, actual, expected, ctx)
    }
}

fn int_float_common<F1: Float, F2: Float>(
    input: (i32, F1),
    actual: F2,
    expected: F2,
    ctx: &CheckCtx,
) -> CheckAction {
    if ctx.basis == Mpfr
        && (ctx.base_name == BaseName::Jn || ctx.base_name == BaseName::Yn)
        && input.1 == F1::NEG_INFINITY
        && actual == F2::ZERO
        && expected == F2::ZERO
    {
        return XFAIL("we disagree with MPFR on the sign of zero");
    }

    // Values near infinity sometimes get cut off for us. `ynf(681, 509.90924) = -inf` but should
    // be -3.2161271e38.
    if ctx.basis == Musl
        && ctx.fn_ident == Identifier::Ynf
        && !expected.is_infinite()
        && actual.is_infinite()
        && (expected.abs().to_bits().abs_diff(actual.abs().to_bits())
            < F2::Int::cast_from(10_000_000u32))
    {
        return XFAIL_NOCHECK;
    }

    // Our bessel functions blow up with large N values
    if ctx.basis == Musl && (ctx.base_name == BaseName::Jn || ctx.base_name == BaseName::Yn) {
        if cfg!(x86_no_sse) {
            // Precision is especially bad on i586, not worth checking.
            return XFAIL_NOCHECK;
        }

        if input.0 > 4000 {
            return XFAIL_NOCHECK;
        } else if input.0 > 100 {
            return CheckAction::AssertWithUlp(2_000_000);
        }
    }
    DEFAULT
}

#[cfg(f16_enabled)]
impl MaybeOverride<(f16, i32)> for SpecialCase {}
impl MaybeOverride<(f32, i32)> for SpecialCase {}
impl MaybeOverride<(f64, i32)> for SpecialCase {}
#[cfg(f128_enabled)]
impl MaybeOverride<(f128, i32)> for SpecialCase {}

impl MaybeOverride<(f32, f32, f32)> for SpecialCase {}
impl MaybeOverride<(f64, f64, f64)> for SpecialCase {}
#[cfg(f128_enabled)]
impl MaybeOverride<(f128, f128, f128)> for SpecialCase {}
