//! Types related to component inputs.
//!
//! NOTE: There is an open issue that changes how component inputs work.
//!  `Remove __afia__$allocate` https://github.com/afia/afia/issues/2519
//!  The current plan is that WebAssembly imports such as `fn _afia_get_color_inputs()` will be used
//!  to retrieve component inputs.
//!  So, it's possible that `ComponentInputs` ends up getting deleted and then the behavior gets
//!  moved into `ComponentImports`. I have not thought about this. For now I am just implementing
//!  abstractions on top of the current system (where we have an `inputs_ptr: *const u8`, while
//!  still trying to ensure that the pieces of the design will be easy to move around to other
//!  places if needed.
//!  See the `Components` chapter in the internal book for more information about component inputs.

use std::ops::RangeInclusive;
use std::ptr;

use crate::_macro::concat_byte_slices;
use crate::{ComponentOutputArgs, FromComponentOutputArgs};

pub mod mock;

/// Used to retrieve the component's inputs.
pub struct ComponentInputs {
    /// A pointer to the component's imports.
    pub(crate) ctx: *mut std::ffi::c_void,
    /// Clone the imports.
    pub(crate) clone_ctx: fn(imports: *mut std::ffi::c_void) -> *mut std::ffi::c_void,
    /// Free the imports.
    pub(crate) free_ctx: fn(imports: *mut std::ffi::c_void),

    /// Get one of the component's text inputs.
    pub(crate) text: fn(imports: *mut std::ffi::c_void, input_id: TextInputId) -> String,
}

impl Drop for ComponentInputs {
    fn drop(&mut self) {
        (self.free_ctx)(self.ctx)
    }
}

impl Clone for ComponentInputs {
    fn clone(&self) -> Self {
        let imports_cloned = (self.clone_ctx)(self.ctx);

        Self {
            ctx: imports_cloned,
            clone_ctx: self.clone_ctx,
            free_ctx: self.free_ctx,

            text: self.text,
        }
    }
}

impl FromComponentOutputArgs for ComponentInputs {
    fn from_args(_args: ComponentOutputArgs) -> Self {
        Self::new_ffi()
    }
}

/// Describes one of a component's inputs.
pub struct ComponentInput {
    /// The input's ID.
    pub input_id: u16,
    /// The input's type, such as Text or Color.
    pub ty: ValueType,
    /// The name for this input.
    pub name: &'static str,
    /// The input's description.
    pub description: &'static str,
}

/// Describes a component input's type, such as Text or Color.
pub enum ValueType {
    /// Describes a textual component input.
    Text {
        /// The minimum and maximum length allowed for values connected to this text input.
        length: RangeInclusive<u32>,
    },
}
impl ValueType {
    // Used by the `define_*_input` macros when encoding the input descriptor to binary.
    #[doc(hidden)]
    pub const fn encode_text_bytes(&self) -> [u8; 10] {
        let encoding_version = 0;
        let text_header = 12;

        match self {
            ValueType::Text { length } => concat_byte_slices::<10>(&[
                &[encoding_version, text_header],
                &length.start().to_le_bytes(),
                &length.end().to_le_bytes(),
            ]),
        }
    }
}

/// Identifies one of a component's inputs.
pub trait InputId: sealed::Sealed {
    #[allow(missing_docs)]
    fn to_u16(&self) -> u16;
}
mod sealed {
    // No current reason to allow other crates to implement `InputId`, so we seal it.
    pub trait Sealed {}
}

/// Used to retrieve one of the component's text inputs.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct TextInputId(u16);
impl TextInputId {
    // Used by the `define_text_input` macro.
    #[doc(hidden)]
    pub const fn _private_new(id: u16) -> Self {
        Self(id)
    }
}

impl InputId for TextInputId {
    fn to_u16(&self) -> u16 {
        self.0
    }
}
impl sealed::Sealed for TextInputId {}

impl ComponentInputs {
    /// Create a `ComponentInputs` that can read inputs from the arguments Afia provides.
    fn new_ffi() -> Self {
        Self {
            ctx: ptr::null_mut(),
            clone_ctx: |_| ptr::null_mut(),
            free_ctx: |_| {},
            text: |imports, input_id| {
                let input_id = input_id.0 as i32;
                let text_handle = unsafe { afia_component_sys::get_text_input(imports, input_id) };

                let text_len = unsafe { afia_component_sys::get_text_len(imports, text_handle) };
                if text_len < 0 {
                    todo!("TODO: error handling if `get_text_len` returns negative error code")
                }

                let text_len = text_len as usize;

                let mut text_out = Vec::with_capacity(text_len);
                unsafe {
                    afia_component_sys::copy_text_bytes(
                        imports,
                        text_handle,
                        0,
                        text_len as i32,
                        text_out.as_mut_ptr(),
                    )
                };
                // SAFETY: we just copied `text_len` bytes into the vec.
                unsafe {
                    text_out.set_len(text_len);
                }
                let text = String::from_utf8(text_out).unwrap();

                unsafe { afia_component_sys::free_text_handle(imports, text_handle) };
                text
            },
        }
    }

    /// Get the text input's value.
    pub fn get_text(&self, text_id: TextInputId) -> String {
        (self.text)(self.ctx, text_id)
    }
}

/// Define a text input for the component.
///
/// ```
/// # use afia_component::input::{ComponentInput, TextInputId, ValueType};
/// # use afia_component::define_text_input;
///
/// const MY_TEXT_INPUT: ComponentInput = ComponentInput {
///     input_id: 123,
///     ty: ValueType::Text { length: 0..=40 },
///     name: "My Text",
///     description: "Text that will be shown inside the paragraph.",
/// };
/// const MY_TEXT_INPUT_ID: TextInputId = define_text_input!(MY_TEXT_INPUT);
/// ```
#[macro_export]
macro_rules! define_text_input {
    ($definition:path) => {
        {
            #[cfg_attr(not(target_arch = "wasm32"), expect(unused, reason = "Rust does not consider this as used for some reason."))]
            const BYTE_COUNT: usize =
              // Version byte count
              1
              // ComponentInputID bytes
              + 2
              // ValueType::Text bytes (version byte, text variant byte, min u32, max u32)
              + 10
              // Name len byte + name bytes
              + 1 + $definition.name.as_bytes().len()
              // Description len bytes + description bytes
              + 2 + $definition.description.as_bytes().len()
            ;

            #[cfg_attr(target_arch = "wasm32", used)]
            #[cfg_attr(target_arch = "wasm32", unsafe(link_section = "__afia__$input_descriptors"))]
            static _AFIA_INPUT_DESCRIPTOR: [u8; BYTE_COUNT] =
                afia_component::_macro::concat_byte_slices(&[
                    &[0],
                    &$definition.input_id.to_le_bytes(),
                    &$definition.ty.encode_text_bytes(),
                    &[$definition.name.len() as u8],
                    $definition.name.as_bytes(),
                    &($definition.description.len() as u16).to_le_bytes(),
                    $definition.description.as_bytes(),
                ]);

            const _ASSERT_IS_COMPONENT_INPUT: $crate::input::ComponentInput = $definition;

            const _ASSERT_INPUT_TYPE_IS_TEXT: () = {
                assert!(matches!($definition.ty, $crate::input::ValueType::Text { .. }));
            };

            $crate::input::TextInputId::_private_new($definition.input_id)
        }
    };
}
