//! OpenGL command.
use quick_xml::events::BytesStart;

use crate::deserialization::{
    Deserialize,
    DeserializeWithFn,
    Deserializer,
    DeserializerError,
    IgnoreEnd,
    ResultExt,
};
use crate::DeserializationError;

/// 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 Deserialize for Command
{
    type Error = Error;

    fn deserialize<TDeserializer: Deserializer>(
        start: &BytesStart,
        deserializer: &mut TDeserializer,
    ) -> Result<Self, Self::Error>
    {
        let prototype = deserializer.de_tag::<Prototype>("proto", IgnoreEnd::No)?;

        let parameters = deserializer.de_tag_list::<Parameter>("param")?;

        deserializer
            .skip_to_tag_end(std::str::from_utf8(start.name().as_ref()).unwrap())?;

        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),

    /// Deserialization failed.
    #[error("Deserialization failed")]
    DeserializationFailed(#[from] DeserializationError),
}

impl From<DeserializerError> for Error
{
    fn from(err: DeserializerError) -> Self
    {
        DeserializationError(err).into()
    }
}

/// 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 Deserialize for Prototype
{
    type Error = PrototypeError;

    fn deserialize<TDeserializer: Deserializer>(
        _start: &BytesStart,
        deserializer: &mut TDeserializer,
    ) -> Result<Self, Self::Error>
    {
        let return_type = deserialize_type::<PrototypeError>(deserializer)?;

        let name = deserializer.de_tag_with::<_, _, DeserializeWithFn<_, _, _>>(
            "name",
            IgnoreEnd::No,
            |_, deserializer| deserializer.de_text(),
        )?;

        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,

    /// No return type was found.
    #[error("No return type was found")]
    MissingReturnType,

    /// Deserialization failed.
    #[error("Deserialization failed")]
    DeserializationFailed(#[from] DeserializationError),
}

impl From<DeserializerError> for PrototypeError
{
    fn from(err: DeserializerError) -> Self
    {
        DeserializationError(err).into()
    }
}

/// 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 Deserialize for Parameter
{
    type Error = ParameterError;

    fn deserialize<TDeserializer: Deserializer>(
        _start: &BytesStart,
        deserializer: &mut TDeserializer,
    ) -> Result<Self, Self::Error>
    {
        let ty = deserialize_type::<ParameterError>(deserializer)?;

        let name = deserializer.de_tag_with::<_, _, DeserializeWithFn<_, _, _>>(
            "name",
            IgnoreEnd::No,
            |_, deserializer| deserializer.de_text(),
        )?;

        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,

    /// Deserialization failed.
    #[error("Deserialization failed")]
    DeserializationFailed(#[from] DeserializationError),
}

impl From<DeserializerError> for ParameterError
{
    fn from(err: DeserializerError) -> Self
    {
        DeserializationError(err).into()
    }
}

fn deserialize_type<Err>(deserializer: &mut impl Deserializer) -> Result<String, Err>
where
    Err: From<DeserializerError>,
{
    let type_before = deserializer.de_text().try_event()?;

    let type_ptype = deserializer
        .de_tag_with::<_, _, DeserializeWithFn<_, _, _>>(
            "ptype",
            IgnoreEnd::No,
            |_, deserializer| deserializer.de_text(),
        )
        .try_event()?;

    let type_after = deserializer.de_text().try_event()?;

    let type_before_after = [type_before.clone(), type_after.clone()]
        .into_iter()
        .flatten();

    Ok(type_ptype.map_or_else(
        || join_space_strs(type_before_after),
        |ptype_text| {
            let before = type_before
                .map(|before| format!("{before} "))
                .unwrap_or_default();

            format!(
                "{before}{ptype_text}{}",
                type_after
                    .map(|after| format!(" {after}"))
                    .unwrap_or_default()
            )
        },
    ))
}

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(" ")
}

#[cfg(test)]
mod tests
{
    use pretty_assertions::assert_str_eq;
    use quick_xml::events::Event;
    use ridicule::predicate::{always, eq, function};

    use super::*;
    use crate::deserialization::MockDeserializer;

    #[test]
    fn deserialize_prototype_works_with_ptype()
    {
        let mut mock_deserializer = MockDeserializer::new();

        mock_deserializer
            .expect_de_text()
            .returning(|_| {
                Err(DeserializerError::UnexpectedEvent {
                    expected_event_name: "text".to_string(),
                    found_event: Event::Start(BytesStart::new("ptype")),
                })
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("GLuint".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("ptype"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|deserializer, tag_name, _, func| {
                func(&BytesStart::new(tag_name), deserializer)
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| {
                Err(DeserializerError::UnexpectedEvent {
                    expected_event_name: "text".to_string(),
                    found_event: Event::Start(BytesStart::new("name")),
                })
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("glDoComplicatedThing".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("name"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|deserializer, tag_name, _, func| {
                func(&BytesStart::new(tag_name), deserializer)
            })
            .times(1);

        let prototype =
            Prototype::deserialize(&BytesStart::new("proto"), &mut mock_deserializer)
                .expect("Expected Ok");

        assert_str_eq!(prototype.name, "glDoComplicatedThing");
        assert_str_eq!(prototype.return_type, "GLuint");
    }

    #[test]
    fn deserialize_prototype_works_with_text()
    {
        let mut mock_deserializer = MockDeserializer::new();

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("void".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("ptype"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|_, _, _, _| {
                Err(DeserializerError::UnexpectedEvent {
                    expected_event_name: "start".to_string(),
                    found_event: Event::Start(BytesStart::new("name")),
                })
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| {
                Err(DeserializerError::UnexpectedEvent {
                    expected_event_name: "text".to_string(),
                    found_event: Event::Start(BytesStart::new("name")),
                })
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("glDoSomeThing".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("name"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|deserializer, tag_name, _, func| {
                func(&BytesStart::new(tag_name), deserializer)
            })
            .times(1);

        let prototype =
            Prototype::deserialize(&BytesStart::new("proto"), &mut mock_deserializer)
                .expect("Expected Ok");

        assert_str_eq!(prototype.name, "glDoSomeThing");
        assert_str_eq!(prototype.return_type, "void");
    }

    #[test]
    fn deserialize_parameter_works_with_ptype_only()
    {
        let mut mock_deserializer = MockDeserializer::new();

        mock_deserializer
            .expect_de_text()
            .returning(|_| {
                Err(DeserializerError::UnexpectedEvent {
                    expected_event_name: "text".to_string(),
                    found_event: Event::Start(BytesStart::new("ptype")),
                })
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("GLenum".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("ptype"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|deserializer, tag_name, _, func| {
                func(&BytesStart::new(tag_name), deserializer)
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| {
                Err(DeserializerError::UnexpectedEvent {
                    expected_event_name: "text".to_string(),
                    found_event: Event::Start(BytesStart::new("name")),
                })
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("value".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("name"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|deserializer, tag_name, _, func| {
                func(&BytesStart::new(tag_name), deserializer)
            })
            .times(1);

        let parameter =
            Parameter::deserialize(&BytesStart::new("param"), &mut mock_deserializer)
                .expect("Expected Ok");

        assert_str_eq!(parameter.name, "value");
        assert_str_eq!(parameter.ty, "GLenum");
    }

    #[test]
    fn deserialize_parameter_works_with_ptype_and_text_after()
    {
        let mut mock_deserializer = MockDeserializer::new();

        mock_deserializer
            .expect_de_text()
            .returning(|_| {
                Err(DeserializerError::UnexpectedEvent {
                    expected_event_name: "text".to_string(),
                    found_event: Event::Start(BytesStart::new("ptype")),
                })
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("GLchar".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("ptype"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|deserializer, tag_name, _, func| {
                func(&BytesStart::new(tag_name), deserializer)
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("*".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("source".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("name"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|deserializer, tag_name, _, func| {
                func(&BytesStart::new(tag_name), deserializer)
            })
            .times(1);

        let parameter =
            Parameter::deserialize(&BytesStart::new("param"), &mut mock_deserializer)
                .expect("Expected Ok");

        assert_str_eq!(parameter.name, "source");
        assert_str_eq!(parameter.ty, "GLchar *");
    }

    #[test]
    fn deserialize_parameter_works_with_ptype_and_text_before_and_after()
    {
        let mut mock_deserializer = MockDeserializer::new();

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("const".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("GLchar".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("ptype"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|deserializer, tag_name, _, func| {
                func(&BytesStart::new(tag_name), deserializer)
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("*".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("name".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("name"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|deserializer, tag_name, _, func| {
                func(&BytesStart::new(tag_name), deserializer)
            })
            .times(1);

        let parameter =
            Parameter::deserialize(&BytesStart::new("param"), &mut mock_deserializer)
                .expect("Expected Ok");

        assert_str_eq!(parameter.name, "name");
        assert_str_eq!(parameter.ty, "const GLchar *");
    }

    #[test]
    fn deserialize_parameter_works_with_text()
    {
        let mut mock_deserializer = MockDeserializer::new();

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("void *".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("ptype"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|_, _, _, _| {
                Err(DeserializerError::UnexpectedEvent {
                    expected_event_name: "start".to_string(),
                    found_event: Event::Start(BytesStart::new("name")),
                })
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| {
                Err(DeserializerError::UnexpectedEvent {
                    expected_event_name: "text".to_string(),
                    found_event: Event::Start(BytesStart::new("name")),
                })
            })
            .times(1);

        mock_deserializer
            .expect_de_text()
            .returning(|_| Ok("pixels".to_string()))
            .times(1);

        mock_deserializer
            .expect_de_tag_with::<String, DeserializerError, DeserializeWithFn<_, _, _>>()
            .with(
                eq("name"),
                function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)),
                always(),
            )
            .returning(|deserializer, tag_name, _, func| {
                func(&BytesStart::new(tag_name), deserializer)
            })
            .times(1);

        let parameter =
            Parameter::deserialize(&BytesStart::new("param"), &mut mock_deserializer)
                .expect("Expected Ok");

        assert_str_eq!(parameter.name, "pixels");
        assert_str_eq!(parameter.ty, "void *");
    }
}