//! HUD construction: band, columns, avatar, and action-bar spawning.

use super::*;


#[cfg(not(target_arch = "wasm32"))]
use crate::avatar_plugin::AvatarResource;

/// Spawns the invisible HUD band that reserves vertical space at the top of
/// the screen so the card layout (computed by `layout::compute_layout` using
/// `HUD_BAND_HEIGHT`) aligns correctly below the score readouts.
///
/// The entity carries no `BackgroundColor` — the green felt shows through.
/// A slim grey background is handled by each content section individually
/// (the bottom action bar has its own `BG_HUD_BAND` background).
pub(super) fn spawn_hud_band(mut commands: Commands) {
    const BASE_TOP: f32 = 0.0;
    commands.spawn((
        Node {
            position_type: PositionType::Absolute,
            top: Val::Px(BASE_TOP),
            left: Val::Px(0.0),
            width: Val::Percent(100.0),
            height: Val::Px(HUD_BAND_HEIGHT),
            ..default()
        },
        ZIndex(Z_HUD - 1),
        SafeAreaAnchoredTop { base_top: BASE_TOP },
        HudBand,
    ));
}

/// Spawns the in-game HUD as a 4-tier vertical column anchored to the
/// top-left of the play area.
///
/// Tiers (top to bottom):
///   1. **Primary** — Score (display weight) · Moves · Timer.
///      Always visible during gameplay.
///   2. **Mode context** — Mode badge · Daily-challenge constraint ·
///      Draw-cycle indicator. Each cell is empty when not relevant; the
///      row collapses visually when all cells are empty.
///   3. **Penalty / bonus** — Undos · Recycles · Auto-complete badge.
///      Both penalty counters share `STATE_WARNING` (the audit found
///      they were inconsistent: Undos amber, Recycles white).
///   4. **Selection** — keyboard-driven pile selector chip.
///
/// The audit identified the original single-row layout (10 readouts in
/// one horizontal flex row, 5+ colour families competing) as the
/// player's #1 complaint. This restructure groups by purpose, lets
/// transient items disappear cleanly, and uses the typography scale to
/// make Score the visual protagonist.
pub(super) fn spawn_hud(font_res: Option<Res<FontResource>>, mut commands: Commands) {
    let font_handle = font_res.as_ref().map(|f| f.0.clone()).unwrap_or_default();
    let font_score = TextFont {
        font: font_handle.clone(),
        font_size: TYPE_HEADLINE,
        ..default()
    };
    let font_lg = TextFont {
        font: font_handle.clone(),
        font_size: TYPE_BODY_LG,
        ..default()
    };
    let font_body = TextFont {
        font: font_handle,
        font_size: TYPE_BODY,
        ..default()
    };

    let row_node = || Node {
        flex_direction: FlexDirection::Row,
        column_gap: VAL_SPACE_3,
        // On a narrow viewport the four tier rows (Score/Moves/Timer,
        // Mode/Challenge/Draw-cycle/Won-previously, Undos/Recycles/
        // Auto-complete, selection chip) can collectively be wider than
        // the available space and overflow into the action-button column
        // on the right. `flex_wrap: Wrap` lets each tier soft-wrap onto
        // a second line; on a desktop window the rows stay single-line
        // because the parent column has no width cap and the row never
        // exceeds the natural line width.
        flex_wrap: FlexWrap::Wrap,
        row_gap: VAL_SPACE_1,
        align_items: AlignItems::Baseline,
        ..default()
    };

    commands
        .spawn((
            Node {
                position_type: PositionType::Absolute,
                left: VAL_SPACE_3,
                top: Val::Px(SPACE_2),
                flex_direction: FlexDirection::Column,
                // Cap the column at 50% of viewport so on narrow
                // (mobile) widths the inner tier rows have a bounded
                // width to wrap against, and the column can't bleed
                // into the right-anchored action button row (also
                // capped at 50%). On desktop 50% of 1920 = 960 px,
                // wider than any tier row's natural width, so the
                // visible layout is unaffected.
                max_width: Val::Percent(50.0),
                row_gap: VAL_SPACE_1,
                ..default()
            },
            ZIndex(Z_HUD),
            SafeAreaAnchoredTop { base_top: SPACE_2 },
            HudColumn,
        ))
        .with_children(|hud| {
            // Tier 1 — primary readouts. Score is the protagonist (HEADLINE);
            // Moves and Timer are supporting context (BODY_LG, secondary tone).
            hud.spawn(row_node()).with_children(|t1| {
                t1.spawn((
                    HudScore,
                    Tooltip::new("Points earned this game. Hidden in Zen mode."),
                    Text::new("Score: 0"),
                    font_score.clone(),
                    TextColor(TEXT_PRIMARY),
                ));
                t1.spawn((
                    HudMoves,
                    Tooltip::new("Moves you've made this game. Counts placements and stock draws."),
                    Text::new("Moves: 0"),
                    font_lg.clone(),
                    TextColor(TEXT_SECONDARY),
                ));
                t1.spawn((
                    HudTime,
                    Tooltip::new("Time on this game. Counts down in Time Attack."),
                    Text::new("0:00"),
                    font_lg.clone(),
                    TextColor(TEXT_SECONDARY),
                ));
            });

            // Tier 2 — mode context. Each cell is empty until update_hud
            // populates it (and clears it when no longer relevant), so the
            // row collapses when nothing in this tier applies.
            hud.spawn(row_node()).with_children(|t2| {
                t2.spawn((
                    HudMode,
                    Tooltip::new("Active game mode. Click Modes to switch."),
                    Text::new(""),
                    font_body.clone(),
                    TextColor(ACCENT_PRIMARY),
                ));
                t2.spawn((
                    HudChallenge,
                    Tooltip::new("Today's daily challenge target. Beat it for bonus XP."),
                    Text::new(""),
                    font_body.clone(),
                    TextColor(STATE_INFO),
                ));
                t2.spawn((
                    HudDrawCycle,
                    Tooltip::new("Cards drawn on the next stock click in Draw-Three."),
                    Text::new(""),
                    font_body.clone(),
                    TextColor(STATE_INFO),
                ));
                t2.spawn((
                    HudWonPreviously,
                    Tooltip::new("You've won this deal before. Same seed in your replay history."),
                    Text::new(""),
                    font_body.clone(),
                    TextColor(STATE_SUCCESS),
                ));
            });

            // Tier 3 — penalty / bonus. Undos and Recycles share the
            // warning hue so they read as the same category ("you took a
            // penalty"); the auto-complete badge stays success-green.
            hud.spawn(row_node()).with_children(|t3| {
                t3.spawn((
                    HudUndos,
                    Tooltip::new("Undos used this game. Any undo blocks the No Undo achievement."),
                    Text::new(""),
                    font_body.clone(),
                    TextColor(STATE_WARNING),
                ));
                t3.spawn((
                    HudRecycles,
                    Tooltip::new(
                        "Times you've recycled the stock. Three or more unlocks Comeback.",
                    ),
                    Text::new(""),
                    font_body.clone(),
                    TextColor(STATE_WARNING),
                ));
                t3.spawn((
                    HudAutoComplete,
                    Tooltip::new("Board is solvable from here. Press Enter to auto-finish."),
                    Text::new(""),
                    font_body.clone(),
                    TextColor(STATE_SUCCESS),
                ));
            });

            // Tier 4 — selection chip. Stays in HUD for now; a future
            // pass can reposition it next to the selected pile.
            hud.spawn(row_node()).with_children(|t4| {
                t4.spawn((
                    HudSelection,
                    Tooltip::new("Pile selected with Tab. Use arrows or Enter to act."),
                    Text::new(""),
                    font_body,
                    TextColor(ACCENT_SECONDARY),
                ));
            });
        });
}

