//! Card flip animation and drag shadows.

use super::*;

use std::collections::HashSet;

use solitaire_core::Card;

use crate::animation_plugin::EffectiveSlideDuration;
use crate::events::{CardFaceRevealedEvent, CardFlippedEvent};
use crate::layout::LayoutResource;
use crate::resources::DragState;
use crate::ui_theme::{
    CARD_SHADOW_ALPHA_DRAG, CARD_SHADOW_COLOR, CARD_SHADOW_LOCAL_Z,
};

/// Listens for `CardFlippedEvent` and inserts a `CardFlipAnim` on the entity.
///
/// Skipped when `EffectiveSlideDuration::slide_secs == 0.0` (Instant speed).
pub(super) fn start_flip_anim(
    mut events: MessageReader<CardFlippedEvent>,
    slide_dur: Option<Res<EffectiveSlideDuration>>,
    mut commands: Commands,
    card_entities: Query<(Entity, &CardEntity)>,
) {
    if slide_dur.is_some_and(|d| d.slide_secs == 0.0) {
        // Instant animation speed — skip the flip effect entirely.
        events.clear();
        return;
    }

    for CardFlippedEvent(flipped_card) in events.read() {
        for (entity, marker) in &card_entities {
            if marker.card == *flipped_card {
                commands.entity(entity).insert(CardFlipAnim {
                    timer: 0.0,
                    phase: FlipPhase::ScalingDown,
                });
                break;
            }
        }
    }
}

/// Advances `CardFlipAnim` each frame, modifying `Transform::scale.x`.
///
/// - Phase `ScalingDown`: lerps scale.x from 1.0 → 0.0 over `FLIP_HALF_SECS`.
/// - At the midpoint the phase switches to `ScalingUp`, scale.x resets to 0,
///   and a `CardFaceRevealedEvent` is fired so audio plays in sync with the reveal.
/// - Phase `ScalingUp`:  lerps scale.x from 0.0 → 1.0 over `FLIP_HALF_SECS`.
/// - When complete the component is removed and scale.x is restored to 1.0.
pub(super) fn tick_flip_anim(
    mut commands: Commands,
    time: Res<Time>,
    mut anims: Query<(Entity, &CardEntity, &mut Transform, &mut CardFlipAnim)>,
    mut reveal_events: MessageWriter<CardFaceRevealedEvent>,
) {
    let dt = time.delta_secs();
    for (entity, card_entity, mut transform, mut anim) in &mut anims {
        anim.timer += dt;
        match anim.phase {
            FlipPhase::ScalingDown => {
                let t = (anim.timer / FLIP_HALF_SECS).min(1.0);
                transform.scale.x = 1.0 - t;
                if t >= 1.0 {
                    anim.phase = FlipPhase::ScalingUp;
                    anim.timer = 0.0;
                    transform.scale.x = 0.0;
                    // Fire the reveal event exactly once, at the phase transition,
                    // so the flip sound is synchronised with the visual face reveal.
                    reveal_events.write(CardFaceRevealedEvent(card_entity.card.clone()));
                }
            }
            FlipPhase::ScalingUp => {
                let t = (anim.timer / FLIP_HALF_SECS).min(1.0);
                transform.scale.x = t;
                if t >= 1.0 {
                    transform.scale.x = 1.0;
                    commands.entity(entity).remove::<CardFlipAnim>();
                }
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Task #38 — Drag-elevation shadow
// ---------------------------------------------------------------------------

/// Maintains a single `ShadowEntity` while cards are being dragged.
///
/// - If a drag is active, spawns (or repositions) a semi-transparent dark
///   sprite behind the top dragged card.
/// - If no drag is active, despawns the shadow entity.
pub(super) fn update_drag_shadow(
    mut commands: Commands,
    drag: Res<DragState>,
    layout: Option<Res<LayoutResource>>,
    card_entities: Query<(&CardEntity, &Transform)>,
    card_index: Res<CardEntityIndex>,
    mut shadow: Local<Option<Entity>>,
) {
    if drag.is_idle() {
        // No drag in progress — remove shadow if it exists.
        if let Some(e) = shadow.take() {
            commands.entity(e).despawn();
        }
        return;
    }

    let Some(layout) = layout else { return };
    let card_w = layout.0.card_size.x;
    let card_h = layout.0.card_size.y;

    // Find the world position of the first (top) dragged card.
    let top_pos = drag.cards.first().and_then(|first_card| {
        card_index
            .get(first_card)
            .and_then(|entity| card_entities.get(entity).ok())
            .map(|(_, t)| t.translation)
    });

    let Some(top_pos) = top_pos else { return };

    // Shadow is slightly larger, offset behind-and-below, at a z slightly
    // below the dragged cards.
    let shadow_pos = top_pos + Vec3::new(-4.0, 4.0, -1.0);

    match *shadow {
        Some(e) => {
            // Reposition the existing shadow.
            commands
                .entity(e)
                .insert(Transform::from_translation(shadow_pos));
        }
        None => {
            // Spawn a new shadow sprite. Alpha tracks the per-card
            // CARD_SHADOW_ALPHA_DRAG token so the Terminal palette's
            // "no box-shadow" policy disables this stack shadow in
            // lockstep with the per-card shadows. Re-enabling shadows
            // is then a one-line change in `ui_theme`, not a hunt
            // through plugin code.
            let e = commands
                .spawn((
                    ShadowEntity,
                    Sprite {
                        color: CARD_SHADOW_COLOR.with_alpha(CARD_SHADOW_ALPHA_DRAG),
                        custom_size: Some(Vec2::new(card_w + 8.0, card_h + 8.0)),
                        ..default()
                    },
                    Transform::from_translation(shadow_pos),
                    Visibility::default(),
                ))
                .id();
            *shadow = Some(e);
        }
    }
}

/// Snaps every per-card [`CardShadow`] between its idle and lifted tunings
/// based on whether the parent [`CardEntity`] is currently in
/// [`DragState::cards`]. Runs every frame; the transition is an instant snap
/// (no lerp) — the existing shake / settle feedback already handles motion
/// at drag-end, so an additional shadow tween would compete with those cues.
///
/// The shadow size is rebuilt from the parent card's current `Sprite`
/// `custom_size` plus the appropriate padding, so the resize handler does
/// not need to pre-tune shadow sizes for the drag state — this system fixes
/// the geometry within one frame.
pub(super) fn update_card_shadows_on_drag(
    drag: Res<DragState>,
    cards: Query<(&CardEntity, &Sprite, &Children), Without<CardShadow>>,
    mut shadows: Query<(&mut Sprite, &mut Transform), With<CardShadow>>,
) {
    let dragged: HashSet<&Card> = drag.cards.iter().collect();

    for (card_entity, card_sprite, children) in cards.iter() {
        let is_dragged = dragged.contains(&card_entity.card);
        let (offset, padding, alpha) = card_shadow_params(is_dragged);
        let Some(card_size) = card_sprite.custom_size else {
            continue;
        };

        for child in children.iter() {
            let Ok((mut shadow_sprite, mut shadow_transform)) = shadows.get_mut(child) else {
                continue;
            };
            shadow_sprite.color = CARD_SHADOW_COLOR.with_alpha(alpha);
            shadow_sprite.custom_size = Some(card_size + padding);
            shadow_transform.translation.x = offset.x;
            shadow_transform.translation.y = offset.y;
            shadow_transform.translation.z = CARD_SHADOW_LOCAL_Z;
        }
    }
}

// ---------------------------------------------------------------------------
// Task #28 — Hint highlight tick system
// ---------------------------------------------------------------------------

