use std::ffi::{CStr, CString};
use std::marker;
use std::ops::Range;
use std::path::Path;
use std::ptr;
use std::slice;

use libc::{c_char, c_int, c_uint, c_void, size_t};

use crate::util::{self, path_to_repo_path, Binding};
use crate::IntoCString;
use crate::ObjectFormat;
use crate::{panic, raw, Error, IndexAddOption, IndexTime, Oid, Repository, Tree};

/// A structure to represent a git [index][1]
///
/// [1]: http://git-scm.com/book/en/Git-Internals-Git-Objects
pub struct Index {
    raw: *mut raw::git_index,
}

/// An iterator over the entries in an index
pub struct IndexEntries<'index> {
    range: Range<usize>,
    index: &'index Index,
}

/// An iterator over the conflicting entries in an index
pub struct IndexConflicts<'index> {
    conflict_iter: *mut raw::git_index_conflict_iterator,
    _marker: marker::PhantomData<&'index Index>,
}

/// A structure to represent the information returned when a conflict is detected in an index entry
pub struct IndexConflict {
    /// The ancestor index entry of the two conflicting index entries
    pub ancestor: Option<IndexEntry>,
    /// The index entry originating from the user's copy of the repository.
    /// Its contents conflict with 'their' index entry
    pub our: Option<IndexEntry>,
    /// The index entry originating from the external repository.
    /// Its contents conflict with 'our' index entry
    pub their: Option<IndexEntry>,
}

/// A callback function to filter index matches.
///
/// Used by `Index::{add_all,remove_all,update_all}`.  The first argument is the
/// path, and the second is the pathspec that matched it.  Return 0 to confirm
/// the operation on the item, > 0 to skip the item, and < 0 to abort the scan.
pub type IndexMatchedPath<'a> = dyn FnMut(&Path, &[u8]) -> i32 + 'a;

/// A structure to represent an entry or a file inside of an index.
///
/// All fields of an entry are public for modification and inspection. This is
/// also how a new index entry is created.
#[allow(missing_docs)]
#[derive(Debug)]
pub struct IndexEntry {
    pub ctime: IndexTime,
    pub mtime: IndexTime,
    pub dev: u32,
    pub ino: u32,
    pub mode: u32,
    pub uid: u32,
    pub gid: u32,
    pub file_size: u32,
    pub id: Oid,
    pub flags: u16,
    pub flags_extended: u16,

    /// The path of this index entry as a byte vector. Regardless of the
    /// current platform, the directory separator is an ASCII forward slash
    /// (`0x2F`). There are no terminating or internal NUL characters, and no
    /// trailing slashes. Most of the time, paths will be valid utf-8 — but
    /// not always. For more information on the path storage format, see
    /// [these git docs][git-index-docs]. Note that libgit2 will take care of
    /// handling the prefix compression mentioned there.
    ///
    /// [git-index-docs]: https://github.com/git/git/blob/a08a83db2bf27f015bec9a435f6d73e223c21c5e/Documentation/technical/index-format.txt#L107-L124
    ///
    /// You can turn this value into a `std::ffi::CString` with
    /// `CString::new(&entry.path[..]).unwrap()`. To turn a reference into a
    /// `&std::path::Path`, see the `bytes2path()` function in the private,
    /// internal `util` module in this crate’s source code.
    pub path: Vec<u8>,
}

impl Index {
    /// Creates a new in-memory index.
    ///
    /// This index object cannot be read/written to the filesystem, but may be
    /// used to perform in-memory index operations.
    ///
    /// This always creates a SHA1 index.
    /// Use [`Index::new_ext`] to create an index with a specific object format.
    pub fn new() -> Result<Index, Error> {
        Self::new_ext(ObjectFormat::Sha1)
    }

    /// Creates a new in-memory index with a specific object format.
    ///
    /// See [`Index::new`] for more details.
    pub fn new_ext(format: ObjectFormat) -> Result<Index, Error> {
        crate::init();
        let mut raw = ptr::null_mut();
        unsafe {
            #[cfg(not(feature = "unstable-sha256"))]
            {
                let _ = format;
                try_call!(raw::git_index_new(&mut raw));
            }
            #[cfg(feature = "unstable-sha256")]
            {
                let mut opts: raw::git_index_options = std::mem::zeroed();
                opts.version = raw::GIT_INDEX_OPTIONS_VERSION;
                opts.oid_type = format.raw();
                try_call!(raw::git_index_new(&mut raw, &opts));
            }
            Ok(Binding::from_raw(raw))
        }
    }

