extern crate structopt;
#[macro_use]
extern crate quick_error;
extern crate chrono;
extern crate colored;
extern crate evtclib;
extern crate humantime;
extern crate num_traits;
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::{Duration, NaiveDateTime};
use num_traits::cast::FromPrimitive;
use regex::Regex;
use structopt::StructOpt;
use walkdir::{DirEntry, WalkDir};

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

mod errors;
use errors::RuntimeError;

mod output;

mod filters;

mod csl;
use csl::CommaSeparatedList;

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

macro_rules! debug {
    ($($arg:tt)*) => {
        if debug_enabled() {
            use std::io::Write;
            let stderr = ::std::io::stderr();
            let mut lock = stderr.lock();
            write!(lock, "[d] ").expect("debug write failed");
            writeln!(lock, $($arg)*).expect("debug write failed");
        }
    }
}

static mut DEBUG_ENABLED: bool = false;

/// Return whether or not debug output should be enabled.
#[inline]
fn debug_enabled() -> bool {
    unsafe { DEBUG_ENABLED }
}

/// 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 = "*")]
    field: CommaSeparatedList<SearchField>,

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

    /// Invert the regular expression (show fights that do not match)
    #[structopt(short = "v", long = "invert-match")]
    invert: bool,

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

    /// Only show logs that are younger than the given time.
    #[structopt(
        short = "a",
        long = "younger",
        parse(try_from_str = "parse_time_arg")
    )]
    after: Option<NaiveDateTime>,

    /// Only show logs that are older than the given time.
    #[structopt(
        short = "b",
        long = "older",
        parse(try_from_str = "parse_time_arg")
    )]
    before: Option<NaiveDateTime>,

    /// Print more debugging information to stderr.
    #[structopt(long = "debug")]
    debug: 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, Hash)]
enum SearchField {
    /// Only search the account name.
    Account,
    /// Only search the character name.
    Character,
}

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

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "account" => Ok(SearchField::Account),
            "character" => Ok(SearchField::Character),
            _ => Err("Must be 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, Hash)]
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" | "kill" => Ok(FightOutcome::Success),
            "wipe" | "fail" => Ok(FightOutcome::Wipe),
            _ => Err("Must be success or wipe"),
        }
    }
}

fn parse_time_arg(input: &str) -> Result<NaiveDateTime, &'static str> {
    if let Ok(duration) = humantime::parse_duration(input) {
        let now = chrono::Local::now().naive_local();
        let chrono_dur = Duration::from_std(duration).expect("Duration out of range!");
        return Ok(now - chrono_dur);
    }
    if let Ok(time) = humantime::parse_rfc3339_weak(input) {
        let timestamp = time
            .duration_since(std::time::SystemTime::UNIX_EPOCH)
            .unwrap()
            .as_secs();
        return Ok(NaiveDateTime::from_timestamp(timestamp as i64, 0));
    }
    Err("unknown time format")
}

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

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

    if opt.debug {
        // We haven't started any threads here yet, so this is fine.
        unsafe { DEBUG_ENABLED = true };
    }

    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") || n.ends_with(".zevtc"))
        .unwrap_or(false)
}

/// Run the grep search with the given options.
fn grep(opt: &Opt) -> Result<(), RuntimeError> {
    rayon::scope(|s| {
        let walker = WalkDir::new(&opt.path);
        for entry in walker {
            let entry = entry?;
            s.spawn(move |_| {
                if is_log_file(&entry) {
                    if let Some(result) = search_log(&entry, opt).unwrap() {
                        output::colored(io::stdout(), &result).unwrap();
                    }
                }
            });
        }
        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") || n.ends_with(".zevtc"))
        .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 {
        debug!("log file cannot be parsed: {:?}", entry.path());
        return Ok(None);
    };

    let info = extract_info(entry, &log);

    let take_log = filters::filter_name(&log, opt) == !opt.invert
        && filters::filter_outcome(&info, opt)
        && filters::filter_time(&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 = get_encounter_name(log)
        .unwrap_or_else(|| {
            debug!(
                "log file has unknown boss: {:?} (id: {:#x})",
                entry.path(),
                log.boss_id()
            );
            "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(i64::from(get_start_timestamp(log)), 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 encounter
fn get_encounter_name(log: &Log) -> Option<&'static str> {
    use evtclib::statistics::gamedata::Boss;
    let boss = Boss::from_u16(log.boss_id())?;
    Some(match boss {
        Boss::ValeGuardian => "Vale Guardian",
        Boss::Gorseval => "Gorseval",
        Boss::Sabetha => "Sabetha",

        Boss::Slothasor => "Slothasor",
        Boss::Matthias => "Matthias",

        Boss::KeepConstruct => "Keep Construct",
        Boss::Xera => "Xera",

        Boss::Cairn => "Cairn",
        Boss::MursaatOverseer => "Mursaat Overseer",
        Boss::Samarog => "Samarog",
        Boss::Deimos => "Deimos",

        Boss::SoullessHorror => "Desmina",
        Boss::Dhuum => "Dhuum",

        Boss::ConjuredAmalgamate => "Conjured Amalgamate",
        Boss::LargosTwins => "Largos Twins",
        Boss::Qadim => "Qadim",

        Boss::Skorvald => "Skorvald",
        Boss::Artsariiv => "Artsariiv",
        Boss::Arkk => "Arkk",

        Boss::MAMA => "MAMA",
        Boss::Siax => "Siax the Corrupted",
        Boss::Ensolyss => "Ensolyss of the Endless Torment",
    })
}

/// 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",

        _ => {
            debug!("Unknown spec (prof: {}, elite: {})", profession, elite);
            "Unknown"
        }
    }
}