use bstr::ByteSlice;
use regex::bytes::Regex;
use std::{cmp::min, io};
use super::git;

pub trait CommitParser {
    fn get_upstream<'a>(&self, commit: &'a [u8], _message: &'a [u8]) -> Vec<&'a [u8]> {
        vec![commit]
    }
}

/// Iterates over pairs of (downstream commit sha, corresponding upstream commit sha).
/// The upstream commit sha is None if no upstream commit reference can be parsed.
pub struct ResolvedCommitsIterator {
    parser: Box<dyn CommitParser + Send>,
    commits: Box<dyn Iterator<Item = io::Result<(Vec<u8>, Vec<u8>)>> + Send>,
    verify_in: Option<git::Git>,
    revert_regex: Option<Regex>,
}

impl ResolvedCommitsIterator {
    pub fn new_with_parser(
        parser: Box<dyn CommitParser + Send>,
        walk: &git::Walk,
    ) -> ResolvedCommitsIterator {
        let commits = Box::new(walk.iter_sha_body());
        ResolvedCommitsIterator {
            parser: parser,
            commits: commits,
            verify_in: None,
            revert_regex: None,
        }
    }

    pub fn new(walk: &git::Walk) -> ResolvedCommitsIterator {
        Self::new_with_parser(Box::new(DownstreamCommitParser::new()), walk)
    }

    /// Skip commits that are reverts.
    pub fn ignore_reverts(mut self) -> Self {
        self.revert_regex = Some(Regex::new(r"^(?:\[[^\]]*\])? *[Rr]evert").unwrap());
        self
    }

    /// Verify that the found references exist in the given repository. They will be also
    /// normalized.
    pub fn verify_in(mut self, repo: git::Git) -> Self {
        self.verify_in = Some(repo);
        self
    }
}

impl Iterator for ResolvedCommitsIterator {
    type Item = io::Result<(Vec<u8>, Option<Vec<u8>>)>;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            let (commit, message) = match self.commits.next()? {
                Ok((commit, message)) => (commit, message),
                Err(err) => return Some(Err(err)),
            };
            if matches!(&self.revert_regex, Some(regex) if regex.is_match(&message)) {
                continue;
            }
            let upstream_commits = self.parser.get_upstream(&commit, &message);
            if upstream_commits.len() != 1 {
                return Some(Ok((commit, None)));
            }
            let upstream = if let Some(repo) = &self.verify_in {
                repo.revparse(upstream_commits[0].as_bytes()).ok()
            } else {
                Some(upstream_commits[0].to_vec())
            };
            return Some(Ok((commit, upstream)));
        }
    }
}

pub struct DownstreamCommitParser {
    upstream_ref: Regex,
}

impl DownstreamCommitParser {
    pub fn new() -> DownstreamCommitParser {
        DownstreamCommitParser {
            upstream_ref: Regex::new(r"(?m)^commit ([0-9a-f]{40})(?: \(.*\))?$|^\(cherry picked from commit ([0-9a-f]{40})\)$").unwrap(),
        }
    }
}

impl CommitParser for DownstreamCommitParser {
    fn get_upstream<'a>(&self, _commit: &'a [u8], message: &'a [u8]) -> Vec<&'a [u8]> {
        let mut result: Vec<&[u8]> = Vec::new();
        for upstream in self.upstream_ref.captures_iter(message)
            .map(|capt| capt.get(1).unwrap_or_else(|| capt.get(2).unwrap()).as_bytes())
        {
            // deduplicate upstream references
            if result.iter().all(|included| {
                let len = min(included.len(), upstream.len());
                &included[.. len] != &upstream[.. len]
            }) {
                result.push(upstream);
            }
        }
        result
    }
}

pub struct KernelStableCommitParser {
    upstream_ref: Regex,
}

impl KernelStableCommitParser {
    pub fn new() -> KernelStableCommitParser {
        KernelStableCommitParser {
            upstream_ref: Regex::new(r"\b[0-9a-f]{40}\b").unwrap(),
        }
    }
}

impl CommitParser for KernelStableCommitParser {
    fn get_upstream<'a>(&self, _commit: &'a [u8], message: &'a [u8]) -> Vec<&'a [u8]> {
        let line = match message.split(|&b| b == b'\n').nth(2) {
            None => return vec![],
            Some(x) => x,
        };
        self.upstream_ref.find_iter(line)
            .map(|matched| matched.as_bytes())
            .collect()
    }
}