    /// Create a new bare Git index object as a memory representation of the Git
    /// index file in 'index_path', without a repository to back it.
    ///
    /// Since there is no ODB or working directory behind this index, any Index
    /// methods which rely on these (e.g. add_path) will fail.
    ///
    /// If you need an index attached to a repository, use the `index()` method
    /// on `Repository`.
    ///
    /// This opens the index assuming SHA1 object format. Use
    /// [`Index::open_ext`] to specify a different format.
    pub fn open(index_path: &Path) -> Result<Index, Error> {
        Self::open_ext(index_path, ObjectFormat::Sha1)
    }

    /// Opens a Git index with a specific object format.
    ///
    /// See [`Index::open`] for more details.
    pub fn open_ext(index_path: &Path, format: ObjectFormat) -> Result<Index, Error> {
        crate::init();
        let mut raw = ptr::null_mut();
        // Normal file path OK (does not need Windows conversion).
        let index_path = index_path.into_c_string()?;
        unsafe {
            #[cfg(not(feature = "unstable-sha256"))]
            {
                let _ = format;
                try_call!(raw::git_index_open(&mut raw, index_path));
            }
            #[cfg(feature = "unstable-sha256")]
            {
                let mut opts: raw::git_index_options = std::mem::zeroed();
                opts.version = raw::GIT_INDEX_OPTIONS_VERSION;
                opts.oid_type = format.raw();
                try_call!(raw::git_index_open(&mut raw, index_path, &opts));
            }
            Ok(Binding::from_raw(raw))
        }
    }

    /// Get index on-disk version.
    ///
    /// Valid return values are 2, 3, or 4.  If 3 is returned, an index
    /// with version 2 may be written instead, if the extension data in
    /// version 3 is not necessary.
    pub fn version(&self) -> u32 {
        unsafe { raw::git_index_version(self.raw) }
    }

    /// Set index on-disk version.
    ///
    /// Valid values are 2, 3, or 4.  If 2 is given, git_index_write may
    /// write an index with version 3 instead, if necessary to accurately
    /// represent the index.
    pub fn set_version(&mut self, version: u32) -> Result<(), Error> {
        unsafe {
            try_call!(raw::git_index_set_version(self.raw, version));
        }
        Ok(())
    }

    /// Add or update an index entry from an in-memory struct
    ///
    /// If a previous index entry exists that has the same path and stage as the
    /// given 'source_entry', it will be replaced. Otherwise, the 'source_entry'
    /// will be added.
    pub fn add(&mut self, entry: &IndexEntry) -> Result<(), Error> {
        let path = CString::new(&entry.path[..])?;

        // libgit2 encodes the length of the path in the lower bits of the
        // `flags` entry, so mask those out and recalculate here to ensure we
        // don't corrupt anything.
        let mut flags = entry.flags & !raw::GIT_INDEX_ENTRY_NAMEMASK;

        if entry.path.len() < raw::GIT_INDEX_ENTRY_NAMEMASK as usize {
            flags |= entry.path.len() as u16;
        } else {
            flags |= raw::GIT_INDEX_ENTRY_NAMEMASK;
        }

        unsafe {
            let raw = raw::git_index_entry {
                dev: entry.dev,
                ino: entry.ino,
                mode: entry.mode,
                uid: entry.uid,
                gid: entry.gid,
                file_size: entry.file_size,
                id: *entry.id.raw(),
                flags,
                flags_extended: entry.flags_extended,
                path: path.as_ptr(),
                mtime: raw::git_index_time {
                    seconds: entry.mtime.seconds(),
                    nanoseconds: entry.mtime.nanoseconds(),
                },
                ctime: raw::git_index_time {
                    seconds: entry.ctime.seconds(),
                    nanoseconds: entry.ctime.nanoseconds(),
                },
            };
            try_call!(raw::git_index_add(self.raw, &raw));
            Ok(())
        }
    }

