use std::{
    cmp::Ordering,
    ops::{BitAnd, BitOr, Mul},
    rc::Rc,
};

use crate::{
    error::MachineError,
    vm::{
        env::Environment,
        instruction::Instruction,
        machine::Machine,
        value::{NumberValue, Value, convert::TryFromValue},
    },
};

#[allow(clippy::type_complexity)]
pub(crate) fn dispatch_arithmetic(
    instruction: Instruction,
) -> fn(&mut Machine, &Rc<Environment>, &[Value]) -> Result<Value, MachineError> {
    match instruction {
        Instruction::Add => builtin_add,
        Instruction::Sub => builtin_sub,
        Instruction::Mul => builtin_mul,
        Instruction::Div => builtin_div,
        Instruction::Mod => builtin_mod,
        Instruction::Gt => builtin_cmp_gt,
        Instruction::Lt => builtin_cmp_lt,
        Instruction::Ge => builtin_cmp_ge,
        Instruction::Le => builtin_cmp_le,
        Instruction::Eq => builtin_cmp_eq,
        Instruction::Ne => builtin_cmp_ne,
        Instruction::Not => builtin_not,
        Instruction::And => builtin_and,
        Instruction::Or => builtin_or,
        _ => unreachable!(),
    }
}

pub(super) fn load(env: &Rc<Environment>) {
    env.defun_native("+", "Adds values", builtin_add);
    env.defun_native("-", "Subtracts values", builtin_sub);
    let mul = env.defun_native("*", "Multiplies values", builtin_mul);
    env.set_global_value("·", mul); // alias for *
    let div = env.defun_native("/", "Divides values", builtin_div);
    env.set_global_value("÷", div); // alias for /
    env.defun_native(
        "%",
        "Calculates a modulo/remainder of value divison",
        builtin_mod,
    );

    env.defun_native(
        ">",
        "Returns #t if the arguments are monotonically decreasing",
        builtin_cmp_gt,
    );
    let ge = env.defun_native(
        ">=",
        "Returns #t if the arguments are monotonically non-increasing",
        builtin_cmp_ge,
    );
    env.set_global_value("≥", ge); // alias for >=
    env.defun_native(
        "<",
        "Returns #t if the arguments are monotonically increasing",
        builtin_cmp_lt,
    );
    let le = env.defun_native(
        "<=",
        "Returns #t if the arguments are monotonically non-decreasing",
        builtin_cmp_le,
    );
    env.set_global_value("≤", le); // alias for <=
    env.defun_native(
        "=",
        "Returns #t if all the arguments are equal",
        builtin_cmp_eq,
    );
    let ne = env.defun_native(
        "/=",
        "Returns #t if none of the arguments are equal",
        builtin_cmp_ne,
    );
    env.set_global_value("≠", ne); // alias for /=

    let and = env.defun_native(
        "and",
        "Returns #t if all of the arguments are trueish",
        builtin_and,
    );
    env.set_global_value("&&", and);
    let or = env.defun_native(
        "or",
        "Returns #t if any of the arguments is trueish",
        builtin_or,
    );
    env.set_global_value("||", or);

    // env.defun_native("&", builtin_bitwise_and);
    // env.defun_native("|", builtin_bitwise_or);
    // env.defun_native("^", builtin_bitwise_xor);
}

fn value_add(a: &Value, b: &Value) -> Result<Value, MachineError> {
    match (a, b) {
        (Value::String(a), _) => Ok(Value::String(format!("{a}{b}").into())),
        (_, Value::String(b)) => Ok(Value::String(format!("{a}{b}").into())),
        (Value::Number(a), Value::Number(b)) => Ok(Value::Number(*a + *b)),
        (Value::Number(a), _) => Ok(Value::Number(*a + NumberValue::try_from_value(b)?)),
        (_, Value::Number(b)) => Ok(Value::Number(NumberValue::try_from_value(a)? + *b)),
        (Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Number(*a + *b)),
        _ => todo!("TODO error"),
    }
}

pub(crate) enum CompareOperation {
    Eq,
    Ne,
    Lt,
    Gt,
    Le,
    Ge,
}

fn builtin_fold<F>(fold: F, mut accumulator: Value, args: &[Value]) -> Result<Value, MachineError>
where
    F: Fn(&Value, &Value) -> Result<Value, MachineError>,
{
    for (i, arg) in args.iter().enumerate() {
        if i == 0 {
            accumulator = arg.clone();
        } else {
            accumulator = fold(&accumulator, arg)?;
        }
    }
    Ok(accumulator)
}

fn builtin_fold_t<'a, T, F>(
    fold: F,
    mut accumulator: T,
    args: &'a [Value],
) -> Result<Value, MachineError>
where
    F: Fn(T, T) -> T,
    T: TryFromValue<'a> + Into<Value>,
{
    for arg in args {
        let arg = T::try_from_value(arg)?;
        accumulator = fold(accumulator, arg);
    }
    Ok(accumulator.into())
}

fn builtin_fold_trueish<F>(
    fold: F,
    mut accumulator: bool,
    args: &[Value],
) -> Result<Value, MachineError>
where
    F: Fn(bool, bool) -> bool,
{
    for arg in args {
        let arg = arg.is_trueish();
        accumulator = fold(accumulator, arg);
    }
    Ok(accumulator.into())
}

