use oden_js_sys as sys;
use std::ffi::NulError;
use thiserror::Error;

mod atom;
mod callback;
mod class;
mod context;
mod conversion;
pub mod module;
mod promise;
mod runtime;
mod value;

pub use atom::{Atom, AtomRef};
pub use class::{Class, ClassID};
pub use context::{Context, ContextRef, EvalFlags};
pub use conversion::*;
pub use promise::{DefaultRejectedPromiseTracker, Promise, RejectedPromiseTracker};
pub use runtime::Runtime;
pub use value::{Value, ValueRef, ValueType};

#[derive(Debug, Error)]
pub enum Error {
    #[error("too many classes have been registered")]
    TooManyClasses,
    #[error("the specified value is not an instance of the class {0}")]
    WrongClass(String),
    #[error("input script contained an embedded NUL byte")]
    UnexpectedNul,
    #[error("the target context is from a different runtime")]
    DifferentRuntime,
    #[error("the specified value had the wrong type (expected {expected:?}, found {found:?})")]
    InvalidType {
        expected: ValueType,
        found: ValueType,
    },
    #[error("argument count mismatch, expected {expected} but received {received}")]
    ArgumentCountMismatch { expected: usize, received: usize },
    #[error("a conversion error occurred: {0}")]
    ConversionError(String),
    #[error("an error occurred calling a rust function: {0}")]
    RustFunctionError(String),
    #[error("an exception was thrown during evaluation: {1}\nStack:\n{2}")]
    Exception(Value, String, String),
    #[error("out of memory")]
    OutOfMemory,
    #[error("an io error occurred: {0}")]
    IOError(std::io::Error),
    #[error("one or more errors occurred parsing {0}: {1}")]
    ParseError(String, String),
}

impl Error {
    // Convert the error into an exception-type object which can be
    // thrown. This is *different* from try_into_value which just propagates
    // the error.
    pub fn to_js_error(&self, context: &ContextRef) -> Value {
        if let Error::Exception(e, _, _) = self {
            e.clone()
        } else {
            let message = self.to_string();
            context.new_error(&message)
        }
    }
}

impl From<NulError> for Error {
    fn from(_: NulError) -> Self {
        Error::UnexpectedNul
    }
}

impl From<std::io::Error> for Error {
    fn from(e: std::io::Error) -> Self {
        Error::IOError(e)
    }
}

impl From<std::str::Utf8Error> for Error {
    fn from(e: std::str::Utf8Error) -> Self {
        Error::ConversionError(e.to_string())
    }
}

pub type Result<T> = core::result::Result<T, Error>;
pub type ValueResult = core::result::Result<Value, Error>;

pub(crate) fn throw_error(context: &ContextRef, error: Error) -> sys::JSValue {
    match error {
        Error::Exception(v, _, _) => unsafe {
            sys::JS_DupValue(context.ctx, v.val);
            sys::JS_Throw(context.ctx, v.val)
        },
        other => throw_string(context, other.to_string()),
    }
}

pub(crate) fn throw_string(context: &ContextRef, message: String) -> sys::JSValue {
    let err = context.new_error(&message);
    unsafe {
        sys::JS_DupValue(context.ctx, err.val);
        sys::JS_Throw(context.ctx, err.val)
    }
}
