//! Low-level abstraction over finding refs inside a commit tree

use super::tree_utils::{self as utils, PairIter};
use crate::{branch::BranchIter, files::Yield, Commit, HashId};
use git2::{ObjectType, Repository, TreeWalkMode, TreeWalkResult};
use std::sync::Arc;

/// A git directory tree walker abstraction
///
/// This type is meant to be used ephemerally, and internally uses the
/// libgit2 `Tree` abstraction to walk directory trees lazily to
/// resolve paths to [`TreeEntry`](self::TreeEntry)'s.
///
/// Note: this type _may_ be removed in the future.  For a more
/// high-level (and stable) API, check
/// [`Explorer`](crate::files::Explorer)
pub struct FileTree {
    repo: Arc<Repository>,
    c: HashId,
}

impl FileTree {
    /// Construct a new FileTree with a repository
    pub(crate) fn new(repo: Arc<Repository>, c: HashId) -> Self {
        Self { repo, c }
    }

    /// Get a FileTree for a new commit
    fn checkout(&self, c: HashId) -> Self {
        Self::new(Arc::clone(&self.repo), c)
    }

    /// Get the history of a path with a branch iterator
    ///
    /// This function is computationally light, by not checking for
    /// renames between commits.  This is done by resolving a path to
    /// a tree reference, and comparing references at the same
    /// position between two commits.
    pub fn base_history(&self, iter: BranchIter, path: &str) -> Option<Vec<Commit>> {
        let mut iter = iter.peekable();
        let mut commits = vec![];

        // Iterate over the branch in commit pairs
        while let (Some(a), b) = iter.next_pair() {
            let _a = a.commit().clone();
            let a = self.checkout(a.commit().id.clone()).resolve(path);
            let b = b.and_then(|c| self.checkout(c.commit().id.clone()).resolve(path));

            match (a, b) {
                // If a path exists in both commits, check if they are
                // the same Ref.  If they are not, a changed the path.
                (Some(a), Some(b)) if a != b => commits.push(_a),
                // If a path only exists in a then it was created
                (Some(_), None) => commits.push(_a),
                // If a path only exists in b, then it was deleted
                (None, Some(_)) => commits.push(_a),
                (_, _) => {}
            }
        }

        Some(commits)
    }

    /// Enumerate a non-leaf tree entry
    pub fn enumerate(&self, path: &str) -> Option<Vec<TreeEntry>> {
        let tree = utils::open_tree(&self.repo, &self.c)?;
        let target = utils::path_split(path);

        let mut entries = vec![];
        tree.walk(TreeWalkMode::PreOrder, |p, e| {
            let path = utils::path_split(p);
            if path == target {
                entries.push(TreeEntry::new(p, &e));
                TreeWalkResult::Ok
            } else {
                TreeWalkResult::Skip
            }
        })
        .ok()?;

        Some(entries)
    }

    /// Resolve a path inside this file tree
    ///
    /// Will return `None` if there is no tree for the selected
    /// commit, or the file inside the tree does not exist.
    pub fn resolve(&self, path: &str) -> Option<TreeEntry> {
        let tree = utils::open_tree(&self.repo, &self.c)?;
        let target = utils::path_split(path);

        // Initialise entry to None as a fallback
        let mut entry = None;

        // Walk over tree and swallor errors (which we use to
        // terminace traversal to speed up indexing time)
        tree.walk(TreeWalkMode::PreOrder, |p, e| {
            if utils::path_cmp(&target, p, e.name().unwrap()) {
                entry = Some(TreeEntry::new(p, &e));
                TreeWalkResult::Ok
            } else {
                TreeWalkResult::Skip
            }
        })
        .ok()?;

        // Return whatever the entry is now
        entry
    }
}

/// An entry in a commit tree
///
/// This type is lazily loaded, and can represent either a Blob or a
/// Directory.  You can resolve its value by calling
/// [`resolve()`](Self::resolve)
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TreeEntry {
    tt: EntryType,
    id: HashId,
    path: String,
    name: Option<String>,
}

impl TreeEntry {
    fn new(path: &str, entry: &git2::TreeEntry) -> Self {
        let tt = match entry.kind() {
            Some(ObjectType::Blob) => EntryType::File,
            Some(ObjectType::Tree) => EntryType::Dir,
            _ => unimplemented!(),
        };
        let id = entry.id().into();
        let path = path.into();
        let name = entry.name().map(|s| s.into());

        Self { tt, id, path, name }
    }

    pub fn id(&self) -> HashId {
        self.id.clone()
    }

    /// Get a reference to the name of this TreeEntry
    pub fn name(&self) -> Option<&String> {
        self.name.as_ref()
    }

    /// Resolve this type to a [`Yield`]()
    pub fn resolve(&self) -> Yield {
        todo!()
    }
}

/// Type of a TreeEntry
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EntryType {
    /// A file that can be loaded
    File,
    /// A directory that can be indexed
    Dir,
}

#[test]
fn s_resolve() {
    let path = env!("CARGO_MANIFEST_DIR").to_owned() + "/test-repo";
    use crate::Repository as Repo;

    eprintln!("Path: `{}`", path);

    let r = Repo::open(&path).unwrap();
    let b = r.branch("master".into()).unwrap();
    let h = b.head();

    let t = h.tree();
    t.resolve("README").unwrap();
}

#[test]
fn s_enumerate() {
    let path = env!("CARGO_MANIFEST_DIR").to_owned() + "/test-repo";
    use crate::Repository as Repo;

    eprintln!("Path: `{}`", path);

    let r = Repo::open(&path).unwrap();
    let b = r.branch("master".into()).unwrap();
    let h = b.head();

    let t = h.tree();
    let entries = t.enumerate("").unwrap();

    assert_eq!(
        entries
            .iter()
            .filter_map(|e| e.name().map(|s| s.as_str()))
            .collect::<Vec<_>>(),
        vec!["README", "test.rs"]
    );
}

#[test]
fn s_history() {
    let path = env!("CARGO_MANIFEST_DIR").to_owned() + "/test-repo";
    use crate::Repository as Repo;

    eprintln!("Path: `{}`", path);

    let r = Repo::open(&path).unwrap();
    let b = r.branch("master".into()).unwrap();

    let head = b.head();
    let iter = b.get_all();

    let tree = head.tree();
    let history = tree.base_history(iter, "test.rs").unwrap();

    dbg!(&history);
    assert_eq!(history.len(), 1);
}
