/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::cell::Cell;
use std::rc::Rc;

use dom_struct::dom_struct;
use js::jsapi::{HandleValueArray, Heap, NewArrayObject, Value};
use js::jsval::{ObjectValue, UndefinedValue};
use js::rust::HandleValue as SafeHandleValue;

use super::bindings::root::{DomRoot, MutNullableDom};
use super::types::{ReadableStream, ReadableStreamDefaultReader};
use crate::dom::bindings::error::Error;
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
use crate::dom::bindings::root::Dom;
use crate::dom::defaultteereadrequest::DefaultTeeReadRequest;
use crate::dom::globalscope::GlobalScope;
use crate::dom::promise::Promise;
use crate::dom::readablestreamdefaultreader::ReadRequest;
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};

#[derive(JSTraceable, MallocSizeOf)]
pub(crate) enum TeeCancelAlgorithm {
    Cancel1Algorithm,
    Cancel2Algorithm,
}

#[dom_struct]
/// <https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaulttee>
pub(crate) struct DefaultTeeUnderlyingSource {
    reflector_: Reflector,
    reader: Dom<ReadableStreamDefaultReader>,
    stream: Dom<ReadableStream>,
    branch_1: MutNullableDom<ReadableStream>,
    branch_2: MutNullableDom<ReadableStream>,
    #[ignore_malloc_size_of = "Rc"]
    reading: Rc<Cell<bool>>,
    #[ignore_malloc_size_of = "Rc"]
    read_again: Rc<Cell<bool>>,
    #[ignore_malloc_size_of = "Rc"]
    canceled_1: Rc<Cell<bool>>,
    #[ignore_malloc_size_of = "Rc"]
    canceled_2: Rc<Cell<bool>>,
    #[ignore_malloc_size_of = "Rc"]
    clone_for_branch_2: Rc<Cell<bool>>,
    #[ignore_malloc_size_of = "Rc"]
    #[allow(clippy::redundant_allocation)]
    reason_1: Rc<Box<Heap<Value>>>,
    #[ignore_malloc_size_of = "Rc"]
    #[allow(clippy::redundant_allocation)]
    reason_2: Rc<Box<Heap<Value>>>,
    #[ignore_malloc_size_of = "Rc"]
    cancel_promise: Rc<Promise>,
    tee_cancel_algorithm: TeeCancelAlgorithm,
}

impl DefaultTeeUnderlyingSource {
    #[allow(clippy::too_many_arguments)]
    #[allow(clippy::redundant_allocation)]
    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
    pub(crate) fn new(
        reader: &ReadableStreamDefaultReader,
        stream: &ReadableStream,
        reading: Rc<Cell<bool>>,
        read_again: Rc<Cell<bool>>,
        canceled_1: Rc<Cell<bool>>,
        canceled_2: Rc<Cell<bool>>,
        clone_for_branch_2: Rc<Cell<bool>>,
        reason_1: Rc<Box<Heap<Value>>>,
        reason_2: Rc<Box<Heap<Value>>>,
        cancel_promise: Rc<Promise>,
        tee_cancel_algorithm: TeeCancelAlgorithm,
        can_gc: CanGc,
    ) -> DomRoot<DefaultTeeUnderlyingSource> {
        reflect_dom_object(
            Box::new(DefaultTeeUnderlyingSource {
                reflector_: Reflector::new(),
                reader: Dom::from_ref(reader),
                stream: Dom::from_ref(stream),
                branch_1: MutNullableDom::new(None),
                branch_2: MutNullableDom::new(None),
                reading,
                read_again,
                canceled_1,
                canceled_2,
                clone_for_branch_2,
                reason_1,
                reason_2,
                cancel_promise,
                tee_cancel_algorithm,
            }),
            &*stream.global(),
            can_gc,
        )
    }

    pub(crate) fn set_branch_1(&self, stream: &ReadableStream) {
        self.branch_1.set(Some(stream));
    }

    pub(crate) fn set_branch_2(&self, stream: &ReadableStream) {
        self.branch_2.set(Some(stream));
    }

