//! Definitions and functions to produce sorted output.

use std::{
    cmp::Ordering,
    fmt::{self, Display, Formatter},
    str::FromStr,
};

use itertools::Itertools;
use thiserror::Error;

use crate::LogResult;

#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Error)]
#[error("an invalid sorting component was given")]
pub struct InvalidComponent;

/// A [`Component`][Component] is anything that can be used to sort the result by.
///
/// Note that some components may not provide a true "sorting" (is Skorvald coming before Dhuum or
/// not?), but are more of a convenience that allows to group logs of the same component together.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Component {
    /// Sort the result date.
    Date,
    /// Sort by the boss.
    Boss,
    /// Sort by the outcome.
    Outcome,
    /// Sort based on whether the Challenge Mote was active or not.
    ChallengeMote,

    /// Sort by the given component but in reverse direction.
    Reverse(Box<Component>),
}

impl FromStr for Component {
    type Err = InvalidComponent;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.starts_with('~') {
            return s[1..].parse().map(|c| Component::Reverse(Box::new(c)));
        }
        match &s.to_lowercase() as &str {
            "date" => Ok(Component::Date),
            "boss" => Ok(Component::Boss),
            "outcome" => Ok(Component::Outcome),
            "cm" => Ok(Component::ChallengeMote),
            _ => Err(InvalidComponent),
        }
    }
}

impl Display for Component {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        match self {
            Component::Date => write!(f, "date"),
            Component::Boss => write!(f, "boss"),
            Component::Outcome => write!(f, "outcome"),
            Component::ChallengeMote => write!(f, "cm"),
            Component::Reverse(ref inner) => write!(f, "~{}", inner),
        }
    }
}

fn boss_id(log: &LogResult) -> u32 {
    log.boss.map(|x| x as u32).unwrap_or(0)
}

impl Component {
    pub fn cmp(&self, lhs: &LogResult, rhs: &LogResult) -> Ordering {
        match self {
            Component::Date => lhs.time.cmp(&rhs.time),
            Component::Boss => boss_id(lhs).cmp(&boss_id(rhs)),
            Component::Outcome => lhs.outcome.cmp(&rhs.outcome),
            Component::ChallengeMote => lhs.is_cm.cmp(&rhs.is_cm),
            Component::Reverse(ref inner) => inner.cmp(lhs, rhs).reverse(),
        }
    }
}

/// A sorting.
///
/// The sorting goes lexicographically, in the sense that if the first chosen component is the same
/// for two elements, the next component will be used to compare two logs.
#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
pub struct Sorting(Vec<Component>);

impl FromStr for Sorting {
    type Err = InvalidComponent;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s == "" {
            return Ok(Sorting::default());
        }
        let parts = s.split(',');
        parts
            .map(FromStr::from_str)
            .collect::<Result<Vec<Component>, _>>()
            .map(Sorting::new)
    }
}

impl Display for Sorting {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        let string = self.0.iter().map(ToString::to_string).join(",");
        f.write_str(&string)
    }
}

impl Sorting {
    pub fn new(components: Vec<Component>) -> Sorting {
        Sorting(components)
    }

    pub fn cmp(&self, lhs: &LogResult, rhs: &LogResult) -> Ordering {
        for component in &self.0 {
            let result = component.cmp(lhs, rhs);
            if result != Ordering::Equal {
                return result;
            }
        }
        Ordering::Equal
    }
}

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

    use chrono::prelude::*;
    use evtclib::Boss as B;

    #[test]
    fn test_parse_component() {
        assert_eq!("date".parse(), Ok(Component::Date));
        assert_eq!("boss".parse(), Ok(Component::Boss));
        assert_eq!("outcome".parse(), Ok(Component::Outcome));
        assert_eq!("cm".parse(), Ok(Component::ChallengeMote));

        assert_eq!("foobar".parse::<Component>(), Err(InvalidComponent));

        assert_eq!(
            "~date".parse(),
            Ok(Component::Reverse(Box::new(Component::Date)))
        );
    }

    #[test]
    fn test_parse_sorting() {
        use Component::*;
        assert_eq!("date".parse(), Ok(Sorting::new(vec![Date])));
        assert_eq!("date,boss".parse(), Ok(Sorting::new(vec![Date, Boss])));
        assert_eq!(
            "date,~boss".parse(),
            Ok(Sorting::new(vec![Date, Reverse(Box::new(Boss))]))
        );
        assert_eq!("".parse(), Ok(Sorting::default()));
    }

    #[test]
    fn test_sorting_cmp() {
        use Component::*;

        let logs: &[&LogResult] = &[
            &LogResult {
                log_file: "".into(),
                time: Utc.ymd(2020, 4, 3).and_hms(12, 0, 0),
                boss: Some(B::Dhuum),
                players: vec![],
                outcome: FightOutcome::Success,
                is_cm: false,
            },
            &LogResult {
                log_file: "".into(),
                time: Utc.ymd(2020, 4, 3).and_hms(13, 0, 0),
                boss: Some(B::Dhuum),
                players: vec![],
                outcome: FightOutcome::Success,
                is_cm: false,
            },
            &LogResult {
                log_file: "".into(),
                time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0),
                boss: Some(B::Dhuum),
                players: vec![],
                outcome: FightOutcome::Success,
                is_cm: false,
            },
            &LogResult {
                log_file: "".into(),
                time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0),
                boss: Some(B::Qadim),
                players: vec![],
                outcome: FightOutcome::Success,
                is_cm: false,
            },
            &LogResult {
                log_file: "".into(),
                time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0),
                boss: Some(B::Dhuum),
                players: vec![],
                outcome: FightOutcome::Success,
                is_cm: false,
            },
        ];

        let sortings: &[(&[Component], &[&LogResult])] = &[
            (&[Date], &[logs[2], logs[3], logs[4], logs[0], logs[1]]),
            (
                &[Reverse(Box::new(Date))],
                &[logs[1], logs[0], logs[2], logs[3], logs[4]],
            ),
            (&[Boss], &[logs[0], logs[1], logs[2], logs[4], logs[3]]),
            (
                &[Boss, Date],
                &[logs[2], logs[4], logs[0], logs[1], logs[3]],
            ),
        ];

        for (sorting, expected) in sortings {
            let mut data = logs.to_vec();
            let sorting = Sorting::new(sorting.to_vec());
            data.sort_by(|a, b| sorting.cmp(a, b));
            assert_eq!(&data, expected, "Sorting with {:?} failed", sorting);
        }
    }
}