//! Input handling for the Settings panel: button presses, keyboard
//! accelerators, focus attachment, and scrolling.

use super::*;

use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
use bevy::ui::{ComputedNode, UiGlobalTransform};
use solitaire_core::DrawStockConfig;
use solitaire_data::{
    AnimSpeed, REPLAY_MOVE_INTERVAL_STEP_SECS, TIME_BONUS_MULTIPLIER_STEP, TOOLTIP_DELAY_STEP_SECS,
    settings::Theme,
};

use crate::events::{
    DeleteAccountRequestEvent, InfoToastEvent, ManualSyncRequestEvent, SyncConfigureRequestEvent,
    SyncLogoutRequestEvent, ToggleSettingsRequestEvent,
};
use crate::ui_focus::{FocusGroup, Focusable, FocusedButton};
use crate::ui_modal::{ModalButton, ModalScrim};
use crate::ui_theme::SPACE_2;

pub(super) fn handle_volume_keys(
    keys: Res<ButtonInput<KeyCode>>,
    mut settings: ResMut<SettingsResource>,
    path: Res<SettingsStoragePath>,
    mut changed: MessageWriter<SettingsChangedEvent>,
    mut toast: MessageWriter<InfoToastEvent>,
) {
    let mut delta = 0.0_f32;
    if keys.just_pressed(KeyCode::BracketLeft) {
        delta -= SFX_STEP;
    }
    if keys.just_pressed(KeyCode::BracketRight) {
        delta += SFX_STEP;
    }
    if delta == 0.0 {
        return;
    }
    let before = settings.0.sfx_volume;
    let after = settings.0.adjust_sfx_volume(delta);
    if (before - after).abs() < f32::EPSILON {
        return;
    }
    persist(&path, &settings.0);
    changed.write(SettingsChangedEvent(settings.0.clone()));
    toast.write(InfoToastEvent(format!(
        "SFX volume: {}%",
        (after * 100.0).round() as i32
    )));
}

/// Opens or closes the Settings panel — `O` keyboard accelerator or
/// `ToggleSettingsRequestEvent` from the HUD Menu popover.
pub(super) fn toggle_settings_screen(
    keys: Res<ButtonInput<KeyCode>>,
    mut requests: MessageReader<ToggleSettingsRequestEvent>,
    mut screen: ResMut<SettingsScreen>,
) {
    let button_clicked = requests.read().count() > 0;
    if keys.just_pressed(KeyCode::KeyO) || button_clicked {
        screen.0 = !screen.0;
    }
}

