use crate::{
    module::loader::{DefaultModuleLoader, ModuleLoader, ModuleSource},
    promise::{PromiseEvent, PromiseHandle},
    throw_error, Atom, AtomRef, ContextRef, DefaultRejectedPromiseTracker, Promise,
    RejectedPromiseTracker, Result, Value, ValueRef,
};
use oden_js_sys as sys;
use std::cell::{RefCell, RefMut};
use std::collections::HashMap;
use std::ffi::CStr;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc;

// NOTE: This is DEEPLY unsafe, but the runtime lifetime dominates the
// lifetime of all of this stuff, so we really *can* hold on to them.
struct PromiseEntry {
    context: *mut sys::JSContext,
    resolve: sys::JSValue,
    reject: sys::JSValue,
}

impl PromiseEntry {
    // NOTE: Takes ownership of resolve and reject.
    fn new(context: *mut sys::JSContext, resolve: sys::JSValue, reject: sys::JSValue) -> Self {
        PromiseEntry {
            context: unsafe { sys::JS_DupContext(context) },
            resolve,
            reject,
        }
    }
}

impl Drop for PromiseEntry {
    fn drop(&mut self) {
        unsafe {
            sys::JS_FreeValue(self.context, self.resolve);
            sys::JS_FreeValue(self.context, self.reject);
            sys::JS_FreeContext(self.context);
        }
    }
}

struct PrivateState {
    refs: u64,
    loader: Arc<RefCell<Box<dyn ModuleLoader>>>,
    promise_send: Sender<(PromiseHandle, PromiseEvent)>,
    promise_recv: Receiver<(PromiseHandle, PromiseEvent)>,
    promise_table: HashMap<PromiseHandle, PromiseEntry>, // !
    rejection_tracker: Arc<Box<dyn RejectedPromiseTracker>>,
}

impl PrivateState {
    pub fn new() -> Box<RefCell<Self>> {
        let (send, recv) = channel();
        Box::new(RefCell::new(PrivateState {
            refs: 1,
            loader: Arc::new(RefCell::new(Box::new(DefaultModuleLoader::new()))),
            promise_send: send,
            promise_recv: recv,
            promise_table: HashMap::new(),
            rejection_tracker: Arc::new(Box::new(DefaultRejectedPromiseTracker::new())),
        }))
    }

    pub unsafe fn from_rt_mut(rt: *mut sys::JSRuntime) -> RefMut<'static, PrivateState> {
        unsafe {
            let ptr = sys::JS_GetRuntimeOpaque(rt) as *const RefCell<PrivateState>;
            ptr.as_ref()
                .expect("We already know this runtime is one of ours!")
                .borrow_mut()
        }
    }

    unsafe extern "C" fn module_loader(
        ctx: *mut sys::JSContext,
        path: *const i8,
        opaque: *mut std::os::raw::c_void,
    ) -> *mut sys::JSModuleDef {
        let path = match CStr::from_ptr(path).to_str() {
            Ok(s) => s,
            Err(_) => return std::ptr::null_mut(),
        };

        let loader = unsafe {
            let ptr = opaque as *const RefCell<PrivateState>;
            ptr.as_ref()
                .expect("We already know this runtime is one of ours!")
                .borrow()
                .loader
                .clone()
        };
        let context = ContextRef::from_raw(ctx);

        let result = {
            let mut loader = loader.borrow_mut();
            loader.load(&context, path)
        };

        match result {
            Ok(ModuleSource::Native(native)) => native.module,
            Ok(ModuleSource::JavaScript(js)) => match context.eval_module(&js, path) {
                Ok(v) => v.module,
                Err(e) => {
                    throw_error(&context, e);
                    std::ptr::null_mut()
                }
            },
            Err(e) => {
                throw_error(&context, e);
                std::ptr::null_mut()
            }
        }
    }

    unsafe extern "C" fn source_mapper(
        ctx: *mut sys::JSContext,
        opaque: *mut std::os::raw::c_void,
        file: u32,
        line: i32,
        pfile: *mut u32,
        pline: *mut i32,
    ) {
        let ctx = ContextRef::from_raw(ctx);
        let file = AtomRef::from_raw(file, &ctx);

        let loader = unsafe {
            let ptr = opaque as *const RefCell<PrivateState>;
            ptr.as_ref()
                .expect("We already know this runtime is one of ours!")
                .borrow()
                .loader
                .clone()
        };

        let result = {
            let loader = loader.borrow();
            loader.map_source(&ctx, &file, line)
        };

        match result {
            Ok((file, line)) => {
                *pfile = Atom::consume(file);
                *pline = line;
            }
            Err(e) => {
                throw_error(&ctx, e);
                *pfile = file.atom;
                *pline = line;
            }
        }
    }

    unsafe extern "C" fn promise_rejection_tracker(
        ctx: *mut sys::JSContext,
        promise: sys::JSValue,
        reason: sys::JSValue,
        is_handled: i32,
        opaque: *mut std::os::raw::c_void,
    ) {
        let ctx = ContextRef::from_raw(ctx);
        let promise = ValueRef::from_raw(promise);
        let reason = ValueRef::from_raw(reason);
        let is_handled = is_handled != 0;
        let handler = unsafe {
            let ptr = opaque as *const RefCell<PrivateState>;
            ptr.as_ref()
                .expect("We already know this runtime is one of ours!")
                .borrow()
                .rejection_tracker
                .clone()
        };
        handler.on_rejected_promise(&ctx, &promise, &reason, is_handled);
    }
}

