extern crate structopt;
#[macro_use]
extern crate quick_error;
extern crate chrono;
extern crate colored;
extern crate evtclib;
extern crate rayon;
extern crate regex;
extern crate walkdir;

use std::fs::File;
use std::io::{self, BufReader};
use std::path::PathBuf;
use std::str::FromStr;

use chrono::NaiveDateTime;
use regex::Regex;
use structopt::StructOpt;
use walkdir::{DirEntry, WalkDir};

use rayon::prelude::*;

use evtclib::{AgentKind, AgentName, EventKind, Log};

mod errors;
use errors::RuntimeError;

mod output;

mod filters;

macro_rules! unwrap {
    ($p:pat = $e:expr => { $r:expr} ) => {
        if let $p = $e {
            $r
        } else {
            panic!("Pattern match failed!");
        }
    };
}

/// A program that allows you to search through all your evtc logs for specific
/// people.
#[derive(StructOpt, Debug)]
#[structopt(name = "raidgrep")]
pub struct Opt {
    /// Path to the folder with logs.
    #[structopt(
        short = "d",
        long = "dir",
        default_value = ".",
        parse(from_os_str)
    )]
    path: PathBuf,

    /// The fields which should be searched.
    #[structopt(short = "f", long = "fields", default_value = "all")]
    field: SearchField,

    /// Only display fights with the given outcome.
    #[structopt(short = "o", long = "outcome")]
    outcome: Option<FightOutcome>,

    /// Disable colored output.
    #[structopt(long = "no-color")]
    no_color: bool,

    /// The regular expression to search for.
    #[structopt(name = "EXPR")]
    expression: Regex,
}

/// A flag indicating which fields should be searched.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum SearchField {
    /// Search all fields.
    All,
    /// Only search the account name.
    Account,
    /// Only search the character name.
    Character,
}

impl SearchField {
    /// True if the state says that the account name should be searched.
    #[inline]
    fn search_account(&self) -> bool {
        *self == SearchField::All || *self == SearchField::Account
    }

    /// True if the state says that the character name should be searched.
    #[inline]
    fn search_character(&self) -> bool {
        *self == SearchField::All || *self == SearchField::Character
    }
}

impl FromStr for SearchField {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "all" => Ok(SearchField::All),
            "account" => Ok(SearchField::Account),
            "character" => Ok(SearchField::Character),
            _ => Err("Must be all, account or character"),
        }
    }
}

/// A log that matches the search criteria.
#[derive(Debug, Clone)]
pub struct LogResult {
    /// The path to the log file.
    log_file: PathBuf,
    /// The time of the recording.
    time: NaiveDateTime,
    /// The name of the boss.
    boss_name: String,
    /// A vector of all participating players.
    players: Vec<Player>,
    /// The outcome of the fight.
    outcome: FightOutcome,
}

/// A player.
#[derive(Debug, Clone)]
pub struct Player {
    /// Account name of the player.
    account_name: String,
    /// Character name of the player.
    character_name: String,
    /// Profession (or elite specialization) as english name.
    profession: String,
    /// Subsquad that the player was in.
    subgroup: u8,
}

/// Outcome of the fight.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FightOutcome {
    Success,
    Wipe,
}

impl FromStr for FightOutcome {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "success" => Ok(FightOutcome::Success),
            "wipe" | "fail" => Ok(FightOutcome::Wipe),
            _ => Err("Must be success or wipe"),
        }
    }
}

fn main() {
    let opt = Opt::from_args();

    if opt.no_color {
        colored::control::set_override(false);
    }

    let result = grep(&opt);
    match result {
        Ok(_) => {}
        Err(e) => {
            eprintln!("Error: {}", e);
        }
    }
}

/// Check if the given entry represents a log file, based on the file name.
fn is_log_file(entry: &DirEntry) -> bool {
    entry
        .file_name()
        .to_str()
        .map(|n| n.ends_with(".evtc") || n.ends_with(".evtc.zip"))
        .unwrap_or(false)
}