    /// Add or update an index entry from a buffer in memory
    ///
    /// This method will create a blob in the repository that owns the index and
    /// then add the index entry to the index. The path of the entry represents
    /// the position of the blob relative to the repository's root folder.
    ///
    /// If a previous index entry exists that has the same path as the given
    /// 'entry', it will be replaced. Otherwise, the 'entry' will be added.
    /// The id and the file_size of the 'entry' are updated with the real value
    /// of the blob.
    ///
    /// This forces the file to be added to the index, not looking at gitignore
    /// rules.
    ///
    /// If this file currently is the result of a merge conflict, this file will
    /// no longer be marked as conflicting. The data about the conflict will be
    /// moved to the "resolve undo" (REUC) section.
    pub fn add_frombuffer(&mut self, entry: &IndexEntry, data: &[u8]) -> Result<(), Error> {
        let path = CString::new(&entry.path[..])?;

        // libgit2 encodes the length of the path in the lower bits of the
        // `flags` entry, so mask those out and recalculate here to ensure we
        // don't corrupt anything.
        let mut flags = entry.flags & !raw::GIT_INDEX_ENTRY_NAMEMASK;

        if entry.path.len() < raw::GIT_INDEX_ENTRY_NAMEMASK as usize {
            flags |= entry.path.len() as u16;
        } else {
            flags |= raw::GIT_INDEX_ENTRY_NAMEMASK;
        }

        unsafe {
            let raw = raw::git_index_entry {
                dev: entry.dev,
                ino: entry.ino,
                mode: entry.mode,
                uid: entry.uid,
                gid: entry.gid,
                file_size: entry.file_size,
                id: *entry.id.raw(),
                flags,
                flags_extended: entry.flags_extended,
                path: path.as_ptr(),
                mtime: raw::git_index_time {
                    seconds: entry.mtime.seconds(),
                    nanoseconds: entry.mtime.nanoseconds(),
                },
                ctime: raw::git_index_time {
                    seconds: entry.ctime.seconds(),
                    nanoseconds: entry.ctime.nanoseconds(),
                },
            };

            let ptr = data.as_ptr() as *const c_void;
            let len = data.len() as size_t;
            try_call!(raw::git_index_add_frombuffer(self.raw, &raw, ptr, len));
            Ok(())
        }
    }

    /// Add or update an index entry from a file on disk
    ///
    /// The file path must be relative to the repository's working folder and
    /// must be readable.
    ///
    /// This method will fail in bare index instances.
    ///
    /// This forces the file to be added to the index, not looking at gitignore
    /// rules.
    ///
    /// If this file currently is the result of a merge conflict, this file will
    /// no longer be marked as conflicting. The data about the conflict will be
    /// moved to the "resolve undo" (REUC) section.
    pub fn add_path(&mut self, path: &Path) -> Result<(), Error> {
        let posix_path = path_to_repo_path(path)?;
        unsafe {
            try_call!(raw::git_index_add_bypath(self.raw, posix_path));
            Ok(())
        }
    }

    /// Add or update index entries matching files in the working directory.
    ///
    /// This method will fail in bare index instances.
    ///
    /// The `pathspecs` are a list of file names or shell glob patterns that
    /// will matched against files in the repository's working directory. Each
    /// file that matches will be added to the index (either updating an
    /// existing entry or adding a new entry). You can disable glob expansion
    /// and force exact matching with the `AddDisablePathspecMatch` flag.
    ///
    /// Files that are ignored will be skipped (unlike `add_path`). If a file is
    /// already tracked in the index, then it will be updated even if it is
    /// ignored. Pass the `AddForce` flag to skip the checking of ignore rules.
    ///
    /// To emulate `git add -A` and generate an error if the pathspec contains
    /// the exact path of an ignored file (when not using `AddForce`), add the
    /// `AddCheckPathspec` flag. This checks that each entry in `pathspecs`
    /// that is an exact match to a filename on disk is either not ignored or
    /// already in the index. If this check fails, the function will return
    /// an error.
    ///
    /// To emulate `git add -A` with the "dry-run" option, just use a callback
    /// function that always returns a positive value. See below for details.
    ///
    /// If any files are currently the result of a merge conflict, those files
    /// will no longer be marked as conflicting. The data about the conflicts
    /// will be moved to the "resolve undo" (REUC) section.
    ///
    /// If you provide a callback function, it will be invoked on each matching
    /// item in the working directory immediately before it is added to /
    /// updated in the index. Returning zero will add the item to the index,
    /// greater than zero will skip the item, and less than zero will abort the
    /// scan an return an error to the caller.
    ///
    /// # Example
    ///
    /// Emulate `git add *`:
    ///
    /// ```no_run
    /// use git2::{Index, IndexAddOption, Repository};
    ///
    /// let repo = Repository::open("/path/to/a/repo").expect("failed to open");
    /// let mut index = repo.index().expect("cannot get the Index file");
    /// index.add_all(["*"].iter(), IndexAddOption::DEFAULT, None);
    /// index.write();
    /// ```
    pub fn add_all<T, I>(
        &mut self,
        pathspecs: I,
        flag: IndexAddOption,
        mut cb: Option<&mut IndexMatchedPath<'_>>,
    ) -> Result<(), Error>
    where
        T: IntoCString,
        I: IntoIterator<Item = T>,
    {
        let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?;
        let ptr = cb.as_mut();
        let callback = ptr
            .as_ref()
            .map(|_| index_matched_path_cb as extern "C" fn(_, _, _) -> _);
        unsafe {
            try_call!(raw::git_index_add_all(
                self.raw,
                &raw_strarray,
                flag.bits() as c_uint,
                callback,
                ptr.map(|p| p as *mut _).unwrap_or(ptr::null_mut()) as *mut c_void
            ));
        }
        Ok(())
    }

