/* 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 https://mozilla.org/MPL/2.0/. */

use std::cell::Cell;
use std::cmp::Ordering;

use dom_struct::dom_struct;
use html5ever::{LocalName, QualName, local_name, namespace_url, ns};
use style::str::split_html_space_chars;
use stylo_atoms::Atom;

use crate::dom::bindings::codegen::Bindings::HTMLCollectionBinding::HTMLCollectionMethods;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object};
use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
use crate::dom::bindings::str::DOMString;
use crate::dom::bindings::trace::JSTraceable;
use crate::dom::bindings::xmlname::namespace_from_domstring;
use crate::dom::element::Element;
use crate::dom::node::{Node, NodeTraits};
use crate::dom::window::Window;
use crate::script_runtime::CanGc;

pub(crate) trait CollectionFilter: JSTraceable {
    fn filter<'a>(&self, elem: &'a Element, root: &'a Node) -> bool;
}

/// An optional `u32`, using `u32::MAX` to represent None.  It would be nicer
/// just to use `Option<u32>` for this, but that would produce word alignment
/// issues since `Option<u32>` uses 33 bits.
#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
struct OptionU32 {
    bits: u32,
}

impl OptionU32 {
    fn to_option(self) -> Option<u32> {
        if self.bits == u32::MAX {
            None
        } else {
            Some(self.bits)
        }
    }

    fn some(bits: u32) -> OptionU32 {
        assert_ne!(bits, u32::MAX);
        OptionU32 { bits }
    }

    fn none() -> OptionU32 {
        OptionU32 { bits: u32::MAX }
    }
}

#[dom_struct]
pub(crate) struct HTMLCollection {
    reflector_: Reflector,
    root: Dom<Node>,
    #[ignore_malloc_size_of = "Trait object (Box<dyn CollectionFilter>) cannot be sized"]
    filter: Box<dyn CollectionFilter + 'static>,
    // We cache the version of the root node and all its decendents,
    // the length of the collection, and a cursor into the collection.
    // FIXME: make the cached cursor element a weak pointer
    cached_version: Cell<u64>,
    cached_cursor_element: MutNullableDom<Element>,
    cached_cursor_index: Cell<OptionU32>,
    cached_length: Cell<OptionU32>,
}

impl HTMLCollection {
    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
    pub(crate) fn new_inherited(
        root: &Node,
        filter: Box<dyn CollectionFilter + 'static>,
    ) -> HTMLCollection {
        HTMLCollection {
            reflector_: Reflector::new(),
            root: Dom::from_ref(root),
            filter,
            // Default values for the cache
            cached_version: Cell::new(root.inclusive_descendants_version()),
            cached_cursor_element: MutNullableDom::new(None),
            cached_cursor_index: Cell::new(OptionU32::none()),
            cached_length: Cell::new(OptionU32::none()),
        }
    }

    /// Returns a collection which is always empty.
    pub(crate) fn always_empty(window: &Window, root: &Node, can_gc: CanGc) -> DomRoot<Self> {
        #[derive(JSTraceable)]
        struct NoFilter;
        impl CollectionFilter for NoFilter {
            fn filter<'a>(&self, _: &'a Element, _: &'a Node) -> bool {
                false
            }
        }

