//! Commit querying operations.

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

impl GitDomain {
    pub fn commit_get(&self, oid: &CommitOid) -> GitResult<CommitMeta> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(CommitMeta::from_git2(&commit))
    }

    pub fn commit_get_prefix(&self, prefix: &str) -> GitResult<CommitMeta> {
        let commit = self
            .repo()
            .find_commit_by_prefix(prefix)
            .map_err(|_e| GitError::InvalidOid(format!("prefix: {}", prefix)))?;
        Ok(CommitMeta::from_git2(&commit))
    }

    pub fn commit_exists(&self, oid: &CommitOid) -> bool {
        match oid.to_oid() {
            Ok(oid) => self.repo.find_commit(oid).is_ok(),
            Err(_) => false,
        }
    }

    pub fn commit_is_commit(&self, oid: &CommitOid) -> bool {
        match oid.to_oid() {
            Ok(oid) => match self.repo.find_object(oid, None) {
                Ok(obj) => obj.kind() == Some(git2::ObjectType::Commit),
                Err(_) => false,
            },
            Err(_) => false,
        }
    }

    pub fn commit_message(&self, oid: &CommitOid) -> GitResult<String> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(commit.message().unwrap_or("").to_string())
    }

    pub fn commit_summary(&self, oid: &CommitOid) -> GitResult<String> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(commit.summary().unwrap_or("").to_string())
    }

    pub fn commit_short_id(&self, oid: &CommitOid) -> GitResult<String> {
        let _ = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        let take = 7.min(oid.0.len());
        Ok(oid.0[..take].to_string())
    }

    pub fn commit_author(&self, oid: &CommitOid) -> GitResult<CommitSignature> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(CommitSignature::from_git2(commit.author()))
    }

    pub fn commit_committer(&self, oid: &CommitOid) -> GitResult<CommitSignature> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(CommitSignature::from_git2(commit.committer()))
    }

    pub fn commit_time(&self, oid: &CommitOid) -> GitResult<i64> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(commit.time().seconds())
    }

    pub fn commit_time_offset(&self, oid: &CommitOid) -> GitResult<i32> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(commit.time().offset_minutes())
    }

    pub fn commit_encoding(&self, oid: &CommitOid) -> GitResult<Option<String>> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(commit.message_encoding().map(String::from))
    }

    pub fn commit_tree_id(&self, oid: &CommitOid) -> GitResult<CommitOid> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(CommitOid::from_git2(commit.tree_id()))
    }

    pub fn commit_parent_count(&self, oid: &CommitOid) -> GitResult<usize> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(commit.parent_count())
    }

    pub fn commit_parent_ids(&self, oid: &CommitOid) -> GitResult<Vec<CommitOid>> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(commit.parent_ids().map(CommitOid::from_git2).collect())
    }

    pub fn commit_parent(&self, oid: &CommitOid, index: usize) -> GitResult<CommitMeta> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        let parent = commit
            .parent(index)
            .map_err(|e| GitError::Internal(e.to_string()))?;
        Ok(CommitMeta::from_git2(&parent))
    }

    pub fn commit_first_parent(&self, oid: &CommitOid) -> GitResult<Option<CommitMeta>> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        if commit.parent_count() > 0 {
            let parent = commit
                .parent(0)
                .map_err(|e| GitError::Internal(e.to_string()))?;
            Ok(Some(CommitMeta::from_git2(&parent)))
        } else {
            Ok(None)
        }
    }

    pub fn commit_is_merge(&self, oid: &CommitOid) -> GitResult<bool> {
        let commit = self
            .repo()
            .find_commit(oid.to_oid()?)
            .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
        Ok(commit.parent_count() > 1)
    }

    pub fn commit_log(
        &self,
        rev: Option<&str>,
        offset: usize,
        limit: usize,
    ) -> GitResult<Vec<CommitMeta>> {
        let mut revwalk = self
            .repo()
            .revwalk()
            .map_err(|e| GitError::Internal(e.to_string()))?;

        if let Some(r) = rev {
            if r.contains("..") {
                revwalk
                    .push_range(r)
                    .map_err(|e| GitError::Internal(e.to_string()))?;
            } else {
                revwalk
                    .push_ref(r)
                    .map_err(|e| GitError::Internal(e.to_string()))?;
            }
        } else {
            revwalk
                .push_head()
                .map_err(|e| GitError::Internal(e.to_string()))?;
        }

        revwalk
            .set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let mut commits = Vec::new();
        let target = offset.saturating_add(limit);
        for oid_result in revwalk {
            let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?;
            if target > 0 && commits.len() >= target {
                break;
            }
            if let Ok(commit) = self.repo.find_commit(oid) {
                commits.push(CommitMeta::from_git2(&commit));
            }
        }

        Ok(commits.into_iter().skip(offset).take(limit).collect())
    }

    pub fn commit_range(&self, from: &str, to: &str) -> GitResult<Vec<CommitMeta>> {
        let range = format!("{}..{}", from, to);
        self.commit_log(Some(&range), 0, 0)
    }

    pub fn commit_count(&self, from: Option<&str>, to: Option<&str>) -> GitResult<usize> {
        let rev = match (from, to) {
            (Some(f), Some(t)) => Some(format!("{}..{}", f, t)),
            (Some(f), None) => Some(f.to_string()),
            (None, Some(t)) => Some(t.to_string()),
            (None, None) => None,
        };
        let commits = self.commit_log(rev.as_deref(), 0, 0)?;
        Ok(commits.len())
    }

    pub fn commit_total(&self, rev: Option<&str>) -> GitResult<usize> {
        self.commit_count(None, rev)
    }
}
