//! Card interaction visuals: hover scale, drag lift, and input buffering.
//!
//! # Hover
//!
//! [`HoverState`] tracks the entity currently under the cursor. A system
//! smoothly lerps `Transform.scale` toward `HOVER_SCALE` on the hovered card
//! and back to 1.0 when the cursor leaves. Scale is only written when no
//! [`CardAnimation`] is active on the entity (the animation takes priority).
//!
//! # Drag visual
//!
//! While [`DragState`] is non-idle, the dragged card entities receive a subtle
//! scale boost (`DRAG_LIFT_SCALE`) and their z-order is pushed up. The exact
//! translation is still controlled by the existing [`crate::input_plugin`] —
//! this system only applies the _visual_ enhancement without touching XY.
//!
//! # Input buffer
//!
//! [`InputBuffer`] stores move/draw/undo actions that arrived while cards are
//! still animating. Call [`InputBuffer::push`] from any system that wants
//! buffering. The drain system fires the oldest buffered action as soon as all
//! [`CardAnimation`] components have cleared, giving a responsive feel on
//! fast repeated clicks.
//!
//! # Visual priority
//!
//! Dragged cards always have the highest z. The existing [`crate::input_plugin`]
//! sets drag z; this module applies scale on top. The ordering constraint
//! `.after(crate::game_plugin::GameMutation)` ensures all game-state changes
//! settle before visual updates run.

use std::collections::VecDeque;

use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use solitaire_core::Card;

use super::animation::CardAnimation;
use super::tuning::AnimationTuning;
use crate::card_plugin::CardEntity;
use crate::events::{DrawRequestEvent, MoveRequestEvent, UndoRequestEvent};
use crate::layout::LayoutResource;
use crate::resources::DragState;

