use std::collections::HashSet;
use std::fmt;
use std::hash::Hash;
use std::str::FromStr;

use super::{FightOutcome, SearchField};
use chrono::Weekday;
use evtclib::statistics::gamedata::Boss;

pub trait Variants: Copy {
    type Output: Iterator<Item = Self>;
    fn variants() -> Self::Output;
}

macro_rules! variants {
    ($target:ident => $($var:ident),+) => {
        impl Variants for $target {
            type Output = ::std::iter::Cloned<::std::slice::Iter<'static, Self>>;
            fn variants() -> Self::Output {
                // Exhaustiveness check
                #[allow(dead_code)]
                fn exhaustiveness_check(value: $target) {
                    match value {
                        $($target :: $var => ()),+
                    }
                }
                // Actual result
                [
                    $($target :: $var),+
                ].iter().cloned()
            }
        }
    };
    ($target:ident => $($var:ident,)+) => {
        variants!($target => $($var),+);
    };
}

variants! { SearchField => Account, Character, Guild }
variants! { FightOutcome => Success, Wipe }
variants! { Weekday => Mon, Tue, Wed, Thu, Fri, Sat, Sun }
variants! { Boss =>
    ValeGuardian, Gorseval, Sabetha,
    Slothasor, Matthias,
    KeepConstruct, Xera,
    Cairn, MursaatOverseer, Samarog, Deimos,
    SoullessHorror, Dhuum,
    ConjuredAmalgamate, LargosTwins, Qadim,
    CardinalAdina, CardinalSabir, QadimThePeerless,

    IcebroodConstruct, VoiceOfTheFallen, FraenirOfJormag, Boneskinner, WhisperOfJormag,

    Skorvald, Artsariiv, Arkk,
    MAMA, Siax, Ensolyss,
}

/// The character that delimits items from each other.
const DELIMITER: char = ',';
/// The character that negates the result.
const NEGATOR: char = '!';

/// A list that is given as comma-separated values.
#[derive(Debug, Clone)]
pub struct CommaSeparatedList<T: Eq + Hash + fmt::Debug> {
    values: HashSet<T>,
}

#[derive(Debug, Clone)]
pub enum ParseError<E> {
    Underlying(E),
}

impl<E: fmt::Display> fmt::Display for ParseError<E> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ParseError::Underlying(ref e) => e.fmt(f),
        }
    }
}

impl<T> FromStr for CommaSeparatedList<T>
where
    T: FromStr + Variants + Hash + Eq + fmt::Debug,
{
    type Err = ParseError<T::Err>;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        if input == "*" {
            Ok(CommaSeparatedList {
                values: T::variants().collect(),
            })
        } else if input.starts_with(NEGATOR) {
            let no_csl = CommaSeparatedList::from_str(&input[1..])?;
            let all_values = T::variants().collect::<HashSet<_>>();
            Ok(CommaSeparatedList {
                values: all_values.difference(&no_csl.values).cloned().collect(),
            })
        } else {
            let parts = input.split(DELIMITER);
            let values = parts
                .map(FromStr::from_str)
                .collect::<Result<HashSet<_>, _>>()
                .map_err(ParseError::Underlying)?;
            Ok(CommaSeparatedList { values })
        }
    }
}

impl<T> CommaSeparatedList<T>
where
    T: Hash + Eq + fmt::Debug,
{
    pub fn contains(&self, value: &T) -> bool {
        self.values.contains(value)
    }

    pub fn values(&self) -> &HashSet<T> {
        &self.values
    }
}

// We allow implicit hasher because then it's a zero-cost conversion, as we're just unwrapping the
// values.
#[allow(clippy::implicit_hasher)]
impl<T> From<CommaSeparatedList<T>> for HashSet<T>
where
    T: Hash + Eq + fmt::Debug,
{
    fn from(csl: CommaSeparatedList<T>) -> Self {
        csl.values
    }
}