//! Commit creation and modification operations.

use git2::Signature;

use crate::commit::types::*;
use crate::{GitDomain, GitError, GitResult};

fn parents_to_git2<'a>(
    repo: &'a git2::Repository,
    parent_ids: &[CommitOid],
) -> GitResult<Vec<git2::Commit<'a>>> {
    parent_ids
        .iter()
        .map(|oid| {
            repo.find_commit(oid.to_oid()?)
                .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))
        })
        .collect()
}

impl GitDomain {
    pub fn commit_default_signature(&self) -> GitResult<CommitSignature> {
        let sig = self
            .repo()
            .signature()
            .map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(CommitSignature::from_git2(sig))
    }

    pub fn commit_signature_now(&self, name: &str, email: &str) -> GitResult<CommitSignature> {
        let sig = Signature::now(name, email).map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(CommitSignature::from_git2(sig))
    }

    pub fn commit_signature_at(
        &self,
        name: &str,
        email: &str,
        time_secs: i64,
        offset_minutes: i32,
    ) -> GitResult<CommitSignature> {
        let time = git2::Time::new(time_secs, offset_minutes);
        let sig =
            Signature::new(name, email, &time).map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(CommitSignature::from_git2(sig))
    }

    pub fn commit_signature_to_git2(&self, sig: &CommitSignature) -> GitResult<Signature<'static>> {
        let time = git2::Time::new(sig.time_secs, sig.offset_minutes);
        Signature::new(&sig.name, &sig.email, &time).map_err(|e| GitError::Internal(e.to_string()))
    }

    pub fn commit_create(
        &self,
        update_ref: Option<&str>,
        author: &CommitSignature,
        committer: &CommitSignature,
        message: &str,
        tree_id: &CommitOid,
        parent_ids: &[CommitOid],
    ) -> GitResult<CommitOid> {
        let author = self.commit_signature_to_git2(author)?;
        let committer = self.commit_signature_to_git2(committer)?;
        let tree = self
            .repo()
            .find_tree(tree_id.to_oid()?)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let parents = parents_to_git2(&*self.repo(), parent_ids)?;
        let parent_refs: Vec<&git2::Commit<'_>> =
            parents.iter().map(|p| p as &git2::Commit).collect();
        let oid = self
            .repo()
            .commit(
                update_ref,
                &author,
                &committer,
                message,
                &tree,
                &parent_refs,
            )
            .map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(CommitOid::from_git2(oid))
    }

    pub fn commit_create_from_index(
        &self,
        update_ref: Option<&str>,
        author: &CommitSignature,
        committer: &CommitSignature,
        message: &str,
        parent_ids: &[CommitOid],
    ) -> GitResult<CommitOid> {
        let author = self.commit_signature_to_git2(author)?;
        let committer = self.commit_signature_to_git2(committer)?;
        let mut index = self
            .repo()
            .index()
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let tree_oid = index
            .write_tree()
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let tree = self
            .repo()
            .find_tree(tree_oid)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let parents = parents_to_git2(&*self.repo(), parent_ids)?;
        let parent_refs: Vec<&git2::Commit<'_>> =
            parents.iter().map(|p| p as &git2::Commit).collect();
        let oid = self
            .repo()
            .commit(
                update_ref,
                &author,
                &committer,
                message,
                &tree,
                &parent_refs,
            )
            .map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(CommitOid::from_git2(oid))
    }

    pub fn commit_sign(
        &self,
        author: &CommitSignature,
        committer: &CommitSignature,
        message: &str,
        tree_id: &CommitOid,
        parent_ids: &[CommitOid],
        gpg_signature: &str,
        signature_field: Option<&str>,
    ) -> GitResult<CommitOid> {
        let author = self.commit_signature_to_git2(author)?;
        let committer = self.commit_signature_to_git2(committer)?;
        let tree = self
            .repo()
            .find_tree(tree_id.to_oid()?)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let parents = parents_to_git2(&*self.repo(), parent_ids)?;
        let parent_refs: Vec<&git2::Commit<'_>> =
            parents.iter().map(|p| p as &git2::Commit).collect();
        let buf = self
            .repo()
            .commit_create_buffer(&author, &committer, message, &tree, &parent_refs)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        let buf_str = std::str::from_utf8(&*buf).map_err(|e| GitError::Internal(e.to_string()))?;
        let oid = self
            .repo()
            .commit_signed(buf_str, gpg_signature, signature_field)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(CommitOid::from_git2(oid))
    }

    pub fn commit_extract_signature(
        &self,
        commit_oid: &CommitOid,
        signature_field: Option<&str>,
    ) -> GitResult<Option<(String, String)>> {
        match self
            .repo()
            .extract_signature(&commit_oid.to_oid()?, signature_field)
        {
            Ok((sig_buf, content_buf)) => Ok(Some((
                String::from_utf8_lossy(&*sig_buf).to_string(),
                String::from_utf8_lossy(&*content_buf).to_string(),
            ))),
            Err(e) => {
                if e.code() == git2::ErrorCode::NotFound {
                    Ok(None)
                } else {
                    Err(GitError::Internal(e.to_string()))
                }
            }
        }
    }

    pub fn commit_empty(
        &self,
        update_ref: Option<&str>,
        author: &CommitSignature,
        committer: &CommitSignature,
        message: &str,
        parent_ids: &[CommitOid],
    ) -> GitResult<CommitOid> {
        let author = self.commit_signature_to_git2(author)?;
        let committer = self.commit_signature_to_git2(committer)?;
        let tree = if let Some(first) = parent_ids.first() {
            let commit = self
                .repo()
                .find_commit(first.to_oid()?)
                .map_err(|_e| GitError::ObjectNotFound(first.to_string()))?;
            commit
                .tree()
                .map_err(|e| GitError::Internal(e.to_string()))?
        } else {
            let mut index = self
                .repo()
                .index()
                .map_err(|e| GitError::Internal(e.to_string()))?;
            let tree_oid = index
                .write_tree()
                .map_err(|e| GitError::Internal(e.to_string()))?;
            self.repo()
                .find_tree(tree_oid)
                .map_err(|e| GitError::Internal(e.to_string()))?
        };
        let parents = parents_to_git2(&*self.repo(), parent_ids)?;
        let parent_refs: Vec<&git2::Commit<'_>> =
            parents.iter().map(|p| p as &git2::Commit).collect();
        let oid = self
            .repo()
            .commit(
                update_ref,
                &author,
                &committer,
                message,
                &tree,
                &parent_refs,
            )
            .map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(CommitOid::from_git2(oid))
    }

    pub fn commit_amend(
        &self,
        commit_oid: &CommitOid,
        update_ref: Option<&str>,
        author: Option<&CommitSignature>,
        committer: Option<&CommitSignature>,
        message_encoding: Option<&str>,
        message: Option<&str>,
        tree_id: Option<&CommitOid>,
    ) -> GitResult<CommitOid> {
        let commit = self
            .repo()
            .find_commit(commit_oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(commit_oid.to_string()))?;
        let author = author
            .map(|a| self.commit_signature_to_git2(a))
            .transpose()?;
        let committer = committer
            .map(|c| self.commit_signature_to_git2(c))
            .transpose()?;
        let tree = tree_id
            .map(|t| {
                self.repo
                    .find_tree(t.to_oid()?)
                    .map_err(|e| GitError::Internal(e.to_string()))
            })
            .transpose()?;
        let oid = commit
            .amend(
                update_ref,
                author.as_ref(),
                committer.as_ref(),
                message_encoding,
                message,
                tree.as_ref(),
            )
            .map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(CommitOid::from_git2(oid))
    }

    pub fn commit_amend_author(
        &self,
        commit_oid: &CommitOid,
        new_author: &CommitSignature,
        update_ref: Option<&str>,
    ) -> GitResult<CommitOid> {
        self.commit_amend(
            commit_oid,
            update_ref,
            Some(new_author),
            None,
            None,
            None,
            None,
        )
    }

    pub fn commit_amend_message(
        &self,
        commit_oid: &CommitOid,
        new_message: &str,
        update_ref: Option<&str>,
    ) -> GitResult<CommitOid> {
        self.commit_amend(
            commit_oid,
            update_ref,
            None,
            None,
            None,
            Some(new_message),
            None,
        )
    }

    pub fn commit_amend_tree(
        &self,
        commit_oid: &CommitOid,
        new_tree_id: &CommitOid,
        update_ref: Option<&str>,
    ) -> GitResult<CommitOid> {
        self.commit_amend(
            commit_oid,
            update_ref,
            None,
            None,
            None,
            None,
            Some(new_tree_id),
        )
    }
}
