use bevy_utils::get_short_name;
use lazy_static::lazy_static;
use regex::Regex;
use termcolor::{Color, ColorSpec, StandardStream, WriteColor};

use crate::SpanStats;

pub fn print_spanstats(
    stdout: &mut StandardStream,
    reference: Option<&SpanStats>,
    comparison: Option<&SpanStats>,
    reference_only: bool,
) {
    match (reference, comparison) {
        (Some(reference), Some(comparison)) if !reference_only => {
            let relative = comparison / reference;

            print!("[count: {:8} | {:8} | ", reference.count, comparison.count);
            print_relative(stdout, relative.count);
            print!("]  [min: ");
            print_delta_time_us(reference.min);
            print!(" | ");
            print_delta_time_us(comparison.min);
            print!(" | ");
            print_relative(stdout, relative.min);
            print!("]  [avg: ");
            print_delta_time_us(reference.avg);
            print!(" | ");
            print_delta_time_us(comparison.avg);
            print!(" | ");
            print_relative(stdout, relative.avg);
            print!("]  [max: ");
            print_delta_time_us(reference.max);
            print!(" | ");
            print_delta_time_us(comparison.max);
            print!(" | ");
            print_relative(stdout, relative.max);
            println!("]");
        }
        (Some(reference), None) if !reference_only => {
            print!(
                "[count: {:8} |          |        ]  [min:  ",
                reference.count
            );
            print_delta_time_us(reference.min);
            print!(" |         |         ]  [avg: ");
            print_delta_time_us(reference.avg);
            print!(" |         |         ]  [max: ");
            print_delta_time_us(reference.max);
            println!(" |         |         ]");
        }
        (None, Some(comparison)) => {
            print!("[count:          | {:8} |         ", comparison.count);
            print!("]  [min:         | ");
            print_delta_time_us(comparison.min);
            print!(" |         ]  [avg:         | ");
            print_delta_time_us(comparison.avg);
            print!(" |         ]  [max:         | ");
            print_delta_time_us(comparison.max);
            println!(" |         ]");
        }
        (Some(reference), _) if reference_only => {
            print!("[count: {:8}]  [min: ", reference.count);
            print_delta_time_us(reference.min);
            print!("]  [avg: ");
            print_delta_time_us(reference.avg);
            print!("]  [max: ");
            print_delta_time_us(reference.max);
            println!("]");
        }
        _ => {}
    }
}

const MARGIN_PERCENT: f32 = 2.0;
fn print_relative(stdout: &mut StandardStream, v: f32) {
    let v_delta_percent = if v.is_nan() { 0.0 } else { (v - 1.0) * 100.0 };
    set_fg(
        stdout,
        if v_delta_percent > MARGIN_PERCENT {
            Color::Red
        } else if v_delta_percent < -MARGIN_PERCENT {
            Color::Green
        } else {
            Color::White
        },
    );
    if v_delta_percent > MARGIN_PERCENT {
        print!("+");
    } else if v_delta_percent >= -MARGIN_PERCENT {
        print!(" ");
    } else {
        print!("-");
    }
    print_base10f32_fixed_width(v_delta_percent.abs(), 1.0);
    print!("%");
    set_fg(stdout, Color::White);
}

// Try to print time values using 4 numeric digits, a decimal point, and the unit
const ONE_US_IN_SECONDS: f32 = 1e-6;

fn print_delta_time_us(dt_us: f32) {
    print_base10f32_fixed_width(dt_us, ONE_US_IN_SECONDS);
    print!("s");
}

fn print_base10f32_fixed_width(v: f32, v_scale: f32) {
    Scale::from_value_and_scale(v, v_scale).print_with_scale(v, v_scale);
}

#[derive(Debug)]
pub struct Scale {
    name: &'static str,
    scale_factor: f32,
}

impl Scale {
    pub const TERA: f32 = 1e12;
    pub const GIGA: f32 = 1e9;
    pub const MEGA: f32 = 1e6;
    pub const KILO: f32 = 1e3;
    pub const UNIT: f32 = 1e0;
    pub const MILLI: f32 = 1e-3;
    pub const MICRO: f32 = 1e-6;
    pub const NANO: f32 = 1e-9;
    pub const PICO: f32 = 1e-12;

    pub fn from_value_and_scale(v: f32, v_scale: f32) -> Self {
        assert!(v >= 0.0);
        if v == 0.0 {
            Self {
                name: " ",
                scale_factor: Self::UNIT,
            }
        } else if v * v_scale >= Self::TERA {
            Self {
                name: "T",
                scale_factor: Self::TERA,
            }
        } else if v * v_scale >= Self::GIGA {
            Self {
                name: "G",
                scale_factor: Self::GIGA,
            }
        } else if v * v_scale >= Self::MEGA {
            Self {
                name: "M",
                scale_factor: Self::MEGA,
            }
        } else if v * v_scale >= Self::KILO {
            Self {
                name: "k",
                scale_factor: Self::KILO,
            }
        } else if v * v_scale >= Self::UNIT {
            Self {
                name: " ",
                scale_factor: Self::UNIT,
            }
        } else if v * v_scale >= Self::MILLI {
            Self {
                name: "m",
                scale_factor: Self::MILLI,
            }
        } else if v * v_scale >= Self::MICRO {
            Self {
                name: "µ",
                scale_factor: Self::MICRO,
            }
        } else if v * v_scale >= Self::NANO {
            Self {
                name: "n",
                scale_factor: Self::NANO,
            }
        } else {
            Self {
                name: "p",
                scale_factor: Self::PICO,
            }
        }
    }

    pub fn print(&self, v: f32) {
        // NOTE: Hacks for rounding to decimal places to ensure precision is correct
        let precision = if ((v * 10.0).round() / 10.0) >= 100.0 {
            1
        } else if ((v * 100.0).round() / 100.0) >= 10.0 {
            2
        } else {
            3
        };
        print!("{v:5.precision$}{}", self.name);
    }

    pub fn print_with_scale(&self, v: f32, v_scale: f32) {
        self.print(v * v_scale / self.scale_factor);
    }
}

lazy_static! {
    static ref SYSTEM_NAME: Regex = Regex::new(r#"system: name="([^"]+)""#).unwrap();
    static ref SYSTEM_OVERHEAD: Regex = Regex::new(r#"system overhead: name="([^"]+)""#).unwrap();
    static ref SYSTEM_COMMANDS: Regex = Regex::new(r#"system_commands: name="([^"]+)""#).unwrap();
}

pub fn simplify_name(name: &str) -> String {
    if let Some(captures) = SYSTEM_NAME.captures(name) {
        return format!(r#"system: name="{}""#, get_short_name(&captures[1]));
    }
    if let Some(captures) = SYSTEM_OVERHEAD.captures(name) {
        return format!(
            r#"system overhead: name="{}""#,
            get_short_name(&captures[1])
        );
    }
    if let Some(captures) = SYSTEM_COMMANDS.captures(name) {
        return format!(
            r#"system_commands: name="{}""#,
            get_short_name(&captures[1])
        );
    }
    name.to_string()
}

fn set_fg(stdout: &mut StandardStream, color: Color) {
    stdout
        .set_color(ColorSpec::new().set_fg(Some(color)))
        .unwrap();
}

pub fn set_bold(stdout: &mut StandardStream, bold: bool) {
    stdout.set_color(ColorSpec::new().set_bold(bold)).unwrap();
}
