use std::io::{BufRead, Read, Seek};
use std::mem;
use itertools::Itertools;
use regex::Regex;
use super::super::errors::IoReadError;
use super::super::file_format::FileFormatReader;
use super::helpers::{
AUTHOR, BOXORDER, COLLECTION, CollectionData, DATE_CREATED, DATE_OF_LAST_CHANGE, GOALORDER,
NumberedLine, PuzzleData, RAW_FILE_NOTES, SOLVER, SavedGameData, TAG_DELIMITERS, TITLE,
VARIANT,
};
use crate::game_board::is_board_line;
use crate::saved_game::is_saved_game_line;
use crate::utilities::is_blank;
use crate::{Collection, Tessellation};
pub struct SokReader {
data: CollectionData,
notes_parser: SokNotesTags,
legacy_sokoban_plus: bool,
}
impl FileFormatReader for SokReader {
fn read<R>(&mut self, src: &mut R) -> Result<Collection, IoReadError>
where
R: Read + BufRead + Seek,
{
self.read_and_split(src)?;
self.parse_title_lines();
self.parse_notes()?;
self.data.create_collection(self.legacy_sokoban_plus)
}
}
impl SokReader {
pub fn new(tessellation_hint: Option<Tessellation>, legacy_sokoban_plus: bool) -> Self {
Self {
legacy_sokoban_plus,
data: CollectionData::new(tessellation_hint),
notes_parser: SokNotesTags::new(match tessellation_hint {
Some(t) => t,
None => Tessellation::Sokoban,
}),
}
}
fn read_and_split<R>(&mut self, src: &mut R) -> Result<(), IoReadError>
where
R: Read + BufRead + Seek,
{
let mut lines: Vec<NumberedLine> = Vec::new();
for (idx, line) in src.lines().enumerate() {
lines.push(NumberedLine { ln: line?, idx });
}
self.split_input(&mut lines);
Ok(())
}
fn parse_notes(&mut self) -> Result<bool, IoReadError> {
self.notes_parser
.extract_collection_attributes(&mut self.data)?;
Self::cleanup_whitespace(&mut self.data.notes);
for puzzle in &mut self.data.puzzles {
self.notes_parser
.extract_puzzle_attributes(puzzle, self.data.header_tessellation_hint)?;
for save_game in &mut puzzle.saved_games {
self.notes_parser.extract_save_game_attributes(save_game);
Self::cleanup_whitespace(&mut save_game.notes);
}
Self::cleanup_whitespace(&mut puzzle.notes);
}
Ok(true)
}
fn parse_title_lines(&mut self) {
let puzzles_len = self.data.puzzles.len();
for puzzle_idx in 0..puzzles_len {
let title = Self::get_and_remove_title_line(Self::choose_ref_notes_before_puzzle(
&mut self.data,
puzzle_idx,
));
self.data.puzzles[puzzle_idx].title = title;
let saves_len = self.data.puzzles[puzzle_idx].saved_games.len();
for save_idx in 0..saves_len {
let title = Self::get_and_remove_title_line(
Self::choose_ref_notes_before_save_game(&mut self.data, puzzle_idx, save_idx),
);
self.data.puzzles[puzzle_idx].saved_games[save_idx].title = title;
}
}
}
fn split_input(&mut self, input_lines: &mut Vec<NumberedLine>) {
let first_board_line_idx = input_lines
.iter()
.position(|num_ln| is_board_line(&num_ln.ln));
match first_board_line_idx {
Some(idx) => {
Self::drain_into(input_lines, idx, &mut self.data.notes);
}
None => {
mem::swap(&mut self.data.notes, input_lines);
}
}
self.split_into_puzzles(input_lines);
self.split_into_saved_games();
}
fn split_into_puzzles(&mut self, input_lines: &mut Vec<NumberedLine>) {
while !input_lines.is_empty() {
let mut puzzle = PuzzleData::new(input_lines[0].idx);
let first_note_line_idx = input_lines
.iter()
.position(|num_ln| !is_board_line(&num_ln.ln));
if let Some(idx) = first_note_line_idx {
puzzle.board = input_lines[..idx]
.iter()
.map(|num_ln| &num_ln.ln)
.join("\n");
let mut dropped: Vec<NumberedLine> = Vec::new();
Self::drain_into(input_lines, idx, &mut dropped);
} else {
puzzle.board = input_lines.iter().map(|num_ln| &num_ln.ln).join("\n");
input_lines.clear();
}
if !input_lines.is_empty() {
let first_board_line_idx = input_lines
.iter()
.position(|num_ln| is_board_line(&num_ln.ln));
match first_board_line_idx {
Some(idx) => {
Self::drain_into(input_lines, idx, &mut puzzle.notes);
}
None => {
mem::swap(&mut puzzle.notes, input_lines);
}
}
}
self.data.puzzles.push(puzzle);
}
}
fn split_into_saved_games(&mut self) {
for puzzle in &mut self.data.puzzles {
let mut input_lines = mem::take(&mut puzzle.notes);
let first_moves_line_idx = input_lines
.iter()
.position(|num_ln| is_saved_game_line(&num_ln.ln));
match first_moves_line_idx {
Some(idx) => {
Self::drain_into(&mut input_lines, idx, &mut puzzle.notes);
}
None => {
puzzle.notes = mem::take(&mut input_lines);
}
}
while !input_lines.is_empty() {
let mut saved_game = SavedGameData::new(input_lines[0].idx);
let first_note_line_idx = input_lines
.iter()
.position(|num_ln| !is_saved_game_line(&num_ln.ln));
if let Some(idx) = first_note_line_idx {
saved_game.moves = input_lines[..idx]
.iter()
.map(|num_ln| num_ln.ln.trim())
.join("");
let mut dropped: Vec<NumberedLine> = Vec::new();
Self::drain_into(&mut input_lines, idx, &mut dropped);
} else {
saved_game.moves = input_lines.iter().map(|num_ln| num_ln.ln.trim()).join("");
input_lines.clear();
}
if !input_lines.is_empty() {
let first_moves_line_idx = input_lines
.iter()
.position(|num_ln| is_saved_game_line(&num_ln.ln));
match first_moves_line_idx {
Some(idx) => {
Self::drain_into(&mut input_lines, idx, &mut saved_game.notes);
}
None => {
mem::swap(&mut input_lines, &mut saved_game.notes);
}
}
}
puzzle.saved_games.push(saved_game);
}
}
}
fn choose_ref_notes_before_puzzle(
data: &mut CollectionData, puzzle_idx: usize,
) -> &mut Vec<NumberedLine> {
if puzzle_idx == 0 {
&mut data.notes
} else {
let previous_puzzle = &mut data.puzzles[puzzle_idx - 1];
if previous_puzzle.saved_games.is_empty() {
&mut previous_puzzle.notes
} else {
let last_idx = previous_puzzle.saved_games.len() - 1;
&mut previous_puzzle.saved_games[last_idx].notes
}
}
}
fn choose_ref_notes_before_save_game(
data: &mut CollectionData, puzzle_idx: usize, saved_game_idx: usize,
) -> &mut Vec<NumberedLine> {
let puzzle = &mut data.puzzles[puzzle_idx];
if saved_game_idx == 0 {
&mut puzzle.notes
} else {
&mut puzzle.saved_games[saved_game_idx - 1].notes
}
}
fn get_and_remove_title_line(notes: &mut Vec<NumberedLine>) -> String {
let candidate_index = notes.iter().rposition(|num_ln| !is_blank(&num_ln.ln));
match candidate_index {
Some(idx) => {
let mut preceding_index: Option<usize> = None;
if idx > 0 {
preceding_index = Some(idx - 1);
}
let mut following_index: Option<usize> = None;
if idx < notes.len() - 1 {
following_index = Some(idx + 1);
}
let preceding_ok = if let Some(pi) = preceding_index {
is_blank(¬es[pi].ln)
} else {
true
};
let following_ok = if let Some(fi) = following_index {
is_blank(¬es[fi].ln)
} else {
true
};
if preceding_ok && following_ok {
let title_line = notes[idx].ln.trim().to_string();
notes.remove(idx);
title_line
} else {
String::new()
}
}
None => String::new(),
}
}
fn drain_into(from: &mut Vec<NumberedLine>, at: usize, into: &mut Vec<NumberedLine>) {
for num_ln in &mut from[..at] {
into.push(NumberedLine {
idx: num_ln.idx,
ln: mem::take(&mut num_ln.ln),
});
}
let mut remainder: Vec<NumberedLine> = Vec::with_capacity(from[at..].len());
for num_ln in &mut from[at..] {
remainder.push(NumberedLine {
idx: num_ln.idx,
ln: mem::take(&mut num_ln.ln),
});
}
mem::swap(from, &mut remainder);
}
fn cleanup_whitespace(notes: &mut Vec<NumberedLine>) {
let mut cleaned: Vec<NumberedLine> = Vec::new();
for num_ln in notes
.iter_mut()
.dedup_by(|l, r| is_blank(&l.ln) && is_blank(&r.ln))
{
cleaned.push(NumberedLine {
idx: num_ln.idx,
ln: mem::take(&mut num_ln.ln),
});
}
if !cleaned.is_empty() && is_blank(&cleaned[0].ln) {
cleaned.remove(0);
}
if !cleaned.is_empty() && is_blank(cleaned.last().map_or("", |l| &l.ln)) {
cleaned.pop();
}
mem::swap(&mut cleaned, notes);
}
}
struct SokNotesTags {
tag_splitter: Regex,
supplied_tessellation_hint: Tessellation,
}
impl SokNotesTags {
#[expect(clippy::expect_used)]
pub fn new(tessellation_hint: Tessellation) -> Self {
Self {
tag_splitter: Regex::new(&format!(
"(?<tag>.*?)[{TAG_DELIMITERS}](?<value>.*)"
))
.expect("Bug: failed to construct Regex!"),
supplied_tessellation_hint: tessellation_hint,
}
}
pub fn is_raw_file_notes_line(line: &str) -> bool { line.starts_with(RAW_FILE_NOTES) }
pub fn get_tag_data(&self, tag: &str, line: &str) -> Option<String> {
let caps = self.tag_splitter.captures(line)?;
(tag.to_lowercase().trim() == caps["tag"].to_lowercase().trim())
.then(|| caps["value"].trim().to_string())
}
pub fn extract_collection_attributes(
&self, dest: &mut CollectionData,
) -> Result<bool, IoReadError> {
let mut remaining_lines: Vec<NumberedLine> = Vec::new();
for line in &mut dest.notes {
if let Some(s) = self.get_tag_data(TITLE, &line.ln) {
dest.title = s;
} else if let Some(s) = self.get_tag_data(AUTHOR, &line.ln) {
dest.author = s;
} else if let Some(s) = self.get_tag_data(COLLECTION, &line.ln) {
dest.title = s;
} else if let Some(s) = self.get_tag_data(DATE_CREATED, &line.ln) {
dest.created_at = s;
} else if let Some(s) = self.get_tag_data(DATE_OF_LAST_CHANGE, &line.ln) {
dest.updated_at = s;
} else if let Some(s) = self.get_tag_data(VARIANT, &line.ln) {
let tessellation = Self::parse_tessellation_str(line.idx, s.as_str())?;
dest.was_tessellation_in_header = true;
dest.header_tessellation_hint = Some(tessellation);
} else if !Self::is_raw_file_notes_line(&line.ln) && !is_blank(&line.ln) {
remaining_lines.push(NumberedLine {
idx: line.idx,
ln: mem::take(&mut line.ln),
});
}
}
mem::swap(&mut dest.notes, &mut remaining_lines);
Ok(true)
}
pub fn extract_puzzle_attributes(
&self, dest: &mut PuzzleData, collection_header_tessellation_hint: Option<Tessellation>,
) -> Result<bool, IoReadError> {
let mut remaining_lines: Vec<NumberedLine> = Vec::new();
let mut tessellation: Option<Tessellation> = None;
for line in &mut dest.notes {
if let Some(s) = self.get_tag_data(TITLE, &line.ln) {
dest.title = s;
} else if let Some(s) = self.get_tag_data(AUTHOR, &line.ln) {
dest.author = s;
} else if let Some(s) = self.get_tag_data(BOXORDER, &line.ln) {
dest.boxorder = s;
dest.sokplus_ln_idx = line.idx;
} else if let Some(s) = self.get_tag_data(GOALORDER, &line.ln) {
dest.goalorder = s;
dest.sokplus_ln_idx = line.idx;
} else if let Some(s) = self.get_tag_data(VARIANT, &line.ln) {
let t = Self::parse_tessellation_str(line.idx, s.as_str())?;
tessellation = Some(t);
} else {
remaining_lines.push(NumberedLine {
idx: line.idx,
ln: mem::take(&mut line.ln),
});
}
}
if let Some(t) = tessellation {
dest.tessellation = t;
} else if let Some(t) = collection_header_tessellation_hint {
dest.tessellation = t;
} else {
dest.tessellation = self.supplied_tessellation_hint;
}
mem::swap(&mut dest.notes, &mut remaining_lines);
Ok(true)
}
pub fn extract_save_game_attributes(&self, dest: &mut SavedGameData) {
let mut remaining_lines: Vec<NumberedLine> = Vec::new();
for line in &mut dest.notes {
if let Some(s) = self.get_tag_data(TITLE, &line.ln) {
dest.title = s;
} else if let Some(s) = self.get_tag_data(AUTHOR, &line.ln) {
dest.solver = s;
} else if let Some(s) = self.get_tag_data(SOLVER, &line.ln) {
dest.solver = s;
} else {
remaining_lines.push(NumberedLine {
idx: line.idx,
ln: mem::take(&mut line.ln),
});
}
}
mem::swap(&mut dest.notes, &mut remaining_lines);
}
fn parse_tessellation_str(ln_pos: usize, value: &str) -> Result<Tessellation, IoReadError> {
match value.trim().to_lowercase().as_str() {
"hexoban" => Ok(Tessellation::Hexoban),
"octoban" => Ok(Tessellation::Octoban),
"trioban" => Ok(Tessellation::Trioban),
"sokoban" | "" => Ok(Tessellation::Sokoban),
_ => Err(IoReadError::new_unknown_tessellation_error(ln_pos, value)),
}
}
}