use std::marker;
use std::ptr;

use libc::{c_int, c_void};

use crate::util::{Binding, IntoCString};
use crate::{panic, raw, tree, Error, Oid, Repository, TreeEntry};

/// Constructor for in-memory trees
pub struct TreeBuilder<'repo> {
    raw: *mut raw::git_treebuilder,
    _marker: marker::PhantomData<&'repo Repository>,
}

impl<'repo> TreeBuilder<'repo> {
    /// Clear all the entries in the builder
    pub fn clear(&mut self) -> Result<(), Error> {
        unsafe {
            try_call!(raw::git_treebuilder_clear(self.raw));
        }
        Ok(())
    }

    /// Get the number of entries
    pub fn len(&self) -> usize {
        unsafe { raw::git_treebuilder_entrycount(self.raw) as usize }
    }

    /// Return `true` if there is no entry
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Get en entry from the builder from its filename
    pub fn get<P>(&self, filename: P) -> Result<Option<TreeEntry<'_>>, Error>
    where
        P: IntoCString,
    {
        let filename = filename.into_c_string()?;
        unsafe {
            let ret = raw::git_treebuilder_get(self.raw, filename.as_ptr());
            if ret.is_null() {
                Ok(None)
            } else {
                Ok(Some(tree::entry_from_raw_const(ret)))
            }
        }
    }

    /// Add or update an entry in the builder
    ///
    /// No attempt is made to ensure that the provided Oid points to
    /// an object of a reasonable type (or any object at all).
    ///
    /// The mode given must be one of 0o040000, 0o100644, 0o100755, 0o120000 or
    /// 0o160000 currently.
    pub fn insert<P: IntoCString>(
        &mut self,
        filename: P,
        oid: Oid,
        filemode: i32,
    ) -> Result<TreeEntry<'_>, Error> {
        let filename = filename.into_c_string()?;
        let filemode = filemode as raw::git_filemode_t;

        let mut ret = ptr::null();
        unsafe {
            try_call!(raw::git_treebuilder_insert(
                &mut ret,
                self.raw,
                filename,
                oid.raw(),
                filemode
            ));
            Ok(tree::entry_from_raw_const(ret))
        }
    }

    /// Remove an entry from the builder by its filename
    pub fn remove<P: IntoCString>(&mut self, filename: P) -> Result<(), Error> {
        let filename = filename.into_c_string()?;
        unsafe {
            try_call!(raw::git_treebuilder_remove(self.raw, filename));
        }
        Ok(())
    }

    /// Selectively remove entries from the tree
    ///
    /// Values for which the filter returns `true` will be kept.  Note
    /// that this behavior is different from the libgit2 C interface.
    pub fn filter<F>(&mut self, mut filter: F) -> Result<(), Error>
    where
        F: FnMut(&TreeEntry<'_>) -> bool,
    {
        let mut cb: &mut FilterCb<'_> = &mut filter;
        let ptr = &mut cb as *mut _;
        let cb: raw::git_treebuilder_filter_cb = Some(filter_cb);
        unsafe {
            try_call!(raw::git_treebuilder_filter(self.raw, cb, ptr as *mut _));
            panic::check();
        }
        Ok(())
    }

    /// Write the contents of the TreeBuilder as a Tree object and
    /// return its Oid
    pub fn write(&self) -> Result<Oid, Error> {
        let mut raw = raw::git_oid {
            id: [0; raw::GIT_OID_RAWSZ],
        };
        unsafe {
            try_call!(raw::git_treebuilder_write(&mut raw, self.raw()));
            Ok(Binding::from_raw(&raw as *const _))
        }
    }
}

type FilterCb<'a> = dyn FnMut(&TreeEntry<'_>) -> bool + 'a;

extern "C" fn filter_cb(entry: *const raw::git_tree_entry, payload: *mut c_void) -> c_int {
    let ret = panic::wrap(|| unsafe {
        // There's no way to return early from git_treebuilder_filter.
        if panic::panicked() {
            true
        } else {
            let entry = tree::entry_from_raw_const(entry);
            let payload = payload as *mut &mut FilterCb<'_>;
            (*payload)(&entry)
        }
    });
    if ret == Some(false) {
        1
    } else {
        0
    }
}

impl<'repo> Binding for TreeBuilder<'repo> {
    type Raw = *mut raw::git_treebuilder;

    unsafe fn from_raw(raw: *mut raw::git_treebuilder) -> TreeBuilder<'repo> {
        TreeBuilder {
            raw: raw,
            _marker: marker::PhantomData,
        }
    }
    fn raw(&self) -> *mut raw::git_treebuilder {
        self.raw
    }
}

impl<'repo> Drop for TreeBuilder<'repo> {
    fn drop(&mut self) {
        unsafe { raw::git_treebuilder_free(self.raw) }
    }
}

#[cfg(test)]
mod tests {
    use crate::ObjectType;

    #[test]
    fn smoke() {
        let (_td, repo) = crate::test::repo_init();

        let mut builder = repo.treebuilder(None).unwrap();
        assert_eq!(builder.len(), 0);
        let blob = repo.blob(b"data").unwrap();
        {
            let entry = builder.insert("a", blob, 0o100644).unwrap();
            assert_eq!(entry.kind(), Some(ObjectType::Blob));
        }
        builder.insert("b", blob, 0o100644).unwrap();
        assert_eq!(builder.len(), 2);
        builder.remove("a").unwrap();
        assert_eq!(builder.len(), 1);
        assert_eq!(builder.get("b").unwrap().unwrap().id(), blob);
        builder.clear().unwrap();
        assert_eq!(builder.len(), 0);
    }

    #[test]
    fn write() {
        let (_td, repo) = crate::test::repo_init();

        let mut builder = repo.treebuilder(None).unwrap();
        let data = repo.blob(b"data").unwrap();
        builder.insert("name", data, 0o100644).unwrap();
        let tree = builder.write().unwrap();
        let tree = repo.find_tree(tree).unwrap();
        let entry = tree.get(0).unwrap();
        assert_eq!(entry.name(), Some("name"));
        let blob = entry.to_object(&repo).unwrap();
        let blob = blob.as_blob().unwrap();
        assert_eq!(blob.content(), b"data");

        let builder = repo.treebuilder(Some(&tree)).unwrap();
        assert_eq!(builder.len(), 1);
    }

    #[test]
    fn filter() {
        let (_td, repo) = crate::test::repo_init();

        let mut builder = repo.treebuilder(None).unwrap();
        let blob = repo.blob(b"data").unwrap();
        let tree = {
            let head = repo.head().unwrap().peel(ObjectType::Commit).unwrap();
            let head = head.as_commit().unwrap();
            head.tree_id()
        };
        builder.insert("blob", blob, 0o100644).unwrap();
        builder.insert("dir", tree, 0o040000).unwrap();
        builder.insert("dir2", tree, 0o040000).unwrap();

        builder.filter(|_| true).unwrap();
        assert_eq!(builder.len(), 3);
        builder
            .filter(|e| e.kind().unwrap() != ObjectType::Blob)
            .unwrap();
        assert_eq!(builder.len(), 2);
        builder.filter(|_| false).unwrap();
        assert_eq!(builder.len(), 0);
    }
}
