use std::fmt::Write as _;
use cbvault_format::cbh::annotations::GameAnnotations;
use cbvault_format::codepage::CodePage;
use cbvault_format::error::Result;
use cbvault_format::game::annotations::timing::Evaluation;
use cbvault_format::game::annotations::{Annotation, GAME_POSITION, Quotation, arrows, language, squares, timing};
#[derive(Clone, Copy)]
pub(super) struct At {
pub(super) stored: u32,
}
pub(super) trait Notes {
fn before(&mut self, at: At, out: &mut String) -> Result<bool>;
fn after(&mut self, at: At, out: &mut String) -> Result<bool>;
}
pub(super) struct Bare;
impl Notes for Bare {
#[inline]
fn before(&mut self, _at: At, _out: &mut String) -> Result<bool> {
Ok(false)
}
#[inline]
fn after(&mut self, _at: At, _out: &mut String) -> Result<bool> {
Ok(false)
}
}
pub(super) struct Commentary<'a> {
pub(super) anns: &'a GameAnnotations<'a>,
pub(super) index: &'a [(i32, u32)],
pub(super) evp: &'a str,
pub(super) parts: &'a mut Parts,
pub(super) moves: u32,
pub(super) last: Option<u32>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) enum Sep {
#[default]
None,
Space,
Semicolon,
}
#[derive(Debug, Default)]
pub(super) struct Part {
pub(super) sep: Sep,
pub(super) text: String,
}
#[derive(Debug, Default)]
pub(super) struct Parts {
items: Vec<Part>,
len: usize,
}
impl Parts {
#[inline]
pub(super) fn clear(&mut self) {
self.len = 0;
}
#[inline]
pub(super) fn as_slice(&self) -> &[Part] {
&self.items[..self.len]
}
#[inline]
pub(super) fn push(&mut self, sep: Sep) -> &mut Part {
if self.len < self.items.len() {
let part = &mut self.items[self.len];
part.sep = sep;
part.text.clear();
} else {
self.items.push(Part { sep, text: String::new() });
}
self.len += 1;
let at = self.len - 1;
&mut self.items[at]
}
#[inline]
pub(super) fn remove(&mut self, at: usize) {
let last = self.len - 1;
if at != last {
self.items.swap(at, last);
}
self.len = last;
}
#[inline]
pub(super) fn get_mut(&mut self, at: usize) -> &mut Part {
&mut self.items[at]
}
#[inline]
pub(super) fn last_mut(&mut self) -> &mut Part {
&mut self.items[self.len - 1]
}
pub(super) fn capacity(&self) -> usize {
self.items.capacity() * size_of::<Part>() + self.items.iter().map(|p| p.text.capacity()).sum::<usize>()
}
}
impl Commentary<'_> {
pub(super) fn game_comment(&mut self, out: &mut String) -> Result<()> {
let (anns, index) = (self.anns, self.index);
self.parts.clear();
let items = [range(index, GAME_POSITION), &[][..]];
graphics(self.parts, anns, items)?;
medals(self.parts, anns, items)?;
if !self.evp.is_empty() {
let part = self.parts.push(Sep::None);
part.text.push_str(self.evp);
}
texts(self.parts, anns, items, false)?;
comment(out, self.parts, "", " ");
Ok(())
}
}
impl Notes for Commentary<'_> {
fn before(&mut self, at: At, out: &mut String) -> Result<bool> {
let (anns, index) = (self.anns, self.index);
self.parts.clear();
texts(self.parts, anns, [range(index, at.stored as i32), &[][..]], true)?;
Ok(comment(out, self.parts, "", " "))
}
fn after(&mut self, at: At, out: &mut String) -> Result<bool> {
let (anns, index, moves, last) = (self.anns, self.index, self.moves, self.last);
let own = range(index, at.stored as i32);
let past: &[(i32, u32)] = if last == Some(at.stored) { past_range(index, moves) } else { &[] };
let items = [own, past];
nags(out, anns, items)?;
self.parts.clear();
evaluations(self.parts, anns, items)?;
times(self.parts, anns, items)?;
graphics(self.parts, anns, items)?;
medals(self.parts, anns, items)?;
texts(self.parts, anns, items, false)?;
past_texts(self.parts, anns, past)?;
Ok(comment(out, self.parts, " ", ""))
}
}
#[inline]
fn push_number(out: &mut String, n: i64) {
if n < 0 {
out.push('-');
push_u64(out, n.unsigned_abs());
} else {
push_u64(out, n as u64);
}
}
#[inline]
fn push_u64(out: &mut String, n: u64) {
let mut digits = [0u8; 20];
let mut i = digits.len();
let mut n = n;
loop {
i -= 1;
digits[i] = b'0' + (n % 10) as u8;
n /= 10;
if n == 0 {
break;
}
}
for &b in &digits[i..] {
out.push(char::from(b));
}
}
#[inline]
fn push_two(out: &mut String, n: u8) {
out.push(char::from(b'0' + n / 10));
out.push(char::from(b'0' + n % 10));
}
#[inline]
fn range(index: &[(i32, u32)], position: i32) -> &[(i32, u32)] {
let start = index.partition_point(|&(p, _)| p < position);
let len = index[start..].iter().take_while(|&&(p, _)| p == position).count();
&index[start..start + len]
}
#[inline]
fn past_range(index: &[(i32, u32)], moves: u32) -> &[(i32, u32)] {
let moves = i32::try_from(moves).unwrap_or(i32::MAX);
&index[index.partition_point(|&(p, _)| p < moves)..]
}
fn nags(out: &mut String, anns: &GameAnnotations<'_>, items: [&[(i32, u32)]; 2]) -> Result<()> {
for &(_, offset) in items.into_iter().flatten() {
let Annotation::Symbols { on_move, on_position, prefix } = anns.annotation_at(offset)? else {
continue;
};
for nag in [prefix, on_move, on_position] {
if nag != 0 {
out.push(' ');
out.push('$');
let _ = write!(out, "{nag}");
}
}
}
Ok(())
}
fn medals(parts: &mut Parts, anns: &GameAnnotations<'_>, items: [&[(i32, u32)]; 2]) -> Result<()> {
for &(_, offset) in items.into_iter().flatten() {
let Annotation::Other { code: 0x22, data } = anns.annotation_at(offset)? else { continue };
let Ok(bytes) = <[u8; 4]>::try_from(data) else { continue };
let part = parts.push(Sep::None);
part.text.push_str("[%mdl ");
push_number(&mut part.text, i64::from(u32::from_be_bytes(bytes)));
part.text.push(']');
}
Ok(())
}
fn evaluations(parts: &mut Parts, anns: &GameAnnotations<'_>, items: [&[(i32, u32)]; 2]) -> Result<()> {
for &(_, offset) in items.into_iter().flatten() {
let Annotation::Other { code: 0x21, data } = anns.annotation_at(offset)? else { continue };
let Some((score, depth)) = timing::evaluation_pair(data) else { continue };
let part = parts.push(Sep::None);
part.text.push_str("[%eval ");
push_number(&mut part.text, i64::from(score));
part.text.push(',');
push_number(&mut part.text, i64::from(depth));
part.text.push(']');
}
Ok(())
}
fn times(parts: &mut Parts, anns: &GameAnnotations<'_>, items: [&[(i32, u32)]; 2]) -> Result<()> {
for &(_, offset) in items.into_iter().flatten() {
let Annotation::Other { code: 0x07, data } = anns.annotation_at(offset)? else { continue };
let Some((h, m, s, hundredths)) = timing::time_spent(data, true) else { continue };
let part = parts.push(Sep::Space);
part.text.push_str("[%emt ");
push_number(&mut part.text, i64::from(h));
part.text.push(':');
push_two(&mut part.text, m);
part.text.push(':');
push_two(&mut part.text, s);
if hundredths != 0 {
part.text.push('.');
push_two(&mut part.text, hundredths);
}
part.text.push(']');
}
Ok(())
}
fn graphics(parts: &mut Parts, anns: &GameAnnotations<'_>, items: [&[(i32, u32)]; 2]) -> Result<()> {
let csl_at = parts.as_slice().len();
parts.push(Sep::None);
let mut marks = 0usize;
for &(_, offset) in items.into_iter().flatten() {
let Annotation::Squares(data) = anns.annotation_at(offset)? else { continue };
for (c, square) in squares(data) {
let Some(colour) = colour(c) else { continue };
if marks == 0 {
parts.get_mut(csl_at).text.push_str("[%csl ");
} else {
parts.get_mut(csl_at).text.push(',');
}
parts.get_mut(csl_at).text.push(colour);
push_name(&mut parts.get_mut(csl_at).text, square);
marks += 1;
}
}
let squares_written = marks > 0;
if squares_written {
parts.get_mut(csl_at).text.push(']');
} else {
parts.remove(csl_at);
}
let cal_at = parts.as_slice().len();
parts.push(Sep::None);
let mut marks = 0usize;
for &(_, offset) in items.into_iter().flatten() {
let Annotation::Arrows(data) = anns.annotation_at(offset)? else { continue };
for (c, from, to) in arrows(data) {
let Some(colour) = colour(c) else { continue };
if marks == 0 {
parts.get_mut(cal_at).text.push_str("[%cal ");
} else {
parts.get_mut(cal_at).text.push(',');
}
parts.get_mut(cal_at).text.push(colour);
push_name(&mut parts.get_mut(cal_at).text, from);
push_name(&mut parts.get_mut(cal_at).text, to);
marks += 1;
}
}
if marks == 0 {
parts.remove(cal_at);
} else {
parts.get_mut(cal_at).text.push(']');
if squares_written {
let arrows = parts.last_mut().text.clone();
parts.remove(cal_at);
parts.get_mut(csl_at).text.push_str(&arrows);
}
}
Ok(())
}
fn texts(parts: &mut Parts, anns: &GameAnnotations<'_>, items: [&[(i32, u32)]; 2], before: bool) -> Result<()> {
let key_of = |&(_, offset): &(i32, u32)| -> Result<Option<(i32, u32)>> {
match anns.annotation_at(offset)? {
Annotation::Text { before: b, language, .. } if b == before => Ok(Some((write_key(language), offset))),
Annotation::Other { code: 0x13, .. } if !before => Ok(Some((QUOTATION_KEY, offset))),
_ => Ok(None),
}
};
let mut count = 0usize;
let mut ordered = true;
let mut prev = i32::MIN;
for item in items.into_iter().flatten() {
let Some((key, _)) = key_of(item)? else { continue };
if key == QUOTATION_KEY {
continue;
}
ordered &= key >= prev;
prev = key;
count += 1;
}
let write = |parts: &mut Parts, offset: u32| -> Result<()> {
match anns.annotation_at(offset)? {
Annotation::Text { language, text, .. } => {
let part = parts.push(Sep::Space);
let part = &mut part.text;
let own_language = language != language::ANY;
clean_into(part, text, own_language);
}
Annotation::Other { code: 0x13, data } => {
if let Some(q) = Quotation::parse_classic(data) {
let part = parts.push(Sep::Semicolon);
q.chessbase_text_into(&mut part.text);
}
}
_ => {}
}
Ok(())
};
if count <= 1 || ordered {
for item in items.into_iter().flatten() {
if let Some((key, offset)) = key_of(item)?
&& key == QUOTATION_KEY
{
write(parts, offset)?;
}
}
for item in items.into_iter().flatten() {
if let Some((key, offset)) = key_of(item)?
&& key != QUOTATION_KEY
{
write(parts, offset)?;
}
}
return Ok(());
}
let mut found: Vec<(i32, u32)> = Vec::with_capacity(count);
for item in items.into_iter().flatten() {
if let Some(hit) = key_of(item)? {
found.push(hit);
}
}
found.sort_by_key(|&(key, _)| key);
for &(_, offset) in &found {
write(parts, offset)?;
}
Ok(())
}
const QUOTATION_KEY: i32 = i32::MIN;
#[inline]
fn write_key(language: u16) -> i32 {
if language == language::ANY { -1 } else { i32::from(language) }
}
fn past_texts(parts: &mut Parts, anns: &GameAnnotations<'_>, past: &[(i32, u32)]) -> Result<()> {
let mut at = 0;
while at < past.len() {
let position = past[at].0;
let end = at + past[at..].iter().take_while(|&&(p, _)| p == position).count();
let group = [&past[at..end], &[][..]];
texts(parts, anns, group, true)?;
texts(parts, anns, group, false)?;
at = end;
}
Ok(())
}
fn comment(out: &mut String, parts: &Parts, pre: &str, post: &str) -> bool {
if parts.as_slice().iter().all(|p| p.text.is_empty()) {
return false;
}
out.push_str(pre);
out.push('{');
let mut first = true;
for part in parts.as_slice() {
if part.text.is_empty() {
continue;
}
if !first {
match part.sep {
Sep::None => {}
Sep::Space => out.push(' '),
Sep::Semicolon => out.push(';'),
}
}
out.push_str(&part.text);
first = false;
}
out.push('}');
out.push_str(post);
true
}
fn clean_into(out: &mut String, bytes: &[u8], own_language: bool) {
out.clear();
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
let bytes = &bytes[..end];
match std::str::from_utf8(bytes) {
Ok(text) => push_clean_text(out, text, own_language, false),
Err(_) => {
let mut text = String::with_capacity(bytes.len());
let mut rest = bytes;
while !rest.is_empty() {
match std::str::from_utf8(rest) {
Ok(tail) => {
text.push_str(tail);
break;
}
Err(e) => {
let good = e.valid_up_to();
if good > 0 {
text.push_str(std::str::from_utf8(&rest[..good]).expect("valid UTF-8"));
}
let bad = e.error_len().unwrap_or(rest.len() - good).min(rest.len() - good);
for &b in &rest[good..good + bad] {
text.push(CodePage::WESTERN.char(b));
}
rest = &rest[good + bad..];
}
}
}
push_clean_text(out, &text, own_language, true);
}
}
while out.ends_with(' ') {
out.pop();
}
}
fn push_clean_text(out: &mut String, text: &str, own_language: bool, code_page: bool) {
let bare = text.chars().count() == 1;
let mut cr = false;
let bytes = text.as_bytes();
let mut at = 0;
while at < bytes.len() {
let run = at;
while at < bytes.len() && plain(bytes[at]) {
at += 1;
}
if at > run {
out.reserve(at - run);
for &b in &bytes[run..at] {
out.push(char::from(b));
}
continue;
}
let c = text[at..].chars().next().expect("a character where the index is");
at += c.len_utf8();
if (code_page && c == DIAGRAM) || c == '\u{e005}' || (own_language && !bare && c == '\u{4}') {
out.push_str("[#]");
cr = false;
continue;
}
let c = if code_page { export_char(c) } else { c };
if c == '\n' && cr {
continue;
}
cr = c == '\r';
match c {
'{' => out.push('('),
'}' => out.push(')'),
c if c.is_whitespace() => {
if !out.is_empty() {
out.push(' ');
}
}
c => out.push(c),
}
}
}
#[inline]
const fn plain(b: u8) -> bool {
b > b' ' && b < 0x7f && b != b'{' && b != b'}'
}
const DIAGRAM: char = '\u{17e}';
#[inline]
fn export_char(c: char) -> char {
match c {
'\u{a2}' => '\u{e024}', '\u{a3}' => '\u{e025}', '\u{a4}' => '\u{e028}', '\u{a5}' => '\u{e027}', '\u{a6}' => '\u{e026}', '\u{a7}' => '\u{e029}', '\u{aa}' => '\u{e01b}', '\u{ab}' => '\u{e00e}', '\u{ac}' => '\u{e01f}', '\u{ad}' => '\u{e001}', '\u{ae}' => '\u{e002}', '\u{af}' => '\u{e003}', '\u{b0}' => '\u{e000}', '\u{a9}' => '\u{e000}', '\u{b1}' => '\u{e00a}', '\u{2022}' => '\u{e02a}', '\u{b2}' => '\u{e02f}', '\u{b3}' => '\u{e02e}', '\u{b5}' => '\u{e01a}', '\u{b9}' => '\u{e020}', '\u{bb}' => '\u{e00f}', '\u{201a}' => '\u{e013}', '\u{201c}' => '\u{e01c}', '\u{201d}' => '\u{e01e}', '\u{201e}' => '\u{e017}', '\u{2018}' => '\u{e018}', '\u{2019}' => '\u{e019}', '\u{2020}' => '\u{e023}', '\u{2021}' => '\u{e01d}', '\u{2026}' => '\u{e00d}', '\u{2030}' => '\u{e02d}', '\u{203a}' => '\u{e00c}', '\u{2122}' => '\u{e021}', '\u{192}' => '\u{e012}', '\u{f7}' => '\u{e009}', '\u{fe}' => '\u{e008}', c => c,
}
}
#[inline]
fn colour(c: u8) -> Option<char> {
match c {
2 => Some('G'),
3 => Some('Y'),
4 => Some('R'),
7 => Some('B'),
_ => None,
}
}
#[inline]
fn push_name(out: &mut String, n: u8) {
if !(1..=64).contains(&n) {
return;
}
let n = u32::from(n) - 1;
out.push((b'a' + (n / 8) as u8) as char);
out.push((b'1' + (n % 8) as u8) as char);
}
pub(super) fn write_evp(out: &mut String, entries: &[Evaluation]) {
let Some(last) = entries.len().checked_sub(1) else { return };
let first = entries.iter().position(|e| e.flag != 0xff).unwrap_or(0);
let _ = write!(out, "[%evp {first},{last}");
for e in &entries[first..] {
let _ = write!(out, ",{}", e.profile_value());
}
out.push(']');
}