/// Run the grep search with the given options.
fn grep(opt: &Opt) -> Result<(), RuntimeError> {
    let walker = WalkDir::new(&opt.path);
    let entries = walker.into_iter().collect::<Vec<_>>();
    entries.into_par_iter().try_for_each(|e| {
        let entry = e?;
        if is_log_file(&entry) {
            if let Some(result) = search_log(&entry, opt)? {
                output::colored(io::stdout(), &result)?;
            }
        }
        Ok(())
    })
}

/// Search the given single log.
///
/// If the log matches, returns `Ok(Some(..))`.
/// If the log doesn't match, returns `Ok(None)`.
/// If there was a fatal error, returns `Err(..)`.
fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>, RuntimeError> {
    let mut input = BufReader::new(File::open(entry.path())?);
    let raw = if entry
        .file_name()
        .to_str()
        .map(|n| n.ends_with(".zip"))
        .unwrap_or(false)
    {
        evtclib::raw::parse_zip(&mut input)
    } else {
        evtclib::raw::parse_file(&mut input)
    };
    let parsed = raw.ok().and_then(|m| evtclib::process(&m).ok());
    let log = if let Some(e) = parsed {
        e
    } else {
        return Ok(None);
    };

    let info = extract_info(entry, &log);

    let take_log = filters::filter_name(&log, opt) && filters::filter_outcome(&info, opt);

    if take_log {
        Ok(Some(info))
    } else {
        Ok(None)
    }
}

/// Extract human-readable information from the given log file.
fn extract_info(entry: &DirEntry, log: &Log) -> LogResult {
    let boss_name = match log.boss().name() {
        AgentName::Single(s) => s,
        _ => "<unknown>",
    }.into();

    let mut players = log
        .players()
        .map(|p| {
            unwrap! { AgentKind::Player { profession, elite } = p.kind() => {
            unwrap! { AgentName::Player {
                account_name,
                character_name,
                subgroup,
            } = p.name() =>
            {
                Player {
                    account_name: account_name.clone(),
                    character_name: character_name.clone(),
                    profession: get_profession_name(*profession, *elite).into(),
                    subgroup: *subgroup,
                }
            }}}}
        }).collect::<Vec<Player>>();
    players.sort_by_key(|p| p.subgroup);

    LogResult {
        log_file: entry.path().to_path_buf(),
        time: NaiveDateTime::from_timestamp(get_start_timestamp(log) as i64, 0),
        boss_name,
        players,
        outcome: get_fight_outcome(log),
    }
}

/// Get the timestamp of the log start time.
fn get_start_timestamp(log: &Log) -> u32 {
    for event in log.events() {
        if let EventKind::LogStart {
            local_timestamp, ..
        } = event.kind
        {
            return local_timestamp;
        }
    }
    0
}

/// Get the outcome of the fight.
fn get_fight_outcome(log: &Log) -> FightOutcome {
    for event in log.events() {
        if let EventKind::Reward { .. } = event.kind {
            return FightOutcome::Success;
        }
    }
    FightOutcome::Wipe
}

/// Get the (english) name for the given profession/elite specialization.
fn get_profession_name(profession: u32, elite: u32) -> &'static str {
    match (profession, elite) {
        (1, 0) => "Guardian",
        (2, 0) => "Warrior",
        (3, 0) => "Engineer",
        (4, 0) => "Ranger",
        (5, 0) => "Thief",
        (6, 0) => "Elementalist",
        (7, 0) => "Mesmer",
        (8, 0) => "Necromancer",
        (9, 0) => "Revenant",

        (1, 27) => "Dragonhunter",
        (2, 18) => "Berserker",
        (3, 43) => "Scrapper",
        (4, 5) => "Druid",
        (5, 7) => "Daredevil",
        (6, 48) => "Tempest",
        (7, 40) => "Chronomancer",
        (8, 34) => "Reaper",
        (9, 52) => "Herald",

        (1, 62) => "Firebrand",
        (2, 61) => "Spellbreaker",
        (3, 57) => "Holosmith",
        (4, 55) => "Soulbeast",
        (5, 58) => "Deadeye",
        (6, 56) => "Weaver",
        (7, 59) => "Mirage",
        (8, 60) => "Scourge",
        (9, 63) => "Renegade",

        _ => "Unknown",
    }
}