//! Touch tap-to-select input mode.
//!
//! When [`TouchInputMode::TapToSelect`] is active (set via [`crate::settings_plugin`]),
//! a single tap on a face-up card **selects** it (showing a visual highlight) instead
//! of immediately auto-moving it. A second tap on a valid destination pile performs
//! the move; a second tap on the same pile (or an invalid target) cancels silently.
//!
//! In [`TouchInputMode::OneTap`] mode this plugin is fully passive — all resources
//! default to their empty state and no highlight is ever shown.
//!
//! ## State machine
//!
//! ```text
//! Idle ──(tap face-up card)──> Selected(pile, cards)
//!   ↑                              │
//!   │   cancel (re-tap or         │  second tap on destination
//!   └── StateChangedEvent) ◄──────┤  → MoveRequestEvent; back to Idle
//!                                  │
//!                                  └── rejected / no destination → back to Idle
//! ```
//!
//! ## Interaction with the existing auto-move flow
//!
//! [`crate::input_plugin::handle_double_tap`] is the entry point: it reads
//! [`TouchSelectionState`] and, in `TapToSelect` mode, populates it on the first
//! tap instead of firing `MoveRequestEvent`. This plugin owns the highlight visual
//! and the state-clear reactions.

use bevy::ecs::message::MessageReader;
use bevy::prelude::*;
use solitaire_core::KlondikePile;
use solitaire_core::Card;

use crate::card_plugin::CardEntity;
use crate::events::StateChangedEvent;
use crate::game_plugin::GameMutation;
use crate::layout::LayoutResource;
use crate::ui_theme::ACCENT_PRIMARY;

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

/// State for the tap-to-select touch flow.
///
/// `selected` is `Some((source_pile, card_ids))` while the player has
/// chosen a source but not yet tapped a destination. `None` is the idle state.
///
/// `card_ids` mirrors `DragState::cards` — the bottom-to-top ordered list of
/// card ids that will be moved (1 for a single card, multiple for a face-up run).
#[derive(Resource, Debug, Default)]
pub struct TouchSelectionState {
    /// Currently selected source pile and the cards to move (bottom-to-top).
    pub selected: Option<(KlondikePile, Vec<Card>)>,
}

impl TouchSelectionState {
    /// Returns `true` when a source is selected.
    pub fn has_selection(&self) -> bool {
        self.selected.is_some()
    }

    /// Takes the current selection, leaving `selected` as `None`.
    pub fn take(&mut self) -> Option<(KlondikePile, Vec<Card>)> {
        self.selected.take()
    }

    /// Sets the current selection.
    pub fn set(&mut self, pile: KlondikePile, cards: Vec<Card>) {
        self.selected = Some((pile, cards));
    }

    /// Clears the selection without returning it.
    pub fn clear(&mut self) {
        self.selected = None;
    }
}

/// Marker component placed on the highlight sprite child of a selected source card.
///
/// Despawned and respawned by [`update_touch_selection_highlight`] whenever
/// [`TouchSelectionState`] changes. The system is gated on `is_changed()` so it
/// is a no-op every frame that the selection is stable.
#[derive(Component)]
pub struct TouchSelectionHighlight;

// ---------------------------------------------------------------------------
// Plugin
// ---------------------------------------------------------------------------

/// Registers all resources and systems for the touch tap-to-select flow.
pub struct TouchSelectionPlugin;

impl Plugin for TouchSelectionPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<TouchSelectionState>().add_systems(
            Update,
            (
                clear_touch_selection_on_state_change,
                update_touch_selection_highlight,
            )
                .chain()
                .after(GameMutation),
        );
    }
}

// ---------------------------------------------------------------------------
// Systems
// ---------------------------------------------------------------------------

/// Clears [`TouchSelectionState`] whenever the board changes (undo, new game,
/// won, forfeit). This prevents stale selections surviving across game resets.
pub(crate) fn clear_touch_selection_on_state_change(
    mut selection: ResMut<TouchSelectionState>,
    mut state_events: MessageReader<StateChangedEvent>,
) {
    if state_events.read().next().is_some() {
        selection.clear();
    }
}

