use libc;
use std::marker;
use std::mem;
use std::ops::Range;
use std::ptr;
use std::str;

use crate::util::Binding;
use crate::{raw, signature, Error, Object, Oid, Signature, Time, Tree};

/// A structure to represent a git [commit][1]
///
/// [1]: http://git-scm.com/book/en/Git-Internals-Git-Objects
pub struct Commit<'repo> {
    raw: *mut raw::git_commit,
    _marker: marker::PhantomData<Object<'repo>>,
}

/// An iterator over the parent commits of a commit.
///
/// Aborts iteration when a commit cannot be found
pub struct Parents<'commit, 'repo> {
    range: Range<usize>,
    commit: &'commit Commit<'repo>,
}

/// An iterator over the parent commits' ids of a commit.
///
/// Aborts iteration when a commit cannot be found
pub struct ParentIds<'commit> {
    range: Range<usize>,
    commit: &'commit Commit<'commit>,
}

impl<'repo> Commit<'repo> {
    /// Get the id (SHA1) of a repository commit
    pub fn id(&self) -> Oid {
        unsafe { Binding::from_raw(raw::git_commit_id(&*self.raw)) }
    }

    /// Get the id of the tree pointed to by this commit.
    ///
    /// No attempts are made to fetch an object from the ODB.
    pub fn tree_id(&self) -> Oid {
        unsafe { Binding::from_raw(raw::git_commit_tree_id(&*self.raw)) }
    }

