//! Resize handling: window-resize snapping, in-place card resizing,
//! and tableau fan spread.

use super::*;

use std::collections::HashMap;

use bevy::window::WindowResized;
use solitaire_core::Card;
use solitaire_core::game_state::GameState;

use crate::animation_plugin::CardAnim;
use crate::events::StateChangedEvent;
use crate::font_plugin::FontResource;
use crate::layout::{Layout, LayoutResource};
use crate::resources::GameStateResource;
use crate::table_plugin::PileMarker;
use crate::ui_theme::{
    CARD_SHADOW_ALPHA_DRAG, CARD_SHADOW_PADDING_DRAG,
    CARD_SHADOW_PADDING_IDLE,
};

/// Coalesces every `WindowResized` event arriving this frame into the latest
/// pending size on [`ResizeThrottle`].
///
/// `WindowResized` fires per pixel of resize drag, so a fast corner drag can
/// emit many events per frame. Reading `.last()` keeps only the final size —
/// every frame's snap target is the most recent window size, never a stale
/// one. Pending stays set across frames until the throttled applier consumes
/// it; that's how we still flush the final "release" position when the user
/// stops dragging.
pub(super) fn collect_resize_events(
    mut events: MessageReader<WindowResized>,
    mut throttle: ResMut<ResizeThrottle>,
) {
    if let Some(ev) = events.read().last() {
        throttle.pending = Some(Vec2::new(ev.width, ev.height));
    }
}

/// Snaps every card sprite to its target position, size, and (in the
/// fallback Text2d label path) font size when the window is resized.
///
/// **In-place mutation only.** Resize is the hot path — events fire per
/// pixel of drag, so this system cannot afford the despawn/respawn churn
/// `update_card_entity` does. We mutate `Sprite.custom_size`, `Transform`,
/// and child `TextFont.font_size` directly, leaving the card image handle,
/// suit/rank, and `CardLabel` entity untouched. Cards keep their identity
/// across resizes; only their size and position change. The full repaint
/// path lives in [`update_card_entity`] and is still used by every non-resize
/// caller (deals, moves, flips, settings toggles).
///
/// **Throttled to ~20 Hz.** [`ResizeThrottle::pending`] is consumed at most
/// once per [`RESIZE_THROTTLE_SECS`]. When events stop arriving, the next
/// tick past the throttle window flushes the final size and clears
/// `pending`, so the steady-state always matches the user's release size.
///
/// **Cancels in-flight slides.** Any `CardAnim` is removed so a mid-slide
/// tween is not retargeted relative to the previous card-size's position.
///
/// The "↺" stock-empty label's `font_size` is derived from
/// `layout.card_size.x`, so this system also reapplies the stock indicator —
/// otherwise the label would not rescale on resize.
///
/// Scheduled after [`collect_resize_events`] (which itself runs after
/// `LayoutSystem::UpdateOnResize`) so `LayoutResource` reflects the latest
/// window size before we read it.
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(super) fn snap_cards_on_window_resize(
    mut commands: Commands,
    time: Res<Time>,
    mut throttle: ResMut<ResizeThrottle>,
    game: Option<Res<GameStateResource>>,
    layout: Option<Res<LayoutResource>>,
    card_images: Option<Res<CardImageSet>>,
    font_res: Option<Res<FontResource>>,
    entities: Query<
        (Entity, &CardEntity, &mut Sprite, &mut Transform),
        (
            Without<CardLabel>,
            Without<CardShadow>,
            Without<CardBackFrame>,
        ),
    >,
    label_query: Query<&mut TextFont, (With<CardLabel>, Without<StockEmptyLabel>)>,
    shadow_query: Query<
        &mut Sprite,
        (
            With<CardShadow>,
            Without<CardEntity>,
            Without<PileMarker>,
            Without<CardBackFrame>,
        ),
    >,
    frame_query: Query<
        &mut Sprite,
        (
            With<CardBackFrame>,
            Without<CardEntity>,
            Without<CardShadow>,
            Without<PileMarker>,
        ),
    >,
    mut pile_markers: Query<
        (Entity, &PileMarker, &mut Sprite),
        (
            Without<CardEntity>,
            Without<CardShadow>,
            Without<CardBackFrame>,
        ),
    >,
    label_children: Query<(Entity, &ChildOf), With<StockEmptyLabel>>,
) {
    if throttle.pending.is_none() {
        return;
    }
    let now = time.elapsed_secs();
    if !should_apply_resize(now, throttle.last_applied_secs) {
        return;
    }

    let Some(game) = game else {
        // Nothing to apply — clear pending so we don't busy-loop.
        throttle.pending = None;
        return;
    };
    let Some(layout) = layout else {
        throttle.pending = None;
        return;
    };

    resize_cards_in_place(
        &mut commands,
        &game.0,
        &layout.0,
        card_images.as_deref(),
        entities,
        label_query,
        shadow_query,
        frame_query,
    );

    let font = font_res.as_ref().map(|f| f.0.clone()).unwrap_or_default();
    apply_stock_empty_indicator(
        &mut commands,
        &game.0,
        &mut pile_markers,
        &label_children,
        &layout.0,
        font,
    );

    throttle.last_applied_secs = now;
    throttle.pending = None;
}

