#![feature(drain_filter)]
#![feature(never_type)]

#[macro_use]
extern crate log;
extern crate tokio;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::sync::Arc;
use std::path::{Path, PathBuf};

use tokio::fs::try_exists;
use tokio::sync::RwLock;
use futures::TryFutureExt;
use futures::{stream, StreamExt};
use tokio_util::compat::TokioAsyncWriteCompatExt;
use archive_reader::Archive;
use async_zip::ZipEntryBuilder;
use async_zip::tokio::write::ZipFileWriter;
use indicatif::MultiProgress;
use image::ImageOutputFormat;
use image::ImageEncoder;


mod cli;
mod format;
mod logger;
mod error;
mod paths;

use error::Error;
use cli::Config;
use cli::FormatFileExt;


#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
	let mut args = cli::parse();

	let multibar = MultiProgress::new();
	multibar.set_move_cursor(true);
	let bar_completed = cli::main_progress_bar(&multibar)?;


	logger::init(args.verbose, Some(multibar.clone()))?;
	trace!("input args: {:#?}", args);


	debug!("preparing input paths");
	let sources = paths::validate_and_unglob(args.input).await?;

	bar_completed.set_length(sources.len() as _);
	bar_completed.set_position(0 as _);

	debug!("preparing output path");
	let outdir = if let Some(output) = args.output {
		tokio::fs::create_dir_all(&output).await?;
		output
	} else {
		std::env::current_dir()? // TODO: potential inplace race!
	};


	let concurrency = args.jobs_fs;
	args.config.jobs /= concurrency;

	let create_inout_task = |path: PathBuf| {
		let outdir = outdir.clone();
		let config = args.config.clone();
		let multibar = multibar.clone();

		let set_initial_progress = |inout: ProcessInOut| async move { Ok(inout) };

		async move {
			let res = open_inout(path, outdir, &config)
			.and_then(set_initial_progress)
			.and_then(|inout| convert_all(inout, &config, Some(multibar)))
			// .and_then(|res| async move {
			// 	// bar.finish_and_clear();
			// 	res
			// })
			.await;
			// weak.upgrade().map(|bar| bar.finish_and_clear());
			res
		}
	};

	let notify = |res: Result<PathBuf, _>| {
		let bar_completed_ref = &bar_completed;
		async move {
			match res {
				Ok(path) => info!("Complete: {}", path.display()),
				Err(err) => error!("{err}"),
			}
			bar_completed_ref.inc(1);
		}
	};


	stream::iter(sources.into_iter()).map(create_inout_task)
	                                 .buffer_unordered(concurrency)
	                                 .for_each(notify)
	                                 .await;

	multibar.clear()?;
	log::logger().flush();
	Ok(())
}


async fn convert_all(mut inout: ProcessInOut, cfg: &Config, multibar: Option<MultiProgress>) -> Result<PathBuf, Error> {
	let jobs = cfg.jobs;
	trace!("jobs per archive: {jobs}");
	let source = inout.reader.path().to_owned();
	let writer = Arc::new(RwLock::new(&mut inout.writer));

	let bar = multibar.map(|mb| {
		                  let len = inout.total_entries;
		                  let pos = len - inout.entries.len();
		                  let text = inout.reader.path().file_name().unwrap().to_string_lossy().to_string();
		                  cli::sub_progress_bar(&mb, len, pos, text)
	                  });

	let convert_entry = |(_i, name): (_, String)| {
		let name = name.to_owned();
		let reader = inout.reader.clone();
		let bar = &bar;

		// Read entries, then convert them, then write to resulting archive
		async move {
			debug!("reading '{name}'");
			let mut buffer = Vec::new();
			let size = reader.read_file(&name, &mut buffer)?;
			let raw_size = buffer.len();
			let name = name.to_owned();

			if size == 0 {
				Err(format!("no data in '{name}'").into())
			} else {
				debug!("transcoding '{name}'");
				let (name, data) = tokio::spawn(transcode(cfg.clone(), buffer, name.clone())).await??;
				trace!("  new size of '{name}': {} vs. {}", data.len(), raw_size);
				bar.as_ref().map(|bar| bar.inc(1));
				Ok::<_, Error>((data, name))
			}
		}.and_then(|(data, name)| {
			let writer = writer.clone();
			async move {
				debug!("writing '{name}' to output archive");
				let compression = async_zip::Compression::Deflate;
				let builder = ZipEntryBuilder::new(name.as_str().into(), compression).deflate_option(async_zip::DeflateOption::Maximum);
				let mut writer = writer.write().await;
				writer.write_entry_whole(builder, &data).await?;
				Ok(name)
			}
		})
	};

	stream::iter(inout.entries.into_iter()).map(convert_entry)
	                                       .buffer_unordered(jobs)
	                                       .for_each(|res| {
		                                       async move {
			                                       match res {
				                                       Ok(name) => info!("Finished: {name}"),
			                                          Err(err) => error!("{err}"),
			                                       }
		                                       }
	                                       })
	                                       .await;
	inout.writer.close().await?;
	Ok(source)
}