    /// Get the tree pointed to by a commit.
    pub fn tree(&self) -> Result<Tree<'repo>, Error> {
        let mut ret = ptr::null_mut();
        unsafe {
            try_call!(raw::git_commit_tree(&mut ret, &*self.raw));
            Ok(Binding::from_raw(ret))
        }
    }

    /// Get access to the underlying raw pointer.
    pub fn raw(&self) -> *mut raw::git_commit {
        self.raw
    }

    /// Get the full message of a commit.
    ///
    /// The returned message will be slightly prettified by removing any
    /// potential leading newlines.
    ///
    /// `None` will be returned if the message is not valid utf-8
    pub fn message(&self) -> Option<&str> {
        str::from_utf8(self.message_bytes()).ok()
    }

    /// Get the full message of a commit as a byte slice.
    ///
    /// The returned message will be slightly prettified by removing any
    /// potential leading newlines.
    pub fn message_bytes(&self) -> &[u8] {
        unsafe { crate::opt_bytes(self, raw::git_commit_message(&*self.raw)).unwrap() }
    }

    /// Get the encoding for the message of a commit, as a string representing a
    /// standard encoding name.
    ///
    /// `None` will be returned if the encoding is not known
    pub fn message_encoding(&self) -> Option<&str> {
        let bytes = unsafe { crate::opt_bytes(self, raw::git_commit_message_encoding(&*self.raw)) };
        bytes.and_then(|b| str::from_utf8(b).ok())
    }

    /// Get the full raw message of a commit.
    ///
    /// `None` will be returned if the message is not valid utf-8
    pub fn message_raw(&self) -> Option<&str> {
        str::from_utf8(self.message_raw_bytes()).ok()
    }

    /// Get the full raw message of a commit.
    pub fn message_raw_bytes(&self) -> &[u8] {
        unsafe { crate::opt_bytes(self, raw::git_commit_message_raw(&*self.raw)).unwrap() }
    }

    /// Get the full raw text of the commit header.
    ///
    /// `None` will be returned if the message is not valid utf-8
    pub fn raw_header(&self) -> Option<&str> {
        str::from_utf8(self.raw_header_bytes()).ok()
    }

    /// Get the full raw text of the commit header.
    pub fn raw_header_bytes(&self) -> &[u8] {
        unsafe { crate::opt_bytes(self, raw::git_commit_raw_header(&*self.raw)).unwrap() }
    }

    /// Get the short "summary" of the git commit message.
    ///
    /// The returned message is the summary of the commit, comprising the first
    /// paragraph of the message with whitespace trimmed and squashed.
    ///
    /// `None` may be returned if an error occurs or if the summary is not valid
    /// utf-8.
    pub fn summary(&self) -> Option<&str> {
        self.summary_bytes().and_then(|s| str::from_utf8(s).ok())
    }

    /// Get the short "summary" of the git commit message.
    ///
    /// The returned message is the summary of the commit, comprising the first
    /// paragraph of the message with whitespace trimmed and squashed.
    ///
    /// `None` may be returned if an error occurs
    pub fn summary_bytes(&self) -> Option<&[u8]> {
        unsafe { crate::opt_bytes(self, raw::git_commit_summary(self.raw)) }
    }

    /// Get the commit time (i.e. committer time) of a commit.
    ///
    /// The first element of the tuple is the time, in seconds, since the epoch.
    /// The second element is the offset, in minutes, of the time zone of the
    /// committer's preferred time zone.
    pub fn time(&self) -> Time {
        unsafe {
            Time::new(
                raw::git_commit_time(&*self.raw) as i64,
                raw::git_commit_time_offset(&*self.raw) as i32,
            )
        }
    }

    /// Creates a new iterator over the parents of this commit.
    pub fn parents<'a>(&'a self) -> Parents<'a, 'repo> {
        Parents {
            range: 0..self.parent_count(),
            commit: self,
        }
    }

    /// Creates a new iterator over the parents of this commit.
    pub fn parent_ids(&self) -> ParentIds<'_> {
        ParentIds {
            range: 0..self.parent_count(),
            commit: self,
        }
    }

    /// Get the author of this commit.
    pub fn author(&self) -> Signature<'_> {
        unsafe {
            let ptr = raw::git_commit_author(&*self.raw);
            signature::from_raw_const(self, ptr)
        }
    }

    /// Get the committer of this commit.
    pub fn committer(&self) -> Signature<'_> {
        unsafe {
            let ptr = raw::git_commit_committer(&*self.raw);
            signature::from_raw_const(self, ptr)
        }
    }

    /// Amend this existing commit with all non-`None` values
    ///
    /// This creates a new commit that is exactly the same as the old commit,
    /// except that any non-`None` values will be updated. The new commit has
    /// the same parents as the old commit.
    ///
    /// For information about `update_ref`, see [`Repository::commit`].
    ///
    /// [`Repository::commit`]: struct.Repository.html#method.commit
    pub fn amend(
        &self,
        update_ref: Option<&str>,
        author: Option<&Signature<'_>>,
        committer: Option<&Signature<'_>>,
        message_encoding: Option<&str>,
        message: Option<&str>,
        tree: Option<&Tree<'repo>>,
    ) -> Result<Oid, Error> {
        let mut raw = raw::git_oid {
            id: [0; raw::GIT_OID_RAWSZ],
        };
        let update_ref = crate::opt_cstr(update_ref)?;
        let encoding = crate::opt_cstr(message_encoding)?;
        let message = crate::opt_cstr(message)?;
        unsafe {
            try_call!(raw::git_commit_amend(
                &mut raw,
                self.raw(),
                update_ref,
                author.map(|s| s.raw()),
                committer.map(|s| s.raw()),
                encoding,
                message,
                tree.map(|t| t.raw())
            ));
            Ok(Binding::from_raw(&raw as *const _))
        }
    }

    /// Get the number of parents of this commit.
    ///
    /// Use the `parents` iterator to return an iterator over all parents.
    pub fn parent_count(&self) -> usize {
        unsafe { raw::git_commit_parentcount(&*self.raw) as usize }
    }

    /// Get the specified parent of the commit.
    ///
    /// Use the `parents` iterator to return an iterator over all parents.
    pub fn parent(&self, i: usize) -> Result<Commit<'repo>, Error> {
        unsafe {
            let mut raw = ptr::null_mut();
            try_call!(raw::git_commit_parent(
                &mut raw,
                &*self.raw,
                i as libc::c_uint
            ));
            Ok(Binding::from_raw(raw))
        }
    }

    /// Get the specified parent id of the commit.
    ///
    /// This is different from `parent`, which will attempt to load the
    /// parent commit from the ODB.
    ///
    /// Use the `parent_ids` iterator to return an iterator over all parents.
    pub fn parent_id(&self, i: usize) -> Result<Oid, Error> {
        unsafe {
            let id = raw::git_commit_parent_id(self.raw, i as libc::c_uint);
            if id.is_null() {
                Err(Error::from_str("parent index out of bounds"))
            } else {
                Ok(Binding::from_raw(id))
            }
        }
    }

    /// Casts this Commit to be usable as an `Object`
    pub fn as_object(&self) -> &Object<'repo> {
        unsafe { &*(self as *const _ as *const Object<'repo>) }
    }

    /// Consumes Commit to be returned as an `Object`
    pub fn into_object(self) -> Object<'repo> {
        assert_eq!(mem::size_of_val(&self), mem::size_of::<Object<'_>>());
        unsafe { mem::transmute(self) }
    }
}

