//! Higher-level rolled up install routines.
//!
//! These were originally just internals of src/install.rs, but are big
//! enough and otherwise useful enough to share.
use crate::core::RtDirs;
use crate::metadata::MetadataLine;
use crate::metadata::SplitTypes;
use crate::util::{self, plural, path_join};
use crate::core::install as install;

use std::io::{stdout, Write as _};
use std::collections::HashMap;
use std::path::{Path, PathBuf};


/// How many files (or hardlinks, etc) do we need in a list to install
/// before we (if enabled) spin up a threadpool and parallelize it?  We
/// should consider making this more configurably in the longer run, but
/// it makes sense to put a floor on it anyway.
///
/// If there're a hundred, we should.  If there're 2, we shouldn't.  So
/// pick a reasonable starting value.
const MIN_PARALLEL: usize = 16;


// Predef some RE's for reuse.  Too bad this isn't const...  I guess I
// could do some Once tricks.
use regex_lite::Regex;
pub(crate) fn re_linker_file() -> Regex
{
	Regex::new(r"^/libexec/ld-elf.*\.so\.[0-9]+$")
		.expect("I can rite regex")
}
pub(crate) fn re_so_file() -> Regex
{
	Regex::new(r".*/lib/.*\.so\.[0-9]+$")
		.expect("I can rite regex")
}



impl SplitTypes
{
	/// Once we have a SplitTypes, install it all.
	pub(crate) fn install(&self, rtdirs: &RtDirs, basedir: &Path,
			dry: bool, parallel: bool)
			-> Result<(), anyhow::Error>
	{
		let sti = self.mk_install(rtdirs, basedir, dry, parallel);
		sti.install()
	}


	pub(crate) fn mk_install<'a>(&'a self, rtdirs: &'a RtDirs,
			basedir: &'a Path, dry: bool, parallel: bool)
			-> SplitTypesInstall
	{
		SplitTypesInstall {
			st: self,
			rtdirs, basedir, dry, parallel,
		}
	}
}



/// A SplitTypes derivative for running an install process
#[derive(Debug, Clone)]
pub(crate) struct SplitTypesInstall<'a>
{
	/// The SplitTypes with all the stuff to install
	st: &'a SplitTypes,

	/// Info about our runtime dirs; mostly used to find the hashfiles to
	/// decompress into output files.
	rtdirs: &'a RtDirs,

	/// Basedir to install into
	basedir: &'a Path,

	/// Whether it's a dry run
	dry: bool,

	/// Enable parallelization for installation
	parallel: bool,
}


impl SplitTypesInstall<'_>
{
	pub(crate) fn install(&self) -> Result<(), anyhow::Error>
	{
		// Now start installing the bits.  f-u.sh just goes through the
		// manifest lexically and splats things in place.  I'm going to
		// do it by type instead; handle all the dirs, then the files,
		// then the links.
		//
		// Potential parallelization depends on the 'par' arg here
		// (disabled for directories, so we can ignore ordering issues),
		// and whether self.parallel is set (higher levels said "OK, do
		// that").
		let dry_do_one = |hm: &HashMap<_, _>, par: bool|
				-> Result<(), anyhow::Error> {
			let par = par && self.parallel;
			match self.dry {
				true => {
					println!("  (dry run, not installing)");
					Ok(())
				},
				false => self.do_mdl_installs(hm, par)
			}
		};

		let dlen = self.st.dirs.len();
		let flen = self.st.files.len();
		let slen = self.st.syms.len();
		let hlen = self.st.hards.len();

		if dlen > 0
		{
			println!("{} director{}", dlen,
				if dlen > 1 { "ies" } else { "y" });
			dry_do_one(&self.st.dirs, false)?;
		}

		if flen > 0
		{
			println!("{} file{}", flen, plural(flen));
			dry_do_one(&self.st.files, true)?;
		}

		if slen > 0
		{
			println!("{} symlink{}", slen, plural(slen));
			dry_do_one(&self.st.syms, true)?;
		}

		if hlen > 0
		{
			println!("{} hardlink{}", hlen, plural(hlen));
			dry_do_one(&self.st.hards, true)?;
		}



		// Second pass: set schg flags.
		let flen = self.st.flags.len();
		if flen > 0 && self.dry
		{
			println!("{flen} flag{}  (dry run)", plural(flen));
		}
		else if flen > 0 && util::euid() != 0
		{
			println!("Not setting {flen} flag{} because you're not root.",
					plural(flen));
		}
		else if flen > 0
		{
			// Not bothering to progress this, there's rarely
			// non-single-digit.
			print!("Setting {flen} flag{}...  ", plural(flen));
			stdout().flush()?;
			for (p, mdl) in &self.st.flags
			{
				let flags = mdl.flags().expect("Must exist if we get here");
				install::flags(p, flags)?;
			}
			println!("Done.");
		}

		Ok(())
	}



