use crate::{load_replay, print_replay_version, FACTION, MISSING};

use faf_replay_parser::aggregator::*;
use faf_replay_parser::scfa::Replay;
use faf_replay_parser::{ParserBuilder, ReplayResult};
use rayon::prelude::*;
use regex::Regex;
use structopt::StructOpt;

use std::borrow::Cow;
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;

#[derive(StructOpt)]
pub struct InfoArgs {
    /// Path to the replay, can be multiple
    #[structopt(parse(from_os_str), required = true, conflicts_with = "cmd")]
    replays: Vec<PathBuf>,
    /// Enables all detailed output
    #[structopt(long = "all", short = "a")]
    all: bool,
    /// Prints additional details about the map
    #[structopt(long = "map")]
    map_info: bool,
    /// Prints additional details about the game options
    #[structopt(long = "options")]
    option_info: bool,
    /// Prints additional details about the players
    #[structopt(long = "players")]
    player_info: bool,
    /// Prints every beat number on which a desync occurred
    #[structopt(long = "desyncs")]
    desyncs: bool,
}
impl InfoArgs {
    /// A convenience for checking if an option is enabled
    fn enabled(&self, option: bool) -> bool {
        self.all || option
    }
}

/// Any info outputted by this command.
struct ReplayInfo {
    replay: Option<Replay>,
    map_info: Option<ReplayMapInfo>,
    players_info: Option<HashMap<String, ReplayPlayerInfo>>,
    options_info: Option<ReplayOptionsInfo>,
}

/// Subcommand `info`
pub fn print_info(args: &InfoArgs) -> ReplayResult<()> {
    let parser = ParserBuilder::new()
        .commands_default()
        .stop_on_desync(false)
        .save_commands(false)
        .build();

    let replay_infos: Vec<ReplayResult<ReplayInfo>> = args
        .replays
        .par_iter()
        .map(|path| {
            let replay_data = load_replay(path)?;

            let replay = parser.parse(&mut replay_data.as_slice())?;

            let map_info = aggregate_replay_map_info(&replay);
            let players_info = aggregate_replay_players_info(&replay);
            let options_info = aggregate_replay_options_info(&replay);

            Ok(ReplayInfo {
                replay: Some(replay),
                map_info: Some(map_info),
                players_info: Some(players_info),
                options_info: Some(options_info),
            })
        })
        .collect();

    ReplayInfo::print_human_readable_list(&replay_infos, &args);

    Ok(())
}

// Not doing this with traits for now
impl ReplayInfo {
    fn print_human_readable(&self, args: &InfoArgs) {
        if let Some(replay) = &self.replay {
            print_replay_version(&replay);
            if let Some(tick) = replay.body.sim.desync_tick {
                println!("\nWARNING: Replay desynced at tick {}", tick);
                if args.enabled(args.desyncs) {
                    println!(
                        "All desynced tick: {:?}",
                        replay.body.sim.desync_ticks.as_ref().unwrap()
                    );
                }
            }
            println!(
                "\n{} ({})",
                self.map_info
                    .as_ref()
                    .and_then(|info| info.name.as_ref().map(String::as_str))
                    .unwrap_or("Unknown Map"),
                format_duration(&Duration::from_millis(replay.body.sim.tick as u64 * 100))
            );
            println!(
                "    {}",
                self.map_info
                    .as_ref()
                    .and_then(|info| info.description.as_ref().map(|d| Regex::new(r"<.*>")
                        .unwrap()
                        .replace_all(&d, "")
                        .into_owned()))
                    .unwrap_or("No description provided".to_string())
            );
        }

        self.map_info.as_ref().map(|info| print_map(args, info));
        self.options_info
            .as_ref()
            .map(|info| print_options(args, info));
        self.players_info.as_ref().map(|info| {
            println!();
            print_players(args, info);
        });
    }

    fn print_human_readable_list(infos: &[ReplayResult<ReplayInfo>], args: &InfoArgs) {
        if !infos.is_empty() {
            println!();
        }
        for (info, path) in infos.iter().zip(&args.replays) {
            println!("{}", path.display());
            match info {
                Ok(info) => {
                    info.print_human_readable(args);
                    println!();
                }
                Err(e) => println!("Failed to parse replay: {:?}", e),
            }
        }
    }
}

fn print_map(args: &InfoArgs, map_info: &ReplayMapInfo) {
    if args.enabled(args.map_info) {
        println!("\nMap");
        map_info
            .preview
            .as_ref()
            .map(|s| println!("    Preview: {}", s));
        println!("    File: {}", &map_info.map_file);
        map_info
            .scenario_file
            .as_ref()
            .map(|s| println!("    Scenario: {}", s));
        map_info
            .script_file
            .as_ref()
            .map(|s| println!("    Script: {}", s));
        map_info
            .save_file
            .as_ref()
            .map(|s| println!("    Save: {}", s));
    }
}

