#![feature(trait_alias)]
use std::collections::HashMap;
use std::fmt;
use std::fs::{self, File};
use std::io::{BufReader, Read, Seek};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};

use anyhow::{anyhow, Context, Error, Result};
use chrono::{DateTime, TimeZone, Utc};
use colored::Colorize;
use log::debug;
use regex::Regex;
use rustyline::Editor;
use structopt::StructOpt;
use walkdir::{DirEntry, WalkDir};

use evtclib::raw::parser::PartialEvtc;
use evtclib::{EliteSpec, EventKind, Log, Profession};

mod fexpr;
mod filters;
use filters::{log::LogFilter, Inclusion};
mod guilds;
mod logger;
mod output;
mod paths;

/// Application name, as it should be used in configuration directory paths.
const APP_NAME: &str = "raidgrep";

/// A program that allows you to search through all your evtc logs for specific people.
///
/// raidgrep supports different predicates that determine whether a log is included or not.
/// Predicates start with a - and optionally take an argument. Predicates can be combined with
/// "and", "or" and "not", and predicates that operate on single players (instead of logs) have to
/// be within an "all(player: ...)" or "any(player: ...)" construct.
///
/// PREDICATES:
///     -character REGEX        True if the character name matches the regex.
///     -account REGEX          True if the account name matches the regex.
///     -name REGEX             True if either character or account name match.
///
///     -success                Only include successful logs.
///     -wipe                   Only include failed logs.
///     -outcome OUTCOMES       Only include logs with the given outcomes.
///     -weekday WEEKDAYS       Only include logs from the given weekdays.
///     -before DATE            Only include logs from before the given date.
///     -after DATE             Only include logs from after the given date.
///     -boss BOSSES            Only include logs from the given bosses.
///     -player REGEX           Shorthand to check if any player in the log has the given name.
///     -include                Always evaluates to including the log.
///     -exclude                Always evaluates to excluding the log.
///
/// BOSS NAMES:
///     The following names can be used with the -boss filter:
///       vg, gorseval, sabetha, slothasor, matthias, kc, xera, cairn,
///       mo, samarog, deimos, desmina, dhuum, ca, largos, qadim,
///       adina, sabir, qadimp, skorvald, artsariiv, arkk, mama, siax,
///       ensolyss, icebrood, fraenir, kodans, boneskinner, whisper.
///     Names can also be comma separated.
#[derive(StructOpt, Debug)]
#[structopt(verbatim_doc_comment)]
pub struct Opt {
    /// Path to the folder with logs.
    #[structopt(short = "d", long = "dir", default_value = ".", parse(from_os_str))]
    path: PathBuf,

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

    /// Print more debugging information to stderr.
    #[structopt(long = "debug")]
    debug: bool,

    /// Load guild information from the API.
    ///
    /// Loading guild information requires network access and slows down the program considerably,
    /// so this is disabled by default.
    #[structopt(long = "guilds")]
    guilds: bool,

    /// Run the REPL.
    ///
    /// The REPL will allow you to keep entering queries which are being searched by raidgrep,
    /// until you manually exit with Crtl+C.
    #[structopt(long)]
    repl: bool,

    /// The filter expression, see PREDICATES for more information.
    expression: Vec<String>,
}

/// A flag indicating which fields should be searched.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum SearchField {
    /// Only search the account name.
    Account,
    /// Only search the character name.
    Character,
    /// Only search the guild name or tag.
    Guild,
}

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),
            "guild" => Ok(SearchField::Guild),
            _ => 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: DateTime<Utc>,
    /// The numeric ID of the boss.
    boss_id: u16,
    /// 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,
    /// Guild ID, ready for API consumption.
    guild_id: Option<String>,
}

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

/// A stripped version of [`LogResult`][LogResult] that is available early in the parsing process.
///
/// This can be used by filters to filter out logs early, before they will be fully parsed.
#[derive(Debug, Clone)]
pub struct EarlyLogResult {
    /// The path to the log file.
    log_file: PathBuf,
    /// The partially parsed evtc.
    evtc: PartialEvtc,
}

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

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