/// Maintains the `TouchSelectionHighlight` outline sprite on the selected source card.
///
/// Rebuilds the highlight set only when [`TouchSelectionState`] or the layout
/// actually changes — not every frame. Existing highlights are despawned first,
/// then a fresh highlight is spawned on every card in the selected stack.
pub(crate) fn update_touch_selection_highlight(
    mut commands: Commands,
    selection: Res<TouchSelectionState>,
    card_entities: Query<(Entity, &CardEntity)>,
    highlights: Query<Entity, With<TouchSelectionHighlight>>,
    layout: Option<Res<LayoutResource>>,
) {
    // Skip when neither the selection nor the layout changed this frame.
    let layout_changed = layout.as_ref().map(|l| l.is_changed()).unwrap_or(false);
    if !selection.is_changed() && !layout_changed {
        return;
    }

    // Despawn stale highlights first.
    for entity in &highlights {
        commands.entity(entity).despawn();
    }

    let Some((_, ref cards)) = selection.selected else {
        return;
    };
    let Some(layout) = layout else {
        return;
    };

    // Highlight every card in the selected stack (bottom-to-top order).
    // The bottom card of the run is the most visually important anchor,
    // but highlighting the whole run gives the player clear confirmation
    // of how many cards are involved in the move.
    let card_size = layout.0.card_size;
    for card in cards {
        spawn_touch_highlight(&mut commands, &card_entities, card, card_size);
    }
}

/// Spawns a [`TouchSelectionHighlight`] sprite as a child of the matching card entity.
fn spawn_touch_highlight(
    commands: &mut Commands,
    card_entities: &Query<(Entity, &CardEntity)>,
    card: &Card,
    card_size: Vec2,
) {
    for (entity, card_entity) in card_entities {
        if card_entity.card == *card {
            commands.entity(entity).with_children(|b| {
                b.spawn((
                    TouchSelectionHighlight,
                    Sprite {
                        color: ACCENT_PRIMARY.with_alpha(0.55),
                        custom_size: Some(card_size + Vec2::splat(6.0)),
                        ..default()
                    },
                    Transform::from_xyz(0.0, 0.0, -0.01),
                    Visibility::default(),
                ));
            });
            return;
        }
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use solitaire_core::Tableau;
    use solitaire_core::{Card, Deck, Rank, Suit};

    /// Three distinct test cards, used in place of the old `vec![1, 2, 3]`
    /// numeric ids. Identity is now the `Card` value.
    fn test_cards() -> [Card; 3] {
        [
            Card::new(Deck::Deck1, Suit::Clubs, Rank::Ace),
            Card::new(Deck::Deck1, Suit::Hearts, Rank::Two),
            Card::new(Deck::Deck1, Suit::Spades, Rank::Three),
        ]
    }

    #[test]
    fn selection_state_default_is_idle() {
        let state = TouchSelectionState::default();
        assert!(!state.has_selection());
        assert!(state.selected.is_none());
    }

    #[test]
    fn set_and_take_roundtrip() {
        let mut state = TouchSelectionState::default();
        let cards = test_cards().to_vec();
        state.set(KlondikePile::Tableau(Tableau::Tableau1), cards.clone());
        assert!(state.has_selection());
        let taken = state.take();
        assert!(taken.is_some());
        let (pile, taken_cards) = taken.unwrap();
        assert_eq!(pile, KlondikePile::Tableau(Tableau::Tableau1));
        assert_eq!(taken_cards, cards);
        assert!(!state.has_selection());
    }

    #[test]
    fn clear_removes_selection() {
        let mut state = TouchSelectionState::default();
        state.set(
            KlondikePile::Stock,
            vec![Card::new(Deck::Deck1, Suit::Diamonds, Rank::King)],
        );
        state.clear();
        assert!(!state.has_selection());
    }

    #[test]
    fn take_on_idle_returns_none() {
        let mut state = TouchSelectionState::default();
        assert!(state.take().is_none());
        assert!(!state.has_selection());
    }

    #[test]
    fn set_overwrites_previous_selection() {
        let mut state = TouchSelectionState::default();
        state.set(
            KlondikePile::Tableau(Tableau::Tableau1),
            vec![Card::new(Deck::Deck1, Suit::Clubs, Rank::Ace)],
        );
        let second = vec![
            Card::new(Deck::Deck1, Suit::Hearts, Rank::Seven),
            Card::new(Deck::Deck1, Suit::Spades, Rank::Eight),
        ];
        state.set(KlondikePile::Tableau(Tableau::Tableau4), second.clone());
        let (pile, cards) = state.take().unwrap();
        assert_eq!(pile, KlondikePile::Tableau(Tableau::Tableau4));
        assert_eq!(cards, second);
    }
}
