use std::{cell::RefCell, rc::Rc};

use llrt_utils::option::{Null, Undefined};
use llrt_utils::primordials::Primordial;
use rquickjs::{
    class::Trace, prelude::This, Class, Ctx, Function, JsLifetime, Object, Promise, Result, Value,
};

use crate::{
    readable::controller::ReadableStreamControllerClass,
    transform::{
        controller::TransformStreamDefaultControllerClass,
        stream::{self as transform_stream, TransformStreamClass},
    },
    utils::promise::{promise_resolved_with, PromisePrimordials},
};

use super::tee::TeeState;

#[derive(Clone)]
pub enum StartAlgorithm<'js> {
    ReturnUndefined,
    Function {
        f: Function<'js>,
        underlying_source: Null<Undefined<Object<'js>>>,
    },
}

impl<'js> StartAlgorithm<'js> {
    pub(crate) fn call(
        &self,
        ctx: Ctx<'js>,
        controller: ReadableStreamControllerClass<'js>,
    ) -> Result<Value<'js>> {
        match self {
            StartAlgorithm::ReturnUndefined => Ok(Value::new_undefined(ctx.clone())),
            StartAlgorithm::Function {
                f,
                underlying_source,
            } => f.call::<_, Value>((This(underlying_source.clone()), controller)),
        }
    }
}

type PullRustFn<'js> =
    Box<dyn Fn(Ctx<'js>, ReadableStreamControllerClass<'js>) -> Result<Promise<'js>> + 'js>;

#[allow(private_interfaces)]
#[derive(Clone)]
pub enum PullAlgorithm<'js> {
    ReturnPromiseUndefined,
    Function {
        f: Function<'js>,
        underlying_source: Null<Undefined<Object<'js>>>,
    },
    RustFunction(Rc<PullRustFn<'js>>),
    Tee(Class<'js, TeeState<'js>>),
    Transform(TransformStreamClass<'js>),
}

impl<'js> Trace<'js> for PullAlgorithm<'js> {
    fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) {
        match self {
            Self::ReturnPromiseUndefined => {},
            Self::Function {
                f,
                underlying_source,
            } => {
                f.trace(tracer);
                underlying_source.trace(tracer);
            },
            Self::RustFunction(_) => {},
            Self::Tee(state) => state.trace(tracer),
            Self::Transform(stream) => stream.trace(tracer),
        }
    }
}

unsafe impl<'js> JsLifetime<'js> for PullAlgorithm<'js> {
    type Changed<'to> = PullAlgorithm<'to>;
}

impl<'js> PullAlgorithm<'js> {
    pub fn from_fn(
        f: impl Fn(Ctx<'js>, ReadableStreamControllerClass<'js>) -> Result<Promise<'js>> + 'js,
    ) -> Self {
        Self::RustFunction(Rc::new(Box::new(f)))
    }

    /// Wrap a one-shot pull closure. Subsequent invocations after the first
    /// resolve with `undefined` without calling `f` again — useful for
    /// streams that enqueue their whole payload in one go and then close.
    pub fn from_fn_once(
        f: impl FnOnce(Ctx<'js>, ReadableStreamControllerClass<'js>) -> Result<Promise<'js>> + 'js,
    ) -> Self {
        type OnceSlot<'js> = Rc<
            RefCell<
                Option<
                    Box<
                        dyn FnOnce(
                                Ctx<'js>,
                                ReadableStreamControllerClass<'js>,
                            ) -> Result<Promise<'js>>
                            + 'js,
                    >,
                >,
            >,
        >;
        let slot: OnceSlot<'js> = Rc::new(RefCell::new(Some(Box::new(f))));
        Self::from_fn(move |ctx, ctrl| {
            if let Some(f) = slot.borrow_mut().take() {
                f(ctx, ctrl)
            } else {
                Ok(PromisePrimordials::get(&ctx)?
                    .promise_resolved_with_undefined
                    .clone())
            }
        })
    }

    pub(super) fn from_tee_state(state: Class<'js, TeeState<'js>>) -> Self {
        Self::Tee(state)
    }

