//! Standalone cancellation primitive for verify-time aborts.
//!
//! [`CancelToken`] is a cheap, cloneable handle wrapping an
//! [`AtomicBool`]. One handle is held by the
//! thread driving the long-running scan
//! ([`SqPackPathVerifier::verify`](crate::SqPackPathVerifier::verify) /
//! [`SqPackPathVerifier::verify_streaming`](crate::SqPackPathVerifier::verify_streaming))
//! and one or more clones live wherever cancellation is initiated —
//! typically a UI event handler or a `tokio::select!` arm.
//!
//! # Why a separate primitive
//!
//! Cancellation can also be signalled by an
//! [`PathVerifyObserver`](crate::PathVerifyObserver) that returns `true` from
//! [`PathVerifyObserver::should_cancel`](crate::PathVerifyObserver::should_cancel),
//! but that route forces a consumer who only wants cancellation to implement
//! the entire trait. The token makes the common case trivial:
//!
//! ```no_run
//! use sqpack_rs::{CancelToken, SqPack, SqPackPathManifest, SqPackPathVerifier};
//!
//! let token = CancelToken::new();
//! let bg = token.clone();
//! std::thread::spawn(move || {
//!     // Some other thread flips the flag on a user gesture.
//!     bg.cancel();
//! });
//!
//! let sq = SqPack::open("/opt/ffxiv/game").unwrap();
//! let manifest = SqPackPathManifest::new();
//! let mut verifier = SqPackPathVerifier::new(&sq).with_cancel_token(token);
//! let report = verifier.verify(&manifest);
//! // `report.cancelled` is true when the token flipped mid-scan.
//! assert!(report.is_clean() || report.cancelled);
//! ```
//!
//! When both a token and an observer are installed, the verifier polls
//! both at each manifest-entry boundary — either source can request
//! cancellation independently, and they compose without a wrapper observer.
//!
//! # Cancellation semantics
//!
//! A cancelled scan returns a partial [`PathVerifyReport`](crate::verify::PathVerifyReport)
//! with `cancelled = true` and the work-done-so-far in `mismatches` /
//! `verified_count` / `skipped_count`. The streaming variant
//! ([`PathVerifyStream`](crate::verify::PathVerifyStream)) stops yielding events when
//! cancelled — `next()` returns `None` — while its accumulators reflect
//! work-done-so-far. This deviates from the sibling `zipatch-rs`'s
//! apply-side `ApplyError::Cancelled` because the verifier's "collect every
//! issue you can find" goal makes a hard-stop error worse than a graceful
//! partial-report exit.

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

/// Cloneable cancellation flag shared between the verify worker and any
/// number of signalling threads.
///
/// `Clone` is `O(1)` — every handle points at the same underlying
/// [`AtomicBool`]. Marking the token cancelled from any handle is visible to
/// every other handle on the next [`Self::is_cancelled`] poll.
///
/// # Threading
///
/// `Send + Sync` is automatic: the inner [`Arc`] of [`AtomicBool`] is
/// trivially shareable across threads. Construct on one thread, clone, and
/// hand the clones to wherever they need to live.
///
/// # Cost
///
/// [`Self::is_cancelled`] is a single [`Ordering::Relaxed`] atomic load and
/// is cheap enough to call at every manifest-entry boundary inside the
/// verifier's hot loop. [`Self::cancel`] is one relaxed store. The token
/// allocates one [`Arc`]-managed [`AtomicBool`] at construction; clones
/// share it.
#[derive(Clone, Debug, Default)]
pub struct CancelToken(Arc<AtomicBool>);

impl CancelToken {
    /// Construct a fresh, un-cancelled token.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Request cancellation.
    ///
    /// Idempotent — calling this on an already-cancelled token is a no-op.
    /// The flip is immediately visible to every clone of this token.
    pub fn cancel(&self) {
        self.0.store(true, Ordering::Relaxed);
    }

    /// Returns `true` if [`Self::cancel`] has been called on any clone of
    /// this token.
    ///
    /// Uses [`Ordering::Relaxed`] — the verifier polls this between
    /// manifest entries and only needs eventual visibility, not
    /// synchronisation with any other memory ops.
    #[must_use]
    pub fn is_cancelled(&self) -> bool {
        self.0.load(Ordering::Relaxed)
    }

    /// Clear the cancellation flag.
    ///
    /// Useful for retry-after-cancel scenarios where the consumer wants to
    /// reuse the same token (and therefore avoid re-wiring its clones)
    /// across multiple verify attempts. The flip is immediately visible to
    /// every clone.
    pub fn reset(&self) {
        self.0.store(false, Ordering::Relaxed);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn new_token_is_not_cancelled() {
        let t = CancelToken::new();
        assert!(!t.is_cancelled());
    }

    #[test]
    fn cancel_flips_the_flag() {
        let t = CancelToken::new();
        t.cancel();
        assert!(t.is_cancelled());
    }

    #[test]
    fn cancel_is_visible_across_clones() {
        let a = CancelToken::new();
        let b = a.clone();
        assert!(!b.is_cancelled());
        a.cancel();
        assert!(
            b.is_cancelled(),
            "cancel on one handle must surface on the clone"
        );
    }

    #[test]
    fn cancel_is_idempotent() {
        let t = CancelToken::new();
        t.cancel();
        t.cancel();
        assert!(t.is_cancelled());
    }

    #[test]
    fn reset_clears_the_flag() {
        let t = CancelToken::new();
        t.cancel();
        assert!(t.is_cancelled());
        t.reset();
        assert!(!t.is_cancelled());
    }

    #[test]
    fn reset_is_visible_across_clones() {
        let a = CancelToken::new();
        let b = a.clone();
        a.cancel();
        assert!(b.is_cancelled());
        b.reset();
        assert!(
            !a.is_cancelled(),
            "reset on one handle must surface on the clone"
        );
    }

    #[test]
    fn cancel_propagates_between_threads() {
        let t = CancelToken::new();
        let bg = t.clone();
        let handle = std::thread::spawn(move || {
            bg.cancel();
        });
        handle.join().unwrap();
        assert!(t.is_cancelled());
    }

    #[test]
    fn token_is_send_and_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<CancelToken>();
    }
}
