//! Helper CLI utility for common tasks.

#![cfg_attr(f16_enabled, feature(f16))]
#![cfg_attr(f128_enabled, feature(f128))]

use std::any::type_name;
use std::env;
use std::num::ParseIntError;
use std::str::FromStr;

use libm::support::{Hexf, hf32, hf64};
#[cfg(feature = "build-mpfr")]
use libm_test::mpfloat::MpOp;
use libm_test::{MathOp, TupleCall};
#[cfg(feature = "build-mpfr")]
use rug::az::{self, Az};

const USAGE: &str = "\
usage:

cargo run -p util -- <SUBCOMMAND>

SUBCOMMAND:
    eval <BASIS> <OP> inputs...
        Evaulate the expression with a given basis. This can be useful for
        running routines with a debugger, or quickly checking input. Examples:
        * eval musl sinf 1.234 # print the results of musl sinf(1.234f32)
        * eval mpfr pow 1.234 2.432 # print the results of mpfr pow(1.234, 2.432)
";

fn main() {
    let args = env::args().collect::<Vec<_>>();
    let str_args = args.iter().map(|s| s.as_str()).collect::<Vec<_>>();

    match &str_args.as_slice()[1..] {
        ["eval", basis, op, inputs @ ..] => do_eval(basis, op, inputs),
        _ => {
            println!("{USAGE}\nunrecognized input `{str_args:?}`");
            std::process::exit(1);
        }
    }
}

