use crate::{AtomRef, ContextRef, Error, Result, Runtime, RustFunction};
use oden_js_sys as sys;
use std::ffi::CStr;
use std::fmt;
use std::ops::{Deref, DerefMut};

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ValueType {
    BigDecimal,
    BigInt,
    BigFloat,
    Symbol,
    Module,
    FunctionBytecode,
    String,
    Object,
    Int,
    Bool,
    Null,
    Undefined,
    Float64,
}

impl From<i32> for ValueType {
    fn from(v: i32) -> Self {
        match v {
            sys::JS_TAG_BIG_DECIMAL => ValueType::BigDecimal,
            sys::JS_TAG_BIG_INT => ValueType::BigInt,
            sys::JS_TAG_BIG_FLOAT => ValueType::BigFloat,
            sys::JS_TAG_SYMBOL => ValueType::Symbol,
            sys::JS_TAG_MODULE => ValueType::Module,
            sys::JS_TAG_FUNCTION_BYTECODE => ValueType::FunctionBytecode,
            sys::JS_TAG_STRING => ValueType::String,
            sys::JS_TAG_OBJECT => ValueType::Object,
            sys::JS_TAG_INT => ValueType::Int,
            sys::JS_TAG_BOOL => ValueType::Bool,
            sys::JS_TAG_NULL => ValueType::Null,
            sys::JS_TAG_UNDEFINED => ValueType::Undefined,
            // sys::JS_TAG_UNINITIALIZED => ValueType::Uninitialized,
            // sys::JS_TAG_CATCH_OFFSET => ValueType::CatchOffset,
            // sys::JS_TAG_EXCEPTION => ValueType::Exception,
            sys::JS_TAG_FLOAT64 => ValueType::Float64,
            _ => panic!("Unknown or unsupported value type"),
        }
    }
}

/// A borrowed JavaScript value.
///
/// Contrast this structure to `Value`, which is the owned equivalent. Mostly
/// you will want to create and manipulate `Value`, but you will occasionally
/// receive `ValueRef` in calls that originate from JavaScript
/// code. (`ValueRef` is to `Value` as `str` is to `String`, if that helps.)
pub struct ValueRef {
    pub(crate) val: sys::JSValue,
}

impl ValueRef {
    pub fn from_raw(val: sys::JSValue) -> Self {
        ValueRef { val }
    }

    /// Get the type of JavaScript value that this is.
    pub fn value_type(&self) -> ValueType {
        // SAFETY: Our lifetime guarantees ensure that this value is still live.
        unsafe { sys::JS_ValueGetTag(self.val) }.into()
    }

    /// True if ValueType is ValueType::Null.
    pub fn is_null(&self) -> bool {
        self.value_type() == ValueType::Null
    }

    /// True if ValueType is ValueType::BigDecimal.
    pub fn is_big_decimal(&self) -> bool {
        self.value_type() == ValueType::BigDecimal
    }

    /// True if ValueType is ValueType::BigInt.
    pub fn is_big_int(&self) -> bool {
        self.value_type() == ValueType::BigInt
    }

    /// True if ValueType is ValueType::BigFloat.
    pub fn is_big_float(&self) -> bool {
        self.value_type() == ValueType::BigFloat
    }

    /// True if ValueType is ValueType::Symbol.
    pub fn is_symbol(&self) -> bool {
        self.value_type() == ValueType::Symbol
    }

    /// True if ValueType is ValueType::Module.
    pub fn is_module(&self) -> bool {
        self.value_type() == ValueType::Module
    }

    /// True if ValueType is ValueType::FunctionBytecode.
    pub fn is_functionbytecode(&self) -> bool {
        self.value_type() == ValueType::FunctionBytecode
    }

    /// True if ValueType is ValueType::String.
    pub fn is_string(&self) -> bool {
        self.value_type() == ValueType::String
    }

    /// True if ValueType is ValueType::Object.
    ///
    /// NOTE! This also returns true for arrays and functions the like; it
    /// has all the problems of JavaScript's typeof() operator. I wish I
    /// could fix the world but I can't.
    pub fn is_object(&self) -> bool {
        self.value_type() == ValueType::Object
    }

    /// True if ValueType is ValueType::Int.
    pub fn is_int(&self) -> bool {
        self.value_type() == ValueType::Int
    }