struct ProcessInOut {
	reader: Arc<Archive>,
	/// Inner files remains to process, already resolved and filtered
	entries: Vec<(usize, String)>, // index, name
	/// total number of entries before any filtering
	total_entries: usize,

	writer: ZipFileWriter<tokio::fs::File>,
}

async fn open_inout(source: impl AsRef<Path>, outdir: impl AsRef<Path>, cfg: &Config) -> Result<ProcessInOut, Error> {
	let (reader, entries, total) = archive_reader(&source).await?;
	let output = paths::output_archive_path(&source, &outdir, cfg.archive);
	let writer = archive_writer(output, cfg.force).await?;
	Ok(ProcessInOut { reader: Arc::new(reader),
	                  entries,
	                  writer,
	                  total_entries: total })
}


async fn archive_writer(path: impl AsRef<Path>, force: bool) -> Result<ZipFileWriter<tokio::fs::File>, Error> {
	let path = path.as_ref();
	debug!("opening output: '{}'", path.display());
	let out_exists = try_exists(&path).await?;

	if out_exists && !force {
		return Err(std::io::Error::new(
			std::io::ErrorKind::AlreadyExists,
			format!("Output file already exists {}", path.display()),
		).into());
	}

	if let Some(parent) = path.parent() {
		tokio::fs::create_dir_all(parent).await?;
	}

	let output_file = tokio::fs::OpenOptions::new().write(true)
	                                               .create_new(!out_exists)
	                                               .truncate(force)
	                                               .open(&path)
	                                               .await?;
	let writer = ZipFileWriter::new(output_file.compat_write());
	Ok(writer)
}


async fn archive_reader(path: impl AsRef<Path>) -> Result<(Archive, Vec<(usize, String)>, usize), Error> {
	debug!("opening input: '{}'", path.as_ref().display());
	let mut archive = Archive::open(&path.as_ref());
	archive.block_size(1024 * 1024);

	trace!("filtering inner files");
	let mut total = 0_usize;
	let names: Vec<_> = archive.list_file_names()?
	                           .map(|name| {
		                           total += 1;
		                           name
	                           })
	                           .enumerate()
	                           .filter_map(|(i, name)| name.ok().map(|s| (i, s)))
	                           .filter(|(_i, name)| {
		                           let uri = Path::new(&name);
		                           let skip = name.ends_with("/") ||
		                                      uri.ends_with("Thumbs.db") ||
		                                      uri.file_name() == Some(&OsStr::new(".DS_Store")) ||
		                                      uri.iter()
		                                         .filter(|item| *item == OsStr::new("__MACOSX"))
		                                         .filter(|item| *item == OsStr::new(".DS_Store"))
		                                         .filter(|item| item.len() > 1 && item.to_string_lossy().starts_with("."))
		                                         .next()
		                                         .is_some();

		                           if skip {
			                           trace!("outfiltered inner file: {}", name);
		                           }
		                           !skip
	                           })
	                           .collect();
	debug!("total: {total}, outfiltered: {}", total - names.len());
	Ok((archive, names, total))
}