impl<'repo> Binding for Commit<'repo> {
    type Raw = *mut raw::git_commit;
    unsafe fn from_raw(raw: *mut raw::git_commit) -> Commit<'repo> {
        Commit {
            raw: raw,
            _marker: marker::PhantomData,
        }
    }
    fn raw(&self) -> *mut raw::git_commit {
        self.raw
    }
}

impl<'repo> std::fmt::Debug for Commit<'repo> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        let mut ds = f.debug_struct("Commit");
        ds.field("id", &self.id());
        if let Some(summary) = self.summary() {
            ds.field("summary", &summary);
        }
        ds.finish()
    }
}

/// Aborts iteration when a commit cannot be found
impl<'repo, 'commit> Iterator for Parents<'commit, 'repo> {
    type Item = Commit<'repo>;
    fn next(&mut self) -> Option<Commit<'repo>> {
        self.range.next().and_then(|i| self.commit.parent(i).ok())
    }
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.range.size_hint()
    }
}

/// Aborts iteration when a commit cannot be found
impl<'repo, 'commit> DoubleEndedIterator for Parents<'commit, 'repo> {
    fn next_back(&mut self) -> Option<Commit<'repo>> {
        self.range
            .next_back()
            .and_then(|i| self.commit.parent(i).ok())
    }
}

impl<'repo, 'commit> ExactSizeIterator for Parents<'commit, 'repo> {}

/// Aborts iteration when a commit cannot be found
impl<'commit> Iterator for ParentIds<'commit> {
    type Item = Oid;
    fn next(&mut self) -> Option<Oid> {
        self.range
            .next()
            .and_then(|i| self.commit.parent_id(i).ok())
    }
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.range.size_hint()
    }
}

/// Aborts iteration when a commit cannot be found
impl<'commit> DoubleEndedIterator for ParentIds<'commit> {
    fn next_back(&mut self) -> Option<Oid> {
        self.range
            .next_back()
            .and_then(|i| self.commit.parent_id(i).ok())
    }
}

impl<'commit> ExactSizeIterator for ParentIds<'commit> {}

impl<'repo> Clone for Commit<'repo> {
    fn clone(&self) -> Self {
        self.as_object().clone().into_commit().ok().unwrap()
    }
}

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

#[cfg(test)]
mod tests {
    #[test]
    fn smoke() {
        let (_td, repo) = crate::test::repo_init();
        let head = repo.head().unwrap();
        let target = head.target().unwrap();
        let commit = repo.find_commit(target).unwrap();
        assert_eq!(commit.message(), Some("initial"));
        assert_eq!(commit.id(), target);
        commit.message_raw().unwrap();
        commit.raw_header().unwrap();
        commit.message_encoding();
        commit.summary().unwrap();
        commit.tree_id();
        commit.tree().unwrap();
        assert_eq!(commit.parents().count(), 0);

        assert_eq!(commit.author().name(), Some("name"));
        assert_eq!(commit.author().email(), Some("email"));
        assert_eq!(commit.committer().name(), Some("name"));
        assert_eq!(commit.committer().email(), Some("email"));

        let sig = repo.signature().unwrap();
        let tree = repo.find_tree(commit.tree_id()).unwrap();
        let id = repo
            .commit(Some("HEAD"), &sig, &sig, "bar", &tree, &[&commit])
            .unwrap();
        let head = repo.find_commit(id).unwrap();

        let new_head = head
            .amend(Some("HEAD"), None, None, None, Some("new message"), None)
            .unwrap();
        let new_head = repo.find_commit(new_head).unwrap();
        assert_eq!(new_head.message(), Some("new message"));
        new_head.into_object();

        repo.find_object(target, None).unwrap().as_commit().unwrap();
        repo.find_object(target, None)
            .unwrap()
            .into_commit()
            .ok()
            .unwrap();
    }
}