/// Reacts to button presses inside the Settings panel.
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(super) fn handle_settings_buttons(
    interaction_query: Query<(&Interaction, &SettingsButton), Changed<Interaction>>,
    mut settings: ResMut<SettingsResource>,
    mut screen: ResMut<SettingsScreen>,
    path: Res<SettingsStoragePath>,
    mut changed: MessageWriter<SettingsChangedEvent>,
    mut sfx_text: Query<
        &mut Text,
        (
            With<SfxVolumeText>,
            Without<MusicVolumeText>,
            Without<DrawModeText>,
            Without<ThemeText>,
            Without<AnimSpeedText>,
            Without<ColorBlindText>,
            Without<HighContrastText>,
            Without<ReduceMotionText>,
        ),
    >,
    mut music_text: Query<
        &mut Text,
        (
            With<MusicVolumeText>,
            Without<SfxVolumeText>,
            Without<DrawModeText>,
            Without<ThemeText>,
            Without<AnimSpeedText>,
            Without<ColorBlindText>,
            Without<HighContrastText>,
            Without<ReduceMotionText>,
        ),
    >,
    mut draw_text: Query<
        &mut Text,
        (
            With<DrawModeText>,
            Without<SfxVolumeText>,
            Without<MusicVolumeText>,
            Without<ThemeText>,
            Without<AnimSpeedText>,
            Without<ColorBlindText>,
            Without<HighContrastText>,
            Without<ReduceMotionText>,
        ),
    >,
    mut theme_text: Query<
        &mut Text,
        (
            With<ThemeText>,
            Without<SfxVolumeText>,
            Without<MusicVolumeText>,
            Without<DrawModeText>,
            Without<AnimSpeedText>,
            Without<ColorBlindText>,
            Without<HighContrastText>,
            Without<ReduceMotionText>,
        ),
    >,
    mut anim_speed_text: Query<
        &mut Text,
        (
            With<AnimSpeedText>,
            Without<SfxVolumeText>,
            Without<MusicVolumeText>,
            Without<DrawModeText>,
            Without<ThemeText>,
            Without<ColorBlindText>,
            Without<HighContrastText>,
            Without<ReduceMotionText>,
        ),
    >,
    mut color_blind_text: Query<
        &mut Text,
        (
            With<ColorBlindText>,
            Without<SfxVolumeText>,
            Without<MusicVolumeText>,
            Without<DrawModeText>,
            Without<ThemeText>,
            Without<AnimSpeedText>,
            Without<HighContrastText>,
            Without<ReduceMotionText>,
        ),
    >,
    mut high_contrast_text: Query<
        &mut Text,
        (
            With<HighContrastText>,
            Without<SfxVolumeText>,
            Without<MusicVolumeText>,
            Without<DrawModeText>,
            Without<ThemeText>,
            Without<AnimSpeedText>,
            Without<ColorBlindText>,
            Without<ReduceMotionText>,
        ),
    >,
    mut reduce_motion_text: Query<
        &mut Text,
        (
            With<ReduceMotionText>,
            Without<SfxVolumeText>,
            Without<MusicVolumeText>,
            Without<DrawModeText>,
            Without<ThemeText>,
            Without<AnimSpeedText>,
            Without<ColorBlindText>,
            Without<HighContrastText>,
        ),
    >,
) {
    for (interaction, button) in &interaction_query {
        if *interaction != Interaction::Pressed {
            continue;
        }
        match button {
            SettingsButton::SfxDown => {
                let before = settings.0.sfx_volume;
                let after = settings.0.adjust_sfx_volume(-SFX_STEP);
                if (before - after).abs() > f32::EPSILON {
                    persist(&path, &settings.0);
                    changed.write(SettingsChangedEvent(settings.0.clone()));
                    if let Ok(mut t) = sfx_text.single_mut() {
                        **t = format!("{after:.2}");
                    }
                }
            }
            SettingsButton::SfxUp => {
                let before = settings.0.sfx_volume;
                let after = settings.0.adjust_sfx_volume(SFX_STEP);
                if (before - after).abs() > f32::EPSILON {
                    persist(&path, &settings.0);
                    changed.write(SettingsChangedEvent(settings.0.clone()));
                    if let Ok(mut t) = sfx_text.single_mut() {
                        **t = format!("{after:.2}");
                    }
                }
            }
            SettingsButton::MusicDown => {
                let before = settings.0.music_volume;
                let after = settings.0.adjust_music_volume(-SFX_STEP);
                if (before - after).abs() > f32::EPSILON {
                    persist(&path, &settings.0);
                    changed.write(SettingsChangedEvent(settings.0.clone()));
                    if let Ok(mut t) = music_text.single_mut() {
                        **t = format!("{after:.2}");
                    }
                }
            }
            SettingsButton::MusicUp => {
                let before = settings.0.music_volume;
                let after = settings.0.adjust_music_volume(SFX_STEP);
                if (before - after).abs() > f32::EPSILON {
                    persist(&path, &settings.0);
                    changed.write(SettingsChangedEvent(settings.0.clone()));
                    if let Ok(mut t) = music_text.single_mut() {
                        **t = format!("{after:.2}");
                    }
                }
            }
            SettingsButton::ToggleDrawMode => {
                settings.0.draw_mode = match settings.0.draw_mode {
                    DrawStockConfig::DrawOne => DrawStockConfig::DrawThree,
                    DrawStockConfig::DrawThree => DrawStockConfig::DrawOne,
                };
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
                if let Ok(mut t) = draw_text.single_mut() {
                    **t = draw_mode_label(&settings.0.draw_mode);
                }
            }
            SettingsButton::CycleAnimSpeed => {
                settings.0.animation_speed = match settings.0.animation_speed {
                    AnimSpeed::Normal => AnimSpeed::Fast,
                    AnimSpeed::Fast => AnimSpeed::Instant,
                    AnimSpeed::Instant => AnimSpeed::Normal,
                };
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
                if let Ok(mut t) = anim_speed_text.single_mut() {
                    **t = anim_speed_label(&settings.0.animation_speed);
                }
            }
            SettingsButton::TooltipDelayDown => {
                let before = settings.0.tooltip_delay_secs;
                let after = settings.0.adjust_tooltip_delay(-TOOLTIP_DELAY_STEP_SECS);
                if (before - after).abs() > f32::EPSILON {
                    persist(&path, &settings.0);
                    changed.write(SettingsChangedEvent(settings.0.clone()));
                    // The Text node is refreshed by `update_tooltip_delay_text`
                    // on the next frame via `settings.is_changed()`.
                }
            }
            SettingsButton::TooltipDelayUp => {
                let before = settings.0.tooltip_delay_secs;
                let after = settings.0.adjust_tooltip_delay(TOOLTIP_DELAY_STEP_SECS);
                if (before - after).abs() > f32::EPSILON {
                    persist(&path, &settings.0);
                    changed.write(SettingsChangedEvent(settings.0.clone()));
                }
            }
            SettingsButton::TimeBonusDown => {
                let before = settings.0.time_bonus_multiplier;
                let after = settings
                    .0
                    .adjust_time_bonus_multiplier(-TIME_BONUS_MULTIPLIER_STEP);
                if (before - after).abs() > f32::EPSILON {
                    persist(&path, &settings.0);
                    changed.write(SettingsChangedEvent(settings.0.clone()));
                    // The Text node is refreshed by
                    // `update_time_bonus_multiplier_text` on the next
                    // frame via `settings.is_changed()`.
                }
            }
            SettingsButton::TimeBonusUp => {
                let before = settings.0.time_bonus_multiplier;
                let after = settings
                    .0
                    .adjust_time_bonus_multiplier(TIME_BONUS_MULTIPLIER_STEP);
                if (before - after).abs() > f32::EPSILON {
                    persist(&path, &settings.0);
                    changed.write(SettingsChangedEvent(settings.0.clone()));
                }
            }
            SettingsButton::ReplayMoveIntervalDown => {
                let before = settings.0.replay_move_interval_secs;
                let after = settings
                    .0
                    .adjust_replay_move_interval(-REPLAY_MOVE_INTERVAL_STEP_SECS);
                if (before - after).abs() > f32::EPSILON {
                    persist(&path, &settings.0);
                    changed.write(SettingsChangedEvent(settings.0.clone()));
                    // The Text node is refreshed by
                    // `update_replay_move_interval_text` on the next
                    // frame via `settings.is_changed()`.
                }
            }
            SettingsButton::ReplayMoveIntervalUp => {
                let before = settings.0.replay_move_interval_secs;
                let after = settings
                    .0
                    .adjust_replay_move_interval(REPLAY_MOVE_INTERVAL_STEP_SECS);
                if (before - after).abs() > f32::EPSILON {
                    persist(&path, &settings.0);
                    changed.write(SettingsChangedEvent(settings.0.clone()));
                }
            }
            SettingsButton::ToggleTheme => {
                settings.0.theme = match settings.0.theme {
                    Theme::Green => Theme::Blue,
                    Theme::Blue => Theme::Dark,
                    Theme::Dark => Theme::Green,
                };
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
                if let Ok(mut t) = theme_text.single_mut() {
                    **t = theme_label(&settings.0.theme);
                }
            }
            SettingsButton::ToggleColorBlind => {
                settings.0.color_blind_mode = !settings.0.color_blind_mode;
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
                if let Ok(mut t) = color_blind_text.single_mut() {
                    **t = color_blind_label(settings.0.color_blind_mode);
                }
            }
            SettingsButton::ToggleHighContrast => {
                settings.0.high_contrast_mode = !settings.0.high_contrast_mode;
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
                if let Ok(mut t) = high_contrast_text.single_mut() {
                    **t = on_off_label(settings.0.high_contrast_mode);
                }
            }
            SettingsButton::ToggleReduceMotion => {
                settings.0.reduce_motion_mode = !settings.0.reduce_motion_mode;
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
                if let Ok(mut t) = reduce_motion_text.single_mut() {
                    **t = on_off_label(settings.0.reduce_motion_mode);
                }
            }
            SettingsButton::ToggleTouchInputMode => {
                use solitaire_data::settings::TouchInputMode;
                settings.0.touch_input_mode = match settings.0.touch_input_mode {
                    TouchInputMode::OneTap => TouchInputMode::TapToSelect,
                    TouchInputMode::TapToSelect => TouchInputMode::OneTap,
                };
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
                // Text refreshed by `update_touch_input_mode_text` next frame.
            }
            SettingsButton::ToggleWinnableDealsOnly => {
                settings.0.winnable_deals_only = !settings.0.winnable_deals_only;
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
                // The Text node is refreshed by `update_winnable_deals_only_text`
                // on the next frame via `settings.is_changed()`.
            }
            SettingsButton::ToggleAnalytics => {
                settings.0.analytics_enabled = !settings.0.analytics_enabled;
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
                // Text refreshed by `update_analytics_enabled_text` next frame.
            }
            SettingsButton::ToggleSmartDefaultSize => {
                settings.0.disable_smart_default_size = !settings.0.disable_smart_default_size;
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
                // The Text node is refreshed by
                // `update_smart_default_size_text` next frame. The
                // sizer system is gated only at startup, so flipping
                // this mid-session takes effect on the next launch —
                // documented on the field in `solitaire_data::Settings`.
            }
            SettingsButton::SelectCardBack(idx) => {
                settings.0.selected_card_back = *idx;
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
            }
            SettingsButton::SelectBackground(idx) => {
                settings.0.selected_background = *idx;
                persist(&path, &settings.0);
                changed.write(SettingsChangedEvent(settings.0.clone()));
            }
            SettingsButton::SelectTheme(theme_id) => {
                if settings.0.selected_theme_id != *theme_id {
                    settings.0.selected_theme_id = theme_id.clone();
                    persist(&path, &settings.0);
                    changed.write(SettingsChangedEvent(settings.0.clone()));
                }
            }
            #[cfg(not(target_arch = "wasm32"))]
            SettingsButton::ScanThemes => {
                // Handled by `handle_scan_themes`.
            }
            SettingsButton::SyncNow
            | SettingsButton::ConnectSync
            | SettingsButton::DisconnectSync
            | SettingsButton::DeleteAccount => {
                // Handled by `handle_sync_buttons`.
            }
            SettingsButton::Done => {
                screen.0 = false;
            }
        }
    }
}

