//! Filter expression language.
//!
//! This module contains methods to parse a given string into an abstract filter tree, check its
//! type and convert it to a [`Filter`][super::filters::Filter].
// Make it available in the grammar mod.
use super::{filters, playerclass::PlayerClass, FightOutcome};

use std::{error, fmt};

use itertools::Itertools;
use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError};
use thiserror::Error;

// Lalrpop chokes on the associated type specification (it doesn't expect the =), so we need to
// define those aliases here in Rust and then import and use them in the grammar.
trait DateProducer = filters::values::Producer<Output = chrono::DateTime<chrono::Utc>>;
trait DurationProducer = filters::values::Producer<Output = chrono::Duration>;
trait CountProducer = filters::values::Producer<Output = u8>;

lalrpop_mod!(#[allow(clippy::all)] pub grammar, "/fexpr/grammar.rs");

#[derive(Debug)]
pub struct FError {
    location: usize,
    data: String,
    kind: FErrorKind,
}

impl fmt::Display for FError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} (at {})", self.kind, self.location)
    }
}

impl error::Error for FError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        Some(&self.kind)
    }
}

#[derive(Debug, Error)]
pub enum FErrorKind {
    #[error("invalid regular expression: {0}")]
    InvalidRegex(#[from] regex::Error),
    #[error("invalid fight outcome")]
    InvalidFightOutcome,
    #[error("invalid weekday")]
    InvalidWeekday,
    #[error("invalid timestamp: {0}")]
    InvalidTimestamp(#[from] chrono::format::ParseError),
    #[error("invalid boss name")]
    InvalidBoss,
    #[error("invalid class name")]
    InvalidClass,
}

/// Shortcut to create a new parser and parse the given input.
pub fn parse_logfilter<'a>(
    input: &'a str,
) -> Result<Box<dyn filters::log::LogFilter>, ParseError<usize, Token<'a>, FError>> {
    grammar::LogFilterParser::new().parse(input)
}

/// Extract the location from the given error.
pub fn location<T>(err: &ParseError<usize, T, FError>) -> usize {
    match *err {
        ParseError::InvalidToken { location } => location,
        ParseError::UnrecognizedEOF { location, .. } => location,
        ParseError::UnrecognizedToken {
            token: (l, _, _), ..
        } => l,
        ParseError::ExtraToken { token: (l, _, _) } => l,
        ParseError::User { ref error } => error.location,
    }
}

/// "Re-quotes" a list of string pieces to a long, whitespace separated string.
///
/// This function is needed because the shell already does some argument parsing, so if the user
/// specifies `-player "godric gobbledygook"` on the command line, we will get `["-player", "godric
/// gobbledygook"]` as the arguments. Howvever, our parser expects a single string, so we re-join
/// the pieces and apply the quotes where necessary.
///
/// Note that this works on a "best guess" method, as we cannot reconstruct the shell's quotes 1:1.
/// This means that some things that work on the command line won't work in the REPL, and vice
/// versa.
///
/// ```
/// assert_eq!(
///     requote(&["-player", "godric gobbledygook"]),
///     r#"-player "godric gobbledygook""#,
/// );
/// ```
pub fn requote<S: AsRef<str>, T: IntoIterator<Item = S>>(items: T) -> String {
    const SPECIAL_CHARS: &[char] = &[' ', '.', '^', '$', '+', '+'];
    items
        .into_iter()
        .map(|part| {
            let part = part.as_ref();
            if part.contains(SPECIAL_CHARS) {
                format!(r#""{}""#, part)
            } else {
                part.into()
            }
        })
        .join(" ")
}

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

    #[test]
    fn test_requote() {
        assert_eq!(
            requote(&["-player", "godric gobbledygook"]),
            r#"-player "godric gobbledygook""#,
        );
        assert_eq!(requote(&["-player", "godric"]), r#"-player godric"#,);
        assert_eq!(requote(&["-player", "g.dric"]), r#"-player "g.dric""#,);
    }
}