/// Spawns the circular avatar / initials button anchored to the top-right
/// of the HUD band. Initial content is seeded from whatever resources are
/// available at startup; `update_hud_avatar` replaces the children whenever
/// `AvatarResource` or `SettingsResource` later changes.
pub(super) fn spawn_hud_avatar(
    font_res: Option<Res<FontResource>>,
    avatar: Option<Res<AvatarResource>>,
    settings: Option<Res<SettingsResource>>,
    mut commands: Commands,
) {
    const SIZE: f32 = 32.0;
    let id = commands
        .spawn((
            HudAvatar,
            Button,
            Tooltip::new("Your profile — tap to open."),
            Node {
                position_type: PositionType::Absolute,
                top: Val::Px(SPACE_2),
                right: VAL_SPACE_3,
                width: Val::Px(SIZE),
                height: Val::Px(SIZE),
                border_radius: BorderRadius::all(Val::Px(SIZE / 2.0)),
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Center,
                ..default()
            },
            BackgroundColor(ACCENT_PRIMARY),
            ZIndex(Z_HUD),
            SafeAreaAnchoredTop { base_top: SPACE_2 },
        ))
        .id();
    spawn_avatar_child(
        &mut commands,
        id,
        avatar.as_deref(),
        settings.as_deref(),
        font_res.as_deref(),
    );
}