macro_rules! handle_call {
    (
        fn_name: $fn_name:ident,
        CFn: $CFn:ty,
        RustFn: $RustFn:ty,
        RustArgs: $RustArgs:ty,
        attrs: [$($attr:meta),*],
        extra: ($basis:ident, $op:ident, $inputs:ident),
        fn_extra: $musl_fn:expr,
    ) => {
        $(#[$attr])*
        if $op == stringify!($fn_name) {
            type Op = libm_test::op::$fn_name::Routine;

            let input = <$RustArgs>::parse($inputs);
            let libm_fn: <Op as MathOp>::RustFn = libm::$fn_name;

            let output = match $basis {
                "libm" => input.call_intercept_panics(libm_fn),
                #[cfg(feature = "build-musl")]
                "musl" => {
                    let musl_fn: <Op as MathOp>::CFn =
                        $musl_fn.unwrap_or_else(|| panic!("no musl function for {}", $op));
                    input.call(musl_fn)
                }
                #[cfg(feature = "build-mpfr")]
                "mpfr" => {
                    let mut mp = <Op as MpOp>::new_mp();
                    Op::run(&mut mp, input)
                }
                _ => panic!("unrecognized or disabled basis '{}'", $basis),
            };
            println!("{output:?} {:x}", Hexf(output));
            return;
        }
    };
}

/// Evaluate the specified operation with a given basis.
fn do_eval(basis: &str, op: &str, inputs: &[&str]) {
    libm_macros::for_each_function! {
        callback: handle_call,
        emit_types: [CFn, RustFn, RustArgs],
        extra: (basis, op, inputs),
        fn_extra: match MACRO_FN_NAME {
            // Not provided by musl
            fmaximum
            | fmaximum_num
            | fmaximum_numf
            | fmaximumf
            | fminimum
            | fminimum_num
            | fminimum_numf
            | fminimumf
            | roundeven
            | roundevenf
            | ALL_F16
            | ALL_F128 => None,
            _ => Some(musl_math_sys::MACRO_FN_NAME)
        }
    }

    panic!("no operation matching {op}");
}

/// Parse a tuple from a space-delimited string.
trait ParseTuple {
    fn parse(input: &[&str]) -> Self;
}

macro_rules! impl_parse_tuple {
    ($ty:ty) => {
        impl ParseTuple for ($ty,) {
            fn parse(input: &[&str]) -> Self {
                assert_eq!(input.len(), 1, "expected a single argument, got {input:?}");
                (parse(input, 0),)
            }
        }

        impl ParseTuple for ($ty, $ty) {
            fn parse(input: &[&str]) -> Self {
                assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
                (parse(input, 0), parse(input, 1))
            }
        }

        impl ParseTuple for ($ty, i32) {
            fn parse(input: &[&str]) -> Self {
                assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
                (parse(input, 0), parse(input, 1))
            }
        }

        impl ParseTuple for (i32, $ty) {
            fn parse(input: &[&str]) -> Self {
                assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
                (parse(input, 0), parse(input, 1))
            }
        }

        impl ParseTuple for ($ty, $ty, $ty) {
            fn parse(input: &[&str]) -> Self {
                assert_eq!(input.len(), 3, "expected three arguments, got {input:?}");
                (parse(input, 0), parse(input, 1), parse(input, 2))
            }
        }
    };
}

#[allow(unused_macros)]
#[cfg(feature = "build-mpfr")]
macro_rules! impl_parse_tuple_via_rug {
    ($ty:ty) => {
        impl ParseTuple for ($ty,) {
            fn parse(input: &[&str]) -> Self {
                assert_eq!(input.len(), 1, "expected a single argument, got {input:?}");
                (parse_rug(input, 0),)
            }
        }

        impl ParseTuple for ($ty, $ty) {
            fn parse(input: &[&str]) -> Self {
                assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
                (parse_rug(input, 0), parse_rug(input, 1))
            }
        }

        impl ParseTuple for ($ty, i32) {
            fn parse(input: &[&str]) -> Self {
                assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
                (parse_rug(input, 0), parse(input, 1))
            }
        }

        impl ParseTuple for (i32, $ty) {
            fn parse(input: &[&str]) -> Self {
                assert_eq!(input.len(), 2, "expected two arguments, got {input:?}");
                (parse(input, 0), parse_rug(input, 1))
            }
        }

        impl ParseTuple for ($ty, $ty, $ty) {
            fn parse(input: &[&str]) -> Self {
                assert_eq!(input.len(), 3, "expected three arguments, got {input:?}");
                (
                    parse_rug(input, 0),
                    parse_rug(input, 1),
                    parse_rug(input, 2),
                )
            }
        }
    };
}

// Fallback for when Rug is not built.
#[allow(unused_macros)]
#[cfg(not(feature = "build-mpfr"))]
macro_rules! impl_parse_tuple_via_rug {
    ($ty:ty) => {
        impl ParseTuple for ($ty,) {
            fn parse(_input: &[&str]) -> Self {
                panic!("parsing this type requires the `build-mpfr` feature")
            }
        }

        impl ParseTuple for ($ty, $ty) {
            fn parse(_input: &[&str]) -> Self {
                panic!("parsing this type requires the `build-mpfr` feature")
            }
        }

        impl ParseTuple for ($ty, i32) {
            fn parse(_input: &[&str]) -> Self {
                panic!("parsing this type requires the `build-mpfr` feature")
            }
        }

        impl ParseTuple for (i32, $ty) {
            fn parse(_input: &[&str]) -> Self {
                panic!("parsing this type requires the `build-mpfr` feature")
            }
        }

        impl ParseTuple for ($ty, $ty, $ty) {
            fn parse(_input: &[&str]) -> Self {
                panic!("parsing this type requires the `build-mpfr` feature")
            }
        }
    };
}

impl_parse_tuple!(f32);
impl_parse_tuple!(f64);

#[cfg(f16_enabled)]
impl_parse_tuple_via_rug!(f16);
#[cfg(f128_enabled)]
impl_parse_tuple_via_rug!(f128);

/// Try to parse the number, printing a nice message on failure.
fn parse<T: FromStr + FromStrRadix>(input: &[&str], idx: usize) -> T {
    let s = input[idx];

    let msg = || format!("invalid {} input '{s}'", type_name::<T>());

    if s.starts_with("0x") || s.starts_with("-0x") {
        return T::from_str_radix(s, 16).unwrap_or_else(|_| panic!("{}", msg()));
    }

    if s.starts_with("0b") {
        return T::from_str_radix(s, 2).unwrap_or_else(|_| panic!("{}", msg()));
    }

    s.parse().unwrap_or_else(|_| panic!("{}", msg()))
}

/// Try to parse the float type going via `rug`, for `f16` and `f128` which don't yet implement
/// `FromStr`.
#[cfg(feature = "build-mpfr")]
fn parse_rug<F>(input: &[&str], idx: usize) -> F
where
    F: libm_test::Float + FromStrRadix,
    rug::Float: az::Cast<F>,
{
    let s = input[idx];

    let msg = || format!("invalid {} input '{s}'", type_name::<F>());

    if s.starts_with("0x") {
        return F::from_str_radix(s, 16).unwrap_or_else(|_| panic!("{}", msg()));
    }

    if s.starts_with("0b") {
        return F::from_str_radix(s, 2).unwrap_or_else(|_| panic!("{}", msg()));
    }

    let x = rug::Float::parse(s).unwrap_or_else(|_| panic!("{}", msg()));
    let x = rug::Float::with_val(F::BITS, x);
    x.az()
}

trait FromStrRadix: Sized {
    fn from_str_radix(s: &str, radix: u32) -> Result<Self, ParseIntError>;
}

impl FromStrRadix for i32 {
    fn from_str_radix(s: &str, radix: u32) -> Result<Self, ParseIntError> {
        let s = strip_radix_prefix(s, radix);
        i32::from_str_radix(s, radix)
    }
}

#[cfg(f16_enabled)]
impl FromStrRadix for f16 {
    fn from_str_radix(s: &str, radix: u32) -> Result<Self, ParseIntError> {
        if radix == 16 && s.contains("p") {
            return Ok(libm::support::hf16(s));
        }

        let s = strip_radix_prefix(s, radix);
        u16::from_str_radix(s, radix).map(Self::from_bits)
    }
}

impl FromStrRadix for f32 {
    fn from_str_radix(s: &str, radix: u32) -> Result<Self, ParseIntError> {
        if radix == 16 && s.contains("p") {
            // Parse as hex float
            return Ok(hf32(s));
        }

        let s = strip_radix_prefix(s, radix);
        u32::from_str_radix(s, radix).map(Self::from_bits)
    }
}

impl FromStrRadix for f64 {
    fn from_str_radix(s: &str, radix: u32) -> Result<Self, ParseIntError> {
        if s.contains("p") {
            return Ok(hf64(s));
        }

        let s = strip_radix_prefix(s, radix);
        u64::from_str_radix(s, radix).map(Self::from_bits)
    }
}

#[cfg(f128_enabled)]
impl FromStrRadix for f128 {
    fn from_str_radix(s: &str, radix: u32) -> Result<Self, ParseIntError> {
        if radix == 16 && s.contains("p") {
            return Ok(libm::support::hf128(s));
        }
        let s = strip_radix_prefix(s, radix);
        u128::from_str_radix(s, radix).map(Self::from_bits)
    }
}

fn strip_radix_prefix(s: &str, radix: u32) -> &str {
    if radix == 16 {
        s.strip_prefix("0x").unwrap()
    } else if radix == 2 {
        s.strip_prefix("0b").unwrap()
    } else {
        s
    }
}
