//! The various formats that [fif](crate) can output to.

use std::io::{self, Write};
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;

use snailquote::escape;

use crate::scan_error::ScanError;
use crate::{Findings, BACKEND};

/// The current version of fif, as defined in Cargo.toml.
const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");

#[doc(hidden)]
type Entries = [Result<Findings, ScanError>];

enum Writable<'a> {
	String(&'a str),
	Path(&'a PathBuf),
	Space,
	Newline,
}

// the lifetime of a lifetime
impl<'a> From<&'a str> for Writable<'a> {
	fn from(s: &'a str) -> Writable<'a> {
		Writable::String(s)
	}
}

impl<'a> From<&'a PathBuf> for Writable<'a> {
	fn from(p: &'a PathBuf) -> Writable<'a> {
		Writable::Path(p)
	}
}

fn smart_write<W: Write>(f: &mut W, writeables: &[Writable]) -> io::Result<()> {
	// ehhhh
	for writeable in writeables {
		match writeable {
			Writable::Space => write!(f, " ")?,
			Writable::Newline => writeln!(f,)?,
			Writable::String(s) => write!(f, "{}", s)?,
			Writable::Path(path) => {
				if let Some(string) = path.to_str() {
					write!(f, "{}", escape(string))?
				} else {
					write!(f, "'''")?;
					#[cfg(unix)]
					f.write_all(&*path.as_os_str().as_bytes())?;
					#[cfg(windows)]
					write!(f, "{}", path.as_os_str().to_string_lossy())?; // TODO: implement bonked strings for windows
																											// f.write_all(&*path.as_os_str().encode_wide().collect::<Vec<u16>>())?;
					write!(f, "'''")?
				}
			}
		}
	}
	Ok(())
}

pub trait Format {
	fn new() -> Self;
	fn rename<W: Write>(&self, f: &mut W, from: &PathBuf, to: &PathBuf) -> io::Result<()>;
	fn no_known_extension<W: Write>(&self, f: &mut W, path: &PathBuf) -> io::Result<()>;
	fn unreadable<W: Write>(&self, f: &mut W, path: &PathBuf) -> io::Result<()>;
	fn unknown_type<W: Write>(&self, f: &mut W, path: &PathBuf) -> io::Result<()>;
	fn header<W: Write>(&self, entries: &Entries, f: &mut W) -> io::Result<()>;
	fn footer<W: Write>(&self, entries: &Entries, f: &mut W) -> io::Result<()>;

	fn write_all<W: Write>(&self, entries: &Entries, f: &mut W) -> io::Result<()> {
		// TODO: clean this up - it's kinda messy
		self.header(entries, f)?;

		for entry in entries {
			match entry {
				Ok(finding) => {
					if let Some(ext) = finding.recommended_extension() {
						self.rename(f, &finding.file, &finding.file.with_extension(ext.as_str()))?
					} else {
						self.no_known_extension(f, &finding.file)?
					}
				}

				Err(error) => {
					// something went wrong 0uo
					match error {
						// failed to read the file
						ScanError::File(path) => self.unreadable(f, path)?,
						// file was read successfully, but we couldn't determine a mimetype
						ScanError::Mime(path) => self.unknown_type(f, path)?,
					}
				}
			}
		}

		self.footer(entries, f)
	}
}

// TODO: maybe make a batch script version for windows
/// Bourne-Shell compatible script.
pub struct Script {}

impl Format for Script {
	fn new() -> Self {
		Self {}
	}

	fn rename<W: Write>(&self, f: &mut W, from: &PathBuf, to: &PathBuf) -> io::Result<()> {
		smart_write(
			f,
			&[
				"mv -v -i -- ".into(),
				from.into(),
				Writable::Space,
				to.into(),
				Writable::Newline,
			],
		)
	}

	fn no_known_extension<W: Write>(&self, f: &mut W, path: &PathBuf) -> io::Result<()> {
		smart_write(
			f,
			&["echo No known extension for ".into(), path.into(), Writable::Newline],
		)
	}

	fn unreadable<W: Write>(&self, f: &mut W, path: &PathBuf) -> io::Result<()> {
		smart_write(f, &["# Failed to read ".into(), path.into(), Writable::Newline])
	}

	fn unknown_type<W: Write>(&self, f: &mut W, path: &PathBuf) -> io::Result<()> {
		smart_write(
			f,
			&[
				"# Failed to detect mime type for ".into(),
				path.into(),
				Writable::Newline,
			],
		)
	}

	fn header<W: Write>(&self, _: &Entries, f: &mut W) -> io::Result<()> {
		writeln!(
			f,
			"#!/usr/bin/env sh\n# Generated by fif {} ({} backend)",
			VERSION.unwrap_or("???"),
			BACKEND
		)?;
		writeln!(f, "\nset -e\n")
	}

	fn footer<W: Write>(&self, _: &Entries, f: &mut W) -> io::Result<()> {
		writeln!(f, "\necho 'Done.'")
	}
}