#[derive(Clone, Debug)]
struct InputError {
    line: String,
    location: usize,
    msg: String,
}

impl fmt::Display for InputError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let prefix = "Input:";
        writeln!(f, "{} {}", prefix.yellow(), self.line)?;
        let prefix_len = prefix.len() + self.location;
        writeln!(f, "{}{}", " ".repeat(prefix_len), " ^-".red())?;
        write!(f, "{}: {}", "Error".red(), self.msg)?;
        Ok(())
    }
}

impl std::error::Error for InputError {}

/// A flag that indicates whether the current search should be interrupted because the user pressed
/// Crtl-C.
static INTERRUPTED: AtomicBool = AtomicBool::new(false);

fn main() {
    let result = run();
    if let Err(err) = result {
        display_error(&err);
    }
}

fn display_error(err: &Error) {
    if let Some(err) = err.downcast_ref::<InputError>() {
        eprintln!("{}", err);
    } else {
        eprintln!("{}: {}", "Error".red(), err);
    }
}

fn run() -> Result<()> {
    let opt = Opt::from_args();

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

    if opt.debug {
        logger::initialize(log::Level::Debug);
    } else {
        logger::initialize(log::Level::Info);
    }

    if opt.guilds {
        guilds::prepare_cache();
    }

    if !opt.repl {
        single(&opt)?;
    } else {
        repl(&opt)?;
    }

    if opt.guilds {
        guilds::save_cache();
    }

    Ok(())
}

fn single(opt: &Opt) -> Result<()> {
    // As a shortcut, we allow only the regular expression to be given, to retain the behaviour
    // before the filter changes.
    if opt.expression.len() == 1 {
        let line = &opt.expression[0];
        let maybe_filter = build_filter(line);
        if maybe_filter.is_err() && !line.starts_with('-') {
            let maybe_regex = Regex::new(line);
            if let Ok(rgx) = maybe_regex {
                let filter = filters::player::any(
                    filters::player::account(rgx.clone()) | filters::player::character(rgx),
                );
                return grep(opt, &*filter);
            }
        }
        return grep(opt, &*maybe_filter?);
    }

    let expr_string = fexpr::requote(&opt.expression);
    let filter = build_filter(&expr_string)?;
    grep(&opt, &*filter)?;
    Ok(())
}

fn repl(opt: &Opt) -> Result<()> {
    ctrlc::set_handler(|| INTERRUPTED.store(true, Ordering::Relaxed))
        .expect("Could not set interrupt hanlder");

    let mut rl = Editor::<()>::new();
    let history_path = paths::history_path();
    if history_path.is_none() {
        debug!("Could not determine the history path");
    }

    maybe_load_history(&mut rl, history_path.as_ref().map(|r| r as &Path));

    loop {
        let line = rl.readline("Query> ")?;
        rl.add_history_entry(&line);
        maybe_save_history(&rl, history_path.as_ref().map(|r| r as &Path));

        let parsed = build_filter(&line);
        INTERRUPTED.store(false, Ordering::Relaxed);
        match parsed {
            Ok(filter) => grep(&opt, &*filter)?,
            Err(err) => display_error(&err),
        }
    }
}

fn maybe_load_history(rl: &mut Editor<()>, path: Option<&Path>) {
    if let Some(path) = path {
        debug!("Loading history from {:?}", path);
        if let Err(e) = rl.load_history(path) {
            debug!("Loading the history failed: {}", e);
        }
    }
}