	/// Iterate over a set of MetadataLine's, doing the installs.
	///
	/// In practice, when we call this, all the MetadataLine's are of a
	/// single type, since we do one type at a time.  The generic
	/// MetadataLine layer is really just to let us wrap up all the stuff
	/// that happens _around_ installing the lines.
	///
	/// This is slightly complicated by the desire to do things in a
	/// particular order.  Since we get run here via install_split(), it
	/// already handles the "do dirs first" etc bits of the ordering.  So
	/// we'll impl f-u.sh's ordering that mostly applies to files.
	///
	/// Strictly, this gives us a different answer, because we're not
	/// intermingling files, symlinks, and hardlinks like f-u.sh is.  It
	/// feels like there are drawbacks both ways though, so I'm going to
	/// forge ahead.
	fn do_mdl_installs(&self, hm: &HashMap<PathBuf, MetadataLine>,
			parallel: bool)
			-> Result<(), anyhow::Error>
	{
		use crate::metadata::MetadataLine as ML;

		let pblen = hm.len().try_into().unwrap();
		let pb = util::mk_progress(pblen);

		// Wrap up doing the actual work of installing a set of paths.
		let install_one = |dst: &Path, mdl: &MetadataLine|
				-> Result<(), anyhow::Error> {
			use crate::metadata::MetadataLine as ML;

			match mdl
			{
				ML::Dir(m)      => install::dir(dst, m)?,
				ML::File(m)     => install::file(dst, m, self.rtdirs)?,
				ML::SymLink(m)  => install::symlink(dst, m)?,
				ML::HardLink(m) => install::link(dst, m, self.basedir)?,
				_ => unreachable!("Impossible!"),
			}

			Ok(())
		};

		let install_them = |paths: &[&PathBuf]|
				-> Result<(), anyhow::Error> {
			for p in paths
			{
				let mdl = hm.get(*p).unwrap();
				let dst = path_join(self.basedir, p);
				install_one(&dst, mdl)?;
				pb.inc(1);
			}

			Ok(())
		};


		// Parallel version.
		// Note that unlike the sequential, this doesn't exit on first
		// error; it'll always run everything, and then return an error
		// if it found one.
		let mut parpool = None;
		let mut install_them_par = |paths: &[&PathBuf]|
				-> Result<(), anyhow::Error> {
			let pool = parpool.get_or_insert_with(|| {
				use rayon::ThreadPoolBuilder as TPB;
				let njobs = crate::core::jobs::cpu();
				TPB::new().num_threads(njobs as usize).build().unwrap()
			});

			pool.scope(|_s| {
				use rayon::prelude::*;
				let retss = paths.par_iter().map(|p| {
					let mdl = hm.get(*p).unwrap();
					let dst = path_join(self.basedir, p);
					install_one(&dst, mdl)?;
					pb.inc(1);
					Ok::<(), anyhow::Error>(())
				}).collect_vec_list();

				// Dump out the "first" error if we got one.
				for rets in retss { for ret in rets { ret?; } }

				// OK then
				Ok(())
			})
		};


		// If one entry is a dir, they're all dirs, so just shortcut and make
		// them all.
		if let Some(ML::Dir(_)) = hm.values().next()
		{
			let paths: Vec<_> = hm.keys().sorted().collect();
			// Never parallelify dirs
			let ret = install_them(&paths);
			pb.finish();
			return ret;
		}


		// As f-u.sh does, split into 3 batches: runtime linker, shared libs,
		// other stuff.  Very few linkers, up to a couple hundred libs, and
		// however much there is everything else, as our size guesses.
		let mut lds    = Vec::with_capacity(2);
		let mut shlibs = Vec::with_capacity(128);
		let mut rest   = Vec::with_capacity(hm.len());

		// What're the rules...
		let linker = re_linker_file();
		let shlib = re_so_file();

		use itertools::Itertools as _;
		for p in hm.keys().sorted()
		{
			// regex_lite really wants a &str for matching, so str-ify our
			// path I guess...
			let pstr = String::from_utf8_lossy(p.as_os_str().as_encoded_bytes());

			if linker.is_match(&pstr)
			{ lds.push(p); }
			else if shlib.is_match(&pstr)
			{ shlibs.push(p); }
			else
			{ rest.push(p); }
		}


		// OK, now go through 'em in order.
		//
		// This is probably the place to start for implementing parallelism
		// in this.  The simplest way is a threadpool-y version of
		// do_mdl_installs_inner().  That's potentially a little silly, since
		// we're going to call this 3 times, and spinning up threadpools for
		// each of those is...  a bit daft.
		let mut doit = |v: &[_]| {
			if v.is_empty() { return Ok(()); }
			let dopar = parallel && v.len() >= MIN_PARALLEL;
			match dopar {
				true  => install_them_par(v),
				false => install_them(v),
			}
		};
		doit(&lds)?;
		doit(&shlibs)?;
		doit(&rest)?;


		// And that's it
		pb.finish();
		Ok(())
	}
}
