//! OpenGL command.
use crate::xml::element::{Elements, FromElements};

/// A command.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Command
{
    prototype: Prototype,
    parameters: Vec<Parameter>,
}

impl Command
{
    /// Returns a new `Command`.
    pub fn new(
        prototype: Prototype,
        parameters: impl IntoIterator<Item = Parameter>,
    ) -> Self
    {
        Self {
            prototype,
            parameters: parameters.into_iter().collect(),
        }
    }

    /// Returns the command prototype.
    #[must_use]
    pub fn prototype(&self) -> &Prototype
    {
        &self.prototype
    }

    /// Returns the command parameters.
    #[must_use]
    pub fn parameters(&self) -> &[Parameter]
    {
        &self.parameters
    }
}

impl FromElements for Command
{
    type Error = Error;

    fn from_elements(
        elements: &crate::xml::element::Elements,
    ) -> Result<Self, Self::Error>
    {
        let proto_element = elements
            .get_first_tagged_element("proto")
            .ok_or(Self::Error::MissingPrototype)?;

        let prototype = Prototype::from_elements(proto_element.child_elements())?;

        let parameters = elements
            .get_all_tagged_elements_with_name("param")
            .into_iter()
            .map(|param_element| Parameter::from_elements(param_element.child_elements()))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(Self {
            prototype,
            parameters,
        })
    }
}

/// [`Command`] error.
#[derive(Debug, thiserror::Error)]
pub enum Error
{
    /// No 'proto' element was found.
    #[error("No 'proto' element was found")]
    MissingPrototype,

    /// Invalid prototype.
    #[error("Invalid prototype")]
    InvalidPrototype(#[from] PrototypeError),

    /// Invalid parameter.
    #[error("Invalid parameter")]
    InvalidParameter(#[from] ParameterError),
}

/// A command prototype.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Prototype
{
    name: String,
    return_type: String,
}

impl Prototype
{
    /// Returns a new `Prototype`.
    pub fn new(name: impl Into<String>, return_type: impl Into<String>) -> Self
    {
        Self {
            name: name.into(),
            return_type: return_type.into(),
        }
    }

    /// Returns the command prototype name.
    #[must_use]
    pub fn name(&self) -> &str
    {
        &self.name
    }

    /// Returns the command prototype return type.
    #[must_use]
    pub fn return_type(&self) -> &str
    {
        &self.return_type
    }
}

impl FromElements for Prototype
{
    type Error = PrototypeError;

    fn from_elements(
        elements: &crate::xml::element::Elements,
    ) -> Result<Self, Self::Error>
    {
        let name = elements
            .get_first_tagged_element("name")
            .ok_or(Self::Error::MissingName)?
            .child_elements()
            .get_first_text_element()
            .cloned()
            .unwrap_or_default();

        let return_type = find_type(elements);

        Ok(Self { name, return_type })
    }
}

/// [`Prototype`] error.
#[derive(Debug, thiserror::Error)]
pub enum PrototypeError
{
    /// No 'name' element was found.
    #[error("No 'name' element was found")]
    MissingName,
}

/// A command parameter.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Parameter
{
    name: String,
    ty: String,
}

impl Parameter
{
    /// Returns a new `Parameter`.
    pub fn new(name: impl Into<String>, ty: impl Into<String>) -> Self
    {
        Self {
            name: name.into(),
            ty: ty.into(),
        }
    }

    /// Returns the name of the command parameter.
    #[must_use]
    pub fn name(&self) -> &str
    {
        &self.name
    }

    /// Returns the type of the command parameter.
    #[must_use]
    pub fn get_type(&self) -> &str
    {
        &self.ty
    }
}

impl FromElements for Parameter
{
    type Error = ParameterError;

    fn from_elements(elements: &Elements) -> Result<Self, Self::Error>
    {
        let name = elements
            .get_first_tagged_element("name")
            .ok_or(Self::Error::MissingName)?
            .child_elements()
            .get_first_text_element()
            .cloned()
            .unwrap_or_default();

        let ty = find_type(elements);

        Ok(Self { name, ty })
    }
}

/// [`Parameter`] error.
#[derive(Debug, thiserror::Error)]
pub enum ParameterError
{
    /// No 'name' element was found.
    #[error("No 'name' element was found")]
    MissingName,
}

fn find_type(elements: &Elements) -> String
{
    let text_type_parts = elements
        .get_all_text_elements()
        .into_iter()
        .map(|text_type_part| text_type_part.trim())
        .filter(|text_type_part| !text_type_part.is_empty())
        .collect::<Vec<_>>();

    let opt_ptype_text = get_ptype_text(elements);

    opt_ptype_text.map_or_else(
        || join_space_strs(text_type_parts.iter()),
        |ptype_text| {
            let Some(first_part) = text_type_parts.first() else {
                return ptype_text.clone();
            };

            let before = if *first_part == "const" { "const " } else { "" };

            let after_start_index = usize::from(*first_part == "const");

            format!(
                "{before}{ptype_text} {}",
                text_type_parts
                    .get(after_start_index..)
                    .map(|parts| join_space_strs(parts.iter()))
                    .unwrap_or_default()
            )
        },
    )
}

fn get_ptype_text(elements: &Elements) -> Option<&String>
{
    let ptype_element = elements.get_first_tagged_element("ptype")?;

    ptype_element.child_elements().get_first_text_element()
}

fn join_space_strs<Strings, StrItem>(strings: Strings) -> String
where
    Strings: Iterator<Item = StrItem>,
    StrItem: ToString,
{
    strings
        .into_iter()
        .map(|string| string.to_string())
        .collect::<Vec<_>>()
        .join(" ")
}