use bevy::prelude::*;

use super::format::{
    format_active_move_row, format_foundations_row, format_kth_next_row, format_kth_recent_row,
    format_move_log_header, format_progress, format_stock_waste_row,
};
use super::*;
use crate::layout::LayoutResource;
use crate::replay_playback::ReplayPlaybackState;
use crate::resources::GameStateResource;
use solitaire_core::{KlondikeInstruction, KlondikePile};

/// Overwrites the banner label whenever the resource changes — covers the
/// `Playing → Completed` transition by swapping "▌ replay" for
/// "▌ replay complete" in place without despawning the overlay.
pub(crate) fn update_banner_label(
    state: Res<ReplayPlaybackState>,
    mut q: Query<&mut Text, With<ReplayOverlayBannerText>>,
) {
    if !state.is_changed() {
        return;
    }
    let label = if state.is_completed() {
        "\u{258C} replay complete" // ▌
    } else if state.is_playing() {
        "\u{258C} replay" // ▌
    } else {
        return;
    };
    for mut text in &mut q {
        **text = label.to_string();
    }
}

/// Repaints the "Move N of M" centre readout every frame the cursor moves.
/// Cheap — early-exits if the resource has not changed since the last
/// frame so idle replays don't churn the text mesh.
pub(crate) fn update_progress_text(
    state: Res<ReplayPlaybackState>,
    mut q: Query<&mut Text, With<ReplayOverlayProgressText>>,
) {
    if !state.is_changed() {
        return;
    }
    let label = format_progress(&state);
    for mut text in &mut q {
        **text = label.clone();
    }
}

/// Repositions the floating progress chip above the destination
/// pile of the most-recently-applied move and repaints its text.
///
/// The chip is hidden when:
/// - the cursor is at 0 (no moves applied yet — chip would have
///   nowhere meaningful to land), OR
/// - the most-recently-applied move was a `StockClick` (no
///   destination pile — stock-click feedback already lives at
///   the stock pile and we don't want the chip to jitter back
///   to the stock pile every cycle).
///
/// When visible, the chip's world-space `Transform.translation`
/// is set to the destination pile's centre plus a fixed upward
/// offset (`card_size.y * 0.6`) so the chip floats just above
/// the top edge of the card. World-space placement (rather than
/// UI-space + camera projection) keeps the math trivial and means
/// the chip stays correctly positioned through window resizes
/// without any extra wiring — `LayoutResource` already drives
/// every other piece of pile geometry.
pub(crate) fn update_floating_progress_chip(
    state: Res<ReplayPlaybackState>,
    layout: Option<Res<LayoutResource>>,
    mut chips: Query<
        (&mut Transform, &mut Visibility, &mut Text2d),
        With<ReplayFloatingProgressChip>,
    >,
) {
    let Some(layout) = layout else {
        return;
    };

    // Resolve the destination pile of the last-applied move (if
    // any). `cursor` is the index of the *next* move to apply, so
    // the most-recently-applied move sits at `cursor - 1`.
    let dest_pile = match state.as_ref() {
        ReplayPlaybackState::Playing { replay, cursor, .. } if *cursor > 0 => {
            // The destination pile is recoverable directly from the
            // instruction — no live state needed. `RotateStock` has no
            // destination (the chip hides over the stock pile).
            match &replay.moves[cursor - 1] {
                KlondikeInstruction::DstFoundation(dst) => {
                    Some(KlondikePile::Foundation(dst.foundation))
                }
                KlondikeInstruction::DstTableau(dst) => Some(KlondikePile::Tableau(dst.tableau)),
                KlondikeInstruction::RotateStock => None,
            }
        }
        _ => None,
    };

    let Some(world_pos) = dest_pile
        .as_ref()
        .and_then(|p| layout.0.pile_positions.get(p).copied())
    else {
        // Nothing to point at — hide every chip and exit.
        for (_, mut visibility, _) in chips.iter_mut() {
            *visibility = Visibility::Hidden;
        }
        return;
    };

    // Position above the destination pile by ~60 % of a card
    // height. Half a card lifts above the centre, the extra 10 %
    // is breathing room above the top edge so the chip doesn't
    // visually clip the card.
    let above = Vec2::new(0.0, layout.0.card_size.y * 0.6);
    let target = (world_pos + above).extend(100.0);
    let label = format_progress(&state);

    for (mut transform, mut visibility, mut text2d) in chips.iter_mut() {
        transform.translation = target;
        *visibility = Visibility::Inherited;
        if **text2d != label {
            **text2d = label.clone();
        }
    }
}

