//! Commit traversal and iteration.

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

#[derive(Debug, Clone)]
pub struct CommitWalkOptions {
    pub rev: Option<String>,
    pub sort: CommitSort,
    pub limit: usize,
    pub first_parent_only: bool,
}

impl CommitWalkOptions {
    pub fn new() -> Self {
        Self {
            rev: None,
            sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()),
            limit: 0,
            first_parent_only: false,
        }
    }

    pub fn rev(mut self, rev: &str) -> Self {
        self.rev = Some(rev.to_string());
        self
    }

    pub fn topological(mut self) -> Self {
        self.sort = CommitSort(git2::Sort::TOPOLOGICAL.bits());
        self
    }

    pub fn time_order(mut self) -> Self {
        self.sort = CommitSort(git2::Sort::TIME.bits());
        self
    }

    pub fn reverse(mut self) -> Self {
        self.sort = CommitSort(self.sort.0 | git2::Sort::REVERSE.bits());
        self
    }

    pub fn limit(mut self, n: usize) -> Self {
        self.limit = n;
        self
    }

    pub fn first_parent(mut self) -> Self {
        self.first_parent_only = true;
        self
    }
}

impl Default for CommitWalkOptions {
    fn default() -> Self {
        Self::new()
    }
}

impl GitDomain {
    pub fn commit_walk(&self, opts: CommitWalkOptions) -> GitResult<Vec<CommitMeta>> {
        let mut revwalk = self
            .repo()
            .revwalk()
            .map_err(|e| GitError::Internal(e.to_string()))?;

        revwalk
            .set_sorting(opts.sort.to_git2())
            .map_err(|e| GitError::Internal(e.to_string()))?;

        if let Some(ref r) = opts.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()))?;
        }

        let mut commits = Vec::new();

        if opts.first_parent_only {
            let mut prev_oid: Option<git2::Oid> = None;

            for oid_result in revwalk {
                let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?;

                if let Some(prev) = prev_oid {
                    if let Ok(commit) = self.repo.find_commit(oid) {
                        if commit.parent_ids().next() == Some(prev) {
                            if limit_check(&commits, opts.limit) {
                                break;
                            }
                            commits.push(CommitMeta::from_git2(&commit));
                            prev_oid = Some(oid);
                        }
                    }
                } else {
                    if let Ok(commit) = self.repo.find_commit(oid) {
                        if limit_check(&commits, opts.limit) {
                            break;
                        }
                        commits.push(CommitMeta::from_git2(&commit));
                        prev_oid = Some(oid);
                    }
                }
            }
        } else {
            for oid_result in revwalk {
                let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?;
                if limit_check(&commits, opts.limit) {
                    break;
                }
                if let Ok(commit) = self.repo.find_commit(oid) {
                    commits.push(CommitMeta::from_git2(&commit));
                }
            }
        }

        Ok(commits)
    }

    pub fn commit_topo_walk(&self, rev: Option<&str>, limit: usize) -> GitResult<Vec<CommitMeta>> {
        self.commit_walk(CommitWalkOptions {
            rev: rev.map(String::from),
            sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()),
            limit,
            first_parent_only: false,
        })
    }

    pub fn commit_reverse_walk(
        &self,
        rev: Option<&str>,
        limit: usize,
    ) -> GitResult<Vec<CommitMeta>> {
        self.commit_walk(CommitWalkOptions {
            rev: rev.map(String::from),
            sort: CommitSort(git2::Sort::TIME.bits() | git2::Sort::REVERSE.bits()),
            limit,
            first_parent_only: false,
        })
    }

    pub fn commit_mainline(&self, rev: Option<&str>, limit: usize) -> GitResult<Vec<CommitMeta>> {
        self.commit_walk(CommitWalkOptions {
            rev: rev.map(String::from),
            sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()),
            limit,
            first_parent_only: true,
        })
    }

    pub fn commit_ancestors(&self, oid: &CommitOid, limit: usize) -> GitResult<Vec<CommitMeta>> {
        let mut revwalk = self
            .repo()
            .revwalk()
            .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()))?;
        revwalk
            .push(oid.to_oid()?)
            .map_err(|e| GitError::Internal(e.to_string()))?;

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

    pub fn commit_descendants(&self, oid: &CommitOid, limit: usize) -> GitResult<Vec<CommitMeta>> {
        let range = format!("{}..", oid);
        self.commit_walk(CommitWalkOptions {
            rev: Some(range),
            sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()),
            limit,
            first_parent_only: false,
        })
    }

    pub fn resolve_rev(&self, rev: &str) -> GitResult<CommitOid> {
        if let Ok(oid) = git2::Oid::from_str(rev) {
            return Ok(CommitOid::from_git2(oid));
        }

        if let Ok(reference) = self.repo.find_reference(rev) {
            if let Some(target) = reference.target() {
                return Ok(CommitOid::from_git2(target));
            }
        }

        if let Ok(commit) = self.repo.revparse_single(rev) {
            return Ok(CommitOid::from_git2(commit.id()));
        }

        Err(GitError::InvalidOid(format!("cannot resolve: {}", rev)))
    }
}

fn limit_check(commits: &[CommitMeta], limit: usize) -> bool {
    limit > 0 && commits.len() >= limit
}