/// In-place "size-only" sibling of [`sync_cards`]: walks every existing card
/// entity, updates `Sprite.custom_size` and the snap-`Transform` to match the
/// fresh layout, and (in fallback solid-colour mode) also updates the child
/// `TextFont.font_size` of any `CardLabel`. No despawning, no `Sprite`
/// replacement, no children rebuild — that's the entire point of this path.
///
/// Called only from the resize handler. Game-state changes (deals, moves,
/// flips, settings toggles) still flow through [`sync_cards`] /
/// [`update_card_entity`], which handle add/remove/repaint correctly.
///
/// Any in-flight `CardAnim` slide is removed so a mid-tween card is not
/// retargeted relative to the previous card-size's position.
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub(super) fn resize_cards_in_place(
    commands: &mut Commands,
    game: &GameState,
    layout: &Layout,
    card_images: Option<&CardImageSet>,
    mut entities: Query<
        (Entity, &CardEntity, &mut Sprite, &mut Transform),
        (
            Without<CardLabel>,
            Without<CardShadow>,
            Without<CardBackFrame>,
        ),
    >,
    mut label_query: Query<&mut TextFont, (With<CardLabel>, Without<StockEmptyLabel>)>,
    mut shadow_query: Query<
        &mut Sprite,
        (
            With<CardShadow>,
            Without<CardEntity>,
            Without<PileMarker>,
            Without<CardBackFrame>,
        ),
    >,
    mut frame_query: Query<
        &mut Sprite,
        (
            With<CardBackFrame>,
            Without<CardEntity>,
            Without<CardShadow>,
            Without<PileMarker>,
        ),
    >,
) {
    let positions = card_positions(game, layout);
    let pos_by_id: HashMap<Card, (Vec2, f32)> = positions
        .into_iter()
        .map(|((c, _face_up), p, z)| (c, (p, z)))
        .collect();

    for (entity, marker, mut sprite, mut transform) in entities.iter_mut() {
        let Some(&(pos, z)) = pos_by_id.get(&marker.card) else {
            continue;
        };
        sprite.custom_size = Some(layout.card_size);
        transform.translation.x = pos.x;
        transform.translation.y = pos.y;
        transform.translation.z = z;
        // Cancel any in-flight slide so it doesn't retarget from a stale
        // mid-animation position computed against the previous card size.
        commands.entity(entity).remove::<CardAnim>();
    }

    // Resize every per-card shadow halo to match the new card size. Both
    // idle and drag states scale with the card body, so we preserve the
    // *current* padding (idle vs drag) by keeping the alpha as-is and only
    // recomputing the geometry. The drag-tracking system runs every frame
    // and will retune offset / alpha / padding-mode within one frame if the
    // drag state diverges from the resized geometry.
    let idle_padding = CARD_SHADOW_PADDING_IDLE;
    let drag_padding = CARD_SHADOW_PADDING_DRAG;
    for mut shadow_sprite in shadow_query.iter_mut() {
        // Choose padding based on the shadow's current alpha — preserves
        // a lifted shadow's larger halo across resize without needing to
        // plumb DragState through the resize handler.
        let alpha = shadow_sprite.color.alpha();
        let padding = if alpha >= CARD_SHADOW_ALPHA_DRAG - 0.001 {
            drag_padding
        } else {
            idle_padding
        };
        shadow_sprite.custom_size = Some(layout.card_size + padding);
    }

    // Only the solid-colour fallback path uses CardLabel/Text2d overlays;
    // when PNG faces are loaded the rank/suit are baked into the image and
    // there is nothing to resize on the label side.
    if card_images.is_none() {
        let new_font_size = layout.card_size.x * FONT_SIZE_FRAC;
        for mut font in label_query.iter_mut() {
            font.font_size = new_font_size;
        }
    }

    // Resize every face-down border frame to match the new card size.
    let frame_size = layout.card_size + Vec2::splat(CARD_BACK_FRAME_PADDING);
    for mut frame_sprite in frame_query.iter_mut() {
        frame_sprite.custom_size = Some(frame_size);
    }
}