    pub(crate) fn call(
        &self,
        ctx: Ctx<'js>,
        promise_primordials: &PromisePrimordials<'js>,
        controller: ReadableStreamControllerClass<'js>,
    ) -> Result<Promise<'js>> {
        match self {
            PullAlgorithm::ReturnPromiseUndefined => {
                Ok(promise_primordials.promise_resolved_with_undefined.clone())
            },
            PullAlgorithm::Function {
                f,
                underlying_source,
            } => promise_resolved_with(
                &ctx,
                promise_primordials,
                f.call::<_, Value>((This(underlying_source.clone()), controller)),
            ),
            PullAlgorithm::RustFunction(f) => f(ctx, controller),
            PullAlgorithm::Tee(state) => {
                crate::readable::stream::tee::tee_pull_algorithm(ctx, state.clone())
            },
            PullAlgorithm::Transform(stream) => {
                transform_stream::source_pull_algorithm(ctx, stream)
            },
        }
    }
}

type CancelRustFn<'js> = Box<dyn FnOnce(Value<'js>) -> Result<Promise<'js>> + 'js>;

#[allow(private_interfaces)]
pub enum CancelAlgorithm<'js> {
    ReturnPromiseUndefined,
    Function {
        f: Function<'js>,
        underlying_source: Null<Undefined<Object<'js>>>,
    },
    RustFunction(Rc<RefCell<Option<CancelRustFn<'js>>>>),
    Tee1(Class<'js, TeeState<'js>>),
    Tee2(Class<'js, TeeState<'js>>),
    Transform {
        stream: TransformStreamClass<'js>,
        controller: TransformStreamDefaultControllerClass<'js>,
    },
}

impl<'js> Clone for CancelAlgorithm<'js> {
    fn clone(&self) -> Self {
        match self {
            Self::ReturnPromiseUndefined => Self::ReturnPromiseUndefined,
            Self::Function {
                f,
                underlying_source,
            } => Self::Function {
                f: f.clone(),
                underlying_source: underlying_source.clone(),
            },
            Self::RustFunction(rc) => Self::RustFunction(rc.clone()),
            Self::Tee1(state) => Self::Tee1(state.clone()),
            Self::Tee2(state) => Self::Tee2(state.clone()),
            Self::Transform { stream, controller } => Self::Transform {
                stream: stream.clone(),
                controller: controller.clone(),
            },
        }
    }
}

impl<'js> Trace<'js> for CancelAlgorithm<'js> {
    fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) {
        match self {
            Self::ReturnPromiseUndefined => {},
            Self::Function {
                f,
                underlying_source,
            } => {
                f.trace(tracer);
                underlying_source.trace(tracer);
            },
            Self::RustFunction(_) => {},
            Self::Tee1(state) | Self::Tee2(state) => state.trace(tracer),
            Self::Transform { stream, controller } => {
                stream.trace(tracer);
                controller.trace(tracer);
            },
        }
    }
}

unsafe impl<'js> JsLifetime<'js> for CancelAlgorithm<'js> {
    type Changed<'to> = CancelAlgorithm<'to>;
}

impl<'js> CancelAlgorithm<'js> {
    pub fn from_fn(f: impl FnOnce(Value<'js>) -> Result<Promise<'js>> + 'js) -> Self {
        Self::RustFunction(Rc::new(RefCell::new(Some(Box::new(f)))))
    }

    pub(super) fn from_tee_state_1(state: Class<'js, TeeState<'js>>) -> Self {
        Self::Tee1(state)
    }

    pub(super) fn from_tee_state_2(state: Class<'js, TeeState<'js>>) -> Self {
        Self::Tee2(state)
    }

    pub(crate) fn call(
        &self,
        ctx: Ctx<'js>,
        promise_primordials: &PromisePrimordials<'js>,
        reason: Value<'js>,
    ) -> Result<Promise<'js>> {
        match self {
            CancelAlgorithm::ReturnPromiseUndefined => {
                Ok(promise_primordials.promise_resolved_with_undefined.clone())
            },
            CancelAlgorithm::Function {
                f,
                underlying_source,
            } => {
                let result: Result<Value> = f.call((This(underlying_source.clone()), reason));
                promise_resolved_with(&ctx, promise_primordials, result)
            },
            CancelAlgorithm::RustFunction(f) => {
                let f = f
                    .borrow_mut()
                    .take()
                    .expect("cancel algorithm must only be called once");
                f(reason)
            },
            CancelAlgorithm::Tee1(state) => {
                crate::readable::stream::tee::tee_cancel_algorithm(ctx, state.clone(), reason, 0)
            },
            CancelAlgorithm::Tee2(state) => {
                crate::readable::stream::tee::tee_cancel_algorithm(ctx, state.clone(), reason, 1)
            },
            CancelAlgorithm::Transform { stream, controller } => {
                transform_stream::source_cancel_algorithm(ctx, stream, controller, reason)
            },
        }
    }
}
