use std::{error, fmt};

use rquickjs::{Ctx, Error as JSError, Exception, Value};
use serde::{de, ser};

/// This type represents all possible errors that can occur when serializing or
/// deserializing JS values.
pub struct Error(Box<ErrorImpl>);

impl Error {
    pub(crate) fn new(msg: impl Into<ErrorImpl>) -> Self {
        Error(Box::new(msg.into()))
    }

    pub fn catch<'js>(self, ctx: &Ctx<'js>) -> CaughtError<'js> {
        self.0.catch(ctx)
    }
}

/// Alias for a `Result` with the error type `rquickjs_serde::Error`.
pub type Result<T> = std::result::Result<T, Error>;

impl fmt::Debug for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Error({})", self.0)
    }
}

impl error::Error for Error {}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(&*self.0, f)
    }
}

impl de::Error for Error {
    fn custom<T: fmt::Display>(msg: T) -> Self {
        Error(Box::new(ErrorImpl::Message(msg.to_string())))
    }
}

impl ser::Error for Error {
    fn custom<T: fmt::Display>(msg: T) -> Self {
        Error(Box::new(ErrorImpl::Message(msg.to_string())))
    }
}

/// The internal representation of an error.
///
/// This enum represents various errors that can occur during JS value serialization or deserialization,
/// including UTF-8 conversion errors, and errors originating from the `rquickjs` library.
#[derive(Debug)]
pub enum ErrorImpl {
    /// A generic error message
    Message(String),
    /// An error originating from the `rquickjs` library.
    Rquickjs(JSError),
}

impl ErrorImpl {
    pub fn catch<'js>(self, ctx: &Ctx<'js>) -> CaughtError<'js> {
        match self {
            ErrorImpl::Message(msg) => CaughtError::Message(msg),
            ErrorImpl::Rquickjs(JSError::Exception) => {
                let value = ctx.catch();
                if let Some(ex) = value
                    .as_object()
                    .and_then(|x| Exception::from_object(x.clone()))
                {
                    CaughtError::Exception(ex)
                } else {
                    CaughtError::Value(value)
                }
            }
            ErrorImpl::Rquickjs(e) => CaughtError::Error(e),
        }
    }
}

impl fmt::Display for ErrorImpl {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ErrorImpl::Message(msg) => write!(f, "{msg}"),
            ErrorImpl::Rquickjs(e) => write!(f, "{e}"),
        }
    }
}

impl From<&str> for ErrorImpl {
    fn from(value: &str) -> Self {
        ErrorImpl::Message(value.to_string())
    }
}

impl From<JSError> for ErrorImpl {
    fn from(value: JSError) -> Self {
        ErrorImpl::Rquickjs(value)
    }
}

/// An error type containing possible thrown exception values.
#[derive(Debug)]
pub enum CaughtError<'js> {
    /// Error was an exception and an instance of Error
    Exception(Exception<'js>),
    /// Error was an exception but not an instance of Error.
    Value(Value<'js>),
    /// Error wasn't an exception
    Error(JSError),
    /// A generic error message
    Message(String),
}

impl<'js> fmt::Display for CaughtError<'js> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            CaughtError::Error(e) => e.fmt(f),
            CaughtError::Exception(e) => e.fmt(f),
            CaughtError::Value(e) => {
                writeln!(f, "Exception generated by quickjs: {e:?}")
            }
            CaughtError::Message(msg) => write!(f, "{msg}"),
        }
    }
}

impl<'js> error::Error for CaughtError<'js> {}
