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

use chrono::{Duration, NaiveDateTime, Weekday};
use num_traits::cast::FromPrimitive;
use regex::Regex;
use structopt::StructOpt;
use walkdir::{DirEntry, WalkDir};
use anyhow::{anyhow, Result};

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

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,

    /// Only show the name of matching files.
    #[structopt(short = "l", long = "files-with-matches")]
    file_name_only: 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>,

    /// Only show logs from the given weekdays.
    #[structopt(
        short = "w",
        long = "weekdays",
        default_value = "*",
        parse(try_from_str = try_from_str_simple_error)
    )]
    weekdays: CommaSeparatedList<Weekday>,

    /// Only show logs from the given encounters.
    #[structopt(
        short = "e",
        long = "bosses",
        default_value = "*",
    )]
    bosses: CommaSeparatedList<evtclib::statistics::gamedata::Boss>,

    /// 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> {
    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(anyhow!("unknown time format"))
}

fn try_from_str_simple_error<T: FromStr>(input: &str) -> Result<T, String>
{
    T::from_str(input).map_err(|_| format!("'{}' is an invalid value", input))
}


enum ZipWrapper<R: Read + Seek> {
    Raw(Option<R>),
    Zipped(zip::ZipArchive<R>),
}

impl<R: Read + Seek> ZipWrapper<R> {
    pub fn raw(input: R) -> Self {
        ZipWrapper::Raw(Some(input))
    }

    pub fn zipped(input: R) -> Self {
        ZipWrapper::Zipped(zip::ZipArchive::new(input).unwrap())
    }

    pub fn get_stream<'a>(&'a mut self) -> Box<(dyn Read + 'a)> {
        match *self {
            ZipWrapper::Raw(ref mut o) => Box::new(o.take().unwrap()),
            ZipWrapper::Zipped(ref mut z) => Box::new(z.by_index(0).unwrap()),
        }
    }
}


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<()> {
    let pipeline = &output::build_pipeline(opt);
    rayon::scope(|s| {
        let walker = WalkDir::new(&opt.path);
        for entry in walker {
            let entry = entry?;
            s.spawn(move |_| {
                if is_log_file(&entry) {
                    let search = search_log(&entry, opt);
                    match search {
                        Ok(None) => (),
                        Ok(Some(result)) => pipeline.push_item(&result),
                        Err(err) => {
                            debug!("Runtime error while scanning {:?}: {}", entry.path(), err);
                        }
                    }
                }
            });
        }
        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>> {
    let file_stream = BufReader::new(File::open(entry.path())?);
    let is_zip = entry
        .file_name()
        .to_str()
        .map(|n| n.ends_with(".zip") || n.ends_with(".zevtc"))
        .unwrap_or(false);
    let mut wrapper = match is_zip {
        false => ZipWrapper::raw(file_stream),
        true => ZipWrapper::zipped(file_stream),
    };
    let mut stream = wrapper.get_stream();
    let partial = evtclib::raw::parser::parse_partial_file(&mut stream)?;

    let early_ok = filters::filter_name(&partial, opt) != opt.invert
        && filters::filter_boss(&partial, opt);

    if !early_ok {
        return Ok(None)
    }

    let raw = evtclib::raw::parser::finish_parsing(partial, &mut stream)?;
    let parsed = evtclib::process(&raw).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_outcome(&info, opt)
        && filters::filter_weekday(&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::CardinalAdina => "Cardinal Adina",
        Boss::CardinalSabir => "Cardinal Sabir",
        Boss::QadimThePeerless => "Qadim The Peerless",

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

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

        Boss::IcebroodConstruct => "Icebrood Construct",
        Boss::VoiceOfTheFallen => "Super Kodan Brothers",
        Boss::FraenirOfJormag => "Fraenir of Jormag",
        Boss::Boneskinner => "Boneskinner",
        Boss::WhisperOfJormag => "Whisper of Jormag",
    })
}

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