    pub fn to_i32(&self, ctx: &ContextRef) -> Result<i32> {
        unsafe {
            let mut res: i32 = 0;
            let ret = sys::JS_ToInt32(ctx.ctx, &mut res, self.val);
            if ret < 0 {
                Err(ctx.exception_error())
            } else {
                Ok(res)
            }
        }
    }

    pub fn to_u32(&self, ctx: &ContextRef) -> Result<u32> {
        unsafe {
            let mut res: u32 = 0;
            let ret = sys::JS_ToUint32(ctx.ctx, &mut res, self.val);
            if ret < 0 {
                Err(ctx.exception_error())
            } else {
                Ok(res)
            }
        }
    }

    pub fn to_i64(&self, ctx: &ContextRef) -> Result<i64> {
        unsafe {
            let mut res: i64 = 0;
            let ret = sys::JS_ToInt64(ctx.ctx, &mut res, self.val);
            if ret < 0 {
                Err(ctx.exception_error())
            } else {
                Ok(res)
            }
        }
    }

    // TODO: tou64.

    /// True if ValueType is ValueType::Bool.
    pub fn is_bool(&self) -> bool {
        self.value_type() == ValueType::Bool
    }

    pub fn to_bool(&self) -> Result<bool> {
        if self.value_type() == ValueType::Bool {
            Ok(unsafe { self.val.u.int32 } > 0)
        } else {
            Err(Error::InvalidType {
                expected: ValueType::Bool,
                found: self.value_type(),
            })
        }
    }

    /// True if ValueType is ValueType::Undefined.
    pub fn is_undefined(&self) -> bool {
        self.value_type() == ValueType::Undefined
    }

    /// True if ValueType is ValueType::Float64.
    pub fn is_float64(&self) -> bool {
        self.value_type() == ValueType::Float64
    }

    pub fn to_float64(&self, ctx: &ContextRef) -> Result<f64> {
        unsafe {
            let mut res: f64 = 0.0;
            let ret = sys::JS_ToFloat64(ctx.ctx, &mut res, self.val);
            if ret < 0 {
                Err(ctx.exception_error())
            } else {
                Ok(res)
            }
        }
    }

    /// Take a reference to this value so that it outlives this current reference.
    ///
    /// (This is a very cheap operation: at most it increments a reference count.)
    pub fn dup(&self, ctx: &ContextRef) -> Value {
        unsafe {
            sys::JS_DupValue(ctx.ctx, self.val);
        }
        Value::from_raw(self.val, ctx)
    }

    /// Evaluate a previously-compiled function or module. This value must be
    /// of type `FunctionBytecode` or `Module`, otherwise an error will be
    /// returned.
    ///
    /// JavsScript exceptions are are returned as values where `value_type`
    /// is `ValueType::Exception`.
    pub fn eval_function(&self, ctx: &ContextRef) -> Result<Value> {
        match self.value_type() {
            ValueType::Module | ValueType::FunctionBytecode => (),
            found => {
                return Err(Error::InvalidType {
                    expected: ValueType::FunctionBytecode,
                    found,
                })
            }
        }

        let result = unsafe {
            // NOTE: JS_EvalFunction consumes its value but that's not what
            // we want here.
            sys::JS_DupValue(ctx.ctx, self.val);
            sys::JS_EvalFunction(ctx.ctx, self.val)
        };
        Ok(Value::from_raw(result, ctx))
    }

    pub fn get_property<'a, T>(&self, ctx: &ContextRef, prop: T) -> Result<Value>
    where
        T: Into<&'a str>,
    {
        let atom = ctx.new_atom(prop.into())?;
        self.get_property_atom(ctx, &atom)
    }

    pub fn get_property_atom(&self, ctx: &ContextRef, prop: &AtomRef) -> Result<Value> {
        ctx.check_exception(unsafe { sys::JS_GetProperty(ctx.ctx, self.val, prop.atom) })
    }

    pub fn get_index(&self, ctx: &ContextRef, index: u32) -> Result<Value> {
        ctx.check_exception(unsafe { sys::JS_GetPropertyUint32(ctx.ctx, self.val, index) })
    }

