use std::{
    ffi::{CStr, CString},
    os::raw::c_void,
    vec,
};

use crate::bindgen::{
    JSCFunctionEnum_JS_CFUNC_generic, JSContext, JSRuntime, JSValue, JS_Eval, JS_GetGlobalObject,
    JS_NewCFunction2, JS_NewContext, JS_NewFloat64, JS_NewRuntime, JS_NewString, JS_SetOpaque,
    JS_SetPropertyStr, JS_ToCStringLen2,
};

pub struct Runtime {
    pub ptr: *mut JSRuntime,
}

pub struct Context {
    pub ptr: *mut JSContext,
}

impl Runtime {
    pub fn new() -> Self {
        unsafe {
            let ptr = JS_NewRuntime();
            Self { ptr }
        }
    }

    pub fn new_context(&self) -> Context {
        unsafe {
            let ptr = JS_NewContext(self.ptr);
            Context { ptr }
        }
    }
}

pub trait FromJs {
    fn from_js(v: JSValue) -> Self;
}

// impl FromJs for f64 {
//     fn from_js(v: JSValue) -> Self {
//         unsafe { v.u.float64 }
//     }
// }

// impl FromJs for String {
//     fn from_js(v: JSValue) -> Self {

//         // v.u

//       // let s = JS_ToCStringLen2(ctx, plen, val1, cesu8)
//     }
// }

pub trait StaticJsFn<A = (), R = ()>: Sized {
    fn apply(  args: Vec<JSValue>) -> impl IntoJs;
}

pub trait IntoJs: Sized {
    fn into_js(self, ctx: *mut JSContext) -> JSValue;
}

impl IntoJs for f64 {
    fn into_js(self, ctx: *mut JSContext) -> JSValue {
        unsafe { JS_NewFloat64(ctx, self) }
    }
}

impl IntoJs for () {
    fn into_js(self, ctx: *mut JSContext) -> JSValue {
        unsafe { JS_NewFloat64(ctx, 0.) }
    }
}

impl IntoJs for String {
    fn into_js(self, ctx: *mut JSContext) -> JSValue {
        println!("====IntoJs1");
        // let str = CString::new(self.clone()).unwrap().clone();
        let str = CString::new("a").unwrap();
        println!("====IntoJs2");
        let ptr = str.as_ptr();
        println!("====IntoJs3");
        std::mem::forget(str);
        println!("====IntoJs4");

        let v = unsafe { JS_NewString(ctx, ptr) };
        println!("====IntoJs5");
        v
    }
}

impl<A, R, F> StaticJsFn<A, R> for F
where
    F: Fn(A) -> R,
    A: IntoJs,
    R: IntoJs,
{
    fn apply(  args: Vec<JSValue>) -> impl IntoJs {
        println!("=======");
        1.
    }
}

impl<A1, A2, R, F> StaticJsFn<(A1, A2), R> for F
where
    F: Fn(A1, A2) -> R,
    A1: IntoJs,
    A2: IntoJs,
    R: IntoJs,
{
    fn apply(  args: Vec<JSValue>) -> impl IntoJs {
        println!("=======");
        1.
    }
}

impl Context {
    pub fn new_function<A, B, F: StaticJsFn<A, B> + 'static>(&self, name: &str, f: F) -> JSValue {
        let name = CString::new(name).unwrap().clone();
        let name_ptr = name.as_ptr();
        let length = 1;

        unsafe extern "C" fn trampoline<A, B, F>(
            ctx: *mut JSContext,
            this_val: JSValue,
            argc: ::std::os::raw::c_int,
            argv: *mut JSValue,
        ) -> JSValue
        where
            F: StaticJsFn<A, B> + 'static,
        {
            println!("call {}", this_val.u.ptr as usize);
            let closure_ptr = this_val.u.ptr;
            let closure: &mut F = &mut *(closure_ptr as *mut F);
            // closure("s".into());
            let r = F ::apply(vec![]);

            // let r = closure.apply(vec![]);
            println!("====1:");
            let v = r.into_js(ctx);
            println!("====2:");
            return v;
        }
        let wrap: unsafe extern "C" fn(*mut JSContext, JSValue, i32, *mut JSValue) -> JSValue =
            trampoline::<A, B, F>;
        let func = Some(wrap);

        unsafe {
            let fun = JS_NewCFunction2(
                self.ptr,
                func,
                name_ptr,
                length,
                JSCFunctionEnum_JS_CFUNC_generic,
                0,
            );

            // let boxed_f = Box::new(f);
            // let ptr = (&*boxed_f) as *const F as *mut c_void;
            // println!("ptr: {} {}", ptr as usize, fun.u.ptr as usize);
            // JS_SetOpaque(fun, ptr);
            fun
        }
    }

    pub fn eval(&self, code: &str, filename: &str, eval_flags: std::os::raw::c_int) {
        let input = CString::new(code).unwrap();
        let input_len = input.as_bytes().len();
        let filename = CString::new(filename).unwrap();

        unsafe {
            let input_ptr = input.as_ptr();
            let filename_ptr = filename.as_ptr();
            let v = JS_Eval(self.ptr, input_ptr, input_len, filename_ptr, eval_flags);
            let ptr = JS_ToCStringLen2(self.ptr, std::ptr::null_mut(), v, 0);

            let output = CStr::from_ptr(ptr);

            println!("output: {:?}", output);
        }
    }

    pub fn set_property(&self, name: &str, this_obj: JSValue, value: JSValue) {
        unsafe {
            let name = CString::new(name).unwrap();
            let name_ptr = name.as_ptr();
            JS_SetPropertyStr(self.ptr, this_obj, name_ptr, value);
        }
    }

    pub fn get_global_object(&self) -> JSValue {
        unsafe { JS_GetGlobalObject(self.ptr) }
    }
}