    /// Clear the contents (all the entries) of an index object.
    ///
    /// This clears the index object in memory; changes must be explicitly
    /// written to disk for them to take effect persistently via `write_*`.
    pub fn clear(&mut self) -> Result<(), Error> {
        unsafe {
            try_call!(raw::git_index_clear(self.raw));
        }
        Ok(())
    }

    /// Get the count of entries currently in the index
    pub fn len(&self) -> usize {
        unsafe { raw::git_index_entrycount(&*self.raw) as usize }
    }

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

    /// Get one of the entries in the index by its position.
    pub fn get(&self, n: usize) -> Option<IndexEntry> {
        unsafe {
            let ptr = raw::git_index_get_byindex(self.raw, n as size_t);
            if ptr.is_null() {
                None
            } else {
                Some(Binding::from_raw(*ptr))
            }
        }
    }

    /// Get an iterator over the entries in this index.
    pub fn iter(&self) -> IndexEntries<'_> {
        IndexEntries {
            range: 0..self.len(),
            index: self,
        }
    }

    /// Get an iterator over the index entries that have conflicts
    pub fn conflicts(&self) -> Result<IndexConflicts<'_>, Error> {
        crate::init();
        let mut conflict_iter = ptr::null_mut();
        unsafe {
            try_call!(raw::git_index_conflict_iterator_new(
                &mut conflict_iter,
                self.raw
            ));
            Ok(Binding::from_raw(conflict_iter))
        }
    }

    /// Get one of the entries in the index by its path.
    pub fn get_path(&self, path: &Path, stage: i32) -> Option<IndexEntry> {
        let path = path_to_repo_path(path).unwrap();
        unsafe {
            let ptr = call!(raw::git_index_get_bypath(self.raw, path, stage as c_int));
            if ptr.is_null() {
                None
            } else {
                Some(Binding::from_raw(*ptr))
            }
        }
    }

    /// Does this index have conflicts?
    ///
    /// Returns `true` if the index contains conflicts, `false` if it does not.
    pub fn has_conflicts(&self) -> bool {
        unsafe { raw::git_index_has_conflicts(self.raw) == 1 }
    }

    /// Get the index entries that represent a conflict of a single file.
    pub fn conflict_get(&self, path: &Path) -> Result<IndexConflict, Error> {
        let path = path_to_repo_path(path)?;
        let mut ancestor = ptr::null();
        let mut our = ptr::null();
        let mut their = ptr::null();

        unsafe {
            try_call!(raw::git_index_conflict_get(
                &mut ancestor,
                &mut our,
                &mut their,
                self.raw,
                path
            ));

            Ok(IndexConflict {
                ancestor: match ancestor.is_null() {
                    false => Some(IndexEntry::from_raw(*ancestor)),
                    true => None,
                },
                our: match our.is_null() {
                    false => Some(IndexEntry::from_raw(*our)),
                    true => None,
                },
                their: match their.is_null() {
                    false => Some(IndexEntry::from_raw(*their)),
                    true => None,
                },
            })
        }
    }

    /// Get the full path to the index file on disk.
    ///
    /// Returns `None` if this is an in-memory index.
    pub fn path(&self) -> Option<&Path> {
        unsafe { crate::opt_bytes(self, raw::git_index_path(&*self.raw)).map(util::bytes2path) }
    }

    /// Update the contents of an existing index object in memory by reading
    /// from the hard disk.
    ///
    /// If force is true, this performs a "hard" read that discards in-memory
    /// changes and always reloads the on-disk index data. If there is no
    /// on-disk version, the index will be cleared.
    ///
    /// If force is false, this does a "soft" read that reloads the index data
    /// from disk only if it has changed since the last time it was loaded.
    /// Purely in-memory index data will be untouched. Be aware: if there are
    /// changes on disk, unwritten in-memory changes are discarded.
    pub fn read(&mut self, force: bool) -> Result<(), Error> {
        unsafe {
            try_call!(raw::git_index_read(self.raw, force));
        }
        Ok(())
    }

    /// Read a tree into the index file with stats
    ///
    /// The current index contents will be replaced by the specified tree.
    pub fn read_tree(&mut self, tree: &Tree<'_>) -> Result<(), Error> {
        unsafe {
            try_call!(raw::git_index_read_tree(self.raw, &*tree.raw()));
        }
        Ok(())
    }

    /// Remove an entry from the index
    pub fn remove(&mut self, path: &Path, stage: i32) -> Result<(), Error> {
        let path = path_to_repo_path(path)?;
        unsafe {
            try_call!(raw::git_index_remove(self.raw, path, stage as c_int));
        }
        Ok(())
    }

    /// Remove an index entry corresponding to a file on disk.
    ///
    /// The file path must be relative to the repository's working folder. It
    /// may exist.
    ///
    /// If this file currently is the result of a merge conflict, this file will
    /// no longer be marked as conflicting. The data about the conflict will be
    /// moved to the "resolve undo" (REUC) section.
    pub fn remove_path(&mut self, path: &Path) -> Result<(), Error> {
        let path = path_to_repo_path(path)?;
        unsafe {
            try_call!(raw::git_index_remove_bypath(self.raw, path));
        }
        Ok(())
    }

    /// Remove all entries from the index under a given directory.
    pub fn remove_dir(&mut self, path: &Path, stage: i32) -> Result<(), Error> {
        let path = path_to_repo_path(path)?;
        unsafe {
            try_call!(raw::git_index_remove_directory(
                self.raw,
                path,
                stage as c_int
            ));
        }
        Ok(())
    }

    /// Removes the index entries that represent a conflict of a single file.
    pub fn conflict_remove(&mut self, path: &Path) -> Result<(), Error> {
        let path = path_to_repo_path(path)?;
        unsafe {
            try_call!(raw::git_index_conflict_remove(self.raw, path));
        }
        Ok(())
    }

    /// Remove all matching index entries.
    ///
    /// If you provide a callback function, it will be invoked on each matching
    /// item in the index immediately before it is removed. Return 0 to remove
    /// the item, > 0 to skip the item, and < 0 to abort the scan.
    pub fn remove_all<T, I>(
        &mut self,
        pathspecs: I,
        mut cb: Option<&mut IndexMatchedPath<'_>>,
    ) -> Result<(), Error>
    where
        T: IntoCString,
        I: IntoIterator<Item = T>,
    {
        let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?;
        let ptr = cb.as_mut();
        let callback = ptr
            .as_ref()
            .map(|_| index_matched_path_cb as extern "C" fn(_, _, _) -> _);
        unsafe {
            try_call!(raw::git_index_remove_all(
                self.raw,
                &raw_strarray,
                callback,
                ptr.map(|p| p as *mut _).unwrap_or(ptr::null_mut()) as *mut c_void
            ));
        }
        Ok(())
    }

    /// Update all index entries to match the working directory
    ///
    /// This method will fail in bare index instances.
    ///
    /// This scans the existing index entries and synchronizes them with the
    /// working directory, deleting them if the corresponding working directory
    /// file no longer exists otherwise updating the information (including
    /// adding the latest version of file to the ODB if needed).
    ///
    /// If you provide a callback function, it will be invoked on each matching
    /// item in the index immediately before it is updated (either refreshed or
    /// removed depending on working directory state). Return 0 to proceed with
    /// updating the item, > 0 to skip the item, and < 0 to abort the scan.
    pub fn update_all<T, I>(
        &mut self,
        pathspecs: I,
        mut cb: Option<&mut IndexMatchedPath<'_>>,
    ) -> Result<(), Error>
    where
        T: IntoCString,
        I: IntoIterator<Item = T>,
    {
        let (_a, _b, raw_strarray) = crate::util::iter2cstrs_paths(pathspecs)?;
        let ptr = cb.as_mut();
        let callback = ptr
            .as_ref()
            .map(|_| index_matched_path_cb as extern "C" fn(_, _, _) -> _);
        unsafe {
            try_call!(raw::git_index_update_all(
                self.raw,
                &raw_strarray,
                callback,
                ptr.map(|p| p as *mut _).unwrap_or(ptr::null_mut()) as *mut c_void
            ));
        }
        Ok(())
    }

    /// Write an existing index object from memory back to disk using an atomic
    /// file lock.
    pub fn write(&mut self) -> Result<(), Error> {
        unsafe {
            try_call!(raw::git_index_write(self.raw));
        }
        Ok(())
    }

    /// Write the index as a tree.
    ///
    /// This method will scan the index and write a representation of its
    /// current state back to disk; it recursively creates tree objects for each
    /// of the subtrees stored in the index, but only returns the OID of the
    /// root tree. This is the OID that can be used e.g. to create a commit.
    ///
    /// The index instance cannot be bare, and needs to be associated to an
    /// existing repository.
    ///
    /// The index must not contain any file in conflict.
    pub fn write_tree(&mut self) -> Result<Oid, Error> {
        let mut raw = crate::util::zeroed_raw_oid();
        unsafe {
            try_call!(raw::git_index_write_tree(&mut raw, self.raw));
            Ok(Binding::from_raw(&raw as *const _))
        }
    }

    /// Write the index as a tree to the given repository
    ///
    /// This is the same as `write_tree` except that the destination repository
    /// can be chosen.
    pub fn write_tree_to(&mut self, repo: &Repository) -> Result<Oid, Error> {
        let mut raw = crate::util::zeroed_raw_oid();
        unsafe {
            try_call!(raw::git_index_write_tree_to(&mut raw, self.raw, repo.raw()));
            Ok(Binding::from_raw(&raw as *const _))
        }
    }

    /// Find the first position of any entries matching a prefix.
    ///
    /// To find the first position of a path inside a given folder, suffix the prefix with a '/'.
    pub fn find_prefix<T: IntoCString>(&self, prefix: T) -> Result<usize, Error> {
        let mut at_pos: size_t = 0;
        let entry_path = prefix.into_c_string()?;
        unsafe {
            try_call!(raw::git_index_find_prefix(
                &mut at_pos,
                self.raw,
                entry_path
            ));
            Ok(at_pos)
        }
    }
}