    pub fn set_property(&mut self, ctx: &ContextRef, prop: &str, val: &ValueRef) -> Result<()> {
        // TODO: Consume API
        let atom = ctx.new_atom(prop)?;
        self.set_property_atom(ctx, &atom, val)
    }

    pub fn set_property_atom(
        &mut self,
        ctx: &ContextRef,
        prop: &AtomRef,
        val: &ValueRef,
    ) -> Result<()> {
        unsafe {
            sys::JS_DupValue(ctx.ctx, val.val);
            let result = sys::JS_SetProperty(ctx.ctx, self.val, prop.atom, val.val);
            if result == -1 {
                Err(ctx.exception_error())
            } else {
                Ok(())
            }
        }
    }

    pub fn set_index(&mut self, ctx: &ContextRef, index: u32, val: &ValueRef) -> Result<()> {
        unsafe {
            sys::JS_DupValue(ctx.ctx, val.val);
            let result = sys::JS_SetPropertyUint32(ctx.ctx, self.val, index, val.val);
            if result == -1 {
                Err(ctx.exception_error())
            } else {
                Ok(())
            }
        }
    }

    pub fn set_fn<F>(
        &mut self,
        ctx: &ContextRef,
        prop: &str,
        func: impl RustFunction<F> + 'static,
    ) -> Result<()> {
        let vr: Value = ctx.new_fn(func)?;
        self.set_property(ctx, prop, &vr)
    }

    pub fn set_dynamic_method<F>(&mut self, ctx: &ContextRef, prop: &str, func: F) -> Result<()>
    where
        F: 'static + Fn(&ContextRef, &ValueRef, &[&ValueRef]) -> Result<Value>,
    {
        let vr: Value = ctx.new_dynamic_fn(func)?;
        self.set_property(ctx, prop, &vr)
    }

    /// Convert this value into a string representation of the same value.
    pub fn to_string(&self, ctx: &ContextRef) -> Result<String> {
        // SAFETY: ctx's life is bound by the lifetime of our Context, and
        // sys::JS_ToCStringLen2 will return non-null, unless we're out of
        // memory.
        let cstr = unsafe {
            let ptr = sys::JS_ToCStringLen2(ctx.ctx, std::ptr::null_mut(), self.val, 0);
            if ptr.is_null() {
                return Err(ctx.exception_error());
            }
            CStr::from_ptr(ptr)
        };

        // NOTE: Javascript only traffics in unicode strings, and QuickJS
        // ensures that the result here will be UTF-8, so we can safely
        // "unwrap" as there's no JavaScript value that should cause it to
        // fail.
        let result: String = String::from(cstr.to_str().unwrap());

        // SAFETY: ctx's life is bound by the lifetime of our Context, and
        // cstr just came from the call above.
        unsafe {
            sys::JS_FreeCString(ctx.ctx, cstr.as_ptr());
        }

        Ok(result)
    }

    pub fn call(&self, ctx: &ContextRef, args: &[&ValueRef]) -> Result<Value> {
        // TODO: There *must* be a way to avoid this allocation.
        let mut args: Vec<sys::JSValue> = args.iter().map(|v| v.val).collect();
        unsafe {
            ctx.check_exception(sys::JS_Call(
                ctx.ctx,
                self.val,
                sys::JS_MakeUndefined(),
                args.len() as i32,
                args.as_mut_ptr(),
            ))
        }
    }

