//! Types related to the DOM elements.

use std::slice;
use std::sync::Arc;

use crate::dom::node::AsDomNode;
use crate::dom::node_list::NodeList;
use crate::input::InputId;
use crate::ComponentImports;

mod child;

/// A DOM element.
#[derive(Clone)]
pub struct DomElement {
    inner: Arc<DomElementInner>,
}
struct DomElementInner {
    element: i64,
    component_imports: ComponentImports,
}
impl DomElement {
    /// If the handle is `0`, returns `None`.
    /// Otherwise, returns a `DomElement`.
    pub fn maybe_new(handle: i64, component_imports: ComponentImports) -> Option<Self> {
        if handle == 0 {
            return None;
        }

        Some(Self {
            inner: Arc::new(DomElementInner {
                element: handle,
                component_imports,
            }),
        })
    }

    /// Will be deleted once we stop using it.
    pub fn temporary_way_to_get_i64(&self) -> i64 {
        self.to_i64()
    }

    pub(crate) fn component_imports(&self) -> &ComponentImports {
        &self.inner.component_imports
    }

    pub(crate) fn component_imports_ptr(&self) -> *const std::ffi::c_void {
        self.inner.component_imports.component_imports_ptr
    }

    pub(crate) fn to_i64(&self) -> i64 {
        self.inner.element
    }

    /// Adding this because some of our older components are using older patterns and this will help
    ///  with migrating them towards using this `afia-component` crate.
    ///  DO NOT use this. It was introduced in the middle of a huge PR to help avoid needing to
    ///  refctor everything at once.
    pub fn do_not_use_this_for_new_stuff_temporary_public_way_to_create_using_existing_i64_handle(
        &self,
        element_handle: i64,
    ) -> Option<Self> {
        Self::maybe_new(element_handle, self.inner.component_imports.clone())
    }

    /// Indicates that this element is related to the given component input id.
    ///
    /// Imagine that this element might be a div whose background color is controlled by a
    /// built-in `Color` component that is connected input 100.
    ///
    /// Associating this element with input 100 will ensure that users click on this element in the
    /// site editor, a floating window will appear a color picker to edit the color.
    ///
    /// This method can be called multiple times to associate the element with multiple inputs.
    pub fn associate_component_input(&self, input_id: impl InputId) {
        unsafe {
            afia_component_sys::associate_element_with_component_input(
                self.inner.component_imports.pointer(),
                self.inner.element,
                input_id.to_u16() as i32,
            )
        }
    }

    /// Set the text content of this element.
    pub fn set_text_content(&self, text: &str) {
        unsafe {
            afia_component_sys::dom_element_set_text_content(
                self.inner.component_imports.pointer(),
                self.inner.element,
                text.as_ptr(),
                text.len(),
            )
        }
    }

    /// Get this element's previous element sibling.
    ///
    /// ## MDN Documentation
    /// https://developer.mozilla.org/en-US/docs/Web/API/Element/previousElementSibling
    pub fn previous_element_sibling(&self) -> Option<DomElement> {
        let sibling = unsafe {
            afia_component_sys::dom_element_previous_element_sibling(
                self.imports_ptr(),
                self.handle(),
            )
        };

        DomElement::maybe_new(sibling, self.inner.component_imports.clone())
    }

    /// Get this element's next element sibling.
    ///
    /// ## MDN Documentation
    /// https://developer.mozilla.org/en-US/docs/Web/API/Element/nextElementSibling
    pub fn next_element_sibling(&self) -> Option<DomElement> {
        let sibling = unsafe {
            afia_component_sys::dom_element_next_element_sibling(self.imports_ptr(), self.handle())
        };

        DomElement::maybe_new(sibling, self.inner.component_imports.clone())
    }

    /// Get the element's tag name, such as "DIV" or "SPAN".
    ///
    /// ## MDN Documentation
    /// https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName
    pub fn tag_name(&self) -> String {
        // TODO: `20` was chosen arbitrarily. This will fail if the tag is more than 20 chars.
        let mut buf = [0; 20];

        let tag_len = unsafe {
            afia_component_sys::element_tag_name(
                self.imports_ptr(),
                self.handle(),
                buf.as_mut_ptr(),
                buf.len(),
            )
        };
        // TODO: If the buffer is was small try again with a larger buffer.
        assert!(buf.len() as i32 >= tag_len);

        let tag_slice = unsafe { slice::from_raw_parts(buf.as_ptr(), tag_len as usize) };
        let tag = str::from_utf8(tag_slice).unwrap();
        tag.to_string()
    }

    /// Simulate a click on the element.
    ///
    /// ## MDN Documentation
    /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click
    pub fn click(&self) {
        unsafe { afia_component_sys::element_click(self.imports_ptr(), self.handle()) }
    }

