use crate::model::*;
use std::str::FromStr;

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum QueryFilter {
    Not(Box<QueryFilter>),
    Tag(String),
    SpecialInName(String),
    SpecialBeforeDate(Date),
    SpecialAfterDate(Date),
    SpecialMinResolution(u64),
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Score {
    RequiredMatch(bool),
    Match(bool),
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum QueryFilterSyntaxError {
    UnknownSpecialFilter,
    ResolutionArgumentInvalid,
    DateArgumentInvalid,
    NameArgumentInvalid,
    ArgumentInvalid,
}

use std::ops::Not;
impl Not for Score {
    type Output = Score;

    fn not(self) -> Score {
        match self {
            Score::RequiredMatch(b) => Score::RequiredMatch(!b),
            Score::Match(b) => Score::Match(!b),
        }
    }
}

impl QueryFilter {
    pub fn is_special(&self) -> bool {
        use QueryFilter::*;
        match self {
            Not(inner) => inner.is_special(),
            Tag(_) => false,
            SpecialInName(_) => true,
            SpecialBeforeDate(_) => true,
            SpecialAfterDate(_) => true,
            SpecialMinResolution(_) => true,
        }
    }

    pub fn score(&self, texture: &Texture) -> Score {
        use QueryFilter::*;
        match self {
            Not(inner) => inner.score(texture).not(),

            Tag(tag) => Score::Match(
                texture
                    .tags
                    .iter()
                    .find(|tt| tt.to_lowercase() == tag.to_lowercase())
                    .is_some(),
            ),

            SpecialInName(name) => Score::RequiredMatch(
                //
                texture.name.contains(name),
            ),

            SpecialBeforeDate(date) => Score::RequiredMatch(texture.added_on <= *date),

            SpecialAfterDate(date) => Score::RequiredMatch(texture.added_on > *date),

            SpecialMinResolution(required) => {
                let smaller_resolution = u64::min(texture.resolution.0, texture.resolution.1);

                Score::RequiredMatch(smaller_resolution >= *required)
            }
        }
    }
}

impl FromStr for QueryFilter {
    type Err = QueryFilterSyntaxError;

    fn from_str(input: &str) -> Result<QueryFilter, QueryFilterSyntaxError> {
        const NEGATION_CHAR: char = '!';
        const SPLIT_CHAR: char = ':';

        if input.is_empty() {
            return Err(QueryFilterSyntaxError::ArgumentInvalid);
        } else if input.starts_with(NEGATION_CHAR) {
            // A Not.
            let inner = Self::from_str(&input[1..])?;
            return Ok(QueryFilter::Not(Box::new(inner)));
        } else if input.contains(SPLIT_CHAR) {
            // A Special Filter
            let mut parts = input.splitn(2, SPLIT_CHAR);
            let filter_name = parts.next().unwrap().to_lowercase();
            let filter_arg = parts.next().unwrap();

            match filter_name.as_str() {
                "n" | "name" => {
                    if filter_arg.is_empty() {
                        return Err(QueryFilterSyntaxError::NameArgumentInvalid);
                    }
                    Ok(QueryFilter::SpecialInName(filter_arg.to_string()))
                }
                "r" | "res" | "resolution" => {
                    let num: u64;

                    if filter_arg.ends_with("k") {
                        num = *&filter_arg[0..(filter_arg.len() - 1)]
                            .parse::<u32>()
                            .map(|n| (n as u64) * 1024)
                            .map_err(|_e| QueryFilterSyntaxError::ResolutionArgumentInvalid)?;
                    } else {
                        num = filter_arg
                            .parse()
                            .map_err(|_e| QueryFilterSyntaxError::ResolutionArgumentInvalid)?;
                    }

                    Ok(QueryFilter::SpecialMinResolution(num))
                }
                "a" | "after" => {
                    let date = Date::from_str(filter_arg)
                        .map_err(|_| QueryFilterSyntaxError::DateArgumentInvalid)?;

                    Ok(QueryFilter::SpecialAfterDate(date))
                }
                "b" | "bef" | "before" => {
                    let date = Date::from_str(filter_arg)
                        .map_err(|_| QueryFilterSyntaxError::DateArgumentInvalid)?;

                    Ok(QueryFilter::SpecialBeforeDate(date))
                }

                _ => Err(QueryFilterSyntaxError::UnknownSpecialFilter),
            }
        } else {
            // A Tag
            Ok(QueryFilter::Tag(input.to_string()))
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn parsing() {
        assert!(QueryFilter::from_str("cats:meep").is_err());
        assert!(QueryFilter::from_str("name:").is_err());
        assert!(QueryFilter::from_str("res:-400k").is_err());
        assert!(QueryFilter::from_str("res:4647846846846864864846868446864846844684784k").is_err());
        assert!(QueryFilter::from_str("!!!!a:80-50-50").is_err());

        assert_eq!(
            QueryFilter::from_str("n:hello"),
            Ok(QueryFilter::SpecialInName("hello".to_string()))
        );

        assert_eq!(
            QueryFilter::from_str("NaMe:hello"),
            Ok(QueryFilter::SpecialInName("hello".to_string()))
        );

        assert_eq!(
            QueryFilter::from_str("res:4k"),
            Ok(QueryFilter::SpecialMinResolution(4096))
        );

        assert_eq!(
            QueryFilter::from_str("res:4096"),
            Ok(QueryFilter::SpecialMinResolution(4096))
        );

        assert_eq!(
            QueryFilter::from_str("a:2019-10-10"),
            Ok(QueryFilter::SpecialAfterDate(Date::new(2019, 10, 10)))
        );

        assert_eq!(
            QueryFilter::from_str("!before:2019-10-10"),
            Ok(QueryFilter::Not(
                QueryFilter::SpecialBeforeDate(Date::new(2019, 10, 10)).into()
            ))
        );

        assert_eq!(
            QueryFilter::from_str("!before:2019-10-10"),
            Ok(QueryFilter::Not(
                QueryFilter::SpecialBeforeDate(Date::new(2019, 10, 10)).into()
            ))
        );

        assert_eq!(
            QueryFilter::from_str("!Wood"),
            Ok(QueryFilter::Not(
                QueryFilter::Tag("Wood".to_string()).into()
            ))
        );
    }
}
