use std::{ffi::CString, marker::PhantomData, mem, ops::Deref, ptr};
use crate::{
    persistent::Outlive, qjs, value::Object, ClassId, Ctx, Error, FromJs, Function, IntoJs, Result,
    Type, Value,
};
mod borrow;
pub use borrow::Ref;
mod refs;
pub use refs::{HasRefs, RefsMarker};
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "classes")))]
pub trait ClassDef {
    const CLASS_NAME: &'static str;
    fn class_id() -> &'static ClassId;
    const HAS_PROTO: bool = false;
    fn init_proto<'js>(_ctx: Ctx<'js>, _proto: &Object<'js>) -> Result<()> {
        Ok(())
    }
    const HAS_STATIC: bool = false;
    fn init_static<'js>(_ctx: Ctx<'js>, _static: &Object<'js>) -> Result<()> {
        Ok(())
    }
    const HAS_REFS: bool = false;
    fn mark_refs(&self, _marker: &RefsMarker) {}
    fn into_js_obj<'js>(self, ctx: Ctx<'js>) -> Result<Value<'js>>
    where
        Self: Sized,
    {
        Class::<Self>::instance(ctx, self).map(|Class(Object(val), _)| val)
    }
    fn from_js_obj<'js>(value: Value<'js>) -> Result<Self>
    where
        Self: Clone + Sized,
    {
        let value = Ref::<Self>::from_js(value.ctx(), value)?;
        Ok((*value).clone())
    }
}
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "classes")))]
pub struct Class<'js, C>(pub(crate) Object<'js>, PhantomData<C>);
impl<'js, C> Clone for Class<'js, C> {
    fn clone(&self) -> Self {
        Class(self.0.clone(), PhantomData)
    }
}
impl<'js, 't, C> Outlive<'t> for Class<'js, C> {
    type Target = Class<'t, C>;
}
impl<'js, C> Deref for Class<'js, C> {
    type Target = Object<'js>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
impl<'js, C> AsRef<Object<'js>> for Class<'js, C> {
    fn as_ref(&self) -> &Object<'js> {
        &self.0
    }
}
impl<'js, C> AsRef<Value<'js>> for Class<'js, C> {
    fn as_ref(&self) -> &Value<'js> {
        &(self.0).0
    }
}
impl<'js, C> Class<'js, C>
where
    C: ClassDef,
{
    #[inline]
    pub(crate) fn id() -> qjs::JSClassID {
        C::class_id().get()
    }
    #[inline]
    pub fn constructor<F>(func: F) -> Constructor<C, F> {
        Constructor(func, PhantomData)
    }
    pub fn static_init(func: &Function<'js>) -> Result<()> {
        if C::HAS_STATIC {
            C::init_static(func.ctx(), func.as_object())?;
        }
        Ok(())
    }
    pub fn instance(ctx: Ctx<'js>, value: C) -> Result<Class<'js, C>> {
        let val =
            unsafe { ctx.handle_exception(qjs::JS_NewObjectClass(ctx.as_ptr(), Self::id() as _)) }?;
        let ptr = Box::into_raw(Box::new(value));
        unsafe { qjs::JS_SetOpaque(val, ptr as _) };
        Ok(Self(
            unsafe { Object::from_js_value(ctx, val) },
            PhantomData,
        ))
    }
    pub fn instance_proto(value: C, proto: Object<'js>) -> Result<Class<'js, C>> {
        let val = unsafe {
            proto.ctx().handle_exception(qjs::JS_NewObjectProtoClass(
                proto.ctx().as_ptr(),
                proto.0.as_js_value(),
                Self::id(),
            ))
        }?;
        let ptr = Box::into_raw(Box::new(value));
        unsafe { qjs::JS_SetOpaque(val, ptr as _) };
        Ok(Self(
            unsafe { Object::from_js_value(proto.ctx(), val) },
            PhantomData,
        ))
    }
    pub(crate) unsafe fn class_ptr(&self) -> *mut C {
        let ptr = qjs::JS_GetOpaque2(self.ctx.as_ptr(), self.value, Self::id()).cast::<C>();
        if ptr.is_null() {
            panic!("invalid class object, class objects with ClassDef C should always point to objects of class C");
        }
        ptr
    }
    pub fn register(ctx: Ctx<'js>) -> Result<()> {
        let rt = unsafe { qjs::JS_GetRuntime(ctx.as_ptr()) };
        let class_id = Self::id();
        let class_name = CString::new(C::CLASS_NAME).expect("class name has an internal null byte");
        if 0 == unsafe { qjs::JS_IsRegisteredClass(rt, class_id) } {
            let class_def = qjs::JSClassDef {
                class_name: class_name.as_ptr(),
                finalizer: Some(Self::finalizer),
                gc_mark: if C::HAS_REFS {
                    Some(Self::gc_mark)
                } else {
                    None
                },
                call: None,
                exotic: ptr::null_mut(),
            };
            if 0 != unsafe { qjs::JS_NewClass(rt, class_id, &class_def) } {
                return Err(Error::Unknown);
            }
        }
        if C::HAS_PROTO {
            let proto = Object::new(ctx)?;
            C::init_proto(ctx, &proto)?;
            unsafe { qjs::JS_SetClassProto(ctx.as_ptr(), class_id, proto.0.into_js_value()) }
        }
        Ok(())
    }
    pub unsafe fn register_raw(ctx: *mut qjs::JSContext) {
        Self::register(Ctx::from_ptr(ctx)).unwrap()
    }
    pub fn prototype(ctx: Ctx<'js>) -> Result<Object<'js>> {
        Ok(Object(unsafe {
            let class_id = Self::id();
            let proto = qjs::JS_GetClassProto(ctx.as_ptr(), class_id);
            let proto = Value::from_js_value(ctx, proto);
            let type_ = proto.type_of();
            if type_ == Type::Object {
                proto
            } else {
                return Err(Error::new_from_js_message(
                    type_.as_str(),
                    "prototype",
                    "Tried to get the prototype of class without prototype",
                ));
            }
        }))
    }
    pub fn from_object(value: Object<'js>) -> Result<Self> {
        if value.instance_of::<C>() {
            Ok(Self(value, PhantomData))
        } else {
            Err(Error::new_from_js("object", C::CLASS_NAME))
        }
    }
    pub fn borrow(&self) -> Ref<'js, C> {
        Ref::new(self.clone())
    }
    #[inline]
    pub fn as_object(&self) -> &Object<'js> {
        &self.0
    }
    #[inline]
    pub fn into_object(self) -> Object<'js> {
        self.0
    }
    #[inline]
    pub fn into_value(self) -> Value<'js> {
        self.into_object().0
    }
    unsafe extern "C" fn gc_mark(
        rt: *mut qjs::JSRuntime,
        val: qjs::JSValue,
        mark_func: qjs::JS_MarkFunc,
    ) {
        let ptr = qjs::JS_GetOpaque(val, Self::id()) as *mut C;
        debug_assert!(!ptr.is_null());
        let inst = &mut *ptr;
        let marker = RefsMarker { rt, mark_func };
        inst.mark_refs(&marker);
    }
    unsafe extern "C" fn finalizer(rt: *mut qjs::JSRuntime, val: qjs::JSValue) {
        let ptr = qjs::JS_GetOpaque(val, Self::id()) as *mut C;
        debug_assert!(!ptr.is_null());
        let inst = Box::from_raw(ptr);
        qjs::JS_FreeValueRT(rt, val);
        mem::drop(inst);
    }
}
impl<'js> Object<'js> {
    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "classes")))]
    pub fn instance_of<C: ClassDef>(&self) -> bool {
        let ptr =
            unsafe { qjs::JS_GetOpaque2(self.0.ctx.as_ptr(), self.0.value, Class::<C>::id()) };
        !ptr.is_null()
    }
    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "classes")))]
    pub fn into_instance<C: ClassDef>(self) -> Option<Class<'js, C>> {
        if self.instance_of::<C>() {
            Some(Class(self, PhantomData))
        } else {
            None
        }
    }
}
impl<'js, C> IntoJs<'js> for Class<'js, C>
where
    C: ClassDef,
{
    fn into_js(self, ctx: Ctx<'js>) -> Result<Value<'js>> {
        self.0.into_js(ctx)
    }
}
impl<'js, C> FromJs<'js> for Class<'js, C>
where
    C: ClassDef,
{
    fn from_js(ctx: Ctx<'js>, value: Value<'js>) -> Result<Self> {
        let value = Object::from_js(ctx, value)?;
        Class::<C>::from_object(value)
    }
}
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "classes")))]
#[repr(transparent)]
pub struct Constructor<C, F>(pub(crate) F, PhantomData<C>);
impl<C, F> AsRef<F> for Constructor<C, F> {
    fn as_ref(&self) -> &F {
        &self.0
    }
}
impl<C, F> Deref for Constructor<C, F> {
    type Target = F;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "classes")))]