    /// Get the element's child nodes.
    ///
    /// ## MDN Documentation
    /// https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes
    pub fn child_nodes(&self) -> NodeList {
        let child_nodes =
            unsafe { afia_component_sys::element_child_nodes(self.imports_ptr(), self.handle()) };
        NodeList::new(child_nodes, self.component_imports().clone())
    }

    /// Get one of the element's attributes.
    ///
    /// ## MDN Documentation
    /// https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute
    pub fn get_attribute(&self, key: &str) -> Option<String> {
        let mut out = [0; 255];
        let result = unsafe {
            afia_component_sys::element_get_attribute(
                self.imports_ptr(),
                self.handle(),
                key.as_ptr(),
                key.len(),
                out.as_mut_ptr(),
                out.len(),
            )
        };
        match result {
            ..-1 => {
                todo!("error handling")
            }
            -1 => None,
            0 => None,
            1.. => {
                let result = result as usize;
                if out.len() < result {
                    todo!("handle scenario where buffer was smaller than the value")
                };
                let value = str::from_utf8(&out[0..result]).unwrap();
                Some(value.to_string())
            }
        }
    }

    /// Get the DOM element's inner text.
    ///
    /// ## MDN Documentation
    /// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText
    pub fn inner_text(&self) -> String {
        let mut out = [0u8; 1000];
        let result = unsafe {
            afia_component_sys::element_inner_text(
                self.imports_ptr(),
                self.handle(),
                out.as_mut_ptr(),
                out.len(),
            )
        };
        match result {
            ..=-1 => {
                todo!("error handling")
            }
            0.. => {
                let result = result as usize;
                if out.len() < result {
                    todo!("handle scenario where buffer was smaller than the value")
                };
                let value = str::from_utf8(&out[0..result]).unwrap();
                value.to_string()
            }
        }
    }
}
impl Drop for DomElementInner {
    fn drop(&mut self) {
        // TODO: Call a function to free the `self.element`
    }
}

impl ComponentImports {
    /// Create a new [`DomElement`].
    pub fn create_element(&self, tag: &str) -> Option<DomElement> {
        let element = unsafe {
            afia_component_sys::create_element(self.component_imports_ptr, tag.as_ptr(), tag.len())
        };

        DomElement::maybe_new(element, self.clone())
    }
}

#[cfg(any(test, feature = "test-utils"))]
mod test_utils {
    use super::*;
    use crate::dom::node::DomNode;

    impl DomElement {
        /// Search the element's children and their children, recursively, for a node that satisfies
        /// the predicate.
        pub fn find_child_node_recursive(
            &self,
            mut predicate: impl FnMut(&DomNode) -> bool,
        ) -> Option<DomNode> {
            return find_inner(self, &mut predicate);

            fn find_inner(
                elem: &DomElement,
                predicate: &mut impl FnMut(&DomNode) -> bool,
            ) -> Option<DomNode> {
                let children = elem.child_nodes();

                'children: for idx in 0.. {
                    let Some(child) = children.item(idx) else {
                        break 'children;
                    };

                    if predicate(&child) {
                        return Some(child);
                    }

                    let Some(child_element) = child.to_element() else {
                        break 'children;
                    };

                    let maybe_matching_node = find_inner(&child_element, predicate);
                    if maybe_matching_node.is_some() {
                        return maybe_matching_node;
                    }
                }

                None
            }
        }

        /// Search the element's children and their children, recursively, for nodes that satisfy
        /// the predicate.
        pub fn filter_child_nodes_recursive(
            &self,
            mut predicate: impl FnMut(&DomNode) -> bool,
        ) -> Vec<DomNode> {
            let mut nodes = Vec::new();
            find_inner(self, &mut nodes, &mut predicate);

            fn find_inner(
                elem: &DomElement,
                found_nodes: &mut Vec<DomNode>,
                predicate: &mut dyn FnMut(&DomNode) -> bool,
            ) {
                let children = elem.child_nodes();

                'children: for idx in 0.. {
                    let Some(child) = children.item(idx) else {
                        break 'children;
                    };

                    if predicate(&child) {
                        found_nodes.push(child.clone());
                    }

                    let Some(element) = child.to_element() else {
                        break 'children;
                    };

                    find_inner(&element, found_nodes, predicate);
                }
            }

            nodes
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Verify that we can find an element's grandchild using [`DomElement::find_child_node_recursive`].
    #[test]
    fn find_grandchild_recursive() {
        let (_emulated, imports) = ComponentImports::new_emulated();

        let parent = imports.create_element("div").unwrap();
        let child = imports.create_element("span").unwrap();
        let grandchild = imports.create_element("label").unwrap();
        grandchild.set_text_content("I am the grandchild.");

        child.append_child(&grandchild);
        parent.append_child(&child);

        let found_grandchild = parent
            .find_child_node_recursive(|node| {
                node.to_element().map(|elem| elem.tag_name()) == Some("LABEL".to_string())
            })
            .unwrap();

        assert_eq!(
            found_grandchild.to_element().unwrap().inner_text(),
            "I am the grandchild."
        );
    }
}