    /// Get the underlying bytes for a an ArrayBuffer object. Obviously it
    /// only works if this is an un-detached ArrayBuffer value.
    pub fn get_array_buffer<'a>(&'a self, ctx: &ContextRef) -> Result<&'a [u8]> {
        unsafe {
            let mut size: usize = 0;
            let buffer = sys::JS_GetArrayBuffer(ctx.ctx, &mut size as *mut usize, self.val);
            if buffer.is_null() {
                Err(ctx.exception_error())
            } else {
                Ok(std::slice::from_raw_parts(buffer, size))
            }
        }
    }

    /// Serialize this value into a byte array. Consume the byte array with
    /// `ContextRef::deserialize()`.
    ///
    /// NOTE: The serialized value is only good for this exact version of
    ///       QuickJS- do *not* expect to be able to save it to disk and
    ///       re-load it. This is for more ephemeral usage: passing values
    ///       between threads, etc.
    ///
    /// NOTE: In theory if we wanted to just serialize a message we could
    ///       avoid a memory copy; we copy here to avoid keeping the runtime
    ///       alive, which we would have to do in order to free the buffer
    ///       correctly. If we were willing to keep the runtime alive...
    ///
    /// TODO: We do not have support for SharedArrayBuffers here, which would
    ///       let us use this function to pass large buffers between Runtime
    ///       instances, like on different threads, without copying. What a
    ///       pity.
    pub fn serialize(&self, ctx: &ContextRef) -> Result<Vec<u8>> {
        unsafe {
            let mut size = 0;
            let data = sys::JS_WriteObject(
                ctx.ctx,
                &mut size,
                self.val,
                sys::JS_WRITE_OBJ_REFERENCE as i32,
            );
            if data.is_null() {
                Err(ctx.exception_error())
            } else {
                let result = std::slice::from_raw_parts(data, size).to_vec();
                sys::js_free(ctx.ctx, data as *mut std::ffi::c_void);
                Ok(result)
            }
        }
    }
}

impl fmt::Debug for ValueRef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Value")
            .field("v", &self.val)
            .finish_non_exhaustive()
    }
}

/// An owned Value from JavaScript. Unlike ValueRef, this type stands in for
/// values that you construct in Rust code; otherwise they are identical.
pub struct Value {
    value: ValueRef,
    rt: Runtime,
}

impl Value {
    /// Take ownership of a value in a specific context.
    ///
    /// **WARNING**: The specified value *must* be associated with at least
    /// the runtime of the specified context, if not the context itself. This
    /// function makes no attempt to validate this.
    pub(crate) fn from_raw(val: sys::JSValue, ctx: &ContextRef) -> Self {
        Value::from_raw_rt(
            val,
            Runtime::from_raw(unsafe { sys::JS_GetRuntime(ctx.ctx) }),
        )
    }

    pub(crate) fn from_raw_rt(val: sys::JSValue, rt: Runtime) -> Self {
        Value {
            value: ValueRef::from_raw(val),
            rt,
        }
    }

    pub fn null(ctx: &ContextRef) -> Self {
        Value {
            value: ValueRef::from_raw(unsafe { sys::JS_MakeNull() }),
            rt: Runtime::from_raw(unsafe { sys::JS_GetRuntime(ctx.ctx) }),
        }
    }

    pub fn undefined(ctx: &ContextRef) -> Self {
        Value {
            value: ValueRef::from_raw(unsafe { sys::JS_MakeUndefined() }),
            rt: Runtime::from_raw(unsafe { sys::JS_GetRuntime(ctx.ctx) }),
        }
    }
}

impl Deref for Value {
    type Target = ValueRef;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

impl DerefMut for Value {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.value
    }
}

impl Drop for Value {
    fn drop(&mut self) {
        unsafe {
            sys::JS_FreeValueRT(self.rt.rt, self.val);
        }
    }
}

impl fmt::Debug for Value {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.value.fmt(f)
    }
}

impl Clone for Value {
    fn clone(&self) -> Self {
        unsafe {
            sys::JS_DupValueRT(self.rt.rt, self.val);
        }
        Value::from_raw_rt(self.val, self.rt.clone())
    }
}

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

    #[test]
    fn value_type() {
        let rt = Runtime::new();
        let mut ctx = Context::new(rt);
        ctx.add_intrinsic_bigfloat();
        ctx.add_intrinsic_bigdecimal();

        let tests = &[
            ("123.333m", ValueType::BigDecimal),
            ("BigInt(9007199254740991)", ValueType::BigInt),
            ("222.23444l", ValueType::BigFloat),
            ("Symbol('hello')", ValueType::Symbol),
            ("\"hello\"", ValueType::String),
            ("(function() { return {'a':123}; })()", ValueType::Object),
            ("123", ValueType::Int),
            ("true", ValueType::Bool),
            ("null", ValueType::Null),
            ("undefined", ValueType::Undefined),
            ("123.45", ValueType::Float64),
        ];

        for (expr, expected) in tests.into_iter() {
            let val = ctx.eval(expr, "script", EvalFlags::STRICT).unwrap();

            assert_eq!(*expected, val.value_type(), "for {}", expr);
        }
    }
}