impl IndexEntry {
    /// Create a raw index entry.
    ///
    /// The returned `raw::git_index_entry` contains a pointer to a `CString` path, which is also
    /// returned because it's lifetime must exceed the lifetime of the `raw::git_index_entry`.
    pub(crate) unsafe fn to_raw(&self) -> Result<(raw::git_index_entry, CString), Error> {
        let path = CString::new(&self.path[..])?;

        // libgit2 encodes the length of the path in the lower bits of the
        // `flags` entry, so mask those out and recalculate here to ensure we
        // don't corrupt anything.
        let mut flags = self.flags & !raw::GIT_INDEX_ENTRY_NAMEMASK;

        if self.path.len() < raw::GIT_INDEX_ENTRY_NAMEMASK as usize {
            flags |= self.path.len() as u16;
        } else {
            flags |= raw::GIT_INDEX_ENTRY_NAMEMASK;
        }

        unsafe {
            let raw = raw::git_index_entry {
                dev: self.dev,
                ino: self.ino,
                mode: self.mode,
                uid: self.uid,
                gid: self.gid,
                file_size: self.file_size,
                id: *self.id.raw(),
                flags,
                flags_extended: self.flags_extended,
                path: path.as_ptr(),
                mtime: raw::git_index_time {
                    seconds: self.mtime.seconds(),
                    nanoseconds: self.mtime.nanoseconds(),
                },
                ctime: raw::git_index_time {
                    seconds: self.ctime.seconds(),
                    nanoseconds: self.ctime.nanoseconds(),
                },
            };

            Ok((raw, path))
        }
    }
}

