//! Stock-pile indicators: the empty-stock recycle hint and count badge.

use super::*;


use bevy::color::Color;
use solitaire_core::KlondikePile;
use solitaire_core::game_state::GameState;

use crate::events::StateChangedEvent;
use crate::font_plugin::FontResource;
use crate::layout::{Layout, LayoutResource};
use crate::resources::GameStateResource;
use crate::table_plugin::{PILE_MARKER_DEFAULT_COLOUR, PileMarker};
use crate::ui_theme::{
    STOCK_BADGE_BG, STOCK_BADGE_FG, TEXT_PRIMARY,
    TYPE_BODY, Z_STOCK_BADGE,
};

/// Sprite colour applied to the stock `PileMarker` when the stock pile is empty,
/// to signal to the player that there are no more cards to draw. Pure white
/// at 0.4 alpha — a deliberate brightness-boost over the default marker so
/// the "empty" state is more visible, not less. Not derived from a palette
/// token: this is a sprite tint, not chrome colour.
const STOCK_EMPTY_DIM_COLOUR: Color = Color::srgba(1.0, 1.0, 1.0, 0.4);

/// Sprite colour applied to the stock `PileMarker` when cards remain in
/// stock. Aliased to [`PILE_MARKER_DEFAULT_COLOUR`] so it tracks the rest
/// of the engine's idle pile-marker tint automatically.
const STOCK_NORMAL_COLOUR: Color = PILE_MARKER_DEFAULT_COLOUR;

/// Shared logic for updating the stock pile marker's dim state and "↺" label.
///
/// If the stock pile is empty the marker sprite is dimmed to
/// `STOCK_EMPTY_DIM_COLOUR` and a child `Text2d` with `StockEmptyLabel` is
/// spawned (if not already present). When the stock is non-empty the marker is
/// restored to `STOCK_NORMAL_COLOUR` and any `StockEmptyLabel` children are
/// despawned.
pub(super) fn apply_stock_empty_indicator<F: bevy::ecs::query::QueryFilter>(
    commands: &mut Commands,
    game: &GameState,
    pile_markers: &mut Query<(Entity, &PileMarker, &mut Sprite), F>,
    label_children: &Query<(Entity, &ChildOf), With<StockEmptyLabel>>,
    layout: &Layout,
    font: Handle<Font>,
) {
    let stock_empty = game.stock_cards().is_empty();

    for (entity, pile_marker, mut sprite) in pile_markers.iter_mut() {
        if pile_marker.0 != KlondikePile::Stock {
            continue;
        }

        if stock_empty {
            // Dim the marker sprite.
            sprite.color = STOCK_EMPTY_DIM_COLOUR;

            // Spawn the "↺" label only if one does not already exist.
            let already_has_label = label_children
                .iter()
                .any(|(_, parent)| parent.parent() == entity);
            if !already_has_label {
                let font_size = layout.card_size.x * 0.4;
                commands.entity(entity).with_children(|b| {
                    b.spawn((
                        StockEmptyLabel,
                        Text2d::new("↺"),
                        TextFont {
                            font: font.clone(),
                            font_size,
                            ..default()
                        },
                        TextColor(TEXT_PRIMARY.with_alpha(0.7)),
                        Transform::from_xyz(0.0, 0.0, 0.1),
                    ));
                });
            }
        } else {
            // Restore normal brightness.
            sprite.color = STOCK_NORMAL_COLOUR;

            // Despawn any existing "↺" label children.
            for (label_entity, parent) in label_children.iter() {
                if parent.parent() == entity {
                    commands.entity(label_entity).despawn();
                }
            }
        }
    }
}

/// Runs at `PostStartup` to apply the stock-empty indicator for the initial
/// game state (before any `StateChangedEvent` fires).
pub(super) fn update_stock_empty_indicator_startup(
    mut commands: Commands,
    game: Res<GameStateResource>,
    layout: Option<Res<LayoutResource>>,
    font_res: Option<Res<FontResource>>,
    mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite)>,
    label_children: Query<(Entity, &ChildOf), With<StockEmptyLabel>>,
) {
    let Some(layout) = layout else { return };
    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,
    );
}

/// Runs each `Update` tick when a `StateChangedEvent` arrives, keeping the
/// stock pile marker dim state and "↺" label in sync with the current stock.
pub(super) fn update_stock_empty_indicator(
    mut events: MessageReader<StateChangedEvent>,
    mut commands: Commands,
    game: Res<GameStateResource>,
    layout: Option<Res<LayoutResource>>,
    font_res: Option<Res<FontResource>>,
    mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite)>,
    label_children: Query<(Entity, &ChildOf), With<StockEmptyLabel>>,
) {
    if events.read().next().is_none() {
        return;
    }
    let Some(layout) = layout else { return };
    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,
    );
}

// ---------------------------------------------------------------------------
// Stock-pile remaining-count badge
//
// Shows a small "N" chip pinned to the bottom-right corner of the stock pile so
// the player can see how many cards remain before the next recycle. The
// existing `StockEmptyLabel` (`↺` overlay) covers the empty-stock case, so
// the badge hides itself when the stock has zero cards — the two indicators
// never render at the same time.
// ---------------------------------------------------------------------------

