use super::{
    FError,
    FErrorKind,
    FightOutcome,
    filters,
    PlayerClass,

    DateProducer,
    DurationProducer,
    CountProducer,
};
use evtclib::Encounter;
use std::collections::HashSet;
use lalrpop_util::ParseError;

use chrono::{DateTime, Local, TimeZone, Utc, Weekday, Duration};
use regex::{Regex, RegexBuilder};

grammar;

extern {
    type Error = FError;
}

pub LogFilter: Box<dyn filters::log::LogFilter> = {
    Disjunction<LogPredicate>,
}

PlayerFilter: Box<dyn filters::player::PlayerFilter> = {
    Disjunction<PlayerPredicate>,
}

Disjunction<T>: T = {
    <a:Disjunction<T>> "or" <b:Conjunction<T>> => a | b,
    Conjunction<T>,
}

Conjunction<T>: T = {
    <a:Conjunction<T>> "and"? <b:Negation<T>> => a & b,
    Negation<T>,
}

Negation<T>: T = {
    "not" <Negation<T>> => ! <>,
    "!" <Negation<T>> => ! <>,
    T,
}

LogPredicate: Box<dyn filters::log::LogFilter> = {
    "-success" => filters::log::success(),
    "-wipe" => filters::log::wipe(),
    "-outcome" <Comma<FightOutcome>> => filters::log::outcome(<>),

    "-weekday" <Comma<Weekday>> => filters::log::weekday(<>),
    "-before" <Date> => filters::values::comparison(
        filters::values::time(), filters::values::CompOp::Less, filters::values::constant(<>)
    ),
    "-after" <Date> => filters::values::comparison(
        filters::values::time(), filters::values::CompOp::Greater, filters::values::constant(<>)
    ),
    "-log-before" <Date> => filters::log::log_before(<>),
    "-log-after" <Date> => filters::log::log_after(<>),

    "-boss" <Comma<Encounter>> => filters::log::encounter(<>),
    "-cm" => filters::log::challenge_mote(),

    "-include" => filters::constant(true),
    "-exclude" => filters::constant(false),

    "-player" <Regex> => filters::player::any(
            filters::player::character(<>.clone())
            | filters::player::account(<>)
        ),

    "all" "(" "player" ":" <PlayerFilter> ")" => filters::player::all(<>),
    "any" "(" "player" ":" <PlayerFilter> ")" => filters::player::any(<>),
    "exists" "(" "player" ":" <PlayerFilter> ")" => filters::player::any(<>),

    <Comparison<DateProducer>>,
    <Comparison<DurationProducer>>,
    <Comparison<CountProducer>>,

    "(" <LogFilter> ")",
}

PlayerPredicate: Box<dyn filters::player::PlayerFilter> = {
    "-character" <Regex> => filters::player::character(<>),
    "-account" <Regex> => filters::player::account(<>),
    "-name" <Regex> =>
        filters::player::account(<>.clone())
        | filters::player::character(<>),

    "-class" <Comma<PlayerClass>> => filters::player::class(<>),

    "(" <PlayerFilter> ")",
}

Regex: Regex = {
    <l:@L> <s:string> =>? RegexBuilder::new(&s[1..s.len() - 1])
        .case_insensitive(true)
        .build()
        .map_err(|error| ParseError::User {
            error: FError {
                location: l,
                kind: error.into(),
            }
        }),
    <l:@L> <s:word> =>? RegexBuilder::new(s)
        .case_insensitive(true)
        .build()
        .map_err(|error| ParseError::User {
            error: FError {
                location: l,
                kind: error.into(),
            }
        }),
}

FightOutcome: FightOutcome = {
    <l:@L> <w:word> =>? w.parse().map_err(|_| ParseError::User {
        error: FError {
            location: l,
            kind: FErrorKind::FightOutcome,
        }
    }),
}

Weekday: Weekday = {
    <l:@L> <w:word> =>? w.parse().map_err(|_| ParseError::User {
        error: FError {
            location: l,
            kind: FErrorKind::Weekday,
        }
    }),
}

Encounter: Encounter = {
    <l:@L> <w:word> =>? w.parse().map_err(|_| ParseError::User {
        error: FError {
            location: l,
            kind: FErrorKind::Boss,
        }
    }),

    <l:@L> <s:string> =>? s[1..s.len() -1].parse().map_err(|_| ParseError::User {
        error: FError {
            location: l,
            kind: FErrorKind::Boss,
        }
    }),
}

PlayerClass: PlayerClass = {
    <l:@L> <w:word> =>? w.parse().map_err(|_| ParseError::User {
        error: FError {
            location: l,
            kind: FErrorKind::Class,
        }
    }),
}

Date: DateTime<Utc> = {
    <l:@L> <d:datetime> =>? Local.datetime_from_str(d, "%Y-%m-%d %H:%M:%S")
        .map_err(|error| ParseError::User {
            error: FError {
                location: l,
                kind: error.into(),
            }
        })
        .map(|d| d.with_timezone(&Utc)),
    <l:@L> <d:date> =>? Local.datetime_from_str(&format!("{} 00:00:00", d), "%Y-%m-%d %H:%M:%S")
        .map_err(|error| ParseError::User {
            error: FError {
                location: l,
                kind: error.into(),
            }
        })
        .map(|d| d.with_timezone(&Utc)),
}

Duration: Duration = {
    duration => Duration::from_std(humantime::parse_duration(<>).unwrap()).unwrap(),
}

CompOp: filters::values::CompOp = {
    "<" => filters::values::CompOp::Less,
    "<=" => filters::values::CompOp::LessEqual,
    "=" => filters::values::CompOp::Equal,
    ">=" => filters::values::CompOp::GreaterEqual,
    ">" => filters::values::CompOp::Greater,
}

Comparison<T>: Box<dyn filters::log::LogFilter> = {
    <lhs:T> <op:CompOp> <rhs:T> => filters::values::comparison(lhs, op, rhs),
}

Comma<T>: HashSet<T> = {
    <v:(<T> ",")*> <e:T> => {
        let mut result = v.into_iter().collect::<HashSet<_>>();
        result.insert(e);
        result
    },
}

DateProducer: Box<dyn DateProducer> = {
    <Date> => filters::values::constant(<>),
    "-time" => filters::values::time(),
}

DurationProducer: Box<dyn DurationProducer> = {
    <Duration> => filters::values::constant(<>),
    "-duration" => filters::values::duration(),
}

CountProducer: Box<dyn CountProducer> = {
    <integer> => filters::values::constant(<>.parse().unwrap()),
    "count" "(" "player" ":" <PlayerFilter> ")" => filters::values::player_count(<>),
    "count" "(" "player" ")" => filters::values::player_count(filters::constant(true)),
}

match {
    "player" => "player",
    "not" => "not",
    "or" => "or",
    "and" => "and",
    "any" => "any",
    "all" => "all",
    "exists" => "exists",
    "count" => "count",

    r"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d" => datetime,
    r"\d\d\d\d-\d\d-\d\d" => date,
    r"((\d+m ?)?\d+s)|(\d+m)" => duration,
    r"\d+" => integer,
    r"[[:alpha:]][\w]*" => word,
    r#""[^"]*""# => string,

    _
}