fn print_options(args: &InfoArgs, options_info: &ReplayOptionsInfo) {
    if args.enabled(args.option_info) {
        println!("\nOptions");
        options_info
            .victory
            .as_ref()
            .map(|s| println!("    Victory: {}", s));
        options_info
            .unit_cap
            .map(|i| println!("    Unit Cap: {}", i));
        options_info
            .share
            .as_ref()
            .map(|s| println!("    Share: {}", s));
        options_info
            .cheats
            .map(|b| println!("    CheatsEnabled: {}", b));
        if options_info.cheats.unwrap_or(true) {
            options_info
                .build_mult
                .map(|f| println!("    BuildMult: {}", f));
            options_info
                .cheat_mult
                .map(|f| println!("    CheatMult: {}", f));
        }
    }
}

fn print_players(args: &InfoArgs, players_info: &HashMap<String, ReplayPlayerInfo>) {
    type Team<'a> = Vec<&'a ReplayPlayerInfo>;

    let mut teams: HashMap<i64, Team> = HashMap::new();

    for (_name, info) in players_info {
        let team = info.team.map_or(-1, |t| t as i64);
        match teams.contains_key(&team) {
            true => teams.get_mut(&team).unwrap().push(info),
            false => {
                teams.insert(team, Vec::new());
                teams.get_mut(&team).unwrap().push(info);
            }
        };
    }

    for (_num, team) in teams.iter_mut() {
        team.sort_unstable_by_key(|p| {
            p.army_name
                .as_ref()
                .map(|n| Cow::Owned(n.to_string()))
                .unwrap_or_else(|| {
                    p.name
                        .as_ref()
                        .map(|n| Cow::Owned(n.to_string()))
                        .unwrap_or(Cow::from(""))
                })
        });
    }

    let mut teams: Vec<(i64, Team)> = teams.drain().collect();
    teams.sort_unstable_by_key(|t| t.0);

    for (num, team) in teams {
        println!("Team {}", num);
        for player in team {
            print_player(args, &player);
        }
        println!();
    }
}

fn print_player(args: &InfoArgs, player: &ReplayPlayerInfo) {
    // Used for excluding fields from the output
    let treat_as_human = player.human.unwrap_or(true);
    // Player name
    print!("    ");
    if let Some(ref clan) = player.clan {
        print!("[{}] ", clan);
    }
    print!(
        "{}",
        player
            .name
            .as_ref()
            .map(String::as_str)
            .unwrap_or("Unknown Player"),
    );

    if treat_as_human {
        print!(
            " ({})",
            player
                .rating
                .map(|r| Cow::from(r.to_string()))
                .unwrap_or(Cow::from("??"))
        );
    } else {
        print!(
            " ({})",
            player
                .ai_personality
                .as_ref()
                .map(String::as_str)
                .unwrap_or("AI")
        );
    }
    print!(
        " {}",
        player
            .faction
            .map(|f| FACTION
                .get(f as usize - 1)
                .map(|f| Cow::from(*f))
                .unwrap_or(Cow::from(f.to_string())))
            .unwrap_or(Cow::from("Missing Faction"))
    );
    println!();
    // End player name

    // Player details
    if args.enabled(args.player_info) {
        if treat_as_human {
            player.id.map(|id| println!("        Id: {}", id));
            player
                .country
                .as_ref()
                .map(|s| println!("        Country: {}", s.to_ascii_uppercase()));
            if player.mean.is_some() || player.deviation.is_some() {
                println!(
                    "        Trueskill: {} \u{00b1} {}",
                    player
                        .mean
                        .map(|m| Cow::from(format!("{:.0}", m)))
                        .unwrap_or(Cow::from(MISSING)),
                    player
                        .deviation
                        .map(|m| Cow::from(format!("{:.0}", m)))
                        .unwrap_or(Cow::from(MISSING)),
                );
            }
        } else {
            player.civilian.map(|b| println!("        Civilian: {}", b));
        }
        player.human.map(|b| println!("        Human: {}", b));
        player
            .start_spot
            .map(|s| println!("        Start Spot: {}", s));
        player
            .army_name
            .as_ref()
            .map(|s| println!("        Army Name: {}", s));
        player
            .army_color
            .map(|c| println!("        Army Color: {}", c));
        player
            .player_color
            .map(|c| println!("        Player Color: {}", c));
        if treat_as_human {
            player.ng.map(|i| println!("        Games Played: {}", i));
            player.pl.map(|i| println!("        PL: {}", i));
            player.rc.as_ref().map(|s| println!("        RC: {}", s));
        }
    }
}

fn format_duration(duration: &Duration) -> String {
    let total_secs = duration.as_secs();
    let (hours, rem) = (total_secs / 3600, total_secs % 3600);
    let (minutes, seconds) = (rem / 60, rem % 60);
    format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}

fn print_header_info(replay: &Replay) {
    let header = &replay.header;

    print_replay_version(replay);
    println!();

    println!("Map: {}", header.map_file);
    println!("Scenario: {:#}", header.scenario);
    println!("Seed: {:.4}", header.seed);
    println!(
        "Cheats: {}",
        if header.cheats_enabled { "On" } else { "Off" }
    );
    println!("Mods: {:#}", header.mods);
    println!();
}