/// Handles sync-related settings buttons: Sync Now, Connect, Disconnect,
/// and Delete Account. Split from `handle_settings_buttons` to stay within
/// Bevy's 16-parameter system limit.
pub(super) fn handle_sync_buttons(
    interaction_query: Query<(&Interaction, &SettingsButton), Changed<Interaction>>,
    mut manual_sync: MessageWriter<ManualSyncRequestEvent>,
    mut configure_sync: MessageWriter<SyncConfigureRequestEvent>,
    mut logout_sync: MessageWriter<SyncLogoutRequestEvent>,
    mut delete_account: MessageWriter<DeleteAccountRequestEvent>,
    mut screen: ResMut<SettingsScreen>,
) {
    for (interaction, button) in &interaction_query {
        if *interaction != Interaction::Pressed {
            continue;
        }
        match button {
            SettingsButton::SyncNow => {
                manual_sync.write(ManualSyncRequestEvent);
            }
            SettingsButton::ConnectSync => {
                // Close settings before the sync-setup modal opens so the
                // guard in open_sync_setup_modal doesn't block on our own scrim.
                screen.0 = false;
                configure_sync.write(SyncConfigureRequestEvent);
            }
            SettingsButton::DisconnectSync => {
                logout_sync.write(SyncLogoutRequestEvent);
            }
            SettingsButton::DeleteAccount => {
                delete_account.write(DeleteAccountRequestEvent);
            }
            _ => {}
        }
    }
}

