use crate::{ContextRef, ValueRef, ValueResult};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::Sender;

#[derive(Eq, PartialEq, Hash, Debug, Clone, Copy)]
pub(crate) struct PromiseHandle(u64);

impl PromiseHandle {
    pub fn new() -> Self {
        static NEXT_ID: AtomicU64 = AtomicU64::new(0);
        PromiseHandle(NEXT_ID.fetch_add(1, Ordering::SeqCst))
    }
}

pub type PromiseResult = Box<dyn FnOnce(&ContextRef) -> ValueResult + Send + 'static>;

pub(crate) enum PromiseEvent {
    Resolved(PromiseResult),
    Rejected(PromiseResult),
}

/// A Promise is a small, thread-safe marker which represents a pending
/// promise inside the JS runtime. This is how you complete the promise: you
/// must either call `resolve` or `reject` before you drop the promise. If
/// you drop the promise without calling either `resolve` or `reject` then it
/// will panic.
///
/// In order to actually *process* the completion, someone must call
/// process_all_jobs on a context or the underlying runtime itself.
#[derive(Debug)]
pub struct Promise {
    complete: bool,
    handle: PromiseHandle,
    channel: Sender<(PromiseHandle, PromiseEvent)>,
}

impl Promise {
    pub(crate) fn new(
        handle: PromiseHandle,
        channel: Sender<(PromiseHandle, PromiseEvent)>,
    ) -> Self {
        Promise {
            complete: false,
            handle,
            channel,
        }
    }

    pub fn resolve<T>(mut self, value: T)
    where
        T: FnOnce(&ContextRef) -> ValueResult + Send + 'static,
    {
        let _ = self
            .channel
            .send((self.handle, PromiseEvent::Resolved(Box::new(value))));
        self.complete = true;
    }

    pub fn reject<T>(mut self, value: T)
    where
        T: FnOnce(&ContextRef) -> ValueResult + Send + 'static,
    {
        let _ = self
            .channel
            .send((self.handle, PromiseEvent::Rejected(Box::new(value))));
        self.complete = true;
    }
}

impl Drop for Promise {
    fn drop(&mut self) {
        assert!(self.complete);
    }
}

pub trait RejectedPromiseTracker {
    fn on_rejected_promise(
        &self,
        ctx: &ContextRef,
        promise: &ValueRef,
        reason: &ValueRef,
        is_handled: bool,
    );
}

impl<T> RejectedPromiseTracker for T
where
    T: Fn(&ContextRef, &ValueRef, &ValueRef, bool) -> (),
{
    fn on_rejected_promise(
        &self,
        ctx: &ContextRef,
        promise: &ValueRef,
        reason: &ValueRef,
        is_handled: bool,
    ) {
        self(ctx, promise, reason, is_handled);
    }
}

pub struct DefaultRejectedPromiseTracker {}

impl DefaultRejectedPromiseTracker {
    pub fn new() -> Self {
        DefaultRejectedPromiseTracker {}
    }
}

impl RejectedPromiseTracker for DefaultRejectedPromiseTracker {
    fn on_rejected_promise(
        &self,
        ctx: &ContextRef,
        _promise: &ValueRef,
        reason: &ValueRef,
        is_handled: bool,
    ) {
        if !is_handled {
            let reason_str = reason.to_string(ctx).expect(
                "Unhandled rejected promise: reason unknown: unable to convert reason to string",
            );
            panic!("Unhandled rejected promise: {reason_str}");
        }
    }
}
