//! Automatic card-to-foundation sequencing once `is_auto_completable` is set.
//!
//! When `GameState::is_auto_completable` becomes `true`, this plugin fires
//! `MoveRequestEvent` for one card per `STEP_INTERVAL` seconds until the game
//! is won. A single toast announces the sequence; no player input is required.
//!
//! The plugin is intentionally passive: it only reads `GameStateResource` and
//! fires `MoveRequestEvent`. If for some reason `next_auto_complete_move`
//! returns `None` (e.g. a transient state), the plugin retries next tick.

use bevy::prelude::*;
use bevy::window::RequestRedraw;

#[cfg(not(target_arch = "wasm32"))]
use crate::audio_plugin::{AudioState, SoundLibrary};
use crate::events::{MoveRequestEvent, StateChangedEvent};
use crate::game_plugin::GameMutation;
use crate::pause_plugin::PausedResource;
use crate::resources::GameStateResource;

/// Volume amplitude used for the auto-complete activation chime.
///
/// Plays the win fanfare at half volume so it is clearly distinguishable from
/// both normal card-place sounds and the full win fanfare that fires later.
#[cfg(not(target_arch = "wasm32"))]
const AUTO_COMPLETE_CHIME_VOLUME: f64 = 0.5;

/// Seconds between consecutive auto-complete moves.
const STEP_INTERVAL: f32 = 0.12;

/// Seconds to wait after detection before firing the first auto-complete move.
///
/// This pause gives the player a moment to register that the game is
/// transitioning into auto-complete mode before cards start moving.
const AUTO_COMPLETE_INITIAL_DELAY: f32 = 0.75;

/// Tracks whether auto-complete is active and when the next move fires.
#[derive(Resource, Default, Debug)]
pub struct AutoCompleteState {
    /// `true` once we've detected `is_auto_completable` and started firing moves.
    pub active: bool,
    /// Countdown to the next move, in seconds.
    cooldown: f32,
}

/// Plugin that drives the auto-complete sequence.
pub struct AutoCompletePlugin;

/// Set wrapping the auto-complete detect/drive chain; HUD readers of
/// [`AutoCompleteState`] order themselves after it (#143).
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct AutoComplete;

impl Plugin for AutoCompletePlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<AutoCompleteState>()
            .add_message::<RequestRedraw>()
            .add_systems(
                Update,
                (
                    detect_auto_complete,
                    on_auto_complete_start,
                    drive_auto_complete,
                )
                    .chain()
                    .in_set(AutoComplete)
                    .after(GameMutation)
                    .before(crate::card_plugin::BoardVisuals),
            );
    }
}

/// Activates auto-complete when `is_auto_completable` flips to `true`.
/// Deactivates it on win or new game (any state where it should not be running).
fn detect_auto_complete(
    mut state: ResMut<AutoCompleteState>,
    game: Res<GameStateResource>,
    mut changed: MessageReader<StateChangedEvent>,
) {
    // Only re-evaluate on state changes to avoid per-frame allocations.
    if changed.is_empty() && !game.is_changed() {
        return;
    }
    changed.clear();

    if game.0.is_won() {
        state.active = false;
        return;
    }
    if game.0.is_auto_completable() && !state.active {
        state.active = true;
        state.cooldown = AUTO_COMPLETE_INITIAL_DELAY;
    } else if !game.0.is_auto_completable() && state.active {
        // `is_auto_completable` only becomes false after an explicit undo
        // (which puts a card back on the tableau or re-fills the stock/waste)
        // or a new-game reset — never as a transient gap during a normal
        // auto-complete sequence.  Deactivate here so `drive_auto_complete`
        // does not keep retrying indefinitely after the player undoes out of
        // the sequence.
        //
        // Note: the transient-`None` case mentioned in older versions of this
        // comment referred to `next_auto_complete_move()` returning `None`, not
        // to `is_auto_completable` being false.  Those are independent fields;
        // `drive_auto_complete` still retries on a transient `None` return from
        // `next_auto_complete_move` because that check happens there, not here.
        state.active = false;
    }
}

/// Plays a distinct chime the moment auto-complete first activates.
///
/// Uses a `Local<bool>` to remember the previous `active` state and fires
/// exactly once on the `false → true` edge. The win fanfare is played at half
/// volume (`AUTO_COMPLETE_CHIME_VOLUME`) so it is clearly recognisable but does
/// not overwhelm the card-place sounds that follow immediately.
#[cfg(not(target_arch = "wasm32"))]
fn on_auto_complete_start(
    state: Res<AutoCompleteState>,
    mut was_active: Local<bool>,
    mut audio: Option<NonSendMut<AudioState>>,
    lib: Option<Res<SoundLibrary>>,
) {
    let now_active = state.active;
    let edge = now_active && !*was_active;
    *was_active = now_active;

    if !edge {
        return;
    }

    let (Some(audio), Some(lib)) = (audio.as_mut(), lib) else {
        return;
    };
    audio.play_sfx_at_volume(&lib.fanfare, AUTO_COMPLETE_CHIME_VOLUME);
}