/// Re-spawns the avatar circle content (image or initials) whenever either
/// [`AvatarResource`] or [`SettingsResource`] changes — covers both the
/// image arriving after download and the username changing after login.
pub(super) fn update_hud_avatar(
    avatar: Option<Res<AvatarResource>>,
    settings: Option<Res<SettingsResource>>,
    font_res: Option<Res<FontResource>>,
    q: Query<Entity, With<HudAvatar>>,
    mut commands: Commands,
) {
    let avatar_changed = avatar.as_ref().is_some_and(|r| r.is_changed());
    let settings_changed = settings.as_ref().is_some_and(|r| r.is_changed());
    if !avatar_changed && !settings_changed {
        return;
    }
    let Ok(entity) = q.single() else {
        return;
    };
    commands.entity(entity).despawn_related::<Children>();
    spawn_avatar_child(
        &mut commands,
        entity,
        avatar.as_deref(),
        settings.as_deref(),
        font_res.as_deref(),
    );
}

/// Populates the avatar container with either the downloaded image or an
/// initials fallback disc. Called from both the startup spawn and the
/// reactive update system so the rendering logic lives in one place.
pub(super) fn spawn_avatar_child(
    commands: &mut Commands,
    parent: Entity,
    avatar: Option<&AvatarResource>,
    settings: Option<&SettingsResource>,
    font_res: Option<&FontResource>,
) {
    const SIZE: f32 = 32.0;
    if let Some(handle) = avatar.and_then(|a| a.0.clone()) {
        // Logged-in with a downloaded avatar: keep the accent disc behind it.
        commands.entity(parent).insert(BackgroundColor(ACCENT_PRIMARY));
        // Image fills the circle container; border_radius clips it to a disc.
        commands.entity(parent).with_children(|b| {
            b.spawn((
                ImageNode::new(handle),
                Node {
                    width: Val::Px(SIZE),
                    height: Val::Px(SIZE),
                    border_radius: BorderRadius::all(Val::Px(SIZE / 2.0)),
                    ..default()
                },
            ));
        });
    } else {
        let initial = settings
            .and_then(|s| match &s.0.sync_backend {
                SyncBackend::SolitaireServer { username, .. } => username.chars().next(),
                SyncBackend::Local => None,
            })
            .and_then(|c| c.to_uppercase().next())
            .unwrap_or('?');
        // Real initial (logged in) keeps the red accent disc; the '?'
        // unauthenticated fallback uses a neutral grey so it reads as a
        // "tap to log in" affordance rather than an error.
        let disc_bg = if initial == '?' {
            BG_ELEVATED_HI
        } else {
            ACCENT_PRIMARY
        };
        commands.entity(parent).insert(BackgroundColor(disc_bg));
        commands.entity(parent).with_children(|b| {
            b.spawn((
                Text::new(initial.to_string()),
                TextFont {
                    font: font_res.map(|f| f.0.clone()).unwrap_or_default(),
                    font_size: 14.0,
                    ..default()
                },
                TextColor(TEXT_PRIMARY),
            ));
        });
    }
}