impl Binding for Index {
    type Raw = *mut raw::git_index;
    unsafe fn from_raw(raw: *mut raw::git_index) -> Index {
        Index { raw }
    }
    fn raw(&self) -> *mut raw::git_index {
        self.raw
    }
}

impl<'index> Binding for IndexConflicts<'index> {
    type Raw = *mut raw::git_index_conflict_iterator;

    unsafe fn from_raw(raw: *mut raw::git_index_conflict_iterator) -> IndexConflicts<'index> {
        IndexConflicts {
            conflict_iter: raw,
            _marker: marker::PhantomData,
        }
    }
    fn raw(&self) -> *mut raw::git_index_conflict_iterator {
        self.conflict_iter
    }
}

extern "C" fn index_matched_path_cb(
    path: *const c_char,
    matched_pathspec: *const c_char,
    payload: *mut c_void,
) -> c_int {
    unsafe {
        let path = CStr::from_ptr(path).to_bytes();
        let matched_pathspec = CStr::from_ptr(matched_pathspec).to_bytes();

        panic::wrap(|| {
            let payload = payload as *mut &mut IndexMatchedPath<'_>;
            (*payload)(util::bytes2path(path), matched_pathspec) as c_int
        })
        .unwrap_or(-1)
    }
}

impl Drop for Index {
    fn drop(&mut self) {
        unsafe { raw::git_index_free(self.raw) }
    }
}