        Self::new(window, root, Box::new(NoFilter), can_gc)
    }

    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
    pub(crate) fn new(
        window: &Window,
        root: &Node,
        filter: Box<dyn CollectionFilter + 'static>,
        can_gc: CanGc,
    ) -> DomRoot<Self> {
        reflect_dom_object(Box::new(Self::new_inherited(root, filter)), window, can_gc)
    }

    /// Create a new  [`HTMLCollection`] that just filters element using a static function.
    pub(crate) fn new_with_filter_fn(
        window: &Window,
        root: &Node,
        filter_function: fn(&Element, &Node) -> bool,
        can_gc: CanGc,
    ) -> DomRoot<Self> {
        #[derive(JSTraceable, MallocSizeOf)]
        pub(crate) struct StaticFunctionFilter(
            // The function *must* be static so that it never holds references to DOM objects, which
            // would cause issues with garbage collection -- since it isn't traced.
            #[no_trace]
            #[ignore_malloc_size_of = "Static function pointer"]
            fn(&Element, &Node) -> bool,
        );
        impl CollectionFilter for StaticFunctionFilter {
            fn filter(&self, element: &Element, root: &Node) -> bool {
                (self.0)(element, root)
            }
        }
        Self::new(
            window,
            root,
            Box::new(StaticFunctionFilter(filter_function)),
            can_gc,
        )
    }

    pub(crate) fn create(
        window: &Window,
        root: &Node,
        filter: Box<dyn CollectionFilter + 'static>,
        can_gc: CanGc,
    ) -> DomRoot<Self> {
        Self::new(window, root, filter, can_gc)
    }

    fn validate_cache(&self) {
        // Clear the cache if the root version is different from our cached version
        let cached_version = self.cached_version.get();
        let curr_version = self.root.inclusive_descendants_version();
        if curr_version != cached_version {
            // Default values for the cache
            self.cached_version.set(curr_version);
            self.cached_cursor_element.set(None);
            self.cached_length.set(OptionU32::none());
            self.cached_cursor_index.set(OptionU32::none());
        }
    }

    fn set_cached_cursor(
        &self,
        index: u32,
        element: Option<DomRoot<Element>>,
    ) -> Option<DomRoot<Element>> {
        if let Some(element) = element {
            self.cached_cursor_index.set(OptionU32::some(index));
            self.cached_cursor_element.set(Some(&element));
            Some(element)
        } else {
            None
        }
    }

    /// <https://dom.spec.whatwg.org/#concept-getelementsbytagname>
    pub(crate) fn by_qualified_name(
        window: &Window,
        root: &Node,
        qualified_name: LocalName,
        can_gc: CanGc,
    ) -> DomRoot<HTMLCollection> {
        // case 1
        if qualified_name == local_name!("*") {
            #[derive(JSTraceable, MallocSizeOf)]
            struct AllFilter;
            impl CollectionFilter for AllFilter {
                fn filter(&self, _elem: &Element, _root: &Node) -> bool {
                    true
                }
            }
            return HTMLCollection::create(window, root, Box::new(AllFilter), can_gc);
        }

        #[derive(JSTraceable, MallocSizeOf)]
        struct HtmlDocumentFilter {
            #[no_trace]
            qualified_name: LocalName,
            #[no_trace]
            ascii_lower_qualified_name: LocalName,
        }
        impl CollectionFilter for HtmlDocumentFilter {
            fn filter(&self, elem: &Element, root: &Node) -> bool {
                if root.is_in_html_doc() && elem.namespace() == &ns!(html) {
                    // case 2
                    HTMLCollection::match_element(elem, &self.ascii_lower_qualified_name)
                } else {
                    // case 2 and 3
                    HTMLCollection::match_element(elem, &self.qualified_name)
                }
            }
        }

        let filter = HtmlDocumentFilter {
            ascii_lower_qualified_name: qualified_name.to_ascii_lowercase(),
            qualified_name,
        };
        HTMLCollection::create(window, root, Box::new(filter), can_gc)
    }

    fn match_element(elem: &Element, qualified_name: &LocalName) -> bool {
        match elem.prefix().as_ref() {
            None => elem.local_name() == qualified_name,
            Some(prefix) => {
                qualified_name.starts_with(&**prefix) &&
                    qualified_name.find(':') == Some(prefix.len()) &&
                    qualified_name.ends_with(&**elem.local_name())
            },
        }
    }

    pub(crate) fn by_tag_name_ns(
        window: &Window,
        root: &Node,
        tag: DOMString,
        maybe_ns: Option<DOMString>,
        can_gc: CanGc,
    ) -> DomRoot<HTMLCollection> {
        let local = LocalName::from(tag);
        let ns = namespace_from_domstring(maybe_ns);
        let qname = QualName::new(None, ns, local);
        HTMLCollection::by_qual_tag_name(window, root, qname, can_gc)
    }

    pub(crate) fn by_qual_tag_name(
        window: &Window,
        root: &Node,
        qname: QualName,
        can_gc: CanGc,
    ) -> DomRoot<HTMLCollection> {
        #[derive(JSTraceable, MallocSizeOf)]
        struct TagNameNSFilter {
            #[no_trace]
            qname: QualName,
        }
        impl CollectionFilter for TagNameNSFilter {
            fn filter(&self, elem: &Element, _root: &Node) -> bool {
                ((self.qname.ns == namespace_url!("*")) || (self.qname.ns == *elem.namespace())) &&
                    ((self.qname.local == local_name!("*")) ||
                        (self.qname.local == *elem.local_name()))
            }
        }
        let filter = TagNameNSFilter { qname };
        HTMLCollection::create(window, root, Box::new(filter), can_gc)
    }

    pub(crate) fn by_class_name(
        window: &Window,
        root: &Node,
        classes: DOMString,
        can_gc: CanGc,
    ) -> DomRoot<HTMLCollection> {
        let class_atoms = split_html_space_chars(&classes).map(Atom::from).collect();
        HTMLCollection::by_atomic_class_name(window, root, class_atoms, can_gc)
    }

    pub(crate) fn by_atomic_class_name(
        window: &Window,
        root: &Node,
        classes: Vec<Atom>,
        can_gc: CanGc,
    ) -> DomRoot<HTMLCollection> {
        #[derive(JSTraceable, MallocSizeOf)]
        struct ClassNameFilter {
            #[no_trace]
            classes: Vec<Atom>,
        }
        impl CollectionFilter for ClassNameFilter {
            fn filter(&self, elem: &Element, _root: &Node) -> bool {
                let case_sensitivity = elem
                    .owner_document()
                    .quirks_mode()
                    .classes_and_ids_case_sensitivity();

                self.classes
                    .iter()
                    .all(|class| elem.has_class(class, case_sensitivity))
            }
        }

        if classes.is_empty() {
            return HTMLCollection::always_empty(window, root, can_gc);
        }

        let filter = ClassNameFilter { classes };
        HTMLCollection::create(window, root, Box::new(filter), can_gc)
    }

    pub(crate) fn children(window: &Window, root: &Node, can_gc: CanGc) -> DomRoot<HTMLCollection> {
        HTMLCollection::new_with_filter_fn(
            window,
            root,
            |element, root| root.is_parent_of(element.upcast()),
            can_gc,
        )
    }

    pub(crate) fn elements_iter_after<'a>(
        &'a self,
        after: &'a Node,
    ) -> impl Iterator<Item = DomRoot<Element>> + 'a {
        // Iterate forwards from a node.
        after
            .following_nodes(&self.root)
            .filter_map(DomRoot::downcast)
            .filter(move |element| self.filter.filter(element, &self.root))
    }

    pub(crate) fn elements_iter(&self) -> impl Iterator<Item = DomRoot<Element>> + '_ {
        // Iterate forwards from the root.
        self.elements_iter_after(&self.root)
    }

    pub(crate) fn elements_iter_before<'a>(
        &'a self,
        before: &'a Node,
    ) -> impl Iterator<Item = DomRoot<Element>> + 'a {
        // Iterate backwards from a node.
        before
            .preceding_nodes(&self.root)
            .filter_map(DomRoot::downcast)
            .filter(move |element| self.filter.filter(element, &self.root))
    }

    pub(crate) fn root_node(&self) -> DomRoot<Node> {
        DomRoot::from_ref(&self.root)
    }
}

