use std::error::Error;

use js_sys::Reflect;
use wasm_bindgen::{JsValue, prelude::*};

use crate::{
    RuntimeError,
    js::{exception::Exception, vm::VMExceptionRef},
};

#[derive(Debug)]
enum InnerTrap {
    User(Box<dyn Error + Send + Sync>),
    Js(JsTrap),
}

/// A struct representing a Trap
#[wasm_bindgen(skip_typescript)]
#[derive(Debug)]
pub struct Trap {
    inner: InnerTrap,
}

impl Trap {
    pub fn user(error: Box<dyn Error + Send + Sync>) -> Self {
        Self {
            inner: InnerTrap::User(error),
        }
    }

    /// Attempts to downcast the `Trap` to a concrete type.
    pub fn downcast<T: Error + 'static>(self) -> Result<T, Self> {
        match self.inner {
            // We only try to downcast user errors
            InnerTrap::User(err) if err.is::<T>() => Ok(*err.downcast::<T>().unwrap()),
            _ => Err(self),
        }
    }

    /// Attempts to downcast the `Trap` to a concrete type.
    pub fn downcast_ref<T: Error + 'static>(&self) -> Option<&T> {
        match &self.inner {
            // We only try to downcast user errors
            InnerTrap::User(err) if err.is::<T>() => err.downcast_ref::<T>(),
            _ => None,
        }
    }

    /// Returns true if the `Trap` is the same as T
    pub fn is<T: Error + 'static>(&self) -> bool {
        match &self.inner {
            InnerTrap::User(err) => err.is::<T>(),
            _ => false,
        }
    }

    /// Returns true if the `Trap` is an exception
    pub fn is_exception(&self) -> bool {
        false
    }

    /// If the `Trap` is an uncaught exception, returns it.
    pub fn to_exception_ref(&self) -> Option<VMExceptionRef> {
        None
    }
}

#[wasm_bindgen]
impl Trap {
    /// A marker method to indicate that an object is an instance of the `Trap`
    /// class.
    pub fn __wbg_wasmer_trap() {}
}

impl std::error::Error for Trap {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self.inner {
            InnerTrap::User(err) => Some(&**err),
            _ => None,
        }
    }
}

impl std::fmt::Display for Trap {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.inner {
            InnerTrap::User(e) => write!(f, "user: {e}"),
            InnerTrap::Js(value) => write!(f, "js: {value}"),
        }
    }
}

impl From<JsValue> for RuntimeError {
    fn from(value: JsValue) -> Self {
        // We try to downcast the error and see if it's an instance of Trap
        // instead, so we don't need to re-wrap it.
        if let Some(obj) = value.dyn_ref()
            && let Some(trap) = downcast_from_ptr(obj)
        {
            return trap.into();
        }

        Self::from(Trap {
            inner: InnerTrap::Js(value.into()),
        })
    }
}

/// This whole mechanism works because the JavaScript wrapper class has a static
/// `__wbg_wasmer_trap()` method which marks that it is a [`Trap`].
///
/// If that method exists, we assume the pointer is valid and safe to cast back
/// to our type.
fn downcast_from_ptr(value: &JsValue) -> Option<Trap> {
    if !value.is_object() {
        return None;
    }

    let prototype = &Reflect::get_prototype_of(value).ok()?;
    let class = prototype.constructor();
    let key = JsValue::from_str("__wbg_wasmer_trap");

    let marker_func: Option<js_sys::Function> = Reflect::get(&class, &key)
        .and_then(|v: JsValue| v.dyn_into())
        .ok();

    marker_func.as_ref()?;

    // Safety: The marker function exists, therefore it's safe to convert back
    // to a Trap.
    unsafe {
        // Note: this assumes the wrapper class generated by #[wasm_bindgen] will
        // always have a `__destroy_into_raw()` method which consumes the `Trap`
        // wrapper and returns a pointer.
        //
        // This is valid as of wasm-bindgen version 0.2.87
        let key = JsValue::from_str("__destroy_into_raw");
        let ptr = Reflect::get(value, &key)
            .ok()
            .and_then(|v| v.dyn_into::<js_sys::Function>().ok())
            .and_then(|destroy_into_raw| destroy_into_raw.call0(value).ok())
            .and_then(|ret| ret.as_f64())?;

        Some(<Trap as wasm_bindgen::convert::FromWasmAbi>::from_abi(
            ptr as u32,
        ))
    }
}

/// A `Send+Sync` version of a JavaScript error.
#[derive(Debug)]
enum JsTrap {
    /// An error message.
    Message(String),
    /// Unable to determine the underlying error.
    Unknown,
}

impl From<JsValue> for JsTrap {
    fn from(value: JsValue) -> Self {
        // Let's try some easy special cases first
        if let Some(error) = value.dyn_ref::<js_sys::Error>() {
            return Self::Message(error.message().into());
        }

        if let Some(s) = value.as_string() {
            return Self::Message(s);
        }

        // Otherwise, we'll try to stringify the error and hope for the best
        if let Some(obj) = value.dyn_ref::<js_sys::Object>() {
            return Self::Message(obj.to_string().into());
        }

        Self::Unknown
    }
}

impl std::fmt::Display for JsTrap {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Message(m) => write!(f, "{m}"),
            Self::Unknown => write!(f, "unknown"),
        }
    }
}

impl From<JsTrap> for Trap {
    fn from(value: JsTrap) -> Self {
        Self {
            inner: InnerTrap::Js(value),
        }
    }
}

impl From<JsTrap> for RuntimeError {
    fn from(value: JsTrap) -> Self {
        Into::<Trap>::into(value).into()
    }
}

impl From<Trap> for RuntimeError {
    fn from(trap: Trap) -> Self {
        if trap.is::<Self>() {
            return trap.downcast::<Self>().unwrap();
        }

        Self::new_from_source(crate::BackendTrap::Js(trap), vec![], None)
    }
}

impl From<RuntimeError> for wasm_bindgen::JsValue {
    fn from(value: RuntimeError) -> Self {
        Self::from(value.to_string())
    }
}

pub(crate) fn raise(error: Box<dyn std::error::Error + Send + Sync>) -> ! {
    let error = Trap::user(error);
    let js_error: JsValue = error.into();
    wasm_bindgen::throw_val(js_error)
}