pub struct WithProto<'js, C>(pub C, pub Object<'js>);
impl<'js, C> IntoJs<'js> for WithProto<'js, C>
where
    C: ClassDef + IntoJs<'js>,
{
    fn into_js(self, _ctx: Ctx<'js>) -> Result<Value<'js>> {
        Class::<C>::instance_proto(self.0, self.1).map(|Class(Object(val), _)| val)
    }
}
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "classes")))]
#[macro_export]
macro_rules! class_def {
    ($name:ident $($rest:tt)*) => {
        $crate::class_def!{@decl $name
                           $crate::class_def!{@parse $($rest)*}}
    };
    (@parse ($proto:ident) { $($body:tt)* } $($rest:tt)*) => {
        $crate::class_def!{@proto _ctx $proto $($body)*}
        $crate::class_def!{@parse $($rest)*}
    };
    (@parse ($ctx:ident, $proto:ident) { $($body:tt)* } $($rest:tt)*) => {
        $crate::class_def!{@proto $ctx $proto $($body)*}
        $crate::class_def!{@parse $($rest)*}
    };
    (@parse @($ctor:ident) { $($body:tt)* } $($rest:tt)*) => {
        $crate::class_def!{@ctor _ctx $ctor $($body)*}
        $crate::class_def!{@parse $($rest)*}
    };
    (@parse @($ctx:ident, $ctor:ident) { $($body:tt)* } $($rest:tt)*) => {
        $crate::class_def!{@ctor $ctx $ctor $($body)*}
        $crate::class_def!{@parse $($rest)*}
    };
    (@parse ~($self:ident, $marker:ident) { $($body:tt)* } $($rest:tt)*) => {
        $crate::class_def!{@mark $self $marker $($body)*}
        $crate::class_def!{@parse $($rest)*}
    };
    (@parse ~ $($rest:tt)*) => {
        $crate::class_def!{@mark this marker $crate::class::HasRefs::mark_refs(this, marker);}
        $crate::class_def!{@parse $($rest)*}
    };
    (@parse) => {};
    (@proto $ctx:ident $proto:ident $($body:tt)*) => {
        const HAS_PROTO: bool = true;
        fn init_proto<'js>($ctx: $crate::Ctx<'js>, $proto: &$crate::Object<'js>) -> $crate::Result<()> {
            $($body)*
            Ok(())
        }
    };
    (@ctor $ctx:ident $ctor:ident $($body:tt)*) => {
        const HAS_STATIC: bool = true;
        fn init_static<'js>($ctx: $crate::Ctx<'js>, $ctor: &$crate::Object<'js>) -> $crate::Result<()> {
            $($body)*
            Ok(())
        }
    };
    (@mark $self:ident $marker:ident $($body:tt)*) => {
        const HAS_REFS: bool = true;
        fn mark_refs(&self, $marker: &$crate::class::RefsMarker) {
            let $self = self;
            $($body)*
        }
    };
    (@decl $name:ident $($body:tt)*) => {
        impl $crate::class::ClassDef for $name {
            const CLASS_NAME: &'static str = stringify!($name);
            fn class_id() -> &'static $crate::ClassId {
                static CLASS_ID: $crate::ClassId = $crate::ClassId::new();
                &CLASS_ID
            }
            $($body)*
        }
        impl<'js> $crate::IntoJs<'js> for $name {
            fn into_js(self, ctx: $crate::Ctx<'js>) -> $crate::Result<$crate::Value<'js>> {
                <$name as $crate::class::ClassDef>::into_js_obj(self, ctx)
            }
        }
    };
}
#[cfg(test)]
mod test {
    use crate::{function::SelfMethod, *};
    use approx::assert_abs_diff_eq as assert_approx_eq;
    use function::{Func, Method};
    #[test]
    fn class_basics() {
        struct Foo(pub StdString);
        class_def!(Foo);
        struct Bar(pub i32);
        class_def!(Bar);
        test_with(|ctx| {
            let global = ctx.globals();
            Class::<Foo>::register(ctx).unwrap();
            Class::<Bar>::register(ctx).unwrap();
            global.set("foo", Foo("I'm foo".into())).unwrap();
            global.set("bar", Bar(14)).unwrap();
            let foo: class::Ref<Foo> = global.get("foo").unwrap();
            assert_eq!(foo.0, "I'm foo");
            let bar: class::Ref<Bar> = global.get("bar").unwrap();
            assert_eq!(bar.0, 14);
            if let Err(Error::FromJs { from, to, .. }) = global.get::<_, class::Ref<Bar>>("foo") {
                assert_eq!(from, "object");
                assert_eq!(to, "Bar");
            } else {
                panic!("An error was expected");
            }
            if let Err(Error::FromJs { from, to, .. }) = global.get::<_, class::Ref<Foo>>("bar") {
                assert_eq!(from, "object");
                assert_eq!(to, "Foo");
            } else {
                panic!("An error was expected");
            }
            Class::<Bar>::register(ctx).unwrap();
            Class::<Foo>::register(ctx).unwrap();
        });
        test_with(|ctx| {
            Class::<Foo>::register(ctx).unwrap();
            Class::<Bar>::register(ctx).unwrap();
        });
    }
    #[test]
    fn point_class() {
        struct Point {
            pub x: f64,
            pub y: f64,
        }
        impl Point {
            pub fn new(x: f64, y: f64) -> Self {
                Self { x, y }
            }
            pub fn zero() -> Self {
                Self::new(0.0, 0.0)
            }
            pub fn get_x(&self) -> f64 {
                self.x
            }
        }
        class_def! {
            Point (proto) {
                proto.set("get_x", Func::from(SelfMethod::<Point,_>::from(Point::get_x)))?;
                proto.set("get_y", Func::from(SelfMethod::<Point,_>::from(|Point { y, .. }: &Point| *y)))?;
            } @(ctor) {
                ctor.set("zero", Func::from(Point::zero))?;
            }
        }
        test_with(|ctx| {
            Class::<Point>::register(ctx).unwrap();
            let global = ctx.globals();
            let ctor = Function::new(ctx, Class::<Point>::constructor(Point::new)).unwrap();
            {
                let ctor = ctor.as_object();
                let proto: Object = ctor.get("prototype").unwrap();
                let ctor_: Function = proto.get("constructor").unwrap();
                assert_eq!(&ctor_.into_object(), ctor);
            }
            global.set("Point", ctor).unwrap();
            let res: f64 = ctx
                .eval(
                    r#"
                        let p = new Point(2, 3);
                        let z = Point.zero();
                        (p.get_x() + z.get_x()) * (p.get_y() + z.get_y())
                    "#,
                )
                .unwrap();
            assert_approx_eq!(res, 6.0);
            let res: f64 = ctx
                .eval(
                    r#"
                        class ColorPoint extends Point {
                            constructor(x, y, color) {
                                super(x, y);
                                this.color = color;
                            }
                            get_color() {
                                return this.color;
                            }
                        }
                        let c = new ColorPoint(3, 5, 2);
                        c.get_x() * c.get_y() + c.get_color()
                    "#,
                )
                .unwrap();
            assert_approx_eq!(res, 17.0);
        });
    }
    #[test]
    fn no_prototype_with_constructor() {
        struct X;
        class_def!(X);
        test_with(|ctx| {
            Class::<X>::register(ctx).unwrap();
            ctx.globals()
                .set("X", Func::new("X", Class::<X>::constructor(|| X)))
                .unwrap();
            ctx.eval::<(), _>("X()").unwrap();
            ctx.eval::<(), _>("new X()").unwrap();
        });
    }
    #[test]
    fn concurrent_register() {
        struct X;
        class_def!(
            X (_proto) {
                println!("X::register");
            }
        );
        fn run() {
            test_with(|ctx| {
                Class::<X>::register(ctx).unwrap();
                let global = ctx.globals();
                global
                    .set("X", Func::from(Class::<X>::constructor(|| X)))
                    .unwrap();
            });
        }
        let h1 = std::thread::spawn(run);
        let h2 = std::thread::spawn(run);
        let h3 = std::thread::spawn(run);
        let h4 = std::thread::spawn(run);
        let h5 = std::thread::spawn(run);
        h1.join().unwrap();
        h2.join().unwrap();
        h3.join().unwrap();
        h4.join().unwrap();
        h5.join().unwrap();
    }
    mod internal_refs {
        use crate::class::{HasRefs, RefsMarker};
        use super::*;
        use std::{cell::RefCell, collections::HashSet};
        struct A {
            name: StdString,
            refs: RefCell<HashSet<Persistent<Class<'static, A>>>>,
        }
        impl HasRefs for A {
            fn mark_refs(&self, marker: &RefsMarker) {
                println!("A::mark {}", self.name);
                self.refs.borrow_mut().mark_refs(marker);
            }
        }
        impl Drop for A {
            fn drop(&mut self) {
                println!("A::drop {}", self.name);
            }
        }
        impl A {
            fn new(name: StdString) -> Self {
                println!("A::new {name}");
                Self {
                    name,
                    refs: RefCell::new(HashSet::new()),
                }
            }
        }
        impl<'js> Class<'js, A> {
            pub fn add(self, val: Persistent<Class<'static, A>>) {
                self.borrow().refs.borrow_mut().insert(val);
            }
            pub fn rm(self, val: Persistent<Class<'static, A>>) {
                self.borrow().refs.borrow_mut().remove(&val);
            }
        }
        class_def!(
            A~ (proto) {
                println!("A::register");
                proto.set("add", Func::from(Method(Class::<A>::add)))?;
                proto.set("rm", Func::from(Method(Class::<A>::rm)))?;
            }
        );
        #[test]
        fn single_ref() {
            test_with(|ctx| {
                Class::<A>::register(ctx).unwrap();
                let global = ctx.globals();
                global
                    .set("A", Func::from(Class::<A>::constructor(A::new)))
                    .unwrap();
                let _: () = ctx
                    .eval(
                        r#"
                        let a = new A("a");
                        let b = new A("b");
                        //a.add(b);
                        b.add(a);
                    "#,
                    )
                    .unwrap();
            });
        }
        #[test]
        fn cross_refs() {
            test_with(|ctx| {
                Class::<A>::register(ctx).unwrap();
                let global = ctx.globals();
                global
                    .set("A", Func::from(Class::<A>::constructor(A::new)))
                    .unwrap();
                let _: () = ctx
                    .eval(
                        r#"
                        let a = new A("a");
                        let b = new A("b");
                        a.add(b);
                        b.add(a);
                    "#,
                    )
                    .unwrap();
            });
        }
        #[test]
        fn ref_loops() {
            test_with(|ctx| {
                Class::<A>::register(ctx).unwrap();
                let global = ctx.globals();
                global
                    .set("A", Func::from(Class::<A>::constructor(A::new)))
                    .unwrap();
                let _: () = ctx
                    .eval(
                        r#"
                        let a = new A("a");
                        let b = new A("b");
                        let c = new A("c");
                        a.add(b);
                        b.add(c);
                        c.add(a);
                    "#,
                    )
                    .unwrap();
            });
        }
        #[test]
        fn managed_rm() {
            test_with(|ctx| {
                Class::<A>::register(ctx).unwrap();
                let global = ctx.globals();
                global
                    .set("A", Func::from(Class::<A>::constructor(A::new)))
                    .unwrap();
                let _: () = ctx
                    .eval(
                        r#"
                        let a = new A("a");
                        let b = new A("b");
                        a.add(b);
                        b.add(a);
                        a.rm(b);
                        b.rm(a);
                    "#,
                    )
                    .unwrap();
            });
        }
    }
}