/// Updates font size and top-left anchor transform of every
/// [`AndroidCornerLabel`] entity when `LayoutResource` changes (orientation
/// change or any window resize). The full despawn/respawn path in
/// `update_card_entity` already handles game-state changes; this system
/// covers the resize-only path where children are mutated in place.
pub(super) fn resize_android_corner_labels(
    layout: Res<LayoutResource>,
    card_images: Option<Res<CardImageSet>>,
    mut text_query: Query<(
        &AndroidCornerLabel,
        &mut Text2d,
        &mut TextFont,
        &mut Transform,
    )>,
    mut bg_query: Query<(&mut Sprite, &mut Transform), AndroidCornerBgFilter>,
) {
    if !layout.is_changed() || card_images.is_none() {
        return;
    }
    let font_size = layout.0.card_size.x * FONT_SIZE_FRAC_MOBILE;
    let inset = 3.0_f32;
    let bg_w = font_size * 2.0;
    let bg_h = font_size * 1.25;
    let text_x = -layout.0.card_size.x / 2.0 + inset;
    let text_y = layout.0.card_size.y / 2.0 - inset;

    for (label, mut text2d, mut font, mut transform) in text_query.iter_mut() {
        text2d.0 = label.0.clone();
        font.font_size = font_size;
        transform.translation.x = text_x;
        transform.translation.y = text_y;
    }
    for (mut sprite, mut transform) in bg_query.iter_mut() {
        sprite.custom_size = Some(Vec2::new(bg_w, bg_h));
        transform.translation.x = text_x + bg_w / 2.0;
        transform.translation.y = text_y - bg_h / 2.0;
    }
}

/// Adjusts `LayoutResource.tableau_fan_frac` (and the face-down companion) so
/// the deepest tableau column fills the available vertical space at every stage
/// of play. Runs after every `StateChangedEvent`.
///
/// Depth is measured across *all* cards in a column, weighting each face-down
/// card by the fixed face-down/face-up step ratio. Counting the face-down
/// portion — not just the face-up tail — is what fills the lower screen on a
/// fresh deal (the deepest column is then six face-down cards under one face-up
/// one): the earlier face-up-only depth was 1, so the fan never spread and the
/// bottom half of a near-square viewport (e.g. an unfolded foldable) sat empty.
///
/// Deeper columns drive the fraction down so everything still fits the window;
/// [`crate::layout::TABLEAU_FAN_FRAC`] floors it to the desktop feel and
/// [`MAX_DYNAMIC_FAN_FRAC`] caps it so a near-empty column doesn't fling its few
/// cards far apart.
pub(super) fn update_tableau_fan_frac(
    mut events: MessageReader<StateChangedEvent>,
    game: Option<Res<GameStateResource>>,
    mut layout: Option<ResMut<LayoutResource>>,
) {
    if events.read().next().is_none() {
        return;
    }
    let Some(game) = game else {
        return;
    };
    let Some(layout) = layout.as_mut() else {
        return;
    };
    crate::layout::apply_dynamic_tableau_fan(&game.0, &mut layout.0);
}

/// PostStartup sibling of [`update_tableau_fan_frac`]. The initial deal is
/// inserted directly as `GameStateResource` at startup without a
/// `StateChangedEvent`, so the event-driven system never fires for it. This
/// runs once, before [`sync_cards_startup`] renders, so the very first board
/// (cold start) already fills the viewport — otherwise a fresh deal on a tall /
/// near-square screen (e.g. an unfolded foldable) renders with the unspread fan
/// and a large empty band below the tableau until the first move.
pub(super) fn fill_tableau_fan_on_startup(
    game: Option<Res<GameStateResource>>,
    mut layout: Option<ResMut<LayoutResource>>,
) {
    let Some(game) = game else {
        return;
    };
    let Some(layout) = layout.as_mut() else {
        return;
    };
    crate::layout::apply_dynamic_tableau_fan(&game.0, &mut layout.0);
}