async fn transcode<S: AsRef<str> + Debug>(cfg: Config, data: Vec<u8>, name: S) -> Result<(String, Vec<u8>), image::ImageError> {
	let cfg = cfg.clone();
	let uri = Path::new(name.as_ref());
	let filename = uri.file_name().expect("filename").to_owned();
	let format = uri.extension()
	                .and_then(|ext| ext.to_str())
	                .map(image::ImageFormat::from_extension)
	                .flatten();

	if format.is_none() {
		if let Some(ext) = uri.extension() {
			match ext.to_string_lossy().as_ref().to_lowercase().as_str() {
				"txt" | "md" | "xml" | "html" | "svg" | "info" | "json" | "yml" | "yaml" => {
					debug!("'{}' Seems to text, so just copying as-is.", uri.display());
					return Ok((name.as_ref().to_string(), data));
				},
				_ => {},
			}
		}
	}

	let out_format = match &cfg.format {
		ImageOutputFormat::Jpeg(_) => ImageOutputFormat::Jpeg(cfg.quality.clamp(0, 100)),
		format => format.to_owned(),
	};

	if Some(out_format.clone()) == format.map(ImageOutputFormat::from) {
		warn!("SKIP with reason: same format: {format:?}");
		return Ok((filename.to_string_lossy().to_string(), data));
	}

	if matches!(format, Some(image::ImageFormat::WebP) | Some(image::ImageFormat::Avif)) {
		warn!("SKIP with reason: src is already good format: {format:?}");
		return Ok((filename.to_string_lossy().to_string(), data));
	}


	let image = if let Some(format) = format {
		image::load_from_memory_with_format(&data, format)
	} else {
		image::load_from_memory(&data)
	};


	if let Ok(image) = image {
		trace!(
		       "original image: {}, len: {} ({format:?}, {:?})",
		       uri.display(),
		       data.len(),
		       image.color()
		);

		let mut output: Vec<u8> = Vec::new();

		match &cfg.format {
			ImageOutputFormat::Avif => {
				use image::codecs::avif::{AvifEncoder, ColorSpace};
				AvifEncoder::new_with_speed_quality(&mut output, cfg.speed, cfg.quality).with_colorspace(ColorSpace::Bt709)
				                                                                        .write_image(
				                                                                                     image.as_bytes(),
				                                                                                     image.width(),
				                                                                                     image.height(),
				                                                                                     image.color(),
				)?;
			},

			ImageOutputFormat::WebP => {
				use image::codecs::webp::{WebPEncoder, WebPQuality};
				let quality = if cfg.lossless {
					WebPQuality::lossless()
				} else {
					WebPQuality::lossy(cfg.quality)
				};
				WebPEncoder::new_with_quality(&mut output, quality).write_image(
				                                                                image.as_bytes(),
				                                                                image.width(),
				                                                                image.height(),
				                                                                image.color(),
				)?;
			},

			ImageOutputFormat::Png => {
				use image::codecs::png::{PngEncoder, CompressionType, FilterType};
				PngEncoder::new_with_quality(&mut output, CompressionType::Best, FilterType::Adaptive).write_image(
				                                                                                                   image.as_bytes(),
				                                                                                                   image.width(),
				                                                                                                   image.height(),
				                                                                                                   image.color(),
				)?;
			},
			format => {
				use std::io::Cursor;
				image.write_to(&mut Cursor::new(&mut output), format.to_owned())?
			},
		}


		let filename = Path::new(&filename).with_extension(cfg.format.ext()).display().to_string();
		trace!("transcoded image: {filename}, len: {} ({:?})", data.len(), cfg.format);


		Ok((filename, output))
	} else {
		warn!("Unable to decode as image: {}, so just copying as-is.", uri.display());
		Ok((name.as_ref().to_string(), data))
	}
}