/// Opens the Profile overlay when the avatar button is pressed.
pub(super) fn handle_avatar_button(
    interaction_query: Query<&Interaction, (With<HudAvatar>, Changed<Interaction>)>,
    mut toggle_profile: MessageWriter<ToggleProfileRequestEvent>,
) {
    for interaction in &interaction_query {
        if *interaction == Interaction::Pressed {
            toggle_profile.write(ToggleProfileRequestEvent);
        }
    }
}

/// Spawns the action button bar anchored to the top-right of the window.
/// Each child is a clickable button mirroring a keyboard accelerator —
/// per the UI-first principle (CLAUDE.md / ARCHITECTURE.md §1) the buttons
/// are the primary entry point and the hotkeys are optional.
///
/// Order (left → right): Undo, Pause, Help, New Game. New Game is rightmost
/// because it's the most consequential action; the destructive button sits
/// on its own visual edge.
pub(super) fn spawn_action_buttons(
    font_res: Option<Res<FontResource>>,
    windows: Query<&Window>,
    mut commands: Commands,
) {
    let action_font_size =
        action_bar_font_size(windows.iter().next().map_or(900.0, |win| win.width()));
    let font = TextFont {
        font: font_res.as_ref().map(|f| f.0.clone()).unwrap_or_default(),
        font_size: action_font_size,
        ..default()
    };

    // Bottom bar: full-width, centered, sits above the gesture-navigation zone.
    // `SafeAreaAnchoredBottom` applies the correct logical-pixel inset once
    // Android reports it (frames 1-3); initial value is 0.0.
    commands
        .spawn((
            Node {
                position_type: PositionType::Absolute,
                bottom: Val::Px(0.0),
                left: Val::Px(0.0),
                width: Val::Percent(100.0),
                flex_direction: FlexDirection::Row,
                flex_wrap: FlexWrap::Wrap,
                justify_content: JustifyContent::Center,
                column_gap: ACTION_BAR_COLUMN_GAP,
                row_gap: VAL_SPACE_2,
                align_items: AlignItems::Center,
                padding: UiRect {
                    left: VAL_SPACE_3,
                    right: VAL_SPACE_3,
                    top: VAL_SPACE_2,
                    bottom: VAL_SPACE_2,
                },
                ..default()
            },
            BackgroundColor(BG_HUD_BAND),
            ZIndex(Z_HUD),
            SafeAreaAnchoredBottom { base_bottom: 0.0 },
            HudActionBar,
        ))
        .with_children(|row| {
            // The trailing `order` argument feeds `Focusable { group: Hud, order }`
            // so Tab cycles the action bar in visual reading order.
            // Undo and Pause are the primary gameplay actions — full brightness.
            // Menu, Help, Hint, Modes, New are navigation/utility — dimmed.
            spawn_action_button(
                row,
                MenuButton,
                ACTION_BAR_LABELS[0],
                None,
                "Open Stats, Achievements, Profile, Settings, or Leaderboard.",
                &font,
                0,
                TEXT_SECONDARY,
            );
            spawn_action_button(
                row,
                UndoButton,
                ACTION_BAR_LABELS[1],
                Some("U"),
                "Take back your last move. Costs points and blocks No Undo.",
                &font,
                1,
                TEXT_PRIMARY,
            );
            spawn_action_button(
                row,
                PauseButton,
                ACTION_BAR_LABELS[2],
                Some("Esc"),
                "Pause the game and freeze the timer.",
                &font,
                2,
                TEXT_PRIMARY,
            );
            spawn_action_button(
                row,
                HelpButton,
                ACTION_BAR_LABELS[3],
                Some("F1"),
                "Show controls, rules, and keyboard shortcuts.",
                &font,
                3,
                TEXT_SECONDARY,
            );
            spawn_action_button(
                row,
                HintButton,
                ACTION_BAR_LABELS[4],
                Some("H"),
                "Highlight a suggested move. Cycles through alternatives on repeat taps.",
                &font,
                4,
                TEXT_SECONDARY,
            );
            spawn_action_button(
                row,
                ModesButton,
                ACTION_BAR_LABELS[5],
                None,
                "Switch modes: Classic, Daily, Zen, Challenge, Time Attack.",
                &font,
                5,
                TEXT_SECONDARY,
            );
            spawn_action_button(
                row,
                NewGameButton,
                ACTION_BAR_LABELS[6],
                Some("N"),
                "Start a fresh deal. Confirms first if a game is in progress.",
                &font,
                6,
                TEXT_SECONDARY,
            );
        });
}

