use crate::{throw_string, Class, ClassID, ContextRef, Error, ValueRef, ValueResult};
use oden_js_sys as sys;
use std::ffi::c_int;
use std::panic::catch_unwind;

pub trait Callback: 'static + Fn(&ContextRef, &ValueRef, &[&ValueRef]) -> ValueResult {}

impl<T> Callback for T where T: 'static + Fn(&ContextRef, &ValueRef, &[&ValueRef]) -> ValueResult {}

struct CallbackObject<T: Callback> {
    callback: T,
}

impl<T: Callback> CallbackObject<T> {
    fn new(callback: T, context: &ContextRef) -> ValueResult {
        let obj = CallbackObject { callback };
        obj.into_value(context)
    }
}

impl<T: Callback> Class for CallbackObject<T> {
    fn class_id() -> &'static ClassID {
        static ID: ClassID = ClassID::new("CallbackObject");
        &ID
    }
}

fn callback_impl<F>(
    ctx: *mut sys::JSContext,
    _this: sys::JSValue,
    argc: c_int,
    argv: *mut sys::JSValue,
    _magic: c_int,
    data: *mut sys::JSValue,
) -> sys::JSValue
where
    F: Callback,
{
    let context: ContextRef = ContextRef::from_raw(ctx);
    let this: ValueRef = ValueRef::from_raw(_this);

    let mut actual_args = Vec::new();
    unsafe {
        for arg in std::slice::from_raw_parts(argv, argc.try_into().unwrap()) {
            actual_args.push(ValueRef::from_raw(*arg));
        }
    }

    let mut args = Vec::new();
    for aa in &actual_args {
        args.push(aa);
    }

    // Grab the callback that we stashed in the value.
    let data_ref = unsafe { ValueRef::from_raw(*data) };
    let result = match CallbackObject::<F>::try_from_value(&data_ref) {
        Ok(closure) => (closure.callback)(&context, &this, &args),
        Err(e) => Err(e),
    };

    match result {
        Ok(v) => {
            // `result` is owned; make sure to dup the value on the way out.  Ideally
            // we would just have a `take` or something on `Value` but I don't quite
            // know how to implement it correctly.
            unsafe {
                let ret = &v.val;
                sys::JS_DupValue(ctx, *ret);
                *ret
            }
        }
        Err(Error::Exception(e, _, _)) => unsafe {
            // If we returned `Error::Exception` then we're propagating an
            // exception through the JS stack, just flip it.
            let exc = &e.val;
            sys::JS_DupValue(ctx, *exc);
            sys::JS_Throw(ctx, *exc)
        },
        Err(err) => throw_string(&context, err.to_string()),
    }
}

unsafe extern "C" fn trampoline<F>(
    ctx: *mut sys::JSContext,
    _this: sys::JSValue,
    argc: c_int,
    argv: *mut sys::JSValue,
    _magic: c_int,
    data: *mut sys::JSValue,
) -> sys::JSValue
where
    F: Callback,
{
    match catch_unwind(|| callback_impl::<F>(ctx, _this, argc, argv, _magic, data)) {
        Ok(r) => r,
        Err(e) => {
            let message = if let Some(e) = e.downcast_ref::<&'static str>() {
                format!("{e}")
            } else {
                format!("Unknown error")
            };

            let context: ContextRef = ContextRef::from_raw(ctx);
            throw_string(&context, message)
        }
    }
}

/// Get an unsafe pointer to the trampoline for a closure. We need a generic
/// function here to intuit the type of the closure, otherwise otherwise
/// there's nothing we can put in the `<>` for `trampoline`. This also
/// ensures that the rust compiler actually generates the trampoline.
fn get_trampoline<F>(_closure: &F) -> sys::JSCFunctionData
where
    F: Callback,
{
    Some(trampoline::<F>)
}

/// Construct a new value that wraps a closure.
pub(crate) fn new_fn<F>(ctx: &ContextRef, func: F) -> sys::JSValue
where
    F: Callback,
{
    let closure = func;
    let callback = get_trampoline(&closure);
    let value = CallbackObject::new(closure, ctx).expect("Unable to create");

    unsafe {
        // Just some silly pointer magic; they want an array but we only have
        // one value so.
        let mut val = value.val;
        sys::JS_NewCFunctionData(ctx.ctx, callback, 0, 0, 1, &mut val)
    }
}
