//! Types related to DOM nodes.
//!
//! ## MDN Documentation
//! https://developer.mozilla.org/en-US/docs/Web/API/Node

use std::sync::Arc;

use afia_component_sys::{node_type, parent_node};

use crate::dom::element::DomElement;
use crate::ComponentImports;

/// A DOM node.
///
/// ## MDN Documentation
/// https://developer.mozilla.org/en-US/docs/Web/API/Node
#[derive(Clone)]
pub struct DomNode {
    inner: Arc<DomNodeInner>,
}
struct DomNodeInner {
    handle: i64,
    imports: ComponentImports,
}

/// Behavior related to DOM nodes.
pub trait AsDomNode: impls::Sealed {
    #[doc(hidden)]
    fn handle(&self) -> i64;

    #[doc(hidden)]
    fn imports(&self) -> &ComponentImports;
    #[doc(hidden)]
    fn imports_ptr(&self) -> *const std::ffi::c_void {
        self.imports().component_imports_ptr
    }

    /// Convert this type into a [`DomNode`].
    fn to_dom_node(&self) -> DomNode {
        DomNode {
            inner: Arc::new(DomNodeInner {
                handle: self.handle(),
                imports: self.imports().clone(),
            }),
        }
    }

    /// Get the Node's type.
    fn node_type(&self) -> NodeType {
        let node_type = unsafe { node_type(self.imports_ptr(), self.handle()) };
        NodeType::from(node_type)
    }

    /// Get the Node's parent.
    fn parent_node(&self) -> Option<DomNode> {
        let parent = unsafe { parent_node(self.imports_ptr(), self.handle()) };
        maybe_node(self.imports(), parent)
    }
}

/// A DOM node's type
///
/// ## MDN Documentation
/// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
#[allow(missing_docs)]
pub enum NodeType {
    Element = 1,
    Text = 3,
}

impl From<i32> for NodeType {
    fn from(value: i32) -> Self {
        match value {
            1 => Self::Element,
            3 => Self::Text,
            _ => unimplemented!(),
        }
    }
}

impl DomNode {
    pub(crate) fn new_maybe(imports: &ComponentImports, node_handle: i64) -> Option<DomNode> {
        maybe_node(imports, node_handle)
    }

    /// Checks if this node is an element and, if so, returns the element.
    pub fn to_element(&self) -> Option<DomElement> {
        if matches!(self.node_type(), NodeType::Element) {
            let elem =
                DomElement::maybe_new(self.inner.handle, self.inner.imports.clone()).unwrap();
            Some(elem)
        } else {
            None
        }
    }
}

fn maybe_node(imports: &ComponentImports, node_handle: i64) -> Option<DomNode> {
    if node_handle == 0 {
        None
    } else {
        Some(DomNode {
            inner: Arc::new(DomNodeInner {
                imports: imports.clone(),
                handle: node_handle,
            }),
        })
    }
}

mod impls {
    use super::*;
    use crate::dom::element::DomElement;
    use crate::dom::text::DomText;

    pub trait Sealed {}

    impl AsDomNode for DomNode {
        fn handle(&self) -> i64 {
            self.inner.handle
        }

        fn imports(&self) -> &ComponentImports {
            &self.inner.imports
        }
    }
    impl Sealed for DomNode {}

    impl AsDomNode for DomElement {
        fn handle(&self) -> i64 {
            self.to_i64()
        }

        fn imports(&self) -> &ComponentImports {
            self.component_imports()
        }
    }
    impl Sealed for DomElement {}

    impl AsDomNode for DomText {
        fn handle(&self) -> i64 {
            self.to_i64()
        }

        fn imports(&self) -> &ComponentImports {
            self.component_imports()
        }
    }
    impl Sealed for DomText {}
}
