use std::collections::HashMap;
use std::fmt::{Display, Formatter, Write};
use std::io::Read;
use std::str::FromStr;
use fast_concat::fast_concat;
use pgn_reader::BufferedReader;
use shakmaty::fen::{Fen, ParseFenError};
use super::visitor::{Visitor};
use crate::{Eco, Outcome, Date, Round, RawHeaderOwned, Movetext};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Pgn<M> {
pub event: Option<RawHeaderOwned>,
pub site: Option<RawHeaderOwned>,
pub date: Option<Result<Date, <Date as FromStr>::Err>>,
pub round: Option<Result<Round, <Round as FromStr>::Err>>,
pub white: Option<RawHeaderOwned>,
pub white_elo: Option<Result<u16, <u16 as FromStr>::Err>>,
pub black: Option<RawHeaderOwned>,
pub black_elo: Option<Result<u16, <u16 as FromStr>::Err>>,
pub outcome: Option<Result<Outcome, <Outcome as FromStr>::Err>>,
pub eco: Option<Result<Eco, <Eco as FromStr>::Err>>,
pub time_control: Option<RawHeaderOwned>,
pub fen: Option<Result<Fen, ParseFenError>>,
pub other_headers: HashMap<Vec<u8>, RawHeaderOwned>,
pub movetext: M,
}
#[cfg(feature = "serde")]
impl<M> serde::Serialize for Pgn<M>
where
M: Display,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_str())
}
}
#[cfg(feature = "serde")]
impl<'de, M> serde::Deserialize<'de> for Pgn<M>
where
M: Display + Movetext,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
Self::from_str(<&str>::deserialize(deserializer)?).map_err(D::Error::custom)?.ok_or_else(|| D::Error::custom("no PGN found"))
}
}
impl<M> Default for Pgn<M> where M: Movetext {
fn default() -> Self {
Self {
event: None,
site: None,
date: None,
round: None,
white: None,
white_elo: None,
black: None,
black_elo: None,
outcome: None,
eco: None,
time_control: None,
fen: None,
other_headers: HashMap::new(),
movetext: M::default(),
}
}
}
impl<M> Pgn<M> where M: Movetext {
pub fn from_reader<R>(reader: &mut BufferedReader<R>) -> Result<Option<Self>, std::io::Error> where R: Read {
let mut pgn = Self::default();
let mut pgn_visitor = Visitor::new(&mut pgn);
if reader.read_game(&mut pgn_visitor)? == Some(()) {
pgn_visitor.end_game();
Ok(Some(pgn))
} else {
Ok(None)
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(str: &str) -> Result<Option<Self>, std::io::Error> {
let mut reader = pgn_reader::BufferedReader::new_cursor(str);
Self::from_reader(&mut reader)
}
pub fn from_reader_all<R>(reader: &mut BufferedReader<R>) -> Vec<Result<Self, std::io::Error>> where R: Read {
let mut pgns = Vec::new();
loop {
let mut pgn = Self::default();
let mut pgn_visitor = Visitor::new(&mut pgn);
let result = reader.read_game(&mut pgn_visitor);
match result {
Ok(Some(())) => {
pgn_visitor.end_game();
pgns.push(Ok(pgn));
},
Err(e) => pgns.push(Err(e)),
Ok(None) => break,
}
}
pgns
}
pub fn from_str_all(str: &str) -> Vec<Result<Self, std::io::Error>> {
let mut reader = pgn_reader::BufferedReader::new_cursor(str);
Self::from_reader_all(&mut reader)
}
}
impl<M> Display for Pgn<M> where M: Display {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
macro_rules! push_pgn_header {
($field:ident, $header:expr) => {
if let Some($field) = &self.$field {
f.write_str(&fast_concat!("[", $header, " \"", &$field.decode_utf8_lossy(), "\"]\n"))?;
}
};
(custom_type: $field:ident, $header:expr) => {
if let Some(Ok($field)) = &self.$field {
f.write_str(&fast_concat!("[", $header, " \"", &$field.to_string(), "\"]\n"))?;
}
};
}
macro_rules! any_fields_some {
($($field:ident),+) => {
$(self.$field.is_some())||+
};
}
push_pgn_header!(event, "Event");
push_pgn_header!(site, "Site");
push_pgn_header!(custom_type: date, "Date");
push_pgn_header!(custom_type: round, "Round");
push_pgn_header!(white, "White");
push_pgn_header!(black, "Black");
push_pgn_header!(custom_type: outcome, "Result");
push_pgn_header!(custom_type: white_elo, "WhiteElo");
push_pgn_header!(custom_type: black_elo, "BlackElo");
push_pgn_header!(custom_type: eco, "ECO");
push_pgn_header!(time_control, "TimeControl");
push_pgn_header!(custom_type: fen, "FEN");
for (key, value) in &self.other_headers {
f.write_str(&fast_concat!("[", &String::from_utf8_lossy(key), " \"", &value.decode_utf8_lossy(), "\"]\n"))?;
}
if any_fields_some!(event, site, date, round, white, black, outcome, white_elo, black_elo, eco, time_control, fen) {
f.write_char('\n')?;
}
self.movetext.fmt(f)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::expect_used)]
#[allow(clippy::unreachable)]
#[allow(clippy::panic)]
mod tests {
use shakmaty::san::SanPlus;
use test_case::test_case;
use crate::movetext::{Sans, Variation};
use crate::samples::*;
#[test_case(&sans0())]
#[test_case(&sans1())]
fn san_vec_to_pgn_from_pgn(sample: &PgnSample<Sans<SanPlus>>) {
sample.test();
}
#[test_case(&variation0())]
#[test_case(&variation1())]
#[test_case(&variation2())]
fn variation_to_pgn_from_pgn(sample: &PgnSample<Variation<SanPlus>>) {
sample.test();
}
}