fn maybe_save_history(rl: &Editor<()>, path: Option<&Path>) {
    let run = |path: &Path| -> Result<()> {
        debug!("Saving history to {:?}", path);
        let parent = path
            .parent()
            .ok_or(anyhow!("Path does not have a parent"))?;
        fs::create_dir_all(parent).context("Could not create directory")?;
        rl.save_history(path)?;
        Ok(())
    };
    if let Some(path) = path {
        if let Err(e) = run(path) {
            debug!("Saving the history failed: {}", 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)
}

/// Small wrapper around `fexpr::parse_logfilter` to convert the returned `Err` to be `'static'.
fn build_filter(expr_string: &str) -> Result<Box<dyn LogFilter>> {
    if expr_string.trim().is_empty() {
        return Err(anyhow!("Expected a filter to be given"));
    }
    Ok(
        fexpr::parse_logfilter(expr_string).map_err(|error| InputError {
            line: expr_string.to_string(),
            location: fexpr::location(&error),
            msg: error.to_string(),
        })?,
    )
}

/// Run the grep search with the given options.
fn grep(opt: &Opt, filter: &dyn LogFilter) -> 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 |_| {
                // Check first if we should even still continue
                if INTERRUPTED.load(Ordering::Relaxed) {
                    return;
                }
                if is_log_file(&entry) {
                    let search = search_log(&entry, filter);
                    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, filter: &dyn LogFilter) -> 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 = if is_zip {
        ZipWrapper::zipped(file_stream)
    } else {
        ZipWrapper::raw(file_stream)
    };
    let mut stream = wrapper.get_stream();
    let partial = evtclib::raw::parser::parse_partial_file(&mut stream)?;

    let early_log = EarlyLogResult {
        log_file: entry.path().to_owned(),
        evtc: partial,
    };
    let early_ok = filter.filter_early(&early_log);
    let partial = early_log.evtc;

    if early_ok == Inclusion::Exclude {
        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 = filter.filter(&info);

    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.encounter_id()
            );
            "unknown"
        })
        .into();

    let guild_ids = get_guild_mapping(log);

    let mut players = log
        .players()
        .map(|p| Player {
            account_name: p.account_name().to_owned(),
            character_name: p.character_name().to_owned(),
            profession: get_profession_name(p.profession(), p.elite()).into(),
            subgroup: p.subgroup(),
            guild_id: guild_ids.get(&p.addr()).cloned(),
        })
        .collect::<Vec<Player>>();
    players.sort_by_key(|p| p.subgroup);

    LogResult {
        log_file: entry.path().to_path_buf(),
        time: Utc.timestamp(i64::from(log.local_end_timestamp().unwrap_or(0)), 0),
        boss_id: log.encounter_id(),
        boss_name,
        players,
        outcome: get_fight_outcome(log),
    }
}

/// Get a mapping of agent IDs to guild API strings.
fn get_guild_mapping(log: &Log) -> HashMap<u64, String> {
    log.events()
        .iter()
        .filter_map(|event| {
            if let EventKind::Guild {
                source_agent_addr,
                api_guild_id,
                ..
            } = event.kind()
            {
                api_guild_id
                    .as_ref()
                    .map(|api_id| (*source_agent_addr, api_id.clone()))
            } else {
                None
            }
        })
        .collect()
}

/// Get the outcome of the fight.
fn get_fight_outcome(log: &Log) -> FightOutcome {
    if log.was_rewarded() {
        FightOutcome::Success
    } else {
        FightOutcome::Wipe
    }
}

/// Get the (english) name for the given encounter
fn get_encounter_name(log: &Log) -> Option<&'static str> {
    use evtclib::Boss;
    Some(match log.encounter()? {
        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: Profession, elite: Option<EliteSpec>) -> &'static str {
    use EliteSpec::*;
    use Profession::*;

    if let Some(elite) = elite {
        match elite {
            Dragonhunter => "Dragonhunter",
            Firebrand => "Firebrand",
            Berserker => "Berserker",
            Spellbreaker => "Spellbreaker",
            Herald => "Herald",
            Renegade => "Renegade",
            Scrapper => "Scrapper",
            Holosmith => "Holosmith",
            Druid => "Druid",
            Soulbeast => "Soulbeast",
            Daredevil => "Daredevil",
            Deadeye => "Deadeye",
            Tempest => "Tempest",
            Weaver => "Weaver",
            Chronomancer => "Chronomancer",
            Mirage => "Mirage",
            Reaper => "Reaper",
            Scourge => "Scourge",
        }
    } else {
        match profession {
            Guardian => "Guardian",
            Warrior => "Warrior",
            Revenant => "Revenant",
            Engineer => "Engineer",
            Ranger => "Ranger",
            Thief => "Thief",
            Elementalist => "Elementalist",
            Mesmer => "Mesmer",
            Necromancer => "Necromancer",
        }
    }
}