/// Spawns a single action button as a child of `row`. Each button shares
/// the same node geometry, idle colour, and `ActionButton` marker so
/// `paint_action_buttons` can recolour all of them with one query.
///
/// `order` is the button's index inside the action bar (0 for the
/// leftmost). It propagates into the [`Focusable`] this function inserts
/// so Phase 2's keyboard focus ring cycles the HUD in visual order.
///
/// `tooltip` is the hover-reveal caption attached via [`Tooltip`]. Every
/// action button ships with one — there is no opt-out — because each button
/// represents a player-triggered action and benefits from a one-line
/// reminder of what it does.
#[allow(clippy::too_many_arguments)]
pub(super) fn spawn_action_button<M: Component>(
    row: &mut ChildSpawnerCommands,
    marker: M,
    label: &str,
    hotkey: Option<&'static str>,
    tooltip: &'static str,
    font: &TextFont,
    order: i32,
    text_color: Color,
) {
    // Hotkey hint chips ("U", "Esc", "F1", "N") are meaningless on a
    // touch device — the button itself is the affordance — and they
    // visibly clutter the narrow-viewport action row. The chevrons on
    // Menu/Modes remain because they indicate dropdown behaviour.
    let hotkey = if SHOW_KEYBOARD_ACCELERATORS {
        hotkey
    } else {
        None
    };

    let hotkey_font = TextFont {
        font: font.font.clone(),
        font_size: TYPE_CAPTION,
        ..default()
    };
    let (pad, min_w, min_h) = action_button_metrics();

    row.spawn((
        marker,
        ActionButton,
        Button,
        Tooltip::new(tooltip),
        Focusable {
            group: FocusGroup::Hud,
            order,
        },
        Node {
            padding: pad,
            min_width: min_w,
            min_height: min_h,
            justify_content: JustifyContent::Center,
            align_items: AlignItems::Center,
            border_radius: BorderRadius::all(Val::Px(RADIUS_MD)),
            column_gap: VAL_SPACE_2,
            ..default()
        },
        BackgroundColor(ACTION_BTN_IDLE),
        BorderColor::all(BORDER_SUBTLE),
        HighContrastBorder::with_default(BORDER_SUBTLE),
    ))
    .with_children(|b| {
        spawn_action_button_label(b, label, font, text_color);
        if let Some(key) = hotkey {
            // Hotkey hint rendered as a dim caption next to the label —
            // keeps the keyboard accelerator discoverable without
            // hijacking the button's primary affordance.
            b.spawn((Text::new(key), hotkey_font, TextColor(TEXT_SECONDARY)));
        }
    });
}