impl<'index> Drop for IndexConflicts<'index> {
    fn drop(&mut self) {
        unsafe { raw::git_index_conflict_iterator_free(self.conflict_iter) }
    }
}

impl<'index> Iterator for IndexEntries<'index> {
    type Item = IndexEntry;
    fn next(&mut self) -> Option<IndexEntry> {
        self.range.next().map(|i| self.index.get(i).unwrap())
    }
}

impl<'index> Iterator for IndexConflicts<'index> {
    type Item = Result<IndexConflict, Error>;
    fn next(&mut self) -> Option<Result<IndexConflict, Error>> {
        let mut ancestor = ptr::null();
        let mut our = ptr::null();
        let mut their = ptr::null();
        unsafe {
            try_call_iter!(raw::git_index_conflict_next(
                &mut ancestor,
                &mut our,
                &mut their,
                self.conflict_iter
            ));
            Some(Ok(IndexConflict {
                ancestor: match ancestor.is_null() {
                    false => Some(IndexEntry::from_raw(*ancestor)),
                    true => None,
                },
                our: match our.is_null() {
                    false => Some(IndexEntry::from_raw(*our)),
                    true => None,
                },
                their: match their.is_null() {
                    false => Some(IndexEntry::from_raw(*their)),
                    true => None,
                },
            }))
        }
    }
}

impl Binding for IndexEntry {
    type Raw = raw::git_index_entry;

    unsafe fn from_raw(raw: raw::git_index_entry) -> IndexEntry {
        let raw::git_index_entry {
            ctime,
            mtime,
            dev,
            ino,
            mode,
            uid,
            gid,
            file_size,
            id,
            flags,
            flags_extended,
            path,
        } = raw;

        // libgit2 encodes the length of the path in the lower bits of `flags`,
        // but if the length exceeds the number of bits then the path is
        // nul-terminated.
        let mut pathlen = (flags & raw::GIT_INDEX_ENTRY_NAMEMASK) as usize;
        if pathlen == raw::GIT_INDEX_ENTRY_NAMEMASK as usize {
            pathlen = CStr::from_ptr(path).to_bytes().len();
        }

        let path = slice::from_raw_parts(path as *const u8, pathlen);

        IndexEntry {
            dev,
            ino,
            mode,
            uid,
            gid,
            file_size,
            id: Binding::from_raw(&id as *const _),
            flags,
            flags_extended,
            path: path.to_vec(),
            mtime: Binding::from_raw(mtime),
            ctime: Binding::from_raw(ctime),
        }
    }

    fn raw(&self) -> raw::git_index_entry {
        // not implemented, may require a CString in storage
        panic!()
    }
}

#[cfg(test)]
mod tests {
    use std::fs::{self, File};
    use std::path::Path;
    use tempfile::TempDir;

    use crate::{
        ErrorCode, Index, IndexEntry, IndexTime, ObjectFormat, Oid, Repository, ResetType,
    };

    #[test]
    fn smoke() {
        let mut index = Index::new().unwrap();
        assert!(index.add_path(&Path::new(".")).is_err());
        index.clear().unwrap();
        assert_eq!(index.len(), 0);
        assert!(index.get(0).is_none());
        assert!(index.path().is_none());
        assert!(index.read(true).is_err());
    }

    #[test]
    fn smoke_from_repo() {
        let (_td, repo) = crate::test::repo_init();
        let mut index = repo.index().unwrap();
        assert_eq!(
            index.path().map(|s| s.to_path_buf()),
            Some(repo.path().join("index"))
        );
        Index::open(&repo.path().join("index")).unwrap();

        index.clear().unwrap();
        index.read(true).unwrap();
        index.write().unwrap();
        index.write_tree().unwrap();
        index.write_tree_to(&repo).unwrap();
    }

    #[test]
    fn add_all() {
        let (_td, repo) = crate::test::repo_init();
        let mut index = repo.index().unwrap();

        let root = repo.path().parent().unwrap();
        fs::create_dir(&root.join("foo")).unwrap();
        File::create(&root.join("foo/bar")).unwrap();
        let mut called = false;
        index
            .add_all(
                ["foo"].iter(),
                crate::IndexAddOption::DEFAULT,
                Some(&mut |a: &Path, b: &[u8]| {
                    assert!(!called);
                    called = true;
                    assert_eq!(b, b"foo");
                    assert_eq!(a, Path::new("foo/bar"));
                    0
                }),
            )
            .unwrap();
        assert!(called);

        called = false;
        index
            .remove_all(
                ["."].iter(),
                Some(&mut |a: &Path, b: &[u8]| {
                    assert!(!called);
                    called = true;
                    assert_eq!(b, b".");
                    assert_eq!(a, Path::new("foo/bar"));
                    0
                }),
            )
            .unwrap();
        assert!(called);
    }

