//! 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 cfg_if::cfg_if;
use libm::support::{Float, Hexf, hf32, hf64};
#[cfg(feature = "build-mpfr")]
use libm_test::mpfloat::MpOp;
use libm_test::{Hex, 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...
    x <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)

    print inputs...
    p inputs...
        For each input, print it in different formats with various floating
        point properties (normal, infinite, etc).
";

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" | "x", basis, op, inputs @ ..] => do_eval(basis, op, inputs),
        ["print" | "p", inputs @ ..] => do_classify(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}");
}

/// Print basic float information to stdout.
fn do_classify(inputs: &[&str]) {
    for s in inputs {
        if let Some(s) = s.strip_suffix("f16") {
            cfg_if! {
                if #[cfg(f16_enabled)] {
                    let s = s.trim_end_matches("_");
                    let x: f16 = parse(&[s], 0);
                    classify_print(x);
                    continue;
                } else {
                    panic!("parsing this type requires f16 support: `{s}`");
                }
            }
        };
        if let Some(s) = s.strip_suffix("f32") {
            let s = s.trim_end_matches("_");
            let x: f32 = parse(&[s], 0);
            classify_print(x);
            continue;
        } else if let Some(s) = s.strip_suffix("f64") {
            let s = s.trim_end_matches("_");
            let x: f64 = parse(&[s], 0);
            classify_print(x);
            continue;
        }
        if let Some(s) = s.strip_suffix("f128") {
            cfg_if! {
                if #[cfg(all(f128_enabled, feature = "build-mpfr"))] {
                    let s = s.trim_end_matches("_");
                    let x: f128 = parse_rug(&[s], 0);
                    classify_print(x);
                    continue;
                } else {
                    panic!("parsing this type requires f128 support and \
                            the `build-mpfr` feature: `{s}`");
                }
            }
        };
        panic!("float type must be specified with a `f*` suffix: `{s}`");
    }
}

fn classify_print<F>(x: F)
where
    F: Float,
    F::Int: Hex,
{
    println!("{x:?}");
    println!("    hex:  {}", Hexf(x));
    println!("    bits: {}", x.to_bits().hex());
    println!("    nan:  {}", x.is_nan());
    println!("    inf:  {}", x.is_infinite());
    println!("    normal: {}", !x.is_subnormal());
    println!("    pos:  {}", x.is_sign_positive());
    println!("    exp:  {} {}", x.ex(), x.ex().hex());
    println!("    exp unbiased: {}", x.exp_unbiased());
    println!("    frac: {} {}", x.frac(), x.frac().hex());
}

/// 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")
            }
        }
    };
}

#[cfg(f16_enabled)]
impl_parse_tuple!(f16);
impl_parse_tuple!(f32);
impl_parse_tuple!(f64);

#[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
    }
}