/// Auto-attaches [`Focusable`] to every bespoke Settings button — icon
/// buttons (volume +/−, toggle, cycle), swatch buttons (card-back,
/// background pickers), and the "Sync Now" button. The "Done" button is
/// already tagged by `attach_focusable_to_modal_buttons` (it carries
/// [`ModalButton`]) and is filtered out here.
///
/// Walks ancestors via [`ChildOf`] to find the [`ModalScrim`] that owns
/// the panel so the new [`Focusable`]'s group is bound to that scrim —
/// same defensive shape as the Phase 1 / 2 attach systems.
#[allow(clippy::type_complexity)]
pub(super) fn attach_focusable_to_settings_buttons(
    mut commands: Commands,
    new_buttons: Query<
        (Entity, &SettingsButton),
        (With<Button>, Without<Focusable>, Without<ModalButton>),
    >,
    parents: Query<&ChildOf>,
    scrims: Query<(), With<ModalScrim>>,
) {
    for (button, settings_button) in &new_buttons {
        let mut current = button;
        let mut scrim_entity: Option<Entity> = None;
        for _ in 0..32 {
            if scrims.get(current).is_ok() {
                scrim_entity = Some(current);
                break;
            }
            match parents.get(current) {
                Ok(parent) => current = parent.parent(),
                Err(_) => break,
            }
        }
        if let Some(scrim) = scrim_entity {
            commands.entity(button).insert(Focusable {
                group: FocusGroup::Modal(scrim),
                order: settings_button.focus_order(),
            });
        }
    }
}