/// Repaints the move-log panel's `▌ MOVE LOG · N/M` header text
/// whenever [`ReplayPlaybackState`] changes. Cheap — early-exits
/// when nothing moved so an idle replay leaves the text mesh
/// untouched.
pub(crate) fn update_move_log_header(
    state: Res<ReplayPlaybackState>,
    mut q: Query<&mut Text, With<ReplayOverlayMoveLogHeader>>,
) {
    if !state.is_changed() {
        return;
    }
    let label = format_move_log_header(&state);
    for mut text in &mut q {
        **text = label.clone();
    }
}

/// Repaints the move-log panel's active-row text whenever
/// [`ReplayPlaybackState`] changes. Same change-detection guard
/// as the header updater. Empty string at `cursor == 0` (no move
/// applied yet) and in non-`Playing` states; populated otherwise.
pub(crate) fn update_move_log_active_row(
    state: Res<ReplayPlaybackState>,
    mut q: Query<&mut Text, With<ReplayOverlayMoveLogActiveRow>>,
) {
    if !state.is_changed() {
        return;
    }
    let label = format_active_move_row(&state);
    for mut text in &mut q {
        **text = label.clone();
    }
}

/// Repaints every "previous move" row text whenever
/// [`ReplayPlaybackState`] changes. Each row's `offset` is read
/// from the marker; `k = offset + 1` feeds [`format_kth_recent_row`]
/// (active is k=1, prev offset 1 is k=2, prev offset 2 is k=3).
/// Rows with `offset >= cursor` paint as empty — the panel
/// gracefully under-fills early in a replay without spurious
/// "out-of-range" text.
pub(crate) fn update_move_log_prev_rows(
    state: Res<ReplayPlaybackState>,
    mut q: Query<(&ReplayOverlayMoveLogPrevRow, &mut Text)>,
) {
    if !state.is_changed() {
        return;
    }
    for (row, mut text) in &mut q {
        let label = format_kth_recent_row(&state, row.offset as usize + 1);
        **text = label;
    }
}

/// Repaints every "next move" row text whenever
/// [`ReplayPlaybackState`] changes. Symmetric to the prev-row
/// updater but feeds [`format_kth_next_row`]. Rows where
/// `cursor + offset > moves.len()` paint as empty — the panel
/// gracefully under-fills late in a replay (e.g. final moves)
/// without spurious out-of-range text.
pub(crate) fn update_move_log_next_rows(
    state: Res<ReplayPlaybackState>,
    mut q: Query<(&ReplayOverlayMoveLogNextRow, &mut Text)>,
) {
    if !state.is_changed() {
        return;
    }
    for (row, mut text) in &mut q {
        let label = format_kth_next_row(&state, row.offset as usize);
        **text = label;
    }
}

/// Repaints the bottom-edge accent scrub fill to mirror cursor progress.
/// Same change-detection guard as the text updaters — the overlay
/// already early-exits when nothing moved, so an idle replay leaves the
/// scrub bar's `Node` untouched.
pub(crate) fn update_scrub_fill(
    state: Res<ReplayPlaybackState>,
    mut q: Query<&mut Node, With<ReplayOverlayScrubFill>>,
) {
    if !state.is_changed() {
        return;
    }
    let pct = scrub_pct(&state);
    for mut node in &mut q {
        node.width = Val::Percent(pct);
    }
}

/// Repaints the foundations row whenever [`GameStateResource`] changes.
/// Split into its own system (rather than combined with the stock/waste
/// updater) to avoid a Bevy B0001 query conflict: two `&mut Text`
/// queries in one system are always ambiguous regardless of marker
/// filters. Each updater owns exactly one `Query<&mut Text, With<…>>`.
pub(crate) fn update_mini_tableau_foundations(
    game: Option<Res<GameStateResource>>,
    mut q: Query<&mut Text, With<ReplayMiniTableauFoundations>>,
) {
    let Some(game) = game else { return };
    if !game.is_changed() {
        return;
    }
    let text = format_foundations_row(&game.0);
    for mut t in &mut q {
        **t = text.clone();
    }
}

/// Repaints the stock/waste row whenever [`GameStateResource`] changes.
/// Sibling of [`update_mini_tableau_foundations`] — same change-detection
/// guard, separate system to avoid the B0001 query conflict.
pub(crate) fn update_mini_tableau_stock_waste(
    game: Option<Res<GameStateResource>>,
    mut q: Query<&mut Text, With<ReplayMiniTableauStockWaste>>,
) {
    let Some(game) = game else { return };
    if !game.is_changed() {
        return;
    }
    let text = format_stock_waste_row(&game.0);
    for mut t in &mut q {
        **t = text.clone();
    }
}
