use crate::parsing::{DURATION_TABLE, IEC_TABLE};
use chrono::prelude::*;
use jam_types::{Slot, JAM_COMMON_ERA};
use std::time::Duration;

pub fn slot_time(slot: Slot) -> String {
	chrono::Local
		.timestamp_opt(JAM_COMMON_ERA as i64 + slot as i64 * 6, 0)
		.unwrap()
		.format("%H:%M:%S")
		.to_string()
}

pub fn slot(slot: Slot) -> String {
	slot.to_string()
		.as_bytes()
		.rchunks(3)
		.rev()
		.map(std::str::from_utf8)
		.collect::<Result<Vec<&str>, _>>()
		.expect("converted from number; qed")
		.join(",")
}

pub fn percent<T: Into<u128>>(n: T, d: T) -> String {
	let ppm = (n.into() as f64 / d.into() as f64 * 100_000f64).ceil() / 1_000f64;
	if ppm >= 1000.0 {
		ppm.round()
	} else if ppm >= 100.0 {
		(ppm * 10f64).round() / 10f64
	} else if ppm >= 10.0 {
		(ppm * 100f64).round() / 100f64
	} else {
		ppm
	}
	.to_string()
}

pub fn bytes<T: Into<u128>>(n: T) -> String {
	units(n, "B", 1024.0)
}

pub fn amount<T: Into<u128>>(n: T) -> String {
	units(n, "", 1000.0)
}

pub fn gas<T: Into<u128>>(n: T) -> String {
	units(n, "", 1000.0)
}

/// Returns an approximate representation of quantity `n` measured in units `ones` using prefix
/// notation.
///
/// `divisor` is the prefix value (e.g. 1000 for SI units, 1024 for IEC units).
///
/// ```rust
/// use jam_tooling::format::units;
/// assert_eq!("10K", units(10_000_u32, "B", 1000.0));
/// assert_eq!("2560B", units(2_u32 * 1024 + 512, "B", 1024.0));
/// ```
pub fn units<T: Into<u128>>(n: T, ones: &str, divisor: f64) -> String {
	const UNITS: [&str; 7] = ["", "K", "M", "G", "T", "P", "E"];
	let mut i = 0;
	let mut n = n.into() as f64;
	while n >= 999.5 && i > 0 || n >= 9999.5 {
		n /= divisor;
		i += 1;
	}
	let q = if n >= 100.0 {
		1.0
	} else if n >= 10.0 {
		10.0
	} else {
		100.0
	};
	format!("{}{}", (n * q + 1.0e-14).round() / q, if i == 0 { ones } else { UNITS[i] })
}

/// A counterpart of [parsing::exact_bytes](crate::parsing::exact_bytes) that produces the
/// string from which the exact same size can be parsed.
pub fn exact_bytes(size: u64) -> String {
	let (number, prefix) = get_iec_prefix(size);
	format!("{number} {prefix}B")
}

fn get_iec_prefix(size: u64) -> (u64, &'static str) {
	if size != 0 {
		for (prefix, scale) in IEC_TABLE.iter() {
			if size.is_multiple_of(*scale) {
				return (size / scale, prefix);
			}
		}
	}
	(size, "")
}

/// Formats duration approximately, with a unit (SI units, days, weeks), using the shortest possible
/// representation with up to one fractional digit.
///
/// Rounds towards zero.
pub fn concise_duration(d: Duration) -> String {
	use std::fmt::Write;
	let nanos = d.as_nanos();
	if nanos == 0 {
		return "0 s".to_string();
	}
	for (unit, scale) in DURATION_TABLE.iter() {
		let scale = u128::from(*scale);
		if nanos >= scale {
			// whole part
			let a = nanos / scale;
			let rem = nanos % scale;
			// first fractional digit
			let b = rem * 10 / scale;
			let mut buf = String::new();
			let _ = write!(&mut buf, "{a}");
			if b != 0 {
				let _ = write!(&mut buf, ".{b}");
			}
			let _ = write!(&mut buf, " {unit}");
			return buf;
		}
	}
	format!("{nanos} ns")
}

/// A counterpart of [parsing::exact_duration](crate::parsing::duration) that produces the
/// string from which the exact same duration can be parsed.
pub fn exact_duration(d: Duration) -> String {
	let (number, unit) = get_duration_unit(d.as_nanos());
	format!("{number} {unit}")
}

fn get_duration_unit(nanos: u128) -> (u128, &'static str) {
	if nanos == 0 {
		return (0, "s");
	}
	for (unit, scale) in DURATION_TABLE.iter() {
		let scale = u128::from(*scale);
		if nanos.is_multiple_of(scale) {
			return (nanos / scale, unit);
		}
	}
	(nanos, "ns")
}

/// Hex-formatted hash without truncation.
pub fn hash(hash: &[u8; 32]) -> String {
	use std::fmt::Write;
	let mut s = String::with_capacity(64);
	for byte in hash.iter() {
		let _ = write!(&mut s, "{byte:02x}");
	}
	s
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn amount_works() {
		assert_eq!(amount(0u32), "0");
		assert_eq!(amount(999u32), "999");
		assert_eq!(amount(9_999u32), "9999");
		assert_eq!(amount(10_000u32), "10K");
		assert_eq!(amount(10_049u32), "10K");
		assert_eq!(amount(10_050u32), "10.1K");
		assert_eq!(amount(100_499u32), "100K");
		assert_eq!(amount(100_500u32), "101K");
		assert_eq!(amount(1_004_999u32), "1M");
		assert_eq!(amount(1_005_000u32), "1.01M");
		assert_eq!(amount(10_049_999u32), "10M");
		assert_eq!(amount(10_050_000u32), "10.1M");
	}

	#[test]
	fn exact_bytes_works() {
		assert_eq!("0 B", exact_bytes(0));
		assert_eq!("1 B", exact_bytes(1));
		assert_eq!("1 KiB", exact_bytes(1024));
		assert_eq!("1 MiB", exact_bytes(1024 * 1024));
	}

	#[test]
	fn concise_duration_works() {
		assert_eq!("0 s", concise_duration(Duration::ZERO));
		assert_eq!("1 s", concise_duration(Duration::from_secs(1)));
		assert_eq!("2 m", concise_duration(Duration::from_secs(120)));
		assert_eq!("2 m", concise_duration(Duration::from_secs(123)));
		assert_eq!("1 m", concise_duration(Duration::from_secs(60)));
		assert_eq!("1 h", concise_duration(Duration::from_secs(60 * 60)));
		assert_eq!("1 d", concise_duration(Duration::from_secs(60 * 60 * 24)));
		assert_eq!("1 w", concise_duration(Duration::from_secs(60 * 60 * 24 * 7)));
		assert_eq!("1.5 s", concise_duration(Duration::from_millis(1500)));
		assert_eq!("1.5 ms", concise_duration(Duration::from_nanos(1_500_000)));
		assert_eq!("1.5 µs", concise_duration(Duration::from_nanos(1_500)));
	}

	#[test]
	fn exact_duration_works() {
		assert_eq!("0 s", exact_duration(Duration::ZERO));
		assert_eq!("1 s", exact_duration(Duration::from_secs(1)));
		assert_eq!("123 s", exact_duration(Duration::from_secs(123)));
		assert_eq!("1 m", exact_duration(Duration::from_secs(60)));
		assert_eq!("1 h", exact_duration(Duration::from_secs(60 * 60)));
		assert_eq!("1 d", exact_duration(Duration::from_secs(60 * 60 * 24)));
		assert_eq!("1 w", exact_duration(Duration::from_secs(60 * 60 * 24 * 7)));
	}
}