// No audio on wasm — stub keeps the system registration unconditional.
#[cfg(target_arch = "wasm32")]
fn on_auto_complete_start(state: Res<AutoCompleteState>, mut was_active: Local<bool>) {
    *was_active = state.active;
}

/// Fires one `MoveRequestEvent` per `STEP_INTERVAL` while auto-complete is active.
fn drive_auto_complete(
    mut state: ResMut<AutoCompleteState>,
    game: Res<GameStateResource>,
    time: Res<Time>,
    paused: Option<Res<PausedResource>>,
    mut moves: MessageWriter<MoveRequestEvent>,
) {
    if !state.active {
        return;
    }
    if paused.is_some_and(|p| p.0) {
        return;
    }

    state.cooldown -= time.delta_secs();
    if state.cooldown > 0.0 {
        return;
    }

    let Some((from, to)) = game.0.next_auto_complete_move() else {
        // No move available yet (race with game state update); try next tick.
        return;
    };

    moves.write(MoveRequestEvent { from, to, count: 1 });
    state.cooldown = STEP_INTERVAL;
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::game_plugin::GamePlugin;
    use crate::table_plugin::TablePlugin;
    use solitaire_core::{Foundation, KlondikePile, Tableau};
    use solitaire_core::{Deck, Rank, Suit};
    use solitaire_core::{DrawStockConfig, game_state::GameState};

    fn headless_app() -> App {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins)
            .add_plugins(GamePlugin)
            .add_plugins(TablePlugin)
            .add_plugins(AutoCompletePlugin);
        app.init_resource::<ButtonInput<KeyCode>>();
        app.update();
        app
    }

    fn seeded_state_with_auto_move() -> (GameState, (KlondikePile, KlondikePile)) {
        let mut g = GameState::new(1, DrawStockConfig::DrawOne);
        g.set_test_stock_cards(Vec::new());
        g.set_test_waste_cards(Vec::new());
        for foundation in [
            Foundation::Foundation1,
            Foundation::Foundation2,
            Foundation::Foundation3,
            Foundation::Foundation4,
        ] {
            g.set_test_foundation_cards(foundation, Vec::new());
        }
        for tableau in [
            Tableau::Tableau1,
            Tableau::Tableau2,
            Tableau::Tableau3,
            Tableau::Tableau4,
            Tableau::Tableau5,
            Tableau::Tableau6,
            Tableau::Tableau7,
        ] {
            g.set_test_tableau_cards(tableau, Vec::new());
        }
        g.set_test_tableau_cards(
            Tableau::Tableau1,
            vec![solitaire_core::Card::new(Deck::Deck1, Suit::Clubs, Rank::Ace)],
        );
        g.set_test_auto_completable(true);
        let expected = (
            KlondikePile::Tableau(Tableau::Tableau1),
            KlondikePile::Foundation(Foundation::Foundation1),
        );
        assert_eq!(g.next_auto_complete_move(), Some(expected));
        (g, expected)
    }

    #[test]
    fn state_starts_inactive() {
        let app = headless_app();
        assert!(!app.world().resource::<AutoCompleteState>().active);
    }

    #[test]
    fn detect_activates_when_auto_completable() {
        let mut app = headless_app();
        let mut g = GameState::new(42, DrawStockConfig::DrawOne);
        g.set_test_auto_completable(true);
        app.world_mut().resource_mut::<GameStateResource>().0 = g;
        app.world_mut().write_message(StateChangedEvent);
        app.update();

        assert!(app.world().resource::<AutoCompleteState>().active);
    }

    #[test]
    fn drive_fires_move_request_when_active() {
        let mut app = headless_app();
        let (g, (expected_from, expected_to)) = seeded_state_with_auto_move();
        app.world_mut().resource_mut::<GameStateResource>().0 = g;
        app.world_mut().write_message(StateChangedEvent);
        app.update(); // detect runs, sets active

        // Zero out the cooldown so drive fires on the next update regardless
        // of the initial delay constant.
        app.world_mut().resource_mut::<AutoCompleteState>().cooldown = 0.0;
        app.update(); // drive fires the move

        let events = app.world().resource::<Messages<MoveRequestEvent>>();
        let mut cursor = events.get_cursor();
        let fired: Vec<_> = cursor.read(events).collect();
        // At least one MoveRequestEvent should have been fired.
        assert!(!fired.is_empty(), "expected at least one MoveRequestEvent");
        assert_eq!(fired[0].from, expected_from);
        assert_eq!(fired[0].to, expected_to);
    }

    #[test]
    fn drive_deactivates_on_win() {
        let mut app = headless_app();
        // Inject a won game state — active should not be set.
        let (mut gs, _) = seeded_state_with_auto_move();
        gs.set_test_won(true);
        app.world_mut().resource_mut::<GameStateResource>().0 = gs;
        app.world_mut().write_message(StateChangedEvent);
        app.update();

        assert!(!app.world().resource::<AutoCompleteState>().active);
    }
}
