use anyhow::{bail, Result};
use colored::*;
use std::collections::HashMap;
use std::time::{Duration, Instant};
pub use aoc_toolbox_derive::{aoc_main, aoc_solver};
pub use clap;
pub use clap::Parser;
pub use std::error::Error;
pub mod utils;
enum Measure {
    Real(Instant),
    Mock,
}
impl Measure {
    fn step(&self) -> Option<Duration> {
        match self {
            Measure::Real(start) => Some(Instant::now().duration_since(*start)),
            Measure::Mock => None,
        }
    }
}
type Solver = fn() -> String;
pub struct Aoc<'a> {
    year: u32,
    with_duration: bool,
    solvers: HashMap<&'a str, HashMap<&'a str, Solver>>,
}
impl<'a> Aoc<'a> {
    pub fn new(year: u32, with_duration: bool) -> Self {
        Aoc {
            year,
            with_duration,
            solvers: HashMap::new(),
        }
    }
    pub fn add_solver(&mut self, day: &'a str, part: &'a str, solver: Solver) {
        let day = self.solvers.entry(day).or_default();
        day.insert(part, solver);
    }
    fn visit_solvers<F>(&self, visit: F)
    where
        F: Fn(&'a str, Option<(&'a str, &Solver)>),
    {
        let mut day_keys: Vec<&&str> = self.solvers.keys().collect();
        day_keys.sort_unstable();
        for day in day_keys {
            visit(day, None);
            let parts = self.solvers.get(day).unwrap();
            let mut part_keys: Vec<&&str> = parts.keys().collect();
            part_keys.sort_unstable();
            for part in part_keys {
                let solver = parts.get(part).unwrap();
                visit(day, Some((part, solver)));
            }
        }
    }
    fn pretty_format(&self, day: &str, part: &str, solver: &Solver) -> String {
        let measure = if self.with_duration {
            Measure::Real(Instant::now())
        } else {
            Measure::Mock
        };
        let result = solver();
        let duration = measure.step();
        if self.with_duration {
            let duration = duration.unwrap();
            format!(
                "{}::{} [{:>4}s {:>4}ms] {} {}",
                day.blue(),
                part.green(),
                duration.as_secs(),
                duration.subsec_millis(),
                "................".cyan(),
                result,
            )
        } else {
            format!(
                "{}::{} {} {}",
                day.blue(),
                part.green(),
                "................".cyan(),
                result
            )
        }
    }
    fn run_all(&self) -> Result<()> {
        self.visit_solvers(|day, part_solver| match part_solver {
            None => println!("{}{}", "# ".yellow(), day.yellow()),
            Some((part, solver)) => {
                println!("  - {}", self.pretty_format(day, part, solver));
            }
        });
        Ok(())
    }
    fn run_day(&self, day: &str) -> Result<()> {
        self.visit_solvers(|visit_day, part_solver| {
            if visit_day == day {
                if let Some((part, solver)) = part_solver {
                    println!("{}", self.pretty_format(day, part, solver));
                }
            }
        });
        Ok(())
    }
    fn run_part(&self, day: &str, part: &str) -> Result<()> {
        self.visit_solvers(|visit_day, part_solver| {
            if visit_day == day {
                match part_solver {
                    Some((visit_part, solver)) if visit_part == part => {
                        println!("{}", self.pretty_format(day, part, solver));
                    }
                    _ => {}
                }
            }
        });
        Ok(())
    }
    pub fn list(&self) {
        self.visit_solvers(|day, part_solver| {
            if let Some((part, _)) = part_solver {
                println!("{}::{}", day.blue(), part.green())
            }
        });
    }
    pub fn run(&self, label: String) -> Result<()> {
        println!("{} {}", "Year".green().bold(), self.year);
        match label.split("::").collect::<Vec<&str>>().as_slice() {
            ["all"] => self.run_all(),
            [day] => self.run_day(day),
            [day, part] => self.run_part(day, part),
            _ => bail!("Invalid label, must be like <day>::<part>"),
        }?;
        Ok(())
    }
}