#[derive(Debug)]
pub struct Runtime {
    pub(crate) rt: *mut sys::JSRuntime,
}

impl Runtime {
    pub fn new() -> Runtime {
        let state = PrivateState::new();
        let rt = unsafe {
            let rt = sys::JS_NewRuntime();
            let state = Box::into_raw(state) as *mut _;
            sys::JS_SetRuntimeOpaque(rt, state);
            sys::JS_SetModuleLoaderFunc(rt, None, Some(PrivateState::module_loader), state);
            sys::JS_SetHostPromiseRejectionTracker(
                rt,
                Some(PrivateState::promise_rejection_tracker),
                state,
            );
            rt
        };
        Runtime { rt }
    }

    pub(crate) fn from_raw(rt: *mut sys::JSRuntime) -> Self {
        let mut state = unsafe { PrivateState::from_rt_mut(rt) };
        state.refs += 1;
        Runtime { rt }
    }

    pub fn set_memory_limit(&mut self, limit: usize) {
        unsafe {
            sys::JS_SetMemoryLimit(self.rt, limit);
        }
    }

    pub fn set_gc_threshold(&mut self, threshold: usize) {
        unsafe {
            sys::JS_SetGCThreshold(self.rt, threshold);
        }
    }

    /// Pass in 0 to disable the maximum size check.
    pub fn set_max_stack_size(&mut self, max_stack: usize) {
        unsafe {
            sys::JS_SetMaxStackSize(self.rt, max_stack);
        }
    }

    /// Set the handler for loading modules. By default this is an instance
    /// of `DefaultModuleLoader`.
    pub fn set_module_loader<T>(&mut self, loader: T)
    where
        T: ModuleLoader + 'static,
    {
        let mut state = unsafe { PrivateState::from_rt_mut(self.rt) };
        if loader.support_source_map() {
            unsafe {
                let opaque = sys::JS_GetRuntimeOpaque(self.rt);
                sys::JS_SetSourceMapFunc(self.rt, Some(PrivateState::source_mapper), opaque);
            }
        } else {
            unsafe {
                sys::JS_SetSourceMapFunc(self.rt, None, std::ptr::null_mut());
            }
        }
        state.loader = Arc::new(RefCell::new(Box::new(loader)));
    }

    /// Set a tracker to be notified whenever a promise is rejected. By
    /// default, this is an instance of `DefaultRejectedPromiseHandler`.
    pub fn set_rejected_promise_tracker<T>(&mut self, tracker: T)
    where
        T: RejectedPromiseTracker + 'static,
    {
        let mut state = unsafe { PrivateState::from_rt_mut(self.rt) };
        state.rejection_tracker = Arc::new(Box::new(tracker));
    }

