//! Funcs for bspatch'ing files
//!
//! Here we just take an input file, patch it into an output, and mention
//! what we made.  We expect a list of input hashes and patch names,
//! where the patch names look like "<inputhash>-<outputhash>", so we
//! will try to patch the file "<tmpdir>/<inputhash>" into the file
//! "<tmpdir>/<outputhash>".  If there's no input file, we'll try
//! decompressing it from "<filesdir>/<inputhash>.gz" first.
use std::path::Path;

use crate::util::hash::Sha256HashBuf;


/// Given a set of patches and a few dirs, apply the patches to create
/// the output files, check the hashes, and store compressed variants.
/// This does a double-step of the patching-pool, followed by the
/// hashcheck-pool.
///
/// On success, returns a Vec<> of the output hashes that succeeded.
/// That means there should be a <filesdir>/<hash>.gz for each of them,
/// and maybe a <tmpdir>/<hash> if keep=true.  The patchfile themselves
/// are untouched.
///
/// A returned error is presumably fatal, meaning something blew up
/// unexpectedly.  However, it's not necessarily broken if we don't
/// successfully apply and create each patch; these patches are only ever
/// an optimization to avoid downloading the whole outfile anyway, so an
/// Err() result only means "damn, we blew up hard", not "well, we didn't
/// succeeed at everything we tried".
pub(crate) fn apply(patches: &[String], tdir: &Path, fdir: &Path)
		-> Result<(), anyhow::Error>
{
	use crate::util::plural_es;

	// OK, first, we apply all the patches.
	let prlen = patches.len();
	let es = plural_es(prlen);
	println!("Trying to apply {prlen} patch{es}.");
	let patchres = rayon(patches, tdir, fdir);

	// Now, any errors, we pretty much ignore.  Any successes are now
	// <outhash>'s that exist as files in <tmpdir>.
	let ( oks, _errs ) = patchres;
	let oklen = oks.len();
	println!("{oklen} successful.");
	if oklen == 0 { return Ok(()); }

	// Check the hashes, and compress them into <filesdir> if they match.
	// Failures may be interesting, but don't globally matter here; if
	// patching fails, we just fetch the final file.
	//
	// XXX patch::rayon gave these back as String's, which is a little
	// inconvenient...  revisit
	let es = plural_es(oklen);
	println!("Checking {oklen} hash{es}.");
	use crate::core::hashcheck as hc;
	let (oks, _errs) = hc::rayon(&oks, tdir, fdir, true);
	let oklen = oks.len();
	println!("{oklen} successful.");
	if oklen == 0 { return Ok(()); }

	// Success
	Ok(())
}




/// Pool entry point: given a set of patches, try to apply them all.
///
/// The patch names look like '<inhash>-<outhash>', so we can derive the
/// input and output files just from that name.  We assume they're
/// already present.
///
/// We don't attempt to validate either of the hashes here; this step
/// will be followed up by validating the hashes of the output files and
/// compressing them into <filesdir> in a separate hashcheck run anyway,
/// so it doesn't much matter if something is wrong in this step.
pub(crate) fn rayon(patches: &[String], tdir: &Path, fdir: &Path)
		-> (Vec<Sha256HashBuf>, Vec<PatchErr>)
{
	let npatch: u64 = patches.len().try_into().unwrap();
	let pb = crate::util::mk_progress(npatch);


	// Setup rayon pool, and start bouncing over the requests
	use rayon::ThreadPoolBuilder as TPB;
	let njobs = crate::core::jobs::cpu();
	let pool = TPB::new().num_threads(njobs as usize)
			.build().unwrap();

	let (oks, errs) = pool.scope(|_s| {
		use rayon::prelude::*;

		// Run 'em all through
		let pruns = patches.into_par_iter().map(|patch| {
			let r = patch_worker(patch, tdir, fdir, true);
			pb.inc(1);
			r
		});

		// Collect
		let mut oks = Vec::new();
		let mut errs = Vec::new();
		for press in pruns.collect_vec_list()
		{
			for pres in press
			{
				match pres
				{
					Ok(f)  => oks.push(f),
					Err(e) => errs.push(e),
				}
			}
		}

		(oks, errs)
	});


	// And return
	pb.finish();
	(oks, errs)
}





/// Error in the patching
#[derive(Debug)]
#[derive(thiserror::Error)]
pub(crate) enum PatchErr
{
	/// Invalid patch name.  This probably indicates the programmer
	/// screwed up.
	#[error("Invalid patch name: '{0}'")]
	BadPatch(String),

	/// Can't find input <inhas> file.
	#[error("No input file: '{0}'")]
	Missing(String),

	/// Some sort of IO error.  This could be our filemaking, or
	/// patching; they both get here.
	#[error("I/O error: {0}")]
	IO(#[from] std::io::Error),

	/// Compression error.
	#[error("Compression error: {0}")]
	Compress(anyhow::Error),
}



/// Patch'ing a single file
fn patch_worker(patch: &str, tdir: &Path, fdir: &Path, keep: bool)
		-> Result<Sha256HashBuf, PatchErr>
{
	use PatchErr as PE;
	use crate::util::path_join;
	use crate::util::compress;
	use crate::util::bspatch;

	// First, split up the patch name into the input and output hashes
	let mut inout: Vec<&str> = patch.splitn(2, '-').collect();
	if inout.len() != 2 { return Err(PE::BadPatch(patch.to_string())); }
	let outhash = inout.pop().ok_or_else(|| PE::BadPatch(patch.to_string()))?;
	let inhash  = inout.pop().ok_or_else(|| PE::BadPatch(patch.to_string()))?;
	if !inout.is_empty() { return Err(PE::BadPatch(patch.to_string())); }
	let _ = inout;

	// Now, where's the source file...
	let srcpath = path_join(tdir, inhash);
	if !srcpath.is_file()
	{
		// Try decompressing it out of filesdir
		let compfile = format!("{inhash}.gz");
		match compress::decompress_gz_dirs(fdir, tdir, &compfile)
		{
			Ok(_) => (),
			Err(e) => {
				// Separate out "didn't find file" from any other IO
				// error.  Now that it's going through anyhow for the
				// context, we need to downcast to check that...
				if let Some(ioe) = e.downcast_ref::<std::io::Error>()
				{
					use std::io::ErrorKind as EK;
					if ioe.kind() == EK::NotFound
					{ Err(PE::Missing(inhash.to_string()))? }
				}

				// Otherwise it's some other compression error
				Err(PE::Compress(e))?;
			},
		}
	}
	if !srcpath.is_file()
	{
		// Shouldn't be possible at this point, but...
		Err(PE::Missing(inhash.to_string()))?;
	}

	// OK, we have the source file.  In theory, we could do some tests on
	// the patchfile and output file here, but I'm going to just assume
	// presence.  We _should_ know the patchfiles exists, or it wouldn't
	// have been fed into us, and if we can't write into the outfile,
	// we'll just transfer up the IO error.  So from here, we just build
	// the paths and pass them to our patcher.
	let dstpath = path_join(tdir, outhash);
	let patchpath = path_join(tdir, patch);
	bspatch::patch(&srcpath, &dstpath, &patchpath)?;

	// Success!  Maybe cleanup, and return.
	if !keep { std::fs::remove_file(&srcpath)?; }
	Ok(outhash.parse().unwrap())
}
