//! Accumulating hunk rewrites, and applying them in one pass.
//!
//! `PatchAccumulator` is pure format manipulation, with no awareness of testing:
//! it holds owned (hunk name, new body) pairs,
//! and `Document::patched_all` splices them all into new document text.
//! The compare-or-regen logic that test harnesses want lives a layer up, in the `check` module;
//! this layer is equally at home in any tool that runs hunks and writes results back
//! (the accumulator owns its contents because the new bodies are newly produced values,
//! while reads of a `Document` stay borrow-only throughout).

use std::fmt;

use crate::document::{Document, Hunk, HunkContent};

/// Pending rewrites of code-hunk bodies, keyed by hunk name.
///
/// Recording the same hunk again replaces the earlier value:
/// at this layer, re-recording is a normal use (think: re-running a producer of the body).
/// Treating a re-record as an error is a testing judgement, and is made a layer up.
#[derive(Default, Clone, Debug)]
pub struct PatchAccumulator {
	/// Insertion-ordered; names unique (`put` replaces in place).
	patches: Vec<(String, String)>,
}

impl PatchAccumulator {
	pub fn new() -> PatchAccumulator {
		PatchAccumulator::default()
	}

	/// Record (or replace) the pending body for a hunk.
	pub fn put(&mut self, hunk: impl Into<String>, body: impl Into<String>) {
		let hunk = hunk.into();
		let body = body.into();
		match self.patches.iter_mut().find(|(name, _)| *name == hunk) {
			Some(entry) => entry.1 = body,
			None => self.patches.push((hunk, body)),
		}
	}

	/// The pending body for a hunk, if one is recorded.
	pub fn get(&self, hunk: &str) -> Option<&str> {
		self.patches
			.iter()
			.find(|(name, _)| name == hunk)
			.map(|(_, body)| body.as_str())
	}

	pub fn is_empty(&self) -> bool {
		self.patches.is_empty()
	}
}

/// Problems applying a `PatchAccumulator` to a document.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum PatchError {
	/// A patch names a hunk not present in the document.
	NoSuchHunk {
		name: String,
	},
	/// A patch names a table hunk; accumulated patches are code-hunk bodies.
	/// (Table rewriting stays one-shot via `Document::patched` until something
	/// actually generates tables.)
	NotACodeHunk {
		name: String,
	},
}

impl fmt::Display for PatchError {
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
		match self {
			PatchError::NoSuchHunk {
				name,
			} => write!(f, "patch names hunk {name:?}, which is not in the document"),
			PatchError::NotACodeHunk {
				name,
			} => write!(f, "patch names hunk {name:?}, which is not a code hunk"),
		}
	}
}

impl std::error::Error for PatchError {}

impl Document {
	/// Apply all accumulated patches, producing new document text.
	/// Pure: the caller writes the result back to wherever the text came from.
	///
	/// Each patched hunk keeps its fence lines (and block_tag) unchanged;
	/// all prose and formatting outside the patched bodies is preserved
	/// byte-for-byte, in one splice pass, with no intermediate re-parse.
	pub fn patched_all(&self, acc: &PatchAccumulator) -> Result<String, PatchError> {
		// Resolve every patch to its hunk up front: fail before splicing anything.
		let mut targets: Vec<(&Hunk, &str)> = Vec::with_capacity(acc.patches.len());
		for (name, body) in &acc.patches {
			let h = self.hunk(name).ok_or_else(|| PatchError::NoSuchHunk {
				name: name.clone(),
			})?;
			if !matches!(h.content, HunkContent::Code { .. }) {
				return Err(PatchError::NotACodeHunk {
					name: name.clone(),
				});
			}
			targets.push((h, body));
		}
		targets.sort_by_key(|(h, _)| h.line_comment);

		let mut out: Vec<&str> = Vec::new();
		let mut cursor = 0;
		for (h, body) in targets {
			// Emit through the fence-open line, then the new body.
			// The closing fence (at line_end) is emitted by the next segment.
			for l in &self.lines[cursor..=h.line_comment + 1] {
				out.push(l);
			}
			// A multi-line body goes in as one element; the final join flattens it.
			let trimmed = body.strip_suffix('\n').unwrap_or(body);
			if !trimmed.is_empty() {
				out.push(trimmed);
			}
			cursor = h.line_end;
		}
		for l in &self.lines[cursor..] {
			out.push(l);
		}
		Ok(out.join("\n"))
	}
}

include!("patch_test.rs");
