//! API for missing commit detection and presentation.
//!
//! The producer of missing commits (i.e. the plugin that searches for missing dependencies)
//! should implement a function that accepts `HunkInfo` and `FileHistory` and returns
//! `MissingResult`:
//!
//! ```
//! fn my_missing_searcher(info: &HunkInfo, history: &FileHistory) -> MissingResult {
//!     if !history.is_downstream(b"123456") {
//!         info.report_missing_downstream(b"123456")?;
//!     }
//!     Ok(())
//! }
//! ```
//!
//! The function is started in its own thread and can do blocking operations. To communicate
//! found commits and progress, the function can call methods of `HunkInfo`. See the struct
//! definition for details.

use bstr::ByteSlice;
use parking_lot::{RawRwLock, RwLock, ArcRwLockWriteGuard};
use smol::channel;
use std::{collections::HashSet, future::Future, ops::Range, sync::Arc};
use super::{fork_point::ForkPoint, git, git::Git};

/// Information about the hunk for which we're finding the missing commits and callbacks
/// for communication.
#[derive(derive_more::Debug)]
pub struct HunkInfo {
    /// Sha of the upstream commit we're backporting.
    #[debug("{:?}", upstream_sha.as_bstr())]
    pub(crate) upstream_sha: Vec<u8>,
    /// The upstream git repository.
    pub(crate) upstream_repo: Git,
    /// The downstream git repository. The dowstream commit we're backporting on top of is
    /// "HEAD" in this repository.
    pub(crate) downstream_repo: Git,
    /// The upstream file we're backporting. It's a relative path starting at the root
    /// of the upstream repository.
    #[debug("{:?}", upstream_file.as_bstr())]
    pub(crate) upstream_file: Vec<u8>,
    /// The downstream file to which we're backporting. It's a relative path starting at the
    /// root of the downstream repository. `upstream_file` and `downstream_file` will differ
    /// when Patchpal detects an out of order or parallel commit backport to downstream.
    #[debug("{:?}", downstream_file.as_bstr())]
    pub(crate) downstream_file: Vec<u8>,
    /// The line range in the upstream commit we're backporting from. This is the line range
    /// with the `upstream_sha` commit applied; if you're looking at the patch, this is the
    /// second set of numbers (the "after" numbers). The range counts inter-line positions
    /// in the file, with 0 being before the first line, 1 being between the first and second
    /// line, etc. The range includes the hunk context. While the context is usually 3 lines,
    /// do not depend on that; we might make it user configurable in the future.
    pub(crate) upstream_range: Range<usize>,
    /// The line range in the downstream file we're backporting to. This is the line range
    /// that was detected by Patchpal as the likely area the hunk should be backported to.
    /// In other words, this is the "before" range downstream if you look at the final
    /// backported patch. Similarly to `upstream_range`, this counts inter-line positions
    /// and includes the context. The context does not have a fixed size.
    pub(crate) downstream_range: Range<usize>,
    /// The fork point. This is the common branch point between the two repositories.
    #[debug("{:?}", fork_point.as_ref().map(|b| b.as_bstr()))]
    pub(crate) fork_point: Option<Vec<u8>>,
    // internal fields
    #[debug(ignore)]
    sender: channel::Sender<Message>,
}

/// The result of the search for missing commits. Returns () when successful, or the error
/// message (as string) when not successful.
pub(crate) type MissingResult = Result<(), String>;

/// Allows efficient querying of history of the given file.
#[derive(Clone)]
pub struct FileHistory {
    /// The map with all upstream commits since the fork point up to the upstream commit
    /// being applied.
    upstream: Arc<RwLock<HashSet<Vec<u8>>>>,
}

impl HunkInfo {
    /// Report a downstream commit sha that's missing upstream. The order is important:
    /// call this with the newest commit first, then the second newest, etc. If it returns
    /// MissingResult, the producer should stop and propagate the value.
    pub(crate) fn report_missing_upstream(&self, sha: &[u8]) -> MissingResult {
        self.sender.send_blocking(Message::MissingUpstream(
            self.downstream_repo.description(sha)
                .map_err(|err| err.to_string())?
        )).map_err(|err| err.to_string())
    }

