use std::borrow::Cow;
use std::io::Write;
use ansiterm::{ANSIString, ANSIStrings};
use itertools::Itertools;
use strum::{EnumCount as _, IntoEnumIterator};
use unicode_width::UnicodeWidthChar;
use crate::fileset::{FileKind, FileStage};
use crate::game::Game;
use crate::report::errors::Errors;
use crate::report::output_style::Styled;
use crate::report::report_struct::pointer_indentation;
use crate::report::{LogReportMetadata, LogReportPointers, OutputStyle, PointedMessage, Severity};
const SPACES_PER_TAB: usize = 4;
const MAX_IDLE_SPACE: usize = 16;
pub fn log_report<O: Write + Send>(
errors: &Errors,
output: &mut O,
report: &LogReportMetadata,
pointers: &LogReportPointers,
additional: usize,
) {
let indentation = pointer_indentation(pointers);
log_line_title(errors, output, report);
let iterator = pointers.iter();
let mut previous = None;
for pointer in iterator {
log_pointer(errors, output, previous, pointer, indentation, report.severity);
previous = Some(pointer);
}
if additional > 0 {
log_count(errors, output, indentation, additional);
}
if let Some(info) = &report.info {
log_line_info(errors, output, indentation, info);
}
if let Some(wiki) = &report.wiki {
log_line_wiki(errors, output, indentation, wiki);
}
_ = writeln!(output);
}
pub fn log_summary<O: Write + Send>(
output: &mut O,
styles: &OutputStyle,
reports: &Vec<(&LogReportMetadata, Cow<'_, LogReportPointers>, usize)>,
) {
let mut counts = [0usize; Severity::COUNT];
for (metadata, _, additional) in reports {
counts[metadata.severity as usize] += 1 + additional;
}
let line = Severity::iter()
.rev()
.flat_map(|sev| {
[
styles.style(Styled::Tag(sev, true)).paint(format!("{sev}")),
styles.style(Styled::Tag(sev, false)).paint(format!(": {}", counts[sev as usize])),
styles.style(Styled::Default).paint(", "),
]
})
.collect_vec();
_ = writeln!(output, "{}", ANSIStrings(&line[..line.len() - 1]));
}
fn log_pointer<O: Write + Send>(
errors: &Errors,
output: &mut O,
previous: Option<&PointedMessage>,
pointer: &PointedMessage,
indentation: usize,
severity: Severity,
) {
if previous.is_none() || !previous.unwrap().loc.same_file(pointer.loc) {
log_line_file_location(errors, output, pointer, indentation);
}
if pointer.loc.line == 0 {
return;
}
if let Some(line) = errors.cache.get_line(pointer.loc) {
let (line, removed, spaces) = line_spacing(line);
log_line_from_source(errors, output, pointer, indentation, line, spaces);
log_line_carets(errors, output, pointer, indentation, line, removed, spaces, severity);
}
}
fn log_line_title<O: Write + Send>(errors: &Errors, output: &mut O, report: &LogReportMetadata) {
let line: &[ANSIString<'static>] = &[
errors
.styles
.style(Styled::Tag(report.severity, true))
.paint(format!("{}", report.severity)),
errors.styles.style(Styled::Tag(report.severity, false)).paint(format!("({})", report.key)),
errors.styles.style(Styled::Default).paint(": "),
errors.styles.style(Styled::ErrorMessage).paint(report.msg.clone()),
];
_ = writeln!(output, "{}", ANSIStrings(line));
}
fn log_line_info<O: Write + Send>(errors: &Errors, output: &mut O, indentation: usize, info: &str) {
let line_info: &[ANSIString<'static>] = &[
errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
errors.styles.style(Styled::Default).paint(" "),
errors.styles.style(Styled::Location).paint("="),
errors.styles.style(Styled::Default).paint(" "),
errors.styles.style(Styled::InfoTag).paint("Info:"),
errors.styles.style(Styled::Default).paint(" "),
errors.styles.style(Styled::Info).paint(info.to_string()),
];
_ = writeln!(output, "{}", ANSIStrings(line_info));
}
fn log_line_wiki<O: Write + Send>(errors: &Errors, output: &mut O, indentation: usize, wiki: &str) {
let line_info: &[ANSIString<'static>] = &[
errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
errors.styles.style(Styled::Default).paint(" "),
errors.styles.style(Styled::Location).paint("="),
errors.styles.style(Styled::Default).paint(" "),
errors.styles.style(Styled::InfoTag).paint("Wiki:"),
errors.styles.style(Styled::Default).paint(" "),
errors.styles.style(Styled::Info).paint(wiki.to_string()),
];
_ = writeln!(output, "{}", ANSIStrings(line_info));
}
fn log_count<O: Write + Send>(errors: &Errors, output: &mut O, indentation: usize, count: usize) {
let line_count: &[ANSIString<'static>] = &[
errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
errors.styles.style(Styled::Location).paint("-->"),
errors.styles.style(Styled::Default).paint(" "),
errors.styles.style(Styled::Location).paint(format!("and {count} other locations")),
];
_ = writeln!(output, "{}", ANSIStrings(line_count));
}
fn log_line_file_location<O: Write + Send>(
errors: &Errors,
output: &mut O,
pointer: &PointedMessage,
indentation: usize,
) {
let line_filename: &[ANSIString<'static>] = &[
errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
errors.styles.style(Styled::Location).paint("-->"),
errors.styles.style(Styled::Default).paint(" "),
errors.styles.style(Styled::Location).paint(format!(
"[{}{}]",
kind_tag(errors, pointer.loc.kind),
stage_tag(pointer.loc.stage)
)),
errors.styles.style(Styled::Default).paint(" "),
errors
.styles
.style(Styled::Location)
.paint(format!("{}", pointer.loc.pathname().display())),
];
_ = writeln!(output, "{}", ANSIStrings(line_filename));
}
fn log_line_from_source<O: Write + Send>(
errors: &Errors,
output: &mut O,
pointer: &PointedMessage,
indentation: usize,
line: &str,
spaces: usize,
) {
let line_from_source: &[ANSIString<'static>] = &[
errors.styles.style(Styled::Location).paint(format!("{:indentation$}", pointer.loc.line,)),
errors.styles.style(Styled::Default).paint(" "),
errors.styles.style(Styled::Location).paint("|"),
errors.styles.style(Styled::Default).paint(" "),
errors.styles.style(Styled::SourceText).paint(format!("{:spaces$}{line}", "")),
];
_ = writeln!(output, "{}", ANSIStrings(line_from_source));
}
#[allow(clippy::too_many_arguments)]
fn log_line_carets<O: Write + Send>(
errors: &Errors,
output: &mut O,
pointer: &PointedMessage,
indentation: usize,
line: &str,
removed: usize,
spaces: usize,
severity: Severity,
) {
if pointer.length == 0 {
return;
}
let mut spacing = String::new();
for c in line.chars().take((pointer.loc.column as usize).saturating_sub(removed + 1)) {
if c == '\t' {
spacing.push('\t');
} else {
for _ in 0..c.width().unwrap_or(0) {
spacing.push(' ');
}
}
}
let line_carets: &[ANSIString] = &[
errors.styles.style(Styled::Default).paint(format!("{:indentation$}", "")),
errors.styles.style(Styled::Default).paint(" "),
errors.styles.style(Styled::Location).paint("|"),
errors.styles.style(Styled::Default).paint(format!(
"{:width$}{spacing}",
"",
width = spaces + 1
)),
errors.styles.style(Styled::Tag(severity, true)).paint(format!(
"{:^^width$}",
"",
width = pointer.length
)),
errors.styles.style(Styled::Default).paint(" "),
errors
.styles
.style(Styled::Tag(severity, true))
.paint(pointer.msg.as_deref().map_or("", |_| "<-- ")),
errors
.styles
.style(Styled::Tag(severity, true))
.paint(pointer.msg.as_deref().unwrap_or("")),
];
_ = writeln!(output, "{}", ANSIStrings(line_carets));
}
pub(crate) fn kind_tag<'a>(errors: &'a Errors<'a>, kind: FileKind) -> &'a str {
match kind {
FileKind::Internal => "Internal",
FileKind::Clausewitz => "Clausewitz",
FileKind::Jomini => "Jomini",
FileKind::Vanilla => match Game::game() {
#[cfg(feature = "ck3")]
Game::Ck3 => "CK3",
#[cfg(feature = "vic3")]
Game::Vic3 => "Vic3",
#[cfg(feature = "imperator")]
Game::Imperator => "Imperator",
#[cfg(feature = "eu5")]
Game::Eu5 => "EU5",
#[cfg(feature = "hoi4")]
Game::Hoi4 => "Hoi4",
},
FileKind::Dlc(idx) => &errors.loaded_dlcs_labels[idx as usize],
FileKind::LoadedMod(idx) => &errors.loaded_mods_labels[idx as usize],
FileKind::Mod => "MOD",
}
}
fn stage_tag(stage: FileStage) -> &'static str {
match stage {
#[cfg(feature = "eu5")]
FileStage::LoadingScreen => "(loadscreen)",
#[cfg(feature = "eu5")]
FileStage::MainMenu => "(menu)",
#[cfg(feature = "eu5")]
FileStage::InGame => "",
FileStage::NoStage => "",
}
}
fn line_spacing(line: &str) -> (&str, usize, usize) {
let mut remove = 0;
let mut spaces = 0;
for c in line.chars() {
if c == ' ' {
spaces += 1;
} else if c == '\t' {
spaces += SPACES_PER_TAB;
} else {
break;
}
remove += 1;
}
spaces = spaces.min(MAX_IDLE_SPACE);
(&line[remove..], remove, spaces)
}