use super::{CommitId, RepoPath};
use crate::{
	error::{Error, Result},
	sync::repository::repo,
};
use git2::{Oid, Repository, Tree};
use scopetime::scope_time;
use std::{
	cmp::Ordering,
	path::{Path, PathBuf},
};

/// `tree_files` returns a list of `FileTree`
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct TreeFile {
	/// path of this file
	pub path: PathBuf,
	/// unix filemode
	pub filemode: i32,
	// internal object id
	id: Oid,
}

/// guarantees sorting the result
pub fn tree_files(
	repo_path: &RepoPath,
	commit: CommitId,
) -> Result<Vec<TreeFile>> {
	scope_time!("tree_files");

	let repo = repo(repo_path)?;

	let commit = repo.find_commit(commit.into())?;
	let tree = commit.tree()?;

	let mut files: Vec<TreeFile> = Vec::new();

	tree_recurse(&repo, &PathBuf::from("./"), &tree, &mut files)?;

	sort_file_list(&mut files);

	Ok(files)
}

fn sort_file_list(files: &mut [TreeFile]) {
	files.sort_by(|a, b| path_cmp(&a.path, &b.path));
}

// applies topologically order on paths sorting
fn path_cmp(a: &Path, b: &Path) -> Ordering {
	let mut comp_a = a.components().peekable();
	let mut comp_b = b.components().peekable();

	loop {
		let a = comp_a.next();
		let b = comp_b.next();

		let a_is_file = comp_a.peek().is_none();
		let b_is_file = comp_b.peek().is_none();

		if a_is_file && !b_is_file {
			return Ordering::Greater;
		} else if !a_is_file && b_is_file {
			return Ordering::Less;
		}

		let cmp = a.cmp(&b);
		if cmp != Ordering::Equal {
			return cmp;
		}
	}
}

/// will only work on utf8 content
pub fn tree_file_content(
	repo_path: &RepoPath,
	file: &TreeFile,
) -> Result<String> {
	scope_time!("tree_file_content");

	let repo = repo(repo_path)?;

	let blob = repo.find_blob(file.id)?;

	if blob.is_binary() {
		return Err(Error::BinaryFile);
	}

	let content = String::from_utf8_lossy(blob.content()).to_string();

	Ok(content)
}

///
fn tree_recurse(
	repo: &Repository,
	path: &Path,
	tree: &Tree,
	out: &mut Vec<TreeFile>,
) -> Result<()> {
	out.reserve(tree.len());

	for e in tree {
		let p = String::from_utf8_lossy(e.name_bytes());
		let path = path.join(p.to_string());
		match e.kind() {
			Some(git2::ObjectType::Blob) => {
				let id = e.id();
				let filemode = e.filemode();
				out.push(TreeFile { path, filemode, id });
			}
			Some(git2::ObjectType::Tree) => {
				let obj = e.to_object(repo)?;
				let tree = obj.peel_to_tree()?;
				tree_recurse(repo, &path, &tree, out)?;
			}
			Some(_) | None => (),
		}
	}
	Ok(())
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::sync::tests::{repo_init, write_commit_file};
	use pretty_assertions::{assert_eq, assert_ne};

	#[test]
	fn test_smoke() {
		let (_td, repo) = repo_init().unwrap();
		let root = repo.path().parent().unwrap();
		let repo_path: &RepoPath =
			&root.as_os_str().to_str().unwrap().into();

		let c1 =
			write_commit_file(&repo, "test.txt", "content", "c1");

		let files = tree_files(repo_path, c1).unwrap();

		assert_eq!(files.len(), 1);
		assert_eq!(files[0].path, PathBuf::from("./test.txt"));

		let c2 =
			write_commit_file(&repo, "test.txt", "content2", "c2");

		let content =
			tree_file_content(repo_path, &files[0]).unwrap();
		assert_eq!(&content, "content");

		let files_c2 = tree_files(repo_path, c2).unwrap();

		assert_eq!(files_c2.len(), 1);
		assert_ne!(files_c2[0], files[0]);
	}

	#[test]
	fn test_sorting() {
		let mut list = ["file", "folder/file", "folder/afile"]
			.iter()
			.map(|f| TreeFile {
				path: PathBuf::from(f),
				filemode: 0,
				id: Oid::ZERO_SHA1,
			})
			.collect::<Vec<_>>();

		sort_file_list(&mut list);

		assert_eq!(
			list.iter()
				.map(|f| f.path.to_string_lossy())
				.collect::<Vec<_>>(),
			vec![
				String::from("folder/afile"),
				String::from("folder/file"),
				String::from("file")
			]
		);
	}

	#[test]
	fn test_sorting_folders() {
		let mut list = ["bfolder/file", "afolder/file"]
			.iter()
			.map(|f| TreeFile {
				path: PathBuf::from(f),
				filemode: 0,
				id: Oid::ZERO_SHA1,
			})
			.collect::<Vec<_>>();

		sort_file_list(&mut list);

		assert_eq!(
			list.iter()
				.map(|f| f.path.to_string_lossy())
				.collect::<Vec<_>>(),
			vec![
				String::from("afolder/file"),
				String::from("bfolder/file"),
			]
		);
	}

	#[test]
	fn test_sorting_folders2() {
		let mut list = ["bfolder/sub/file", "afolder/file"]
			.iter()
			.map(|f| TreeFile {
				path: PathBuf::from(f),
				filemode: 0,
				id: Oid::ZERO_SHA1,
			})
			.collect::<Vec<_>>();

		sort_file_list(&mut list);

		assert_eq!(
			list.iter()
				.map(|f| f.path.to_string_lossy())
				.collect::<Vec<_>>(),
			vec![
				String::from("afolder/file"),
				String::from("bfolder/sub/file"),
			]
		);
	}

	#[test]
	fn test_path_cmp() {
		assert_eq!(
			path_cmp(
				&PathBuf::from("bfolder/sub/file"),
				&PathBuf::from("afolder/file")
			),
			Ordering::Greater
		);
	}

	#[test]
	fn test_path_file_cmp() {
		assert_eq!(
			path_cmp(
				&PathBuf::from("a"),
				&PathBuf::from("afolder/file")
			),
			Ordering::Greater
		);
	}
}