    /// Report an upstream commit sha that's missing downstream. Similarly to
    /// `missing_upstream`, the order is important.
    pub(crate) fn report_missing_downstream(&self, sha: &[u8]) -> MissingResult {
        self.sender.send_blocking(Message::MissingDownstream(
            self.upstream_repo.description(sha)
                .map_err(|err| err.to_string())?
        )).map_err(|err| err.to_string())
    }

    fn report_result(&self, result: MissingResult) {
        // Ignore any errors. We can't do much when the channel got closed; the thread is
        // terminating anyway.
        let _ = self.sender.send_blocking(Message::Finished(result));
    }
}

impl FileHistory {
    /// Creates a new FileHistory for the given files. The computation of the history
    /// is scheduled to be executed asynchronously in the global thread pool. Any access
    /// to this struct will be blocked until that work finishes.
    pub fn new(
        upstream_repo: Git,
        upstream_sha: Vec<u8>,
        upstream_file: Vec<u8>,
        fork_point: ForkPoint,
    ) -> Self {
        let result = FileHistory {
            upstream: Arc::new(RwLock::new(HashSet::new())),
        };
        let upstream_w = result.upstream.write_arc();
        rayon::spawn_fifo(move || compute_history(
            upstream_w,
            upstream_repo,
            upstream_sha,
            upstream_file,
            fork_point,
        ));
        result
    }

    pub(crate) fn is_upstream(&self, sha: &[u8]) -> bool {
        self.upstream.read_arc().get(sha).is_some()
    }
}

fn compute_history(
    mut upstream: ArcRwLockWriteGuard<RawRwLock, HashSet<Vec<u8>>>,
    upstream_repo: Git,
    upstream_sha: Vec<u8>,
    upstream_file: Vec<u8>,
    fork_point: ForkPoint,
) {
    for sha in upstream_repo
        .walk(&upstream_sha)
        .base(fork_point.get())
        .file(Some(&upstream_file))
        .iter_sha()
        .map_while(|item| item.ok())
    {
        upstream.insert(sha);
    }
}

enum Message {
    MissingUpstream(git::Description),
    MissingDownstream(git::Description),
    Finished(MissingResult),
}

/// Implement this trait for GUI and other consumers of missing dependencies.
pub trait Consumer<T: Clone> {
    fn received_missing_upstream(&self, data: T, commit: git::Description);
    fn received_missing_downstream(&self, data: T, commit: git::Description);
    fn received_missing_result(&self, data: T, result: MissingResult);
}

/// GUI or other consumers call this to spawn a searcher in a thread.
pub fn spawn_missing_searcher<T: Clone>(
    consumer: impl Consumer<T> + 'static,
    consumer_data: T,
    producer: impl FnOnce(&HunkInfo, &FileHistory) -> MissingResult + Send + 'static,
    upstream_sha: Vec<u8>,
    upstream_repo: Git,
    downstream_repo: Git,
    upstream_file: Vec<u8>,
    downstream_file: Vec<u8>,
    upstream_range: Range<usize>,
    downstream_range: Range<usize>,
    fork_point: ForkPoint,
    file_history: FileHistory,
) -> impl Future<Output = ()> {
    let (sender, receiver) = channel::unbounded();
    let mut hunk_info = HunkInfo {
        upstream_sha,
        upstream_repo,
        downstream_repo,
        upstream_file,
        downstream_file,
        upstream_range,
        downstream_range,
        fork_point: None,
        sender,
    };
    rayon::spawn_fifo(move || {
        hunk_info.fork_point = fork_point.get().map(Vec::from);
        let result = producer(&hunk_info, &file_history);
        hunk_info.report_result(result);
    });
    async move {
        loop {
            let msg = match receiver.recv().await {
                Ok(msg) => msg,
                Err(_) => {
                    consumer.received_missing_result(
                        consumer_data.clone(),
                        Err("The helper thread crashed".into()),
                    );
                    break;
                }
            };
            match msg {
                Message::MissingUpstream(sha) => {
                    consumer.received_missing_upstream(consumer_data.clone(), sha);
                }
                Message::MissingDownstream(sha) => {
                    consumer.received_missing_downstream(consumer_data.clone(), sha);
                }
                Message::Finished(result) => {
                    consumer.received_missing_result(consumer_data.clone(), result);
                    break;
                }
            }
        }
    }
}
