// Convert PXS vars to JS vars.
// Convert JS vars to PXS vars.

use std::{ffi::c_void, sync::Arc};

use etffi::ptr_magic::PtrMagic;

use crate::{js::{SmartJSValue, object::create_object, quickjs}, pxs_error, shared::{
    PxsRes, PxsResult, object::get_object, pxs_Runtime, var::{pxs_Var, pxs_VarObject}
}};

/// JS PXS Container.
/// Holds `SmartJSValue`
struct JSPXSContainer {
    value: SmartJSValue
}

impl JSPXSContainer {
    /// Create a new JSPXSContainer from a Value.
    pub fn from_value(value: SmartJSValue) -> Self {
        JSPXSContainer { value }
    }
}

impl PtrMagic for JSPXSContainer {}

/// JS Object deleters
unsafe extern "C" fn js_deleter(ptr: *mut c_void) {
    if ptr.is_null() {
        return;
    }

    // We will be dropping this dude.
    let _ = JSPXSContainer::from_raw(ptr as *mut JSPXSContainer);
    // Value gets dropped automatiacll.
}

/// Convert a JS Value into a pxs_Var
pub(super) fn js_into_pxs(value: &SmartJSValue) -> PxsResult {
    // let value = value.clone();
    if value.is_int() {
        Ok(pxs_Var::new_i64(value.as_i32()? as i64))
    } else if value.is_float() {
        Ok(pxs_Var::new_f64(
            value.as_f64()?,
        ))
    } else if value.is_bool() {
        Ok(pxs_Var::new_bool(value.as_bool()))
    } else if value.is_string() {
        Ok(pxs_Var::new_string(value.as_string()?))
    } else if value.is_array() {
        let mut values = vec![];
        let length = value.get_prop("length").as_i32()?;

        for i in 0..length {
            let val = value.get_prop_pos(i as u32);
            values.push(js_into_pxs(&val)?);
        }

        Ok(pxs_Var::new_list_with(values))
    } else if value.is_function() {
        Ok(pxs_Var::new_function(
            JSPXSContainer::from_value(value.clone()).into_raw() as *mut c_void,
            Some(js_deleter),
        ))
    } else if value.is_exception() {
        // let exce = value.as_exception().unwrap();
        Ok(pxs_Var::new_exception(value.get_error_exception().unwrap()))
    } else if value.is_error() {
        Ok(pxs_Var::new_exception(value.get_error_exception().unwrap()))
    } else if value.is_undefined() || value.is_null() {
        Ok(pxs_Var::new_null())
    } else {
        // As object.
        Ok(pxs_Var::new_object(pxs_VarObject::new_lang_only(JSPXSContainer::from_value(value.clone()).into_void()), Some(js_deleter)))
    }
}

/// Convert a `pxs_Var` into a JS Value.
pub(super) fn pxs_into_js(context: *mut quickjs::JSContext, var: &pxs_Var) -> PxsRes<SmartJSValue> {
    match var.tag {
        crate::shared::var::pxs_VarType::pxs_Int64 => Ok(SmartJSValue::new_i32(context, var.get_i64()? as i32)),
        // TODO: support UInt
        crate::shared::var::pxs_VarType::pxs_UInt64 => Ok(SmartJSValue::new_i32(context, var.get_u64()? as i32)),
        crate::shared::var::pxs_VarType::pxs_String => Ok(SmartJSValue::new_string(context, var.get_string()?)),
        crate::shared::var::pxs_VarType::pxs_Bool => Ok(SmartJSValue::new_bool(context, var.get_bool()?)),
        crate::shared::var::pxs_VarType::pxs_Float64 => Ok(SmartJSValue::new_f64(context, var.get_f64()?)),
        crate::shared::var::pxs_VarType::pxs_Null => Ok(SmartJSValue::new_null(context)),
        crate::shared::var::pxs_VarType::pxs_Object => {
            // Pass pointer back
            let container_ptr = var.get_object_ptr();
            if container_ptr.is_null() {
                // I want to return Exception("Object pointer not found.")
                return pxs_error!("Object pointer not found");
            }
            let container = unsafe{JSPXSContainer::from_borrow_void(container_ptr)};

            // Return a Value. 
            Ok(container.value.clone())
        },
        crate::shared::var::pxs_VarType::pxs_HostObject => {
            let idx = var.get_host_idx();
            let po = get_object(idx).unwrap();
            let lang_ptr_is_null = po.lang_ptr.lock().unwrap().is_null();
            if lang_ptr_is_null {
                // Create new object
                let obj: SmartJSValue = create_object(context, idx, Arc::clone(&po));
                // Box it
                let container = JSPXSContainer::from_value(obj);
                po.update_lang_ptr(container.into_void());
                po.update_pxs_free_method(js_deleter);
            }
            // Get smart value and return raw value...
            let lang_ptr = po.lang_ptr.lock().unwrap();
            let container = unsafe { JSPXSContainer::from_borrow_void(*lang_ptr) };
            Ok(container.value.clone())
        },
        crate::shared::var::pxs_VarType::pxs_List => {
            let arr = SmartJSValue::new_array(context);
            let vars = &var.get_list().unwrap().vars;
            for i in 0..vars.len() {
                // convert to JS value
                let mut value = pxs_into_js(context, &vars[i])?;
                arr.set_prop_pos(i as u32, &mut value);
            }
            
            Ok(arr)
        },
        crate::shared::var::pxs_VarType::pxs_Function => {
            // What?
            let container_ptr = var.get_function().unwrap();
            if container_ptr.is_null() {
                return pxs_error!("Function pointer not found");
            }
            let container = unsafe{JSPXSContainer::from_borrow_void(container_ptr)};
            Ok(container.value.clone())
        },
        crate::shared::var::pxs_VarType::pxs_Factory => {
            // Call and return
            let factory = var.get_factory().unwrap();
            let res = factory.call(pxs_Runtime::pxs_JavaScript);
            // convert into js
            pxs_into_js(context, &res)
        },
        crate::shared::var::pxs_VarType::pxs_Exception => {
            let message = var.get_string().unwrap();
            let error = SmartJSValue::new_exception(context, message, "Exception".to_string());

            unsafe {
                Ok(SmartJSValue::new_owned(quickjs::JS_Throw(context, error.dupped_value()), context))
            }
        },
        crate::shared::var::pxs_VarType::pxs_Map => {
            let object = SmartJSValue::new_object(context);
            
            let map = var.get_map().unwrap();
            let keys = map.keys();

            for k in keys {
                let item = map.get_item(k);
                if let Some(item) = item {
                    let js_key = pxs_into_js(context, k)?;
                    let mut js_val = pxs_into_js(context, item)?;

                    object.set_prop_value(&js_key, &mut js_val);
                }
            }
            
            Ok(object)
        },
    }
}
