use std::num::{ParseFloatError, ParseIntError};

macro_rules! keyword {
    (
        $(#[$attr: meta])*
        $visibility: vis enum $name: ident
        {
            $(
                $(#[keyword(rename = $variant_rename: literal)])?
                $variant: ident,
            )*
        }
    ) => {
        $(#[$attr])*
        $visibility enum $name
        {
            $($variant,)*
        }

        impl $crate::file_format::wavefront::common::Keyword for $name
        {
            fn from_str(
                keyword: &str,
                line_no: usize
            ) -> Result<Keyword, $crate::file_format::wavefront::common::ParsingError>
            {
                use $crate::file_format::wavefront::common::ParsingError;

                match keyword {
                    $(
                        $crate::util::or!(
                            ($($variant_rename)?)
                            else (stringify!($variant))
                        ) => Ok(Keyword::$variant),)*
                    _ => Err(ParsingError::UnknownKeyword {
                        keyword: keyword.to_string(),
                        line_no,
                    }),
                }
            }
        }

        impl std::fmt::Display for $name
        {
            fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
            {
                match self {
                    $(Self::$variant => write!(formatter, stringify!($variant)),)*
                }
            }
        }
    };
}

pub(crate) use keyword;

pub fn parse_statement_line<KeywordT>(
    line: &str,
    line_no: usize,
) -> Result<Option<Statement<KeywordT>>, ParsingError>
where
    KeywordT: Keyword,
{
    if line.is_empty() || line.starts_with('#') {
        return Ok(None);
    }

    let mut parts = line.split(' ');
    let keyword = KeywordT::from_str(parts.next().unwrap(), line_no)?;

    let arguments = parts
        .map(|part| Value::parse::<KeywordT>(part, line_no))
        .collect::<Result<Vec<_>, _>>()?;

    Ok(Some(Statement { keyword, arguments }))
}

#[derive(Debug, thiserror::Error)]
pub enum ParsingError
{
    #[error("Unknown keyword '{keyword}' at line {line_no}")]
    UnknownKeyword
    {
        keyword: String, line_no: usize
    },

    #[error("Invalid float value '{value}'")]
    InvalidFloatValue
    {
        err: ParseFloatError, value: String
    },

    #[error("Invalid integer value '{value}' at line {line_no}")]
    InvalidIntegerValue
    {
        err: ParseIntError,
        value: String,
        line_no: usize,
    },

    #[error("Invalid type of value '{0}'")]
    UnknownValueType(String),

    #[error("Missing triplet value at line {line_no}")]
    MissingTripletValue
    {
        triplet_index: usize,
        line_no: usize,
    },

    #[error("Missing argument to {keyword} at {line_no}")]
    MissingArgument
    {
        keyword: String, line_no: usize
    },

    #[error("Unexpected type of argument {argument_index} at line {line_no}")]
    UnexpectedArgumentType
    {
        argument_index: usize,
        line_no: usize,
    },
}

pub trait Keyword: ToString + Sized
{
    fn from_str(keyword: &str, line_no: usize) -> Result<Self, ParsingError>;
}

type TripletNumber = u32;

#[derive(Debug, Clone)]
pub struct Triplet(
    pub TripletNumber,
    pub Option<TripletNumber>,
    pub Option<TripletNumber>,
);

impl Triplet
{
    fn parse<KeywordT: Keyword>(
        triplet: &str,
        line_no: usize,
    ) -> Result<Self, ParsingError>
    {
        let mut parts = triplet.splitn(3, '/');

        let first = parts
            .next()
            .ok_or(ParsingError::MissingTripletValue { triplet_index: 0, line_no })
            .and_then(|value| {
                value.parse::<TripletNumber>().map_err(|err| {
                    ParsingError::InvalidIntegerValue {
                        err,
                        value: value.to_string(),
                        line_no,
                    }
                })
            })?;

        let second = parts
            .next()
            .ok_or(ParsingError::MissingTripletValue { triplet_index: 0, line_no })
            .and_then(|value| {
                if value.is_empty() {
                    return Ok(None);
                }

                Ok(Some(value.parse::<TripletNumber>().map_err(|err| {
                    ParsingError::InvalidIntegerValue {
                        err,
                        value: value.to_string(),
                        line_no,
                    }
                })?))
            })?;

        let third = parts
            .next()
            .ok_or(ParsingError::MissingTripletValue { triplet_index: 2, line_no })
            .and_then(|value| {
                if value.is_empty() {
                    return Ok(None);
                }

                Ok(Some(value.parse::<TripletNumber>().map_err(|err| {
                    ParsingError::InvalidIntegerValue {
                        err,
                        value: value.to_string(),
                        line_no,
                    }
                })?))
            })?;

        Ok(Self(first, second, third))
    }
}

#[derive(Debug)]
pub enum Value
{
    #[allow(dead_code)]
    Integer(i32),
    Float(f32),
    Triplet(Triplet),
    Text(String),
}

impl Value
{
    fn parse<KeywordT: Keyword>(value: &str, line_no: usize)
        -> Result<Self, ParsingError>
    {
        if value.contains(|character: char| character.is_ascii_digit())
            && value.contains('.')
        {
            return Ok(Self::Float(value.parse().map_err(|err| {
                ParsingError::InvalidFloatValue { err, value: value.to_string() }
            })?));
        }

        if value.contains('/')
            && value
                .chars()
                .all(|character| character.is_ascii_digit() || character == '/')
        {
            return Ok(Self::Triplet(Triplet::parse::<KeywordT>(value, line_no)?));
        }

        if !value.is_empty()
            && value
                .chars()
                .all(|character: char| character.is_ascii_digit())
        {
            return Ok(Self::Integer(value.parse().map_err(|err| {
                ParsingError::InvalidIntegerValue {
                    err,
                    value: value.to_string(),
                    line_no,
                }
            })?));
        }

        if value.is_ascii() {
            return Ok(Self::Text(value.to_string()));
        }

        Err(ParsingError::UnknownValueType(value.to_string()))
    }

    pub fn to_text(
        &self,
        argument_index: usize,
        line_no: usize,
    ) -> Result<&str, ParsingError>
    {
        match self {
            Value::Text(value) => Ok(value),
            _ => Err(ParsingError::UnexpectedArgumentType { argument_index, line_no }),
        }
    }
}

#[derive(Debug)]
pub struct Statement<KeywordT: Keyword>
{
    pub keyword: KeywordT,
    pub arguments: Vec<Value>,
}

impl<KeywordT: Keyword> Statement<KeywordT>
{
    pub fn get_float_arg(&self, index: usize, line_no: usize)
        -> Result<f32, ParsingError>
    {
        match self.arguments.get(index) {
            Some(Value::Float(value)) => Ok(*value),
            Some(_) => Err(ParsingError::UnexpectedArgumentType {
                argument_index: index,
                line_no,
            }),
            None => Err(ParsingError::MissingArgument {
                keyword: self.keyword.to_string(),
                line_no,
            }),
        }
    }

    pub fn get_text_arg(&self, index: usize, line_no: usize)
        -> Result<&str, ParsingError>
    {
        match self.arguments.get(index) {
            Some(Value::Text(value)) => Ok(value),
            Some(_) => Err(ParsingError::UnexpectedArgumentType {
                argument_index: index,
                line_no,
            }),
            None => Err(ParsingError::MissingArgument {
                keyword: self.keyword.to_string(),
                line_no,
            }),
        }
    }

    pub fn get_triplet_arg(
        &self,
        index: usize,
        line_no: usize,
    ) -> Result<Triplet, ParsingError>
    {
        match self.arguments.get(index) {
            Some(Value::Triplet(value)) => Ok(value.clone()),
            Some(_) => Err(ParsingError::UnexpectedArgumentType {
                argument_index: index,
                line_no,
            }),
            None => Err(ParsingError::MissingArgument {
                keyword: self.keyword.to_string(),
                line_no,
            }),
        }
    }
}

#[cfg(test)]
mod tests
{
    use std::fmt::Display;

    use super::{parse_statement_line, Value};
    use crate::file_format::wavefront::common::ParsingError;

    #[derive(Debug, PartialEq, Eq)]
    enum Keyword
    {
        V,
        Vn,
        F,
    }

    impl super::Keyword for Keyword
    {
        fn from_str(keyword: &str, _line_no: usize) -> Result<Self, ParsingError>
        {
            Ok(match keyword {
                "v" => Self::V,
                "vn" => Self::Vn,
                "f" => Self::F,
                _ => {
                    panic!("Unknown keyword '{keyword}'");
                }
            })
        }
    }

    impl Display for Keyword
    {
        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
        {
            match self {
                Self::V => write!(formatter, "v"),
                Self::Vn => write!(formatter, "vn"),
                Self::F => write!(formatter, "f"),
            }
        }
    }

    #[test]
    fn parse_statement_line_works()
    {
        assert!(parse_statement_line::<Keyword>("", 1)
            .is_ok_and(|statement| statement.is_none()));

        let res = parse_statement_line::<Keyword>("v", 3);

        assert!(matches!(res, Ok(Some(statement)) if statement.keyword == Keyword::V));
    }

    #[test]
    fn parse_statement_line_with_args_works()
    {
        let statement = parse_statement_line::<Keyword>("vn 45 86 11", 3)
            .expect("Expected Ok")
            .expect("Expected Some");

        assert_eq!(statement.keyword, Keyword::Vn);

        assert!(matches!(statement.arguments[0], Value::Integer(45)));
        assert!(matches!(statement.arguments[1], Value::Integer(86)));
        assert!(matches!(statement.arguments[2], Value::Integer(11)));
    }

    #[test]
    fn parse_statement_line_with_triplet_arg()
    {
        let statement = parse_statement_line::<Keyword>("f 67/925/23", 1)
            .expect("Expected Ok")
            .expect("Expected Some");

        assert_eq!(statement.keyword, Keyword::F);

        let Value::Triplet(triplet) = &statement.arguments[0] else {
            panic!("Argument is not a triplet");
        };

        assert_eq!(triplet.0, 67);
        assert_eq!(triplet.1, Some(925));
        assert_eq!(triplet.2, Some(23));
    }

    #[test]
    fn parse_statement_line_with_two_value_triplet_arg()
    {
        let statement = parse_statement_line::<Keyword>("v 102//336", 1)
            .expect("Expected Ok")
            .expect("Expected Some");

        assert_eq!(statement.keyword, Keyword::V);

        let Value::Triplet(triplet) = &statement.arguments[0] else {
            panic!("Argument is not a triplet");
        };

        assert_eq!(triplet.0, 102);
        assert!(triplet.1.is_none());
        assert_eq!(triplet.2, Some(336));
    }
}