use std::fmt::Display;

use num::{Integer, Zero, cast::AsPrimitive};

/// Pretty-print a quantity of bytes
pub fn pp_bytes_si<T: Integer + Display + Zero + Clone + AsPrimitive<f64>>(bytes: T) -> String {
	const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
	const THRESHOLD: f64 = 1000.0;

	if bytes.is_zero() {
		return "0 B".to_owned();
	}

	let bytes_f: f64 = bytes.as_();
	let unit_index = (bytes_f.log10() / THRESHOLD.log10()).floor() as usize;
	let unit_index = unit_index.min(UNITS.len() - 1);

	let value = bytes_f / THRESHOLD.powi(unit_index as i32);

	if unit_index == 0 {
		format!("{} {}", bytes, UNITS[unit_index])
	} else if value >= 100.0 {
		format!("{:.0} {}", value, UNITS[unit_index])
	} else if value >= 10.0 {
		format!("{:.1} {}", value, UNITS[unit_index])
	} else {
		format!("{:.2} {}", value, UNITS[unit_index])
	}
}

/// Pretty-print a quantity of bytes
pub fn pp_bytes_b2<T: Integer + Display + Zero + Clone + AsPrimitive<f64>>(bytes: T) -> String {
	const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
	const THRESHOLD: f64 = 1024.0;

	if bytes.is_zero() {
		return "0 B".to_owned();
	}

	let bytes_f: f64 = bytes.as_();
	let unit_index = (bytes_f.log2() / THRESHOLD.log2()).floor() as usize;
	let unit_index = unit_index.min(UNITS.len() - 1);

	let value = bytes_f / THRESHOLD.powi(unit_index as i32);

	if unit_index == 0 {
		format!("{} {}", bytes, UNITS[unit_index])
	} else if value >= 100.0 {
		format!("{:.0} {}", value, UNITS[unit_index])
	} else if value >= 10.0 {
		format!("{:.1} {}", value, UNITS[unit_index])
	} else {
		format!("{:.2} {}", value, UNITS[unit_index])
	}
}

/// Pretty-print an integer with comma separators
pub fn pp_int_commas<T: Integer + Display>(value: T) -> String {
	let s = value.to_string();
	let mut result = String::new();
	let bytes = s.as_bytes();

	for (i, &byte) in bytes.iter().enumerate() {
		if i > 0 && (bytes.len() - i).is_multiple_of(3) {
			result.push(',');
		}
		result.push(byte as char);
	}

	result
}

/// Convert bytes to a hex string
#[inline]
pub fn to_hex(bytes: &[u8]) -> String {
	bytes.iter().map(|b| format!("{:02x}", b)).collect()
}

/// Parse a hex string as bytes
pub fn from_hex(hex_str: &str) -> Result<Vec<u8>, &'static str> {
	if !hex_str.len().is_multiple_of(2) {
		return Err("hex string must have even length");
	}

	let mut bytes = Vec::with_capacity(hex_str.len() / 2);

	for chunk in hex_str.as_bytes().chunks(2) {
		let hex_byte = std::str::from_utf8(chunk).map_err(|_err| "invalid UTF-8 in hex string")?;
		let byte = u8::from_str_radix(hex_byte, 16).map_err(|_err| "invalid hex character")?;
		bytes.push(byte);
	}

	Ok(bytes)
}

/// Truncate a string from the left, replacing the first character with "…" if truncated
pub fn truncate_left(length: usize, string: &str) -> String {
	if string.chars().count() <= length {
		return string.to_owned();
	}

	if length == 0 {
		return String::new();
	}

	if length == 1 {
		return "…".to_owned();
	}

	let chars: Vec<char> = string.chars().collect();
	let start_index = chars.len() - (length - 1);
	let mut result = String::from("…");
	result.extend(&chars[start_index..]);
	result
}

/// Truncate a string from the right, replacing the last character with "…" if truncated
pub fn truncate_right(length: usize, string: &str) -> String {
	if string.chars().count() <= length {
		return string.to_owned();
	}

	if length == 0 {
		return String::new();
	}

	if length == 1 {
		return "…".to_owned();
	}

	let chars: Vec<char> = string.chars().collect();
	let mut result = String::new();
	result.extend(&chars[0..length - 1]);
	result.push('…');
	result
}
