//! See [`EmulatedImports`] for documentation.

use std::sync::Arc;
use std::{ffi, mem};

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

/// Used to emulate the imports that a component calls.
///
/// One use case is for testing components.
/// During tests a component might use the [`ComponentImports`] from
/// [`EmulatedImports::component_imports`], whereas in when running in the Afia runtime it would
/// use [`ComponentImports::new_dynamically_linked`].
pub struct EmulatedImports {
    emulated_imports_ptr: Arc<EmulatedImportsPtr>,
}

pub(crate) struct EmulatedImportsPtr(*mut std::ffi::c_void);

impl Drop for EmulatedImportsPtr {
    fn drop(&mut self) {
        unsafe { afia_component_sys::free_emulated_imports(self.0) }
    }
}

impl EmulatedImports {
    /// Create a new `EmulatedImports`.
    pub fn new() -> Self {
        let imports = unsafe { afia_component_sys::new_emulated_imports() };
        Self {
            emulated_imports_ptr: Arc::new(EmulatedImportsPtr(imports)),
        }
    }

    /// Create a [`ComponentImports`] that uses this `EmulatedImports`.
    ///
    /// After the `EmulatedImports` instance has been dropped it is undefined behavior to use this
    /// [`ComponentImports`] instance, or any objects that were creating from this
    /// `ComponentImports` instance.
    pub fn component_imports(&self) -> ComponentImports {
        ComponentImports {
            component_imports_ptr: self.emulated_imports_ptr(),
            created_via_emulated_imports: Some(self.emulated_imports_ptr.clone()),
        }
    }

    /// Get the DOM as a string.
    ///
    /// TODO: Delete this and instead create an `__afia__$dom$outer_html` method that a component
    ///  can use to retrieve an element's outer HTML.
    ///  Benefit is that users' test and implementation code will be able to use `outer_html`,
    ///  whereas [`EmulatedImports::outer_html`] is meant to be used during tests and is not designed
    ///  to be used by components that are running in a real website.
    ///  In summary, add an `ImportKind::OuterHtml` and `crates/site-component-tests mod dom` test
    ///  case for `outer_html`, then delete this [`EmulatedImports::outer_html`].
    pub fn outer_html(&self, element: DomElement) -> String {
        let outer_html = unsafe {
            afia_component_sys::emulated_imports_dom_outer_html(
                self.emulated_imports_ptr(),
                element.to_i64(),
            )
        };
        let outer_html_str = std::str::from_utf8(unsafe {
            std::slice::from_raw_parts(outer_html.pointer, outer_html.len)
        })
        .unwrap();
        let outer_html_string = outer_html_str.to_string();

        unsafe { afia_component_sys::free_allocated_string(outer_html) };

        outer_html_string
    }

    /// After using [`DomElement::add_event_listener_with_callback`] to add an event listener, the event handler
    /// will call this function that is passed into [`EmulatedImports::set_handle_events`].
    pub fn set_handle_events<T>(
        &self,
        handle_event: extern "C" fn(event_handle: i64, ctx: *mut T),
    ) {
        // SAFETY: TODO think through whether this cast is safe.
        let handle_event: extern "C" fn(event_handle: i64, ctx: *mut ffi::c_void) =
            unsafe { mem::transmute(handle_event) };

        unsafe {
            afia_component_sys::emulated_imports_set_event_handler(
                self.emulated_imports_ptr(),
                handle_event,
            )
        }
    }

    fn emulated_imports_ptr(&self) -> *mut std::ffi::c_void {
        self.emulated_imports_ptr.0
    }
}
