use crate::{
    Error, Number, TulispContext, TulispObject, builtin::functions::core::reduce_with,
    destruct_eval_bind, eval::EvalInto,
};

#[inline(always)]
fn compare_impl<F: Fn(&Number, &Number) -> bool>(
    ctx: &mut TulispContext,
    args: &TulispObject,
    cmp: F,
) -> Result<TulispObject, Error> {
    if args.cdr_and_then(|x| Ok(x.consp()))? {
        let first = args.car_and_then(|x| x.eval_into(ctx))?;
        Ok(recursive_compare(ctx, first, args.cdr()?, cmp)?.into())
    } else {
        Err(Error::out_of_range(format!(
            "Comparison requires at least 2 arguments"
        )))
    }
}

fn recursive_compare<F: Fn(&Number, &Number) -> bool>(
    ctx: &mut TulispContext,
    first: Number,
    args: TulispObject,
    cmp: F,
) -> Result<bool, Error> {
    let second: Number = args.car()?.eval_into(ctx)?;
    if cmp(&first, &second) {
        if args.cdr_and_then(|x| Ok(x.consp()))? {
            recursive_compare(ctx, second, args.cdr()?, cmp)
        } else {
            Ok(true)
        }
    } else {
        Ok(false)
    }
}

pub(crate) fn add(ctx: &mut TulispContext) {
    ctx.defspecial(">", |ctx, args| {
        compare_impl(ctx, args, std::cmp::PartialOrd::gt)
    });

    ctx.defspecial(">=", |ctx, args| {
        compare_impl(ctx, args, std::cmp::PartialOrd::ge)
    });

    ctx.defspecial("<", |ctx, args| {
        compare_impl(ctx, args, std::cmp::PartialOrd::lt)
    });

    ctx.defspecial("<=", |ctx, args| {
        compare_impl(ctx, args, std::cmp::PartialOrd::le)
    });

    fn max(ctx: &mut TulispContext, rest: &TulispObject) -> Result<TulispObject, Error> {
        reduce_with(ctx, rest, |a, b| Ok(if a > b { a } else { b }))
    }
    ctx.defspecial("max", max);

    fn min(ctx: &mut TulispContext, rest: &TulispObject) -> Result<TulispObject, Error> {
        reduce_with(ctx, rest, |a, b| Ok(if a < b { a } else { b }))
    }
    ctx.defspecial("min", min);

    ctx.defspecial("abs", |ctx, args| {
        destruct_eval_bind!(ctx, (number) = args);

        Ok(number.try_float()?.abs().into())
    });
}

#[cfg(test)]
mod tests {
    use crate::{TulispContext, test_utils::eval_assert_equal};

    #[test]
    fn test_abs() {
        let ctx = &mut TulispContext::new();

        eval_assert_equal(ctx, "(abs -4.0)", "4.0");
        eval_assert_equal(ctx, "(abs 0.0)", "0.0");
        eval_assert_equal(ctx, "(abs 2.25)", "2.25");
        eval_assert_equal(ctx, "(abs -3)", "3.0");
        eval_assert_equal(ctx, "(abs 0)", "0.0");
        eval_assert_equal(ctx, "(abs 5)", "5.0");
    }
}