/// Inset (in pixels) from the bottom-right corner of the stock pile sprite to
/// the centre of the count badge. Anchoring to the bottom-right keeps the chip
/// clear of the rank/suit pip in the card's top-left corner. Both components
/// move the centre *inward* from that corner: `x` is subtracted from the right
/// edge, `y` is added to the bottom edge. The `x` magnitude must satisfy
/// `x >= STOCK_BADGE_SIZE.x / 2` so the badge right edge stays inside the stock
/// pile and never overlaps the adjacent waste pile — critical on Android where
/// `H_GAP_DIVISOR = 32` gives an inter-pile gap of only ~4 px.
const STOCK_BADGE_INSET: Vec2 = Vec2::new(20.0, 8.0);

/// Width / height of the badge background sprite, in world pixels. Sized so
/// a 2-digit count (max "24") fits comfortably with `TYPE_BODY` (14 pt) text.
const STOCK_BADGE_SIZE: Vec2 = Vec2::new(34.0, 20.0);

/// Returns the count of cards currently in the stock pile.
///
/// Pure helper extracted so the count source is identical between the spawn
/// system, the update system, and the unit tests.
pub(super) fn stock_card_count(game: &GameState) -> usize {
    game.stock_cards().len()
}

/// Returns the world-space `Vec3` for the centre of the stock-count badge,
/// given the current `Layout`. The badge sits at the bottom-right corner of
/// the stock pile sprite, inset by [`STOCK_BADGE_INSET`], so it stays clear of
/// the rank/suit pip in the card's top-left corner.
pub(super) fn stock_badge_translation(layout: &Layout) -> Vec3 {
    // Empty layouts don't contain a Stock entry — fall back to origin so
    // the badge stays in a deterministic spot until the layout is filled.
    let pile_pos = layout
        .pile_positions
        .get(&KlondikePile::Stock)
        .copied()
        .unwrap_or(Vec2::ZERO);
    let half = layout.card_size * 0.5;
    // Anchor to the bottom-right corner, then move the centre inward.
    let x = pile_pos.x + half.x - STOCK_BADGE_INSET.x;
    let y = pile_pos.y - half.y + STOCK_BADGE_INSET.y;
    Vec3::new(x, y, Z_STOCK_BADGE)
}

/// Spawns the stock-count badge entity (background sprite + child text)
/// into the world. Called once, when the badge does not yet exist.
pub(super) fn spawn_stock_count_badge(
    commands: &mut Commands,
    layout: &Layout,
    font: Option<&Handle<Font>>,
    count: usize,
) {
    let translation = stock_badge_translation(layout);
    let visibility = if count == 0 {
        Visibility::Hidden
    } else {
        Visibility::Inherited
    };
    let text_font = TextFont {
        font: font.cloned().unwrap_or_default(),
        font_size: TYPE_BODY,
        ..default()
    };

    commands
        .spawn((
            StockCountBadge,
            Sprite {
                color: STOCK_BADGE_BG,
                custom_size: Some(STOCK_BADGE_SIZE),
                ..default()
            },
            Transform::from_translation(translation),
            visibility,
        ))
        .with_children(|b| {
            b.spawn((
                StockCountBadgeText,
                Text2d::new(format!("{count}")),
                text_font,
                TextColor(STOCK_BADGE_FG),
                // Slightly above the chip background so the digits aren't
                // occluded by the sprite they sit on.
                Transform::from_xyz(0.0, 0.0, 0.1),
            ));
        });
}

/// Spawns the stock-pile remaining-count badge if it does not yet exist,
/// and otherwise updates its text and visibility in place.
///
/// Visibility rule: hidden when the stock is empty (the existing `↺`
/// `StockEmptyLabel` overlay covers that state), shown when one or more
/// cards remain.
///
/// Position is recomputed from `LayoutResource` every tick so the badge
/// follows the stock pile across `WindowResized` layout updates without
/// needing a dedicated resize handler.
#[allow(clippy::too_many_arguments)]
pub(super) fn update_stock_count_badge(
    mut commands: Commands,
    game: Option<Res<GameStateResource>>,
    layout: Option<Res<LayoutResource>>,
    font: Option<Res<FontResource>>,
    mut badges: Query<(Entity, &mut Transform, &mut Visibility), With<StockCountBadge>>,
    children: Query<&Children, With<StockCountBadge>>,
    mut texts: Query<&mut Text2d, With<StockCountBadgeText>>,
) {
    let Some(game) = game else { return };
    let Some(layout) = layout else { return };

    let count = stock_card_count(&game.0);
    let translation = stock_badge_translation(&layout.0);
    let target_visibility = if count == 0 {
        Visibility::Hidden
    } else {
        Visibility::Inherited
    };

    if badges.is_empty() {
        spawn_stock_count_badge(&mut commands, &layout.0, font.as_ref().map(|f| &f.0), count);
        return;
    }

    for (entity, mut transform, mut visibility) in badges.iter_mut() {
        transform.translation = translation;
        if *visibility != target_visibility {
            *visibility = target_visibility;
        }
        // Update the child text to reflect the latest count. The text node
        // is created at spawn time, so under normal operation we always
        // have exactly one child here.
        if let Ok(badge_children) = children.get(entity) {
            for child in badge_children.iter() {
                if let Ok(mut text) = texts.get_mut(child) {
                    let new = format!("{count}");
                    if text.0 != new {
                        text.0 = new;
                    }
                }
            }
        }
    }
}