    #[test]
    fn smoke_add() {
        let (_td, repo) = crate::test::repo_init();
        let mut index = repo.index().unwrap();

        let root = repo.path().parent().unwrap();
        fs::create_dir(&root.join("foo")).unwrap();
        File::create(&root.join("foo/bar")).unwrap();
        index.add_path(Path::new("foo/bar")).unwrap();
        index.write().unwrap();
        assert_eq!(index.iter().count(), 1);

        // Make sure we can use this repo somewhere else now.
        let id = index.write_tree().unwrap();
        let tree = repo.find_tree(id).unwrap();
        let sig = repo.signature().unwrap();
        let id = repo.refname_to_id("HEAD").unwrap();
        let parent = repo.find_commit(id).unwrap();
        let commit = repo
            .commit(Some("HEAD"), &sig, &sig, "commit", &tree, &[&parent])
            .unwrap();
        let obj = repo.find_object(commit, None).unwrap();
        repo.reset(&obj, ResetType::Hard, None).unwrap();

        let td2 = TempDir::new().unwrap();
        let url = crate::test::path2url(&root);
        let repo = Repository::clone(&url, td2.path()).unwrap();
        let obj = repo.find_object(commit, None).unwrap();
        repo.reset(&obj, ResetType::Hard, None).unwrap();
    }

    #[test]
    fn add_then_read() {
        let mut index = Index::new_ext(ObjectFormat::Sha1).unwrap();
        let mut e = entry();
        e.path = b"foobar".to_vec();
        index.add(&e).unwrap();
        let e = index.get(0).unwrap();
        assert_eq!(e.path.len(), 6);
    }

    #[test]
    fn add_then_find() {
        let mut index = Index::new_ext(ObjectFormat::Sha1).unwrap();
        let mut e = entry();
        e.path = b"foo/bar".to_vec();
        index.add(&e).unwrap();
        let mut e = entry();
        e.path = b"foo2/bar".to_vec();
        index.add(&e).unwrap();
        assert_eq!(index.get(0).unwrap().path, b"foo/bar");
        assert_eq!(
            index.get_path(Path::new("foo/bar"), 0).unwrap().path,
            b"foo/bar"
        );
        assert_eq!(index.find_prefix(Path::new("foo2/")), Ok(1));
        assert_eq!(
            index.find_prefix(Path::new("empty/")).unwrap_err().code(),
            ErrorCode::NotFound
        );
    }

    #[test]
    fn add_frombuffer_then_read() {
        let (_td, repo) = crate::test::repo_init();
        let mut index = repo.index().unwrap();

        let mut e = entry();
        e.path = b"foobar".to_vec();
        let content = b"the contents";
        index.add_frombuffer(&e, content).unwrap();
        let e = index.get(0).unwrap();
        assert_eq!(e.path.len(), 6);

        let b = repo.find_blob(e.id).unwrap();
        assert_eq!(b.content(), content);
    }

    fn entry() -> IndexEntry {
        IndexEntry {
            ctime: IndexTime::new(0, 0),
            mtime: IndexTime::new(0, 0),
            dev: 0,
            ino: 0,
            mode: 0o100644,
            uid: 0,
            gid: 0,
            file_size: 0,
            #[cfg(not(feature = "unstable-sha256"))]
            id: Oid::ZERO_SHA1,
            #[cfg(feature = "unstable-sha256")]
            id: Oid::ZERO_SHA256,
            flags: 0,
            flags_extended: 0,
            path: Vec::new(),
        }
    }

    #[test]
    #[cfg(feature = "unstable-sha256")]
    fn index_sha256() {
        let (_td, repo) = crate::test::repo_init_sha256();
        let mut index = repo.index().unwrap();

        // Test opening with correct format
        Index::open_ext(&repo.path().join("index"), ObjectFormat::Sha256).unwrap();

        // Test basic operations with SHA256
        index.clear().unwrap();
        index.read(true).unwrap();
        index.write().unwrap();
        let tree_id = index.write_tree().unwrap();

        // Verify OID is 32 bytes (SHA256)
        assert_eq!(tree_id.as_bytes().len(), 32);
    }

    #[test]
    #[cfg(feature = "unstable-sha256")]
    fn smoke_in_memory_index_sha256() {
        let _index = Index::new_ext(ObjectFormat::Sha256).unwrap();
    }
}