    pub fn run_gc(&mut self) {
        unsafe {
            sys::JS_RunGC(self.rt);
        }
    }

    /// Construct a new promise.
    pub fn new_promise(&self, context: &ContextRef) -> Result<(Value, Promise)> {
        unsafe {
            let mut state = PrivateState::from_rt_mut(self.rt);
            let mut resolving_funcs: [sys::JSValue; 2] =
                [sys::JS_MakeUndefined(), sys::JS_MakeUndefined()];
            let val = sys::JS_NewPromiseCapability(
                context.ctx,
                &mut resolving_funcs as *mut sys::JSValue,
            );

            if sys::JS_ValueGetTag(val) == sys::JS_TAG_EXCEPTION {
                return Err(context.exception_error());
            }
            let handle = PromiseHandle::new();
            state.promise_table.insert(
                handle,
                PromiseEntry::new(context.ctx, resolving_funcs[0], resolving_funcs[1]),
            );

            let promise = Promise::new(handle, state.promise_send.clone());
            drop(state); // Need to drop state so that the value constructor can addref.
            Ok((Value::from_raw(val, context), promise))
        }
    }

    /// Process all pending async jobs. This includes all promise resolutions.
    fn process_promise_completions(&self) {
        let mut promises = vec![];

        // First, gather all the completed promises into a temporary vector
        // so that we only need to borrow our mutable state for a short
        // period of time.
        let mut state = unsafe { PrivateState::from_rt_mut(self.rt) };
        while let Ok((handle, evt)) = state.promise_recv.try_recv() {
            if let Some(entry) = state.promise_table.remove(&handle) {
                promises.push((entry, evt));
            }
        }
        drop(state); // Don't need our internal state anymore.

        // Nowe we can complete all the promises.
        for (entry, evt) in promises {
            let ctx = ContextRef::from_raw(entry.context);
            let (callback, value) = match evt {
                PromiseEvent::Resolved(v) => (entry.resolve, v),
                PromiseEvent::Rejected(v) => (entry.reject, v),
            };

            // Convert the result into a JS value, which we can only
            // really do while we are on this thread.
            let value = value(&ctx).expect("Should be able to convert promise result to value");

            // Call the particular callback and make sure it doesn't throw.
            ctx.check_exception(unsafe {
                let mut args = [value.val];
                sys::JS_Call(
                    entry.context,
                    callback,
                    sys::JS_MakeUndefined(),
                    1,
                    args.as_mut_ptr(),
                )
            })
            .expect("Exception thrown by promise callback");
        }
    }

    /// Process all pending async jobs. This includes all promise resolutions.
    pub fn process_all_jobs(&self) -> Result<()> {
        self.process_promise_completions();

        loop {
            let mut ctx1: *mut sys::JSContext = std::ptr::null_mut();
            let err = unsafe { sys::JS_ExecutePendingJob(self.rt, &mut ctx1) };
            if err == 0 {
                break;
            } else if err < 0 {
                return Err(ContextRef::from_raw(ctx1).exception_error());
            }
        }
        Ok(())
    }
}

impl Clone for Runtime {
    fn clone(&self) -> Self {
        Runtime::from_raw(self.rt)
    }
}

impl Drop for Runtime {
    fn drop(&mut self) {
        let should_free = {
            let mut state = unsafe {
                let ptr = sys::JS_GetRuntimeOpaque(self.rt) as *const RefCell<PrivateState>;
                ptr.as_ref()
                    .expect("We already know this runtime is one of ours!")
                    .borrow_mut()
            };
            state.refs -= 1;
            state.refs == 0
        };

        if should_free {
            unsafe {
                let opaque = sys::JS_GetRuntimeOpaque(self.rt);
                sys::JS_RunGC(self.rt);
                sys::JS_FreeRuntime(self.rt);

                if !opaque.is_null() {
                    // Just let the system drop it here.
                    let _ = Box::from_raw(opaque as *mut RefCell<PrivateState>);
                }
            }
        }
    }
}