/// Type alias to reduce complexity in hover/drag query signatures.
type CardTransformQuery<'w, 's> =
    Query<'w, 's, (Entity, &'static mut Transform), (With<CardEntity>, Without<CardAnimation>)>;

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/// Lerp speed for drag scale interpolation.
const DRAG_LERP_SPEED: f32 = 20.0;

/// Maximum number of buffered inputs retained.
const INPUT_BUFFER_CAPACITY: usize = 4;

// ---------------------------------------------------------------------------
// Resources
// ---------------------------------------------------------------------------

/// Tracks the entity currently under the cursor and the interpolated hover scale.
#[derive(Resource, Debug, Default)]
pub struct HoverState {
    /// Entity currently hovered (`None` when cursor is off all cards or dragging).
    pub entity: Option<Entity>,
    /// Current interpolated scale applied to the hovered card.
    pub scale: f32,
}

/// Describes a user action that arrived while cards were still animating.
#[derive(Debug, Clone)]
pub enum BufferedInput {
    Move {
        from: MoveRequestEvent,
    },
    Draw,
    Undo,
}

/// FIFO queue of inputs deferred until ongoing animations complete.
///
/// Populate via [`InputBuffer::push`] and consume via the drain system.
/// Capped at [`INPUT_BUFFER_CAPACITY`] — further pushes when full are silently
/// dropped to prevent stale action pileup.
#[derive(Resource, Debug, Default)]
pub struct InputBuffer {
    pub(crate) queue: VecDeque<BufferedInput>,
}

impl InputBuffer {
    /// Enqueues an input if the buffer is not full.
    pub fn push(&mut self, input: BufferedInput) {
        if self.queue.len() < INPUT_BUFFER_CAPACITY {
            self.queue.push_back(input);
        }
    }

    /// Returns `true` when no inputs are pending.
    pub fn is_empty(&self) -> bool {
        self.queue.is_empty()
    }

    /// Returns how many inputs are queued.
    pub fn len(&self) -> usize {
        self.queue.len()
    }
}

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

/// Detects which card is under the cursor and updates [`HoverState`].
///
/// Clears hover when [`DragState`] is active (dragging takes visual priority).
/// Picks the topmost card (highest `translation.z`) when multiple cards overlap.
pub(crate) fn detect_hover(
    windows: Query<&Window, With<PrimaryWindow>>,
    cameras: Query<(&Camera, &GlobalTransform)>,
    drag: Option<Res<DragState>>,
    layout: Option<Res<LayoutResource>>,
    cards: Query<(Entity, &Transform), With<CardEntity>>,
    mut hover: ResMut<HoverState>,
) {
    let is_dragging = drag.as_ref().is_some_and(|d| !d.is_idle());
    if is_dragging {
        hover.entity = None;
        return;
    }

    let Some(layout) = layout else { return };
    let Some(cursor_world) = cursor_world(&windows, &cameras) else {
        hover.entity = None;
        return;
    };

    let half_w = layout.0.card_size.x * 0.5;
    let half_h = layout.0.card_size.y * 0.5;

    let mut best: Option<(Entity, f32)> = None;
    for (entity, transform) in &cards {
        let pos = transform.translation.truncate();
        if (cursor_world.x - pos.x).abs() < half_w && (cursor_world.y - pos.y).abs() < half_h {
            let z = transform.translation.z;
            if best.is_none_or(|(_, bz)| z > bz) {
                best = Some((entity, z));
            }
        }
    }

    hover.entity = best.map(|(e, _)| e);
}

/// Applies the hover scale to the currently hovered card via smooth lerp.
///
/// Uses [`AnimationTuning`] to get the platform-appropriate hover scale.
/// On touch (`hover_scale == 1.0`) this becomes a no-op — there is no
/// hover affordance on a touchscreen.
///
/// Only runs on cards that have **no active [`CardAnimation`]** — animated
/// cards control their own scale. When hover changes entities, the previous
/// entity's scale is snapped back to 1.0 to avoid leaving a permanently
/// enlarged card.
pub(crate) fn apply_hover_scale(
    time: Res<Time>,
    tuning: Res<AnimationTuning>,
    mut hover_state: ResMut<HoverState>,
    mut cards: CardTransformQuery,
) {
    let dt = time.delta_secs();
    let target_entity = hover_state.entity;
    let hover_target = tuning.hover_scale;
    let lerp_speed = tuning.hover_lerp_speed;

    for (entity, mut transform) in &mut cards {
        let target_scale = if Some(entity) == target_entity {
            hover_target
        } else {
            1.0
        };

        let current = transform.scale.x;
        let new_scale = current + (target_scale - current) * (lerp_speed * dt).min(1.0);
        transform.scale = Vec3::splat(new_scale);
    }

    // Update the tracked scale for external inspection.
    hover_state.scale = if let Some(entity) = target_entity {
        cards.get(entity).map_or(hover_target, |(_, t)| t.scale.x)
    } else {
        1.0
    };
}

/// Applies a scale boost to committed dragged card entities.
///
/// Uses [`AnimationTuning`] for the platform-correct drag scale. Only applies
/// to cards whose drag has been *committed* (threshold crossed); cards in the
/// pending-drag state stay at scale 1.0. Does **not** modify `translation.xy`
/// — `InputPlugin` owns drag translation.
pub(crate) fn apply_drag_visual(
    time: Res<Time>,
    drag: Option<Res<DragState>>,
    tuning: Res<AnimationTuning>,
    mut cards: Query<(Entity, &CardEntity, &mut Transform), (Without<CardAnimation>,)>,
) {
    let dt = time.delta_secs();
    let drag_scale = tuning.drag_scale;

    // Only lift cards that are in a *committed* drag. Pending drags (below
    // threshold) must stay at scale 1.0 to avoid visible premature lift.
    let (dragged_cards, committed): (&[Card], bool) = drag
        .as_ref()
        .map_or((&[], false), |d| (d.cards.as_slice(), d.committed));

    for (_, card, mut transform) in &mut cards {
        let is_active_drag = committed && dragged_cards.contains(&card.card);
        let target_scale = if is_active_drag { drag_scale } else { 1.0 };
        let current = transform.scale.x;
        let new_scale = current + (target_scale - current) * (DRAG_LERP_SPEED * dt).min(1.0);
        transform.scale = Vec3::splat(new_scale);
    }
}

/// Fires the oldest buffered input when no [`CardAnimation`] components remain.
///
/// Call this system late in the `Update` schedule so freshly-removed animations
/// are already gone before the drain runs.
pub(crate) fn drain_input_buffer(
    mut buffer: ResMut<InputBuffer>,
    anims: Query<&CardAnimation>,
    mut move_events: MessageWriter<MoveRequestEvent>,
    mut draw_events: MessageWriter<DrawRequestEvent>,
    mut undo_events: MessageWriter<UndoRequestEvent>,
) {
    if !anims.is_empty() {
        return;
    }
    match buffer.queue.pop_front() {
        Some(BufferedInput::Move { from }) => {
            move_events.write(from);
        }
        Some(BufferedInput::Draw) => {
            draw_events.write(DrawRequestEvent);
        }
        Some(BufferedInput::Undo) => {
            undo_events.write(UndoRequestEvent);
        }
        None => {}
    }
}

// ---------------------------------------------------------------------------
// Cursor helper (mirrors the pattern used by input_plugin)
// ---------------------------------------------------------------------------

/// Converts the cursor screen position to 2-D world coordinates.
///
/// Returns `None` when the cursor is outside the window or no camera is found.
fn cursor_world(
    windows: &Query<&Window, With<PrimaryWindow>>,
    cameras: &Query<(&Camera, &GlobalTransform)>,
) -> Option<Vec2> {
    let window = windows.single().ok()?;
    let cursor = window.cursor_position()?;
    let (camera, camera_transform) = cameras.single().ok()?;
    camera.viewport_to_world_2d(camera_transform, cursor).ok()
}

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

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

    #[test]
    fn input_buffer_capacity_is_respected() {
        let mut buf = InputBuffer::default();
        for _ in 0..INPUT_BUFFER_CAPACITY + 5 {
            buf.push(BufferedInput::Draw);
        }
        assert_eq!(
            buf.len(),
            INPUT_BUFFER_CAPACITY,
            "buffer must not exceed capacity"
        );
    }

    #[test]
    fn input_buffer_is_fifo() {
        let mut buf = InputBuffer::default();
        buf.push(BufferedInput::Draw);
        buf.push(BufferedInput::Undo);

        matches!(buf.queue.pop_front().unwrap(), BufferedInput::Draw);
        matches!(buf.queue.pop_front().unwrap(), BufferedInput::Undo);
    }

    #[test]
    fn input_buffer_empty_initially() {
        let buf = InputBuffer::default();
        assert!(buf.is_empty());
        assert_eq!(buf.len(), 0);
    }

    #[test]
    fn input_buffer_len_increments() {
        let mut buf = InputBuffer::default();
        buf.push(BufferedInput::Draw);
        assert_eq!(buf.len(), 1);
        buf.push(BufferedInput::Undo);
        assert_eq!(buf.len(), 2);
    }

    #[test]
    fn hover_state_default_has_no_entity() {
        let state = HoverState::default();
        assert!(state.entity.is_none());
        assert_eq!(state.scale, 0.0);
    }
}