    /// <https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaulttee>
    /// Let pullAlgorithm be the following steps:
    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
    pub(crate) fn pull_algorithm(&self, can_gc: CanGc) -> Rc<Promise> {
        let cx = GlobalScope::get_cx();
        // If reading is true,
        if self.reading.get() {
            // Set readAgain to true.
            self.read_again.set(true);
            // Return a promise resolved with undefined.
            rooted!(in(*cx) let mut rval = UndefinedValue());
            return Promise::new_resolved(&self.stream.global(), cx, rval.handle(), can_gc);
        }

        // Set reading to true.
        self.reading.set(true);

        // Let readRequest be a read request with the following items:
        let tee_read_request = DefaultTeeReadRequest::new(
            &self.stream,
            &self.branch_1.get().expect("Branch 1 should be set."),
            &self.branch_2.get().expect("Branch 2 should be set."),
            self.reading.clone(),
            self.read_again.clone(),
            self.canceled_1.clone(),
            self.canceled_2.clone(),
            self.clone_for_branch_2.clone(),
            self.cancel_promise.clone(),
            self,
            can_gc,
        );

        // Rooting: the tee read request is rooted above.
        let read_request = ReadRequest::DefaultTee {
            tee_read_request: Dom::from_ref(&tee_read_request),
        };

        // Perform ! ReadableStreamDefaultReaderRead(reader, readRequest).
        self.reader.read(cx, &read_request, can_gc);

        // Return a promise resolved with undefined.
        rooted!(in(*cx) let mut rval = UndefinedValue());
        Promise::new_resolved(&self.stream.global(), cx, rval.handle(), can_gc)
    }

    /// <https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaulttee>
    /// Let cancel1Algorithm be the following steps, taking a reason argument
    /// and
    /// Let cancel2Algorithm be the following steps, taking a reason argument
    #[allow(unsafe_code)]
    pub(crate) fn cancel_algorithm(
        &self,
        cx: SafeJSContext,
        global: &GlobalScope,
        reason: SafeHandleValue,
        can_gc: CanGc,
    ) -> Option<Result<Rc<Promise>, Error>> {
        match self.tee_cancel_algorithm {
            TeeCancelAlgorithm::Cancel1Algorithm => {
                // Set canceled_1 to true.
                self.canceled_1.set(true);

                // Set reason_1 to reason.
                self.reason_1.set(reason.get());

                // If canceled_2 is true,
                if self.canceled_2.get() {
                    self.resolve_cancel_promise(cx, global, can_gc);
                }
                // Return cancelPromise.
                Some(Ok(self.cancel_promise.clone()))
            },
            TeeCancelAlgorithm::Cancel2Algorithm => {
                // Set canceled_2 to true.
                self.canceled_2.set(true);

                // Set reason_2 to reason.
                self.reason_2.set(reason.get());

                // If canceled_1 is true,
                if self.canceled_1.get() {
                    self.resolve_cancel_promise(cx, global, can_gc);
                }
                // Return cancelPromise.
                Some(Ok(self.cancel_promise.clone()))
            },
        }
    }

    #[allow(unsafe_code)]
    fn resolve_cancel_promise(&self, cx: SafeJSContext, global: &GlobalScope, can_gc: CanGc) {
        // Let compositeReason be ! CreateArrayFromList(« reason_1, reason_2 »).
        rooted_vec!(let mut reasons_values);
        reasons_values.push(self.reason_1.get());
        reasons_values.push(self.reason_2.get());

        let reasons_values_array = HandleValueArray::from(&reasons_values);
        rooted!(in(*cx) let reasons = unsafe { NewArrayObject(*cx, &reasons_values_array) });
        rooted!(in(*cx) let reasons_value = ObjectValue(reasons.get()));

        // Let cancelResult be ! ReadableStreamCancel(stream, compositeReason).
        let cancel_result = self
            .stream
            .cancel(cx, global, reasons_value.handle(), can_gc);

        // Resolve cancelPromise with cancelResult.
        self.cancel_promise.resolve_native(&cancel_result, can_gc);
    }
}