/// Vertical padding (logical px) added around the focused button when
/// scrolling it into view. Keeps the focus ring's halo visible above /
/// below the viewport edge.
const FOCUS_SCROLL_PADDING: f32 = SPACE_2;

/// When the focused entity sits outside the visible Settings scroll
/// viewport, adjust the viewport's [`ScrollPosition`] so the button is
/// fully visible. No-op when:
///
/// - `FocusedButton` is `None`
/// - the focused entity has no [`UiGlobalTransform`] / [`ComputedNode`]
///   (e.g. a freshly-spawned modal hasn't laid out yet)
/// - the focused entity is not a descendant of the
///   [`SettingsPanelScrollable`] container
///
/// The viewport's visible Y range is `[scroll_y, scroll_y +
/// viewport_height]` in physical pixels (matching `ComputedNode.size`).
/// The focused button's vertical extent is computed from its
/// `UiGlobalTransform.translation.y` (centre, physical) ± half its
/// `ComputedNode.size.y`. Because the scroll container's local
/// coordinates run [0, content_height] and the visible window is
/// [scroll_y, scroll_y + viewport], we convert the button's window-
/// space Y to container-local Y by subtracting the container's window-
/// space top and adding the current scroll offset.
#[allow(clippy::type_complexity)]
pub(super) fn scroll_focus_into_view(
    focused: Res<FocusedButton>,
    parents: Query<&ChildOf>,
    nodes: Query<(&UiGlobalTransform, &ComputedNode)>,
    mut containers: Query<
        (&mut ScrollPosition, &UiGlobalTransform, &ComputedNode),
        With<SettingsPanelScrollable>,
    >,
) {
    let Some(target) = focused.0 else { return };
    // Gather button geometry.
    let Ok((target_transform, target_node)) = nodes.get(target) else {
        return;
    };

    // Walk ancestors looking for the scroll container. Bounded to keep
    // a malformed hierarchy from hanging the system.
    let mut current = target;
    let mut container_entity: Option<Entity> = None;
    for _ in 0..32 {
        if containers.get(current).is_ok() {
            container_entity = Some(current);
            break;
        }
        match parents.get(current) {
            Ok(parent) => current = parent.parent(),
            Err(_) => break,
        }
    }
    let Some(container) = container_entity else {
        return;
    };

    let Ok((mut scroll, container_transform, container_node)) = containers.get_mut(container)
    else {
        return;
    };

    // Geometry is reported in physical pixels by `ComputedNode.size` and
    // `UiGlobalTransform.translation`. `ScrollPosition` is in logical px,
    // so convert via `inverse_scale_factor` before we write.
    let inv = target_node.inverse_scale_factor;
    let target_height = target_node.size().y;
    let target_centre_y = target_transform.translation.y;
    let target_top = target_centre_y - target_height * 0.5;
    let target_bottom = target_centre_y + target_height * 0.5;

    let container_height = container_node.size().y;
    let container_top = container_transform.translation.y - container_height * 0.5;

    // Convert button window-space Y to container-local Y. The container
    // is currently scrolled by `scroll.0.y` *logical* pixels — multiply
    // by physical-per-logical to compare with physical pixel extents.
    let scroll_phys = scroll.0.y / inv.max(f32::EPSILON);
    let viewport_top = container_top + scroll_phys;
    let viewport_bottom = viewport_top + container_height;

    // Layout may not have run yet (zero size on first frame) — no
    // sensible scroll target until the container has dimensions.
    if container_height <= 0.0 {
        return;
    }

    let pad_phys = FOCUS_SCROLL_PADDING / inv.max(f32::EPSILON);
    if target_top < viewport_top {
        // Button extends above the viewport — scroll up.
        let new_top = target_top - pad_phys;
        let delta = new_top - viewport_top;
        scroll.0.y = ((scroll_phys + delta) * inv).max(0.0);
    } else if target_bottom > viewport_bottom {
        // Button extends below the viewport — scroll down.
        let new_bottom = target_bottom + pad_phys;
        let delta = new_bottom - viewport_bottom;
        scroll.0.y = ((scroll_phys + delta) * inv).max(0.0);
    }
}

/// Scrolls the settings panel inner card in response to mouse-wheel events.
///
/// `offset_y` increases downward (0 = top of content). Scrolling down (ev.y < 0)
/// adds to the offset; scrolling up subtracts. Clamped to >= 0 so it never
/// scrolls past the top.
pub(super) fn scroll_settings_panel(
    mut scroll_evr: MessageReader<MouseWheel>,
    screen: Res<SettingsScreen>,
    mut scrollables: Query<&mut ScrollPosition, With<SettingsPanelScrollable>>,
) {
    if !screen.0 {
        scroll_evr.clear();
        return;
    }
    let delta_y: f32 = scroll_evr
        .read()
        .map(|ev| match ev.unit {
            MouseScrollUnit::Line => ev.y * 50.0,
            MouseScrollUnit::Pixel => ev.y,
        })
        .sum();
    if delta_y == 0.0 {
        return;
    }
    for mut sp in scrollables.iter_mut() {
        sp.0.y = (sp.0.y - delta_y).max(0.0);
    }
}

// ---------------------------------------------------------------------------
// Window geometry persistence
// ---------------------------------------------------------------------------
