aocli 0.0.9

Advent of Code helper CLI
use std::{
    env, fmt,
    io::{self, Write},
    path::{Path, PathBuf},
};

use colored::{ColoredString, Colorize};

use crate::network::{DayCompletion, YearCompletion};

fn log(header: ColoredString, message: impl fmt::Display) {
    let len = header.len();
    let padding = if len >= 9 { 0 } else { 9 - len };
    eprintln!(
        "{}{}{}{}",
        " ".repeat(padding),
        header,
        ": ".dimmed(),
        message
    );
}

#[macro_export]
macro_rules! success {
    ($($arg:tt)*) => {{
        display::print_success(format!($($arg)*));
    }}
}

pub use success;

pub fn print_success(message: String) {
    log("success".green().bold(), message);
}

#[macro_export]
macro_rules! info {
    ($($arg:tt)*) => {{
        display::print_info(format!($($arg)*));
    }}
}

pub use info;

pub fn print_info(message: String) {
    log("info".yellow().bold(), message);
}

pub(crate) fn error(message: String) {
    log("error".red().bold(), message);
}

pub(crate) fn cause(message: String) {
    log("cause".normal(), message);
}

pub(crate) fn usage(message: String) {
    log("usage".normal(), format!("aoc {message}"));
}

pub(crate) fn or(message: String) {
    log("or".normal(), format!("aoc {message}"));
}

pub fn answer(got: &str, expected: Option<&str>, time: u64) -> bool {
    let got = Answer::new(got);
    let time = colored_time(time);
    if let Some(expected) = expected {
        let expected = Answer::new(expected);
        if got.answer == expected.answer {
            print!(
                "{}{}{}{}",
                "[".dimmed(),
                got.display().green().bold(),
                "]  ".dimmed(),
                time
            );
        } else {
            let (got, symbol) = if got.is_multiline && !expected.is_multiline {
                (got.display().yellow().bold(), "-")
            } else {
                (got.display().red().bold(), "")
            };
            print!(
                "{}{}{}{}{}{}{}{}",
                "[".dimmed(),
                got,
                "] ".dimmed(),
                symbol.dimmed(),
                " [".dimmed(),
                expected.display().green().bold(),
                "]  ".dimmed(),
                time
            );
        }
    } else {
        print!(
            "{}{}{}{}",
            "[".dimmed(),
            got.display().yellow().bold(),
            "]  ".dimmed(),
            time
        );
    }
    got.is_multiline
}

pub fn just_answer(answer: &str, correct: bool) {
    let answer = Answer::new(answer);
    let answer = if correct {
        answer.display().green().bold()
    } else {
        answer.display().red().bold()
    };
    println!("{}{}{}", "[".dimmed(), answer, "]".dimmed());
}

pub fn incomplete() {
    println!("{}", "incomplete".yellow());
}

pub fn wait() {
    println!("{}", "wait".yellow());
}

pub fn answer_full(
    year: &str,
    day: &str,
    part: &str,
    got: &str,
    expected: Option<&str>,
    time: u64,
) {
    day_part(year, day, part);
    if answer(got, expected, time) {
        println!();
        println!("{got}");
    } else {
        println!();
    }
}

pub fn unimplemented() {
    println!("{}", "unimplemented".yellow());
}

pub fn panic() {
    println!("{}", "panic".red());
}

pub fn panic_input(input: &str) {
    println!("{}  ({})", "panic".red(), input);
}

pub fn no_input() {
    part("*");
    println!("{}", "no input".yellow());
}

pub fn build_error() {
    part("*");
    println!("{}", "build error".red());
}

pub fn run_error() {
    println!("{}", "error".red());
}

pub fn day(year: &str, day: &str) {
    print!("{}{}{}", year, "/".dimmed(), day);
    let _ = io::stdout().flush();
}

pub fn part(part: &str) {
    print!("{}{}{}", "/".dimmed(), part, ": ".dimmed());
    let _ = io::stdout().flush();
}

pub fn day_part(year: &str, day: &str, part: &str) {
    print!(
        "{}{}{}{}{}{}",
        year,
        "/".dimmed(),
        day,
        "/".dimmed(),
        part,
        ": ".dimmed()
    );
    let _ = io::stdout().flush();
}

pub fn submit_error() {
    println!("{}", "error".red());
}

fn display_time(time: u64) -> String {
    let (div, unit) = match time {
        _ if time < 1_000_000 => (1_000, "μs"),
        _ if time < 1_000_000_000 => (1_000_000, "ms"),
        _ => (1_000_000_000, "s"),
    };
    format!("{}{unit}", time as f64 / div as f64)
}

fn colored_time(time: u64) -> ColoredString {
    let text = display_time(time);
    match time {
        _ if time < 20_000_000 => text.green(),
        _ if time < 200_000_000 => text.yellow(),
        _ => text.red(),
    }
}

pub fn stats(total_time: u64, num_parts: u8) {
    log("parts".normal(), format!("{num_parts:02}"));
    if num_parts > 0 {
        log("total".normal(), display_time(total_time));
        log(
            "average".normal(),
            display_time(total_time / num_parts as u64),
        );
    }
}

pub fn path(path: &Path) -> String {
    if path.is_absolute() {
        if let Ok(current_dir) = env::current_dir() {
            if path.starts_with(&current_dir) {
                let path_len = path.components().count();
                let current_len = current_dir.components().count();
                if path_len >= current_len.max(3) {
                    let path: PathBuf = path.components().skip(current_len - 1).collect();
                    return path.display().to_string();
                }
            }
        }
    }
    path.display().to_string()
}

struct Answer<'a> {
    answer: &'a str,
    is_multiline: bool,
}

impl<'a> Answer<'a> {
    fn new(answer: &'a str) -> Self {
        Self {
            answer,
            is_multiline: answer.lines().count() > 1,
        }
    }

    fn display(&'a self) -> &'a str {
        if self.is_multiline {
            "???"
        } else {
            self.answer
        }
    }
}

pub fn completion_header() {
    println!("{}{}{}", " ".repeat(14), "1".repeat(10), "2".repeat(6));
    println!("{}{}12345", " ".repeat(4 + 1), "1234567890".repeat(2),);
}

pub fn year_completion(year: &str, year_completion: YearCompletion) {
    print!("{year} ");
    for day in year_completion.days {
        print!(
            "{}",
            match day {
                DayCompletion::None => " ".into(),
                DayCompletion::Partial => "".dimmed(),
                DayCompletion::Full => "".yellow(),
            }
        );
    }
    println!(" {}", format!("{:02}", year_completion.total).yellow());
}