pub(crate) fn builtin_add(
    _vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    builtin_fold(value_add, Value::Number(0.into()), args)
}
pub(crate) fn builtin_sub(
    _vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    match args {
        [] => Err(MachineError::InvalidArgumentCount),
        [x] => {
            let x = NumberValue::try_from_value(x)?;
            Ok((-x).into())
        }
        [x, rest @ ..] => {
            let mut accumulator = NumberValue::try_from_value(x)?;
            for argument in rest {
                let value = NumberValue::try_from_value(argument)?;
                accumulator -= value;
            }
            Ok(Value::Number(accumulator))
        }
    }
}
pub(crate) fn builtin_mul(
    _vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    builtin_fold_t(Mul::mul, NumberValue::from(1), args)
}
pub(crate) fn builtin_mod(
    _vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    let [x, y] = args else {
        return Err(MachineError::InvalidArgumentCount);
    };
    let x = NumberValue::try_from_value(x)?;
    let y = NumberValue::try_from_value(y)?;
    Ok(Value::Number(x % y))
}
pub(crate) fn builtin_div(
    _vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    match args {
        [] => Err(MachineError::InvalidArgumentCount),
        [x] => {
            let x = NumberValue::try_from_value(x)?;
            Ok(x.reciprocal().into())
        }
        [x, rest @ ..] => {
            let mut accumulator = NumberValue::try_from_value(x)?;
            for argument in rest {
                let argument = NumberValue::try_from_value(argument)?;
                accumulator /= argument;
            }
            Ok(accumulator.into())
        }
    }
}

pub(crate) fn builtin_and(
    _vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    builtin_fold_trueish(BitAnd::bitand, true, args)
}
pub(crate) fn builtin_or(
    _vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    builtin_fold_trueish(BitOr::bitor, false, args)
}
// pub(crate) fn builtin_and(
//     vm: &mut Machine,
//     _env: &Rc<Environment>,
//     args: &[Value],
// ) -> Result<Value, MachineError> {
//     builtin_fold_t(BitAnd::bitand, true, args)
// }
// pub(crate) fn builtin_or(
//     vm: &mut Machine,
//     _env: &Rc<Environment>,
//     args: &[Value],
// ) -> Result<Value, MachineError> {
//     builtin_fold_t(BitOr::bitor, false, args)
// }
// pub(crate) fn builtin_bitwise_and(
//     vm: &mut Machine,
//     _env: &Rc<Environment>,
//     args: &[Value],
// ) -> Result<Value, MachineError> {
//     builtin_fold_t(BitAnd::bitand, 1, args)
// }
// pub(crate) fn builtin_bitwise_or(
//     vm: &mut Machine,
//     _env: &Rc<Environment>,
//     args: &[Value],
// ) -> Result<Value, MachineError> {
//     builtin_fold_t(BitOr::bitor, 0, args)
// }
// pub(crate) fn builtin_bitwise_xor(
//     vm: &mut Machine,
//     _env: &Rc<Environment>,
//     args: &[Value],
// ) -> Result<Value, MachineError> {
//     builtin_fold_t(BitXor::bitxor, 0, args)
// }

pub(crate) fn builtin_cmp(
    _vm: &mut Machine,
    args: &[Value],
    op: CompareOperation,
) -> Result<Value, MachineError> {
    fn ordering(a: &Value, b: &Value) -> Option<Ordering> {
        match (a, b) {
            (Value::Number(a), Value::Number(b)) => PartialOrd::partial_cmp(a, b),
            (Value::Boolean(a), Value::Boolean(b)) => Some(Ord::cmp(a, b)),
            (Value::String(a), Value::String(b)) => Some(Ord::cmp(a, b)),
            (Value::Identifier(a), Value::Identifier(b)) => Some(Ord::cmp(a, b)),
            _ => None,
        }
    }

    let mut accumulator = true;
    if !args.is_empty() {
        for i in 0..args.len() - 1 {
            let a = &args[i];
            let b = &args[i + 1];

            match op {
                CompareOperation::Eq => {
                    accumulator &= a == b;
                    continue;
                }
                CompareOperation::Ne => {
                    accumulator &= a != b;
                    continue;
                }
                _ => (),
            }

            let ord = ordering(a, b);

            // No ordering is specified
            let Some(ord) = ord else {
                accumulator = false;
                continue;
            };

            let check = match op {
                CompareOperation::Eq => ord.is_eq(),
                CompareOperation::Ne => ord.is_ne(),
                CompareOperation::Lt => ord.is_lt(),
                CompareOperation::Gt => ord.is_gt(),
                CompareOperation::Le => ord.is_le(),
                CompareOperation::Ge => ord.is_ge(),
            };
            accumulator &= check;
        }
    }
    Ok(accumulator.into())
}
pub(crate) fn builtin_cmp_eq(
    vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    builtin_cmp(vm, args, CompareOperation::Eq)
}
pub(crate) fn builtin_cmp_ne(
    vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    builtin_cmp(vm, args, CompareOperation::Ne)
}
pub(crate) fn builtin_cmp_gt(
    vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    builtin_cmp(vm, args, CompareOperation::Gt)
}
pub(crate) fn builtin_cmp_lt(
    vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    builtin_cmp(vm, args, CompareOperation::Lt)
}
pub(crate) fn builtin_cmp_ge(
    vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    builtin_cmp(vm, args, CompareOperation::Ge)
}
pub(crate) fn builtin_cmp_le(
    vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    builtin_cmp(vm, args, CompareOperation::Le)
}

pub(crate) fn builtin_not(
    _vm: &mut Machine,
    _env: &Rc<Environment>,
    args: &[Value],
) -> Result<Value, MachineError> {
    let [x] = args else {
        return Err(MachineError::InvalidArgumentCount);
    };
    let result = match x {
        Value::Boolean(value) => Value::Boolean(!*value),
        _ => Value::Boolean((!x.is_trueish()).into()),
    };
    Ok(result)
}
