use std::fmt::Debug;
use std::ops::Deref;
use libquickjs_ng_sys as q;
use crate::utils::make_cstring;
use crate::{ExecutionError, ValueError};
use super::OwnedJsValue;
#[derive(Clone, Debug, PartialEq)]
pub struct OwnedJsObject {
    value: OwnedJsValue,
}
impl OwnedJsObject {
    pub fn try_from_value(value: OwnedJsValue) -> Result<Self, ValueError> {
        if !value.is_object() {
            Err(ValueError::Internal("Expected an object".to_string()))
        } else {
            Ok(Self { value })
        }
    }
    pub fn into_value(self) -> OwnedJsValue {
        self.value
    }
    pub fn properties_iter(&self) -> Result<OwnedJsPropertyIterator, ValueError> {
        let prop_iter = OwnedJsPropertyIterator::from_object(self.value.context(), self.clone())?;
        Ok(prop_iter)
    }
    pub fn property(&self, name: &str) -> Result<Option<OwnedJsValue>, ExecutionError> {
        let cname = make_cstring(name)?;
        let value = {
            let raw = unsafe {
                q::JS_GetPropertyStr(self.value.context(), self.value.value, cname.as_ptr())
            };
            OwnedJsValue::new(self.value.context(), raw)
        };
        let tag = value.tag();
        if tag.is_exception() {
            Err(ExecutionError::Internal(format!(
                "Exception while getting property '{}'",
                name
            )))
        }
        else {
            Ok(Some(value))
        }
    }
    pub fn property_require(&self, name: &str) -> Result<OwnedJsValue, ExecutionError> {
        self.property(name)?
            .ok_or_else(|| ExecutionError::Internal(format!("Property '{}' not found", name)))
    }
    pub fn is_promise(&self) -> Result<bool, ExecutionError> {
        if let Some(p) = self.property("then")? {
            if p.is_function() {
                return Ok(true);
            }
        }
        if let Some(p) = self.property("catch")? {
            if p.is_function() {
                return Ok(true);
            }
        }
        Ok(false)
    }
    pub fn set_property(&self, name: &str, value: OwnedJsValue) -> Result<(), ExecutionError> {
        let cname = make_cstring(name)?;
        unsafe {
            let ret = q::JS_SetPropertyStr(
                self.value.context(),
                self.value.value,
                cname.as_ptr(),
                value.value,
            );
            if ret < 0 {
                Err(ExecutionError::Internal(
                    "Could not set property".to_string(),
                ))
            } else {
                std::mem::forget(value);
                Ok(())
            }
        }
    }
}
impl Deref for OwnedJsObject {
    type Target = OwnedJsValue;
    fn deref(&self) -> &Self::Target {
        &self.value
    }
}
#[derive(Clone, Debug, PartialEq)]
pub struct OwnedJsPropertyIterator {
    context: *mut q::JSContext,
    object: OwnedJsObject,
    properties: *mut q::JSPropertyEnum,
    length: u32,
    cur_index: u32,
}
impl OwnedJsPropertyIterator {
    pub fn from_object(
        context: *mut q::JSContext,
        object: OwnedJsObject,
    ) -> Result<Self, ValueError> {
        let mut properties: *mut q::JSPropertyEnum = std::ptr::null_mut();
        let mut length: u32 = 0;
        let flags = (q::JS_GPN_STRING_MASK | q::JS_GPN_SYMBOL_MASK | q::JS_GPN_ENUM_ONLY) as i32;
        let ret = unsafe {
            q::JS_GetOwnPropertyNames(
                context,
                &mut properties,
                &mut length,
                object.value.value,
                flags,
            )
        };
        if ret != 0 {
            return Err(ValueError::Internal(
                "Could not get object properties".into(),
            ));
        }
        Ok(Self {
            context,
            object,
            properties,
            length,
            cur_index: 0,
        })
    }
}
impl Iterator for OwnedJsPropertyIterator {
    type Item = Result<OwnedJsValue, ExecutionError>;
    fn next(&mut self) -> Option<Self::Item> {
        let cur_index = self.cur_index / 2;
        let is_key = (self.cur_index % 2) == 0;
        if cur_index >= self.length {
            return None;
        }
        let prop = unsafe { self.properties.offset(cur_index as isize) };
        let value = if is_key {
            let pair_key = unsafe { q::JS_AtomToString(self.context, (*prop).atom) };
            let tag = unsafe { q::JS_ValueGetTag(pair_key) };
            if tag == q::JS_TAG_EXCEPTION {
                return Some(Err(ExecutionError::Internal(
                    "Could not get object property name".into(),
                )));
            }
            OwnedJsValue::new(self.context, pair_key)
        } else {
            let pair_value =
                unsafe { q::JS_GetProperty(self.context, self.object.value.value, (*prop).atom) };
            let tag = unsafe { q::JS_ValueGetTag(pair_value) };
            if tag == q::JS_TAG_EXCEPTION {
                return Some(Err(ExecutionError::Internal(
                    "Could not get object property".into(),
                )));
            }
            OwnedJsValue::new(self.context, pair_value)
        };
        self.cur_index += 1;
        Some(Ok(value))
    }
}
impl Drop for OwnedJsPropertyIterator {
    fn drop(&mut self) {
        unsafe {
            q::JS_FreePropertyEnum(self.context, self.properties, self.length);
        }
    }
}