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

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;

#[derive(StructOpt, Debug)]
#[structopt(name = "raidgrep")]
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.
    /// Possible values: all, account, character
    #[structopt(short = "f", long = "fields", default_value = "all")]
    field: SearchField,

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

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum SearchField {
    All,
    Account,
    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"),
        }
    }
}

#[derive(Debug, Clone)]
pub struct LogResult {
    log_file: PathBuf,
    time: NaiveDateTime,
    boss_name: String,
    players: Vec<Player>,
    outcome: FightOutcome,
}

#[derive(Debug, Clone)]
pub struct Player {
    account_name: String,
    character_name: String,
    profession: String,
    subgroup: u8,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FightOutcome {
    Success,
    Wipe,
}

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

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)
}

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(())
    })
}

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);
    };

    for player in log.players() {
        match player.name() {
            AgentName::Player {
                account_name,
                character_name,
                ..
            } => {
                if ((opt.field == SearchField::All || opt.field == SearchField::Account)
                    && opt.expression.is_match(account_name))
                    || ((opt.field == SearchField::All || opt.field == SearchField::Character)
                        && opt.expression.is_match(character_name))
                {
                    return Ok(Some(extract_info(entry, &log)));
                }
            }
            _ => unreachable!(),
        }
    }

    Ok(None)
}

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| {
            if let AgentKind::Player { profession, elite } = p.kind() {
                if let 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,
                    }
                } else {
                    unreachable!()
                }
            } else {
                unreachable!()
            }
        }).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),
    }
}

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

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

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