use crate::{ContextRef, Error, Result, Value, ValueRef, ValueResult};
use oden_js_sys as sys;
use std::cell::{Ref, RefCell, RefMut};
use std::ffi::CString;
use std::fmt;
use std::sync::Mutex;

pub struct ClassID {
    name: &'static str,
    value: Mutex<Option<sys::JSClassID>>,
}

impl ClassID {
    pub const fn new(name: &'static str) -> Self {
        ClassID {
            name,
            value: Mutex::new(None),
        }
    }

    pub fn name(&self) -> &'static str {
        self.name
    }

    pub fn get(&self) -> sys::JSClassID {
        let mut locked = self.value.lock().unwrap();
        match *locked {
            None => {
                let id = unsafe {
                    let mut id: sys::JSClassID = 0;
                    sys::JS_NewClassID(&mut id);
                    id
                };
                *locked = Some(id);
                id
            }
            Some(id) => id,
        }
    }
}

impl fmt::Debug for ClassID {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let id_str = {
            let locked = self.value.lock().unwrap();
            match *locked {
                None => format!("none"),
                Some(id) => format!("{id}"),
            }
        };

        f.debug_struct("ClassID")
            .field("id", &id_str)
            .field("name", &self.name)
            .finish()
    }
}

/// Implement the `Class` trait on rust types that you want to embed into
/// JavaScript values.
pub trait Class: Sized {
    fn class_id() -> &'static ClassID;

    fn into_value(self, context: &ContextRef) -> ValueResult {
        let class_id = Self::class_id();

        // Check to see if the class is registered with the runtime. If not,
        // register it.
        if !context.is_registered_class(class_id) {
            let class_name = match CString::new(class_id.name()) {
                Ok(cs) => Ok(cs),
                Err(_) => Err(Error::UnexpectedNul),
            }?;

            let class_def = sys::JSClassDef {
                class_name: class_name.as_ptr(),
                finalizer: Some(Self::finalizer),
                gc_mark: if Self::needs_mark() {
                    Some(Self::gc_mark)
                } else {
                    None
                },
                call: None,
                exotic: std::ptr::null_mut(),
            };

            if 0 != unsafe {
                sys::JS_NewClass(sys::JS_GetRuntime(context.ctx), class_id.get(), &class_def)
            } {
                return Err(Error::TooManyClasses);
            }
        }

        // Check to see if the class proto is registered. We always register
        // the prototype as an object, so if quickjs says the prototype is
        // NULL then we can go ahead and register our own object.
        unsafe {
            let pval =
                Value::from_raw(sys::JS_GetClassProto(context.ctx, class_id.get()), &context);
            if pval.is_null() {
                let proto = Self::prototype(context)?;
                sys::JS_DupValue(context.ctx, proto.val);
                sys::JS_SetClassProto(context.ctx, class_id.get(), proto.val);
            }
        }

        // Now that the class is registered and the prototype is registered
        // we can just ask quickjs to create the object for us, referencing
        // the existing prototype.
        let val = context.check_exception(unsafe {
            sys::JS_NewObjectClass(context.ctx, class_id.get() as i32)
        })?;

        // Jam the value onto the heap, and thence into the opaque slot of
        // the JS value. We'll be sure to free the box when the value is
        // destroyed, because of the finalizer that we register in the class
        // description above.
        let b = Box::new(RefCell::new(self));
        unsafe {
            sys::JS_SetOpaque(val.val, Box::into_raw(b) as *mut _);
        }
        Ok(val)
    }