impl HTMLCollectionMethods<crate::DomTypeHolder> for HTMLCollection {
    /// <https://dom.spec.whatwg.org/#dom-htmlcollection-length>
    fn Length(&self) -> u32 {
        self.validate_cache();

        if let Some(cached_length) = self.cached_length.get().to_option() {
            // Cache hit
            cached_length
        } else {
            // Cache miss, calculate the length
            let length = self.elements_iter().count() as u32;
            self.cached_length.set(OptionU32::some(length));
            length
        }
    }

    /// <https://dom.spec.whatwg.org/#dom-htmlcollection-item>
    fn Item(&self, index: u32) -> Option<DomRoot<Element>> {
        self.validate_cache();

        if let Some(element) = self.cached_cursor_element.get() {
            // Cache hit, the cursor element is set
            if let Some(cached_index) = self.cached_cursor_index.get().to_option() {
                match cached_index.cmp(&index) {
                    Ordering::Equal => {
                        // The cursor is the element we're looking for
                        Some(element)
                    },
                    Ordering::Less => {
                        // The cursor is before the element we're looking for
                        // Iterate forwards, starting at the cursor.
                        let offset = index - (cached_index + 1);
                        let node: DomRoot<Node> = DomRoot::upcast(element);
                        let mut iter = self.elements_iter_after(&node);
                        self.set_cached_cursor(index, iter.nth(offset as usize))
                    },
                    Ordering::Greater => {
                        // The cursor is after the element we're looking for
                        // Iterate backwards, starting at the cursor.
                        let offset = cached_index - (index + 1);
                        let node: DomRoot<Node> = DomRoot::upcast(element);
                        let mut iter = self.elements_iter_before(&node);
                        self.set_cached_cursor(index, iter.nth(offset as usize))
                    },
                }
            } else {
                // Cache miss
                // Iterate forwards through all the nodes
                self.set_cached_cursor(index, self.elements_iter().nth(index as usize))
            }
        } else {
            // Cache miss
            // Iterate forwards through all the nodes
            self.set_cached_cursor(index, self.elements_iter().nth(index as usize))
        }
    }

    /// <https://dom.spec.whatwg.org/#dom-htmlcollection-nameditem>
    fn NamedItem(&self, key: DOMString) -> Option<DomRoot<Element>> {
        // Step 1.
        if key.is_empty() {
            return None;
        }

        let key = Atom::from(key);

        // Step 2.
        self.elements_iter().find(|elem| {
            elem.get_id().is_some_and(|id| id == key) ||
                (elem.namespace() == &ns!(html) && elem.get_name().is_some_and(|id| id == key))
        })
    }

    // https://dom.spec.whatwg.org/#dom-htmlcollection-item
    fn IndexedGetter(&self, index: u32) -> Option<DomRoot<Element>> {
        self.Item(index)
    }

    // check-tidy: no specs after this line
    fn NamedGetter(&self, name: DOMString) -> Option<DomRoot<Element>> {
        self.NamedItem(name)
    }

    // https://dom.spec.whatwg.org/#interface-htmlcollection
    fn SupportedPropertyNames(&self) -> Vec<DOMString> {
        // Step 1
        let mut result = vec![];

        // Step 2
        for elem in self.elements_iter() {
            // Step 2.1
            if let Some(id_atom) = elem.get_id() {
                let id_str = DOMString::from(&*id_atom);
                if !result.contains(&id_str) {
                    result.push(id_str);
                }
            }
            // Step 2.2
            if *elem.namespace() == ns!(html) {
                if let Some(name_atom) = elem.get_name() {
                    let name_str = DOMString::from(&*name_atom);
                    if !result.contains(&name_str) {
                        result.push(name_str)
                    }
                }
            }
        }

        // Step 3
        result
    }
}