    fn try_from_value_mut<'a>(value: &'a ValueRef) -> Result<RefMut<'a, Self>> {
        let class = Self::class_id();

        // SAFETY: value.val is known to be valid, from the ValueRef.
        let ptr = unsafe { sys::JS_GetOpaque(value.val, class.get()) as *const RefCell<Self> };
        if ptr.is_null() {
            return Err(Error::WrongClass(class.name().to_string()));
        }

        // SAFETY: The pointer will live as long as the value, which we have
        //         a reference to and whose lifetime we propagate into the
        //         reference to Self.
        Ok(unsafe { ptr.as_ref() }
            .expect("already checked for null")
            .borrow_mut())
    }

    fn from_value_mut<'a>(value: &'a ValueRef) -> RefMut<'a, Self> {
        Self::try_from_value_mut(value).expect("Wrong type for value")
    }

    fn try_from_value<'a>(value: &'a ValueRef) -> Result<Ref<'a, Self>> {
        let class = Self::class_id();

        // SAFETY: value.val is known to be valid, from the ValueRef.
        let ptr = unsafe { sys::JS_GetOpaque(value.val, class.get()) as *const RefCell<Self> };
        if ptr.is_null() {
            return Err(Error::WrongClass(class.name().to_string()));
        }

        // SAFETY: The pointer will live as long as the value, which we have
        //         a reference to and whose lifetime we propagate into the
        //         reference to Self.
        Ok(unsafe { ptr.as_ref() }
            .expect("already checked for null")
            .borrow())
    }

    fn from_value<'a>(value: &'a ValueRef) -> Ref<'a, Self> {
        match Self::try_from_value(value) {
            Ok(r) => r,
            Err(_) => panic!(
                "Expected a {} but got a value {:?}",
                Self::class_id().name(),
                value
            ),
        }
    }

    fn prototype(context: &ContextRef) -> ValueResult {
        context.new_object()
    }

    /// If your class contains a Value or ValueRef, `needs_mark` must return
    /// true and you must implement `mark`. (See the documentation for `mark`
    /// for more information.)
    ///
    /// This method returns `true` by default. If you know that you do not
    /// contain any Value or ValueRefs then you may override this to return
    /// `false`, as an optimization.
    fn needs_mark() -> bool {
        true
    }

    /// If your class contians a Value or ValueRef then you *must* implement
    /// this method and return `true` from `needs_mark`. Call the provided
    /// closure with each `ValueRef` that you store, so that the garbage
    /// collector won't collect it.
    fn mark<F>(&self, _mark: F)
    where
        F: Fn(&ValueRef) -> (),
    {
        // By default, nothing.
    }

    unsafe extern "C" fn gc_mark(
        _rt: *mut sys::JSRuntime,
        val: sys::JSValue,
        mark_func: sys::JS_MarkFunc,
    ) {
        let class = Self::class_id();

        // SAFETY: value.val is known to be valid, from the ValueRef.
        let ptr = unsafe { sys::JS_GetOpaque(val, class.get()) as *const RefCell<Self> };
        if !ptr.is_null() {
            return;
        }

        // SAFETY: The pointer will live as long as the value, which we have
        //         a reference to and whose lifetime we propagate into the
        //         reference to Self.
        let self_ref = unsafe { ptr.as_ref() }
            .expect("already checked for null")
            .borrow();

        self_ref.mark(|v| sys::JS_MarkValue(_rt, v.val, mark_func));
    }

    unsafe extern "C" fn finalizer(_rt: *mut sys::JSRuntime, val: sys::JSValue) {
        unsafe {
            let opaque = sys::JS_GetOpaque(val, Self::class_id().get());
            if !opaque.is_null() {
                // Just let the system drop it here.
                let _ = Box::from_raw(opaque as *mut RefCell<Self>);
            }
        };
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Context, Runtime};

    #[derive(Debug)]
    struct X {
        x: i32,
    }

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

    #[derive(Debug)]
    struct Y {
        y: i64,
    }

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

    #[test]
    fn class_ids_distinct() {
        assert_ne!(X::class_id().get(), Y::class_id().get());
    }

    #[test]
    fn round_trips() {
        let rt = Runtime::new();
        let ctx = Context::new(rt);

        let x = X { x: 76 };
        let val = x.into_value(&ctx).expect("Unable to create value!");

        let r = X::try_from_value(&val).expect("Should be able to get an X back out!");
        assert_eq!(r.x, 76);
    }

    #[test]
    fn double_create() {
        let rt = Runtime::new();
        let ctx = Context::new(rt);

        let x = X { x: 76 };
        let _v1 = x.into_value(&ctx).expect("Unable to create value!");

        let x = X { x: 76 };
        let _v2 = x.into_value(&ctx).expect("Unable to create value!");
    }

    #[test]
    fn bad_class_id() {
        let rt = Runtime::new();
        let ctx = Context::new(rt);

        let y = Y { y: 110 };
        let val = y.into_value(&ctx).expect("Unable to create value!");

        match X::try_from_value(&val) {
            Err(Error::WrongClass(_)) => (),
            Err(e) => assert!(false, "Got an error but of the wrong type: {e}"),
            Ok(_) => assert!(false, "Expected to have an error when I unwrap"),
        };

        let r = Y::try_from_value(&val).expect("Should be able to get a Y back out!");
        assert_eq!(r.y, 110);
    }
}
