//! Galaxy UI: parameter slider panel (left) and selected-system
//! info panel (right).
//!
//! Bevy 0.16 does not ship a native Slider widget, so each parameter is a
//! row of `Label  Value  [-] [+]` buttons. Each `+/-` button carries a
//! [`ParamButton`] marker identifying which field to mutate.
//!
//! Rows are grouped under section headers (Galaxy, Core, Disk, Top Beam,
//! Bottom Beam) for visual clarity now that the panel spans five conceptual
//! layers.
//!
//! ## Multi-disk UI
//!
//! Disk layers use a **tab selector** pattern: a row of numbered buttons
//! (1 2 3 …) picks which disk the parameter rows below edit. An "Add Disk"
//! button appears when below [`MAX_DISKS`]; a "Remove" button appears for
//! non-primary disks. This keeps the UI at constant height regardless of
//! how many disks are active.

use bevy::{input::mouse::MouseWheel, prelude::*, ui::ScrollPosition, window::PrimaryWindow};

use super::{generate_galaxy, GalaxySpawned};
use crate::gameplay::campaign::CampaignDraft;
use crate::gameplay::galaxy::params::*;
use crate::state::AppState;

// ── Markers ─────────────────────────────────────────────────────────────────

#[derive(Component)]
pub struct GalaxyControlPanel;

/// Marker for the scrollable inner container of the control panel.
#[derive(Component)]
pub struct GalaxyScrollContent;

#[derive(Component)]
pub struct GalaxyInfoPanel;

/// Identifies which parameter a `+/-` button mutates and which direction.
#[derive(Component, Clone, Copy)]
pub enum ParamButton {
    SeedDecr,
    SeedIncr,
    SizeDecr,
    SizeIncr,
    // Core
    CoreCountDecr,
    CoreCountIncr,
    CoreRadiusDecr,
    CoreRadiusIncr,
    // Disk — tab selection + add/remove
    DiskTab(usize),
    DiskAdd,
    DiskRemove,
    // Disk — per-field controls (target the currently-selected disk)
    DiskCountDecr,
    DiskCountIncr,
    DiskArmsDecr,
    DiskArmsIncr,
    DiskTwistDecr,
    DiskTwistIncr,
    DiskTiltDecr,
    DiskTiltIncr,
    DiskRotationDecr,
    DiskRotationIncr,
    // Top beam (+Y)
    BeamTopEnabled,
    BeamTopThicknessDecr,
    BeamTopThicknessIncr,
    BeamTopLengthDecr,
    BeamTopLengthIncr,
    BeamTopDensityDecr,
    BeamTopDensityIncr,
    BeamTopTaperDecr,
    BeamTopTaperIncr,
    BeamTopGravityDecr,
    BeamTopGravityIncr,
    // Bottom beam (-Y)
    BeamBottomEnabled,
    BeamBottomThicknessDecr,
    BeamBottomThicknessIncr,
    BeamBottomLengthDecr,
    BeamBottomLengthIncr,
    BeamBottomDensityDecr,
    BeamBottomDensityIncr,
    BeamBottomTaperDecr,
    BeamBottomTaperIncr,
    BeamBottomGravityDecr,
    BeamBottomGravityIncr,
    /// Randomize all params to random valid values within their bounds.
    RandomSettings,
    /// Randomize: bump seed by 1 (equivalent to the docs "Regenerate" button).
    Regenerate,
    /// Reset orbit camera to default orientation.
    CenterView,
    /// Accept the current galaxy and continue to character creation.
    CreateGalaxy,
}

/// Resource tracking which disk layer is currently selected in the tab bar.
/// Always clamped to a valid index when the panel is rebuilt.
#[derive(Resource, Debug, Clone, Copy)]
pub struct SelectedDisk(pub usize);

impl Default for SelectedDisk {
    fn default() -> Self {
        Self(0)
    }
}

/// Flag resource inserted when the disk count changes (add/remove). The
/// [`rebuild_scroll_content`] system reads and removes it, re-spawning
/// the scroll area with the correct number of tab buttons.
#[derive(Resource)]
pub(super) struct RebuildScrollContent;

// ── Styling constants ───────────────────────────────────────────────────────

const PANEL_BG: Color = Color::srgb(0.05, 0.07, 0.12);
const PANEL_BORDER: Color = Color::srgb(0.25, 0.40, 0.62);
const TEXT_BRIGHT: Color = Color::srgb(0.82, 0.90, 1.0);
const TEXT_DIM: Color = Color::srgb(0.55, 0.65, 0.82);
const SECTION_TEXT: Color = Color::srgb(0.42, 0.62, 0.86);
const BUTTON_BG: Color = Color::srgb(0.10, 0.14, 0.22);
const TAB_ACTIVE_BG: Color = Color::srgb(0.18, 0.28, 0.46);
const TAB_INACTIVE_BG: Color = Color::srgb(0.08, 0.11, 0.18);

const PANEL_WIDTH: f32 = 300.0;
const PANEL_PADDING: f32 = 14.0;
const TITLE_FONT_SIZE: f32 = 20.0;
const SECTION_FONT_SIZE: f32 = 12.0;
const LABEL_FONT_SIZE: f32 = 15.0;
const VALUE_FONT_SIZE: f32 = 15.0;
const BUTTON_FONT_SIZE: f32 = 16.0;
const HELP_FONT_SIZE: f32 = 12.0;
const TAB_FONT_SIZE: f32 = 14.0;

// ── Setup ───────────────────────────────────────────────────────────────────

pub fn setup_galaxy_ui(mut commands: Commands, params: Res<GalaxyParams>) {
    commands.init_resource::<SelectedDisk>();
    spawn_control_panel(&mut commands, &params);
    spawn_info_panel_empty(&mut commands);
}

fn spawn_control_panel(commands: &mut Commands, params: &GalaxyParams) {
    commands
        .spawn((
            Node {
                position_type: PositionType::Absolute,
                left: Val::Px(12.0),
                top: Val::Px(12.0),
                bottom: Val::Px(12.0),
                width: Val::Px(PANEL_WIDTH),
                flex_direction: FlexDirection::Column,
                border: UiRect::all(Val::Px(1.0)),
                ..default()
            },
            BackgroundColor(PANEL_BG),
            BorderColor(PANEL_BORDER),
            BorderRadius::all(Val::Px(8.0)),
            GalaxyControlPanel,
            GalaxySpawned,
        ))
        .with_children(|parent| {
            // Sticky title — pinned above the scroll area.
            parent.spawn((
                Text::new("Galaxy Parameters"),
                TextFont {
                    font_size: TITLE_FONT_SIZE,
                    ..default()
                },
                TextColor(TEXT_BRIGHT),
                Node {
                    padding: UiRect::px(PANEL_PADDING, PANEL_PADDING, PANEL_PADDING, 4.0),
                    ..default()
                },
            ));

            // Randomize button — pinned above the scroll area.
            parent
                .spawn((
                    Button,
                    Node {
                        height: Val::Px(32.0),
                        margin: UiRect::px(PANEL_PADDING, PANEL_PADDING, 4.0, PANEL_PADDING),
                        justify_content: JustifyContent::Center,
                        align_items: AlignItems::Center,
                        border: UiRect::all(Val::Px(1.0)),
                        ..default()
                    },
                    BackgroundColor(BUTTON_BG),
                    BorderColor(PANEL_BORDER),
                    BorderRadius::all(Val::Px(6.0)),
                    ParamButton::RandomSettings,
                ))
                .with_children(|btn| {
                    btn.spawn((
                        Text::new("Randomize Settings"),
                        TextFont {
                            font_size: BUTTON_FONT_SIZE,
                            ..default()
                        },
                        TextColor(TEXT_BRIGHT),
                    ));
                });

            parent
                .spawn((
                    Button,
                    Node {
                        height: Val::Px(36.0),
                        margin: UiRect::px(PANEL_PADDING, PANEL_PADDING, 4.0, PANEL_PADDING),
                        justify_content: JustifyContent::Center,
                        align_items: AlignItems::Center,
                        border: UiRect::all(Val::Px(1.0)),
                        ..default()
                    },
                    BackgroundColor(Color::srgb(0.10, 0.28, 0.22)),
                    BorderColor(Color::srgb(0.30, 0.72, 0.45)),
                    BorderRadius::all(Val::Px(6.0)),
                    ParamButton::CreateGalaxy,
                ))
                .with_children(|btn| {
                    btn.spawn((
                        Text::new("Create Galaxy"),
                        TextFont {
                            font_size: BUTTON_FONT_SIZE,
                            ..default()
                        },
                        TextColor(TEXT_BRIGHT),
                    ));
                });

            // Scrollable content area.
            parent
                .spawn((
                    Node {
                        flex_grow: 1.0,
                        flex_direction: FlexDirection::Column,
                        row_gap: Val::Px(6.0),
                        padding: UiRect::px(
                            PANEL_PADDING,
                            PANEL_PADDING,
                            PANEL_PADDING,
                            PANEL_PADDING,
                        ),
                        overflow: Overflow::scroll_y(),
                        ..default()
                    },
                    ScrollPosition::default(),
                    GalaxyScrollContent,
                ))
                .with_children(|scroll| {
                    spawn_scroll_contents_with_tabs(scroll, params, 0);
                });
        });
}

/// Build all rows inside the scroll area. This is the version used by
/// [`spawn_control_panel`] for the initial build. The full rebuild variant
/// that knows the current params / active tab is [`spawn_scroll_contents_with_tabs`].
fn spawn_scroll_contents(scroll: &mut ChildSpawnerCommands) {
    // Delegate to the tab-aware builder with default params context.
    // The initial build uses a single default disk.
    let default_params = GalaxyParams::default();
    spawn_scroll_contents_with_tabs(scroll, &default_params, 0);
}

/// Marker for the disk "Remove" button so we can toggle its visibility.
#[derive(Component)]
struct DiskRemoveButton;

/// Marker for disk tab buttons so we can restyle the active tab.
#[derive(Component)]
struct DiskTabButton(usize);

// ── Section / row helpers ───────────────────────────────────────────────────

/// Section header row — small dim label with a top margin to separate groups.
fn spawn_section(parent: &mut ChildSpawnerCommands, label: &str) {
    parent.spawn((
        Text::new(label.to_uppercase()),
        TextFont {
            font_size: SECTION_FONT_SIZE,
            ..default()
        },
        TextColor(SECTION_TEXT),
        Node {
            margin: UiRect::top(Val::Px(4.0)),
            ..default()
        },
    ));
}

/// Build a `[Label ........ value  [-] [+]]` row. The `value_key` is stored in
/// the marker as a label for the value text child; the actual value is read
/// from `GalaxyParams` at button-press time and updated by
/// `refresh_control_panel_values` each frame.
fn spawn_param_row(
    parent: &mut ChildSpawnerCommands,
    label: &str,
    value_key: &str,
    decr: ParamButton,
    incr: ParamButton,
) {
    parent
        .spawn(Node {
            width: Val::Percent(100.0),
            flex_direction: FlexDirection::Row,
            align_items: AlignItems::Center,
            column_gap: Val::Px(6.0),
            ..default()
        })
        .with_children(|row| {
            // Label — fixed width so values align.
            row.spawn((
                Text::new(label),
                TextFont {
                    font_size: LABEL_FONT_SIZE,
                    ..default()
                },
                TextColor(TEXT_DIM),
                Node {
                    width: Val::Px(100.0),
                    ..default()
                },
            ));

            // Value — flexible width, right-aligned textually by spacer below.
            row.spawn((
                Text::new("—"),
                TextFont {
                    font_size: VALUE_FONT_SIZE,
                    ..default()
                },
                TextColor(TEXT_BRIGHT),
                Node {
                    flex_grow: 1.0,
                    ..default()
                },
                ParamValue(value_key.to_string()),
            ));

            // [-] button.
            spawn_icon_button(row, "−", decr);
            // [+] button.
            spawn_icon_button(row, "+", incr);
        });
}

/// `[Label ........ value   [ø]]` — a single-button row for boolean toggles
/// (Enabled / Disabled). The button cycles the field on each press; the value
/// text displays "On"/"Off" via [`refresh_control_panel_values`].
fn spawn_toggle_row(
    parent: &mut ChildSpawnerCommands,
    label: &str,
    value_key: &str,
    toggle: ParamButton,
) {
    parent
        .spawn(Node {
            width: Val::Percent(100.0),
            flex_direction: FlexDirection::Row,
            align_items: AlignItems::Center,
            column_gap: Val::Px(6.0),
            ..default()
        })
        .with_children(|row| {
            row.spawn((
                Text::new(label),
                TextFont {
                    font_size: LABEL_FONT_SIZE,
                    ..default()
                },
                TextColor(TEXT_DIM),
                Node {
                    width: Val::Px(100.0),
                    ..default()
                },
            ));
            row.spawn((
                Text::new("—"),
                TextFont {
                    font_size: VALUE_FONT_SIZE,
                    ..default()
                },
                TextColor(TEXT_BRIGHT),
                Node {
                    flex_grow: 1.0,
                    ..default()
                },
                ParamValue(value_key.to_string()),
            ));
            // Wider button so the toggle label "ø" / "✓" reads clearly.
            parent_button(row, toggle, "ø", 34.0);
        });
}

/// Marker on the value Text node so `refresh_control_panel_values` can find
/// and update it. The `String` is the param field name (e.g. "seed", "twist").
#[derive(Component)]
pub(crate) struct ParamValue(pub(crate) String);

fn spawn_icon_button(parent: &mut ChildSpawnerCommands, label: &str, marker: ParamButton) {
    parent_button(parent, marker, label, 26.0);
}

fn parent_button(parent: &mut ChildSpawnerCommands, marker: ParamButton, label: &str, width: f32) {
    parent
        .spawn((
            Button,
            Node {
                width: Val::Px(width),
                height: Val::Px(26.0),
                justify_content: JustifyContent::Center,
                align_items: AlignItems::Center,
                border: UiRect::all(Val::Px(1.0)),
                ..default()
            },
            BackgroundColor(BUTTON_BG),
            BorderColor(PANEL_BORDER),
            BorderRadius::all(Val::Px(4.0)),
            marker,
        ))
        .with_children(|btn| {
            btn.spawn((
                Text::new(label),
                TextFont {
                    font_size: BUTTON_FONT_SIZE,
                    ..default()
                },
                TextColor(TEXT_BRIGHT),
            ));
        });
}

/// Spawn the info panel in its "no selection" state. The selection module
/// replaces its contents whenever [`super::SelectedStar`] changes.
fn spawn_info_panel_empty(commands: &mut Commands) {
    commands
        .spawn((
            Node {
                position_type: PositionType::Absolute,
                right: Val::Px(12.0),
                top: Val::Px(12.0),
                width: Val::Px(PANEL_WIDTH),
                padding: UiRect::all(Val::Px(PANEL_PADDING)),
                flex_direction: FlexDirection::Column,
                row_gap: Val::Px(4.0),
                border: UiRect::all(Val::Px(1.0)),
                ..default()
            },
            BackgroundColor(PANEL_BG),
            BorderColor(PANEL_BORDER),
            BorderRadius::all(Val::Px(8.0)),
            GalaxyInfoPanel,
            GalaxySpawned,
        ))
        .with_children(|parent| {
            parent.spawn((
                Text::new("Selected System"),
                TextFont {
                    font_size: TITLE_FONT_SIZE,
                    ..default()
                },
                TextColor(TEXT_BRIGHT),
                Node {
                    margin: UiRect::bottom(Val::Px(8.0)),
                    ..default()
                },
            ));
            parent.spawn((
                Text::new("Click a star to inspect."),
                TextFont {
                    font_size: LABEL_FONT_SIZE,
                    ..default()
                },
                TextColor(TEXT_DIM),
            ));
        });
}

// ── Button handler ──────────────────────────────────────────────────────────

pub fn param_button_handler(
    mut commands: Commands,
    mut params: ResMut<GalaxyParams>,
    mut selected_disk: ResMut<SelectedDisk>,
    mut next_state: ResMut<NextState<AppState>>,
    mut campaign: ResMut<CampaignDraft>,
    query: Query<(&Interaction, &ParamButton), Changed<Interaction>>,
) {
    for (interaction, button) in &query {
        let &Interaction::Pressed = interaction else {
            continue;
        };
        match button {
            // ── Galaxy ────────────────────────────────────────────────────
            ParamButton::SeedDecr => {
                params.seed = params.seed.wrapping_sub(SEED_STEP);
                params.bump_generation();
            }
            ParamButton::SeedIncr => {
                params.seed = params.seed.wrapping_add(SEED_STEP);
                params.bump_generation();
            }
            ParamButton::SizeDecr => {
                params.size = (params.size - SIZE_STEP).max(SIZE_MIN);
                params.bump_generation();
            }
            ParamButton::SizeIncr => {
                params.size = (params.size + SIZE_STEP).min(SIZE_MAX);
                params.bump_generation();
            }
            // ── Core ──────────────────────────────────────────────────────
            ParamButton::CoreCountDecr => {
                params.core.count = params
                    .core
                    .count
                    .saturating_sub(CORE_COUNT_STEP)
                    .max(CORE_COUNT_MIN);
                params.bump_generation();
            }
            ParamButton::CoreCountIncr => {
                params.core.count = (params.core.count + CORE_COUNT_STEP).min(CORE_COUNT_MAX);
                params.bump_generation();
            }
            ParamButton::CoreRadiusDecr => {
                params.core.radius = (params.core.radius - CORE_RADIUS_STEP).max(CORE_RADIUS_MIN);
                params.bump_generation();
            }
            ParamButton::CoreRadiusIncr => {
                params.core.radius = (params.core.radius + CORE_RADIUS_STEP).min(CORE_RADIUS_MAX);
                params.bump_generation();
            }
            // ── Disk tab management ───────────────────────────────────────
            ParamButton::DiskTab(n) => {
                selected_disk.0 = *n;
            }
            ParamButton::DiskAdd => {
                if params.disks.len() < MAX_DISKS {
                    params.disks.push(DiskParams::default());
                    selected_disk.0 = params.disks.len() - 1;
                    params.bump_generation();
                    commands.insert_resource(RebuildScrollContent);
                }
            }
            ParamButton::DiskRemove => {
                if params.disks.len() > 1 && selected_disk.0 < params.disks.len() {
                    params.disks.remove(selected_disk.0);
                    selected_disk.0 = (selected_disk.0).min(params.disks.len() - 1);
                    params.bump_generation();
                    commands.insert_resource(RebuildScrollContent);
                }
            }
            // ── Disk params (target selected_disk) ────────────────────────
            ParamButton::DiskCountDecr => {
                let disk = selected_disk_mut(&mut params, selected_disk.0);
                disk.count = disk
                    .count
                    .saturating_sub(DISK_COUNT_STEP)
                    .max(DISK_COUNT_MIN);
                params.bump_generation();
            }
            ParamButton::DiskCountIncr => {
                let disk = selected_disk_mut(&mut params, selected_disk.0);
                disk.count = (disk.count + DISK_COUNT_STEP).min(DISK_COUNT_MAX);
                params.bump_generation();
            }
            ParamButton::DiskArmsDecr => {
                let disk = selected_disk_mut(&mut params, selected_disk.0);
                disk.arms = disk.arms.saturating_sub(1).max(DISK_ARMS_MIN);
                params.bump_generation();
            }
            ParamButton::DiskArmsIncr => {
                let disk = selected_disk_mut(&mut params, selected_disk.0);
                disk.arms = disk.arms.saturating_add(1).min(DISK_ARMS_MAX);
                params.bump_generation();
            }
            ParamButton::DiskTwistDecr => {
                let disk = selected_disk_mut(&mut params, selected_disk.0);
                disk.twist = (disk.twist - DISK_TWIST_STEP).max(DISK_TWIST_MIN);
                params.bump_generation();
            }
            ParamButton::DiskTwistIncr => {
                let disk = selected_disk_mut(&mut params, selected_disk.0);
                disk.twist = (disk.twist + DISK_TWIST_STEP).min(DISK_TWIST_MAX);
                params.bump_generation();
            }
            ParamButton::DiskTiltDecr => {
                let disk = selected_disk_mut(&mut params, selected_disk.0);
                disk.tilt = (disk.tilt - DISK_TILT_STEP).max(DISK_TILT_MIN);
                params.bump_generation();
            }
            ParamButton::DiskTiltIncr => {
                let disk = selected_disk_mut(&mut params, selected_disk.0);
                disk.tilt = (disk.tilt + DISK_TILT_STEP).min(DISK_TILT_MAX);
                params.bump_generation();
            }
            ParamButton::DiskRotationDecr => {
                let disk = selected_disk_mut(&mut params, selected_disk.0);
                disk.rotation_offset =
                    (disk.rotation_offset - DISK_ROTATION_STEP).max(DISK_ROTATION_MIN);
                params.bump_generation();
            }
            ParamButton::DiskRotationIncr => {
                let disk = selected_disk_mut(&mut params, selected_disk.0);
                disk.rotation_offset =
                    (disk.rotation_offset + DISK_ROTATION_STEP).min(DISK_ROTATION_MAX);
                params.bump_generation();
            }

            // ── Top Beam (+Y) ─────────────────────────────────────────────
            ParamButton::BeamTopEnabled => {
                params.beam_top.enabled = !params.beam_top.enabled;
                params.bump_generation();
            }
            ParamButton::BeamTopThicknessDecr => {
                params.beam_top.thickness =
                    (params.beam_top.thickness - BEAM_THICKNESS_STEP).max(BEAM_THICKNESS_MIN);
                params.bump_generation();
            }
            ParamButton::BeamTopThicknessIncr => {
                params.beam_top.thickness =
                    (params.beam_top.thickness + BEAM_THICKNESS_STEP).min(BEAM_THICKNESS_MAX);
                params.bump_generation();
            }
            ParamButton::BeamTopLengthDecr => {
                params.beam_top.length =
                    (params.beam_top.length - BEAM_LENGTH_STEP).max(BEAM_LENGTH_MIN);
                params.bump_generation();
            }
            ParamButton::BeamTopLengthIncr => {
                params.beam_top.length =
                    (params.beam_top.length + BEAM_LENGTH_STEP).min(BEAM_LENGTH_MAX);
                params.bump_generation();
            }
            ParamButton::BeamTopDensityDecr => {
                params.beam_top.count = params
                    .beam_top
                    .count
                    .saturating_sub(BEAM_COUNT_STEP)
                    .max(BEAM_COUNT_MIN);
                params.bump_generation();
            }
            ParamButton::BeamTopDensityIncr => {
                params.beam_top.count =
                    (params.beam_top.count + BEAM_COUNT_STEP).min(BEAM_COUNT_MAX);
                params.bump_generation();
            }
            ParamButton::BeamTopTaperDecr => {
                params.beam_top.taper =
                    (params.beam_top.taper - BEAM_TAPER_STEP).max(BEAM_TAPER_MIN);
                params.bump_generation();
            }
            ParamButton::BeamTopTaperIncr => {
                params.beam_top.taper =
                    (params.beam_top.taper + BEAM_TAPER_STEP).min(BEAM_TAPER_MAX);
                params.bump_generation();
            }
            ParamButton::BeamTopGravityDecr => {
                params.beam_top.gravity =
                    (params.beam_top.gravity - BEAM_GRAVITY_STEP).max(BEAM_GRAVITY_MIN);
                params.bump_generation();
            }
            ParamButton::BeamTopGravityIncr => {
                params.beam_top.gravity =
                    (params.beam_top.gravity + BEAM_GRAVITY_STEP).min(BEAM_GRAVITY_MAX);
                params.bump_generation();
            }
            // ── Bottom Beam (-Y) ──────────────────────────────────────────
            ParamButton::BeamBottomEnabled => {
                params.beam_bottom.enabled = !params.beam_bottom.enabled;
                params.bump_generation();
            }
            ParamButton::BeamBottomThicknessDecr => {
                params.beam_bottom.thickness =
                    (params.beam_bottom.thickness - BEAM_THICKNESS_STEP).max(BEAM_THICKNESS_MIN);
                params.bump_generation();
            }
            ParamButton::BeamBottomThicknessIncr => {
                params.beam_bottom.thickness =
                    (params.beam_bottom.thickness + BEAM_THICKNESS_STEP).min(BEAM_THICKNESS_MAX);
                params.bump_generation();
            }
            ParamButton::BeamBottomLengthDecr => {
                params.beam_bottom.length =
                    (params.beam_bottom.length - BEAM_LENGTH_STEP).max(BEAM_LENGTH_MIN);
                params.bump_generation();
            }
            ParamButton::BeamBottomLengthIncr => {
                params.beam_bottom.length =
                    (params.beam_bottom.length + BEAM_LENGTH_STEP).min(BEAM_LENGTH_MAX);
                params.bump_generation();
            }
            ParamButton::BeamBottomDensityDecr => {
                params.beam_bottom.count = params
                    .beam_bottom
                    .count
                    .saturating_sub(BEAM_COUNT_STEP)
                    .max(BEAM_COUNT_MIN);
                params.bump_generation();
            }
            ParamButton::BeamBottomDensityIncr => {
                params.beam_bottom.count =
                    (params.beam_bottom.count + BEAM_COUNT_STEP).min(BEAM_COUNT_MAX);
                params.bump_generation();
            }
            ParamButton::BeamBottomTaperDecr => {
                params.beam_bottom.taper =
                    (params.beam_bottom.taper - BEAM_TAPER_STEP).max(BEAM_TAPER_MIN);
                params.bump_generation();
            }
            ParamButton::BeamBottomTaperIncr => {
                params.beam_bottom.taper =
                    (params.beam_bottom.taper + BEAM_TAPER_STEP).min(BEAM_TAPER_MAX);
                params.bump_generation();
            }
            ParamButton::BeamBottomGravityDecr => {
                params.beam_bottom.gravity =
                    (params.beam_bottom.gravity - BEAM_GRAVITY_STEP).max(BEAM_GRAVITY_MIN);
                params.bump_generation();
            }
            ParamButton::BeamBottomGravityIncr => {
                params.beam_bottom.gravity =
                    (params.beam_bottom.gravity + BEAM_GRAVITY_STEP).min(BEAM_GRAVITY_MAX);
                params.bump_generation();
            }
            ParamButton::RandomSettings => {
                use rand::Rng;
                let mut rng = rand::thread_rng();
                params.seed = rng.gen();
                params.size = rng.gen_range(SIZE_MIN..=SIZE_MAX);
                // Round size to step.
                params.size = (params.size / SIZE_STEP).round() * SIZE_STEP;
                params.core.count = rng.gen_range(CORE_COUNT_MIN..=CORE_COUNT_MAX);
                params.core.radius = rng.gen_range(CORE_RADIUS_MIN..=CORE_RADIUS_MAX);
                params.core.radius =
                    (params.core.radius / CORE_RADIUS_STEP).round() * CORE_RADIUS_STEP;
                // Randomize disks: 1–3 layers.
                let disk_count = rng.gen_range(1..=3.min(MAX_DISKS));
                params.disks.clear();
                for _ in 0..disk_count {
                    params.disks.push(randomize_disk(&mut rng));
                }
                selected_disk.0 = 0;
                randomize_beam(&mut params.beam_top, &mut rng);
                randomize_beam(&mut params.beam_bottom, &mut rng);
                params.bump_generation();
                commands.insert_resource(RebuildScrollContent);
            }
            ParamButton::Regenerate => params.reseed_and_bump(),
            // CenterView is handled by `reset_view_button_handler` (needs Commands).
            ParamButton::CenterView => {}
            ParamButton::CreateGalaxy => {
                let galaxy = generate_galaxy(&params);
                bevy::log::info!(
                    "Created galaxy: seed={}, generation={}, systems={}, connections={}",
                    params.seed,
                    params.generation,
                    galaxy.systems.len(),
                    galaxy.connections.len(),
                );
                campaign.save_galaxy(galaxy);
                next_state.set(AppState::CharacterCreation);
            }
        }
    }
}

/// Helper: get a mutable reference to the currently-selected disk.
/// Clamps the index to the valid range as a safety measure.
fn selected_disk_mut(params: &mut GalaxyParams, index: usize) -> &mut DiskParams {
    let idx = index.min(params.disks.len().saturating_sub(1));
    &mut params.disks[idx]
}

/// Handle the CenterView button: insert [`ResetOrbitCamera`] so
/// [`crate::camera::apply_orbit_reset`] can snap the orbit camera back to
/// its default orientation next frame. Kept in a separate system because it
/// needs `Commands` (not just `ResMut<GalaxyParams>`).
pub fn reset_view_button_handler(
    mut commands: Commands,
    reset_flag: Option<Res<crate::camera::ResetOrbitCamera>>,
    query: Query<(&Interaction, &ParamButton), Changed<Interaction>>,
) {
    for (interaction, button) in &query {
        if *interaction != Interaction::Pressed {
            continue;
        }
        if matches!(button, ParamButton::CenterView) && reset_flag.is_none() {
            commands.insert_resource(crate::camera::ResetOrbitCamera);
        }
    }
}

/// Refresh the displayed parameter values every frame so the UI stays in sync.
/// Bevy UI has no native data binding, so we manually patch the `Text` children
/// — cheap (~30 nodes) and avoids re-spawning the whole panel.
///
/// Disk-related values read from the currently-selected disk
/// ([`SelectedDisk`]) so the tab selector works correctly.
pub fn refresh_control_panel_values(
    params: Res<GalaxyParams>,
    selected_disk: Res<SelectedDisk>,
    mut values: Query<(&ParamValue, &mut Text)>,
) {
    // Resolve the selected disk, clamped to a valid index.
    let Some(disk) = params
        .disks
        .get(selected_disk.0.min(params.disks.len().saturating_sub(1)))
    else {
        return;
    };

    for (marker, mut text) in &mut values {
        let new = match marker.0.as_str() {
            // Galaxy
            "seed" => format!("{}", params.seed),
            "size" => format!("{:.0}", params.size),
            // Core
            "core_count" => format!("{}", params.core.count),
            "core_radius" => format!("{:.0}", params.core.radius),
            // Disk — reads from selected disk
            "disk_count" => format!("{}", disk.count),
            "disk_arms" => format!("{}", disk.arms),
            "disk_twist" => format!("{:.1}", disk.twist),
            "disk_tilt" => format!("{:.0}°", disk.tilt.to_degrees()),
            "disk_rotation" => format!("{:.0}°", disk.rotation_offset.to_degrees()),

            // Top beam
            "beam_top_enabled" => {
                format!("{}", if params.beam_top.enabled { "On" } else { "Off" })
            }
            "beam_top_thickness" => format!("{:.0}", params.beam_top.thickness),
            "beam_top_length" => format!("{:.0}", params.beam_top.length),
            "beam_top_density" => format!("{}", params.beam_top.count),
            "beam_top_taper" => format!("{:.1}", params.beam_top.taper),
            "beam_top_gravity" => format!("{:.1}", params.beam_top.gravity),
            // Bottom beam
            "beam_bottom_enabled" => {
                format!(
                    "{}",
                    if params.beam_bottom.enabled {
                        "On"
                    } else {
                        "Off"
                    }
                )
            }
            "beam_bottom_thickness" => format!("{:.0}", params.beam_bottom.thickness),
            "beam_bottom_length" => format!("{:.0}", params.beam_bottom.length),
            "beam_bottom_density" => format!("{}", params.beam_bottom.count),
            "beam_bottom_taper" => format!("{:.1}", params.beam_bottom.taper),
            "beam_bottom_gravity" => format!("{:.1}", params.beam_bottom.gravity),
            _ => continue,
        };
        if text.0 != new {
            text.0 = new;
        }
    }
}

// ── Scroll content rebuild ──────────────────────────────────────────────────

/// When the disk count changes (add/remove/randomize), the tab bar and the
/// remove button need to be rebuilt. This system detects the flag, despawns
/// the old scroll content entity, and re-spawns it with the correct tab count.
pub fn rebuild_scroll_content(
    mut commands: Commands,
    flag: Option<Res<RebuildScrollContent>>,
    scroll_content: Query<Entity, With<GalaxyScrollContent>>,
    control_panel: Query<Entity, With<GalaxyControlPanel>>,
    params: Res<GalaxyParams>,
    selected_disk: Res<SelectedDisk>,
) {
    let Some(_) = flag else {
        return;
    };
    // Consume the flag.
    commands.remove_resource::<RebuildScrollContent>();

    let Ok(scroll_entity) = scroll_content.single() else {
        return;
    };
    let Ok(panel_entity) = control_panel.single() else {
        return;
    };

    // Despawn the old scroll content entity (and all its children).
    commands.entity(scroll_entity).despawn();

    // Re-spawn a fresh scroll content as a child of the panel.
    commands.entity(panel_entity).with_children(|parent| {
        parent
            .spawn((
                Node {
                    flex_grow: 1.0,
                    flex_direction: FlexDirection::Column,
                    row_gap: Val::Px(6.0),
                    padding: UiRect::px(PANEL_PADDING, PANEL_PADDING, PANEL_PADDING, PANEL_PADDING),
                    overflow: Overflow::scroll_y(),
                    ..default()
                },
                ScrollPosition::default(),
                GalaxyScrollContent,
            ))
            .with_children(|scroll| {
                spawn_scroll_contents_with_tabs(scroll, &params, selected_disk.0);
            });
    });
}

/// Full scroll content builder that includes the actual tab buttons for the
/// current disk count. This is the authoritative spawn function — the simpler
/// [`spawn_scroll_contents`] delegates to this when we need dynamic tabs.
fn spawn_scroll_contents_with_tabs(
    scroll: &mut ChildSpawnerCommands,
    params: &GalaxyParams,
    active_disk: usize,
) {
    // ── Galaxy (seed + size) ────────────────────────────────────────
    spawn_section(scroll, "Galaxy");
    spawn_param_row(
        scroll,
        "Seed",
        "seed",
        ParamButton::SeedDecr,
        ParamButton::SeedIncr,
    );
    spawn_param_row(
        scroll,
        "Size",
        "size",
        ParamButton::SizeDecr,
        ParamButton::SizeIncr,
    );

    // ── Core ────────────────────────────────────────────────────────
    spawn_section(scroll, "Core");
    spawn_param_row(
        scroll,
        "Core Count",
        "core_count",
        ParamButton::CoreCountDecr,
        ParamButton::CoreCountIncr,
    );
    spawn_param_row(
        scroll,
        "Core Radius",
        "core_radius",
        ParamButton::CoreRadiusDecr,
        ParamButton::CoreRadiusIncr,
    );

    // ── Disk (tab bar + shared parameter rows) ──────────────────────
    spawn_section(scroll, "Disk");

    // Tab bar: [1] [2] ... [+]
    let tab_bar_active = active_disk.min(params.disks.len().saturating_sub(1));
    scroll
        .spawn(Node {
            width: Val::Percent(100.0),
            flex_direction: FlexDirection::Row,
            column_gap: Val::Px(4.0),
            align_items: AlignItems::Center,
            ..default()
        })
        .with_children(|row| {
            for i in 0..params.disks.len() {
                let is_active = i == tab_bar_active;
                let bg = if is_active {
                    TAB_ACTIVE_BG
                } else {
                    TAB_INACTIVE_BG
                };
                let border = if is_active {
                    Color::srgb(0.40, 0.60, 0.90)
                } else {
                    PANEL_BORDER
                };
                let text_color = if is_active { TEXT_BRIGHT } else { TEXT_DIM };
                row.spawn((
                    Button,
                    Node {
                        width: Val::Px(30.0),
                        height: Val::Px(24.0),
                        justify_content: JustifyContent::Center,
                        align_items: AlignItems::Center,
                        border: UiRect::all(Val::Px(1.0)),
                        ..default()
                    },
                    BackgroundColor(bg),
                    BorderColor(border),
                    BorderRadius::all(Val::Px(4.0)),
                    ParamButton::DiskTab(i),
                    DiskTabButton(i),
                ))
                .with_children(|btn| {
                    btn.spawn((
                        Text::new(format!("{}", i + 1)),
                        TextFont {
                            font_size: TAB_FONT_SIZE,
                            ..default()
                        },
                        TextColor(text_color),
                    ));
                });
            }

            // [+ Add Disk] button — only if below max.
            if params.disks.len() < MAX_DISKS {
                row.spawn((
                    Button,
                    Node {
                        width: Val::Px(26.0),
                        height: Val::Px(24.0),
                        justify_content: JustifyContent::Center,
                        align_items: AlignItems::Center,
                        border: UiRect::all(Val::Px(1.0)),
                        ..default()
                    },
                    BackgroundColor(BUTTON_BG),
                    BorderColor(PANEL_BORDER),
                    BorderRadius::all(Val::Px(4.0)),
                    ParamButton::DiskAdd,
                ))
                .with_children(|btn| {
                    btn.spawn((
                        Text::new("+"),
                        TextFont {
                            font_size: TAB_FONT_SIZE,
                            ..default()
                        },
                        TextColor(TEXT_DIM),
                    ));
                });
            }
        });

    // Shared disk parameter rows.
    spawn_param_row(
        scroll,
        "Disk Count",
        "disk_count",
        ParamButton::DiskCountDecr,
        ParamButton::DiskCountIncr,
    );
    spawn_param_row(
        scroll,
        "Disk Arms",
        "disk_arms",
        ParamButton::DiskArmsDecr,
        ParamButton::DiskArmsIncr,
    );
    spawn_param_row(
        scroll,
        "Disk Twist",
        "disk_twist",
        ParamButton::DiskTwistDecr,
        ParamButton::DiskTwistIncr,
    );
    spawn_param_row(
        scroll,
        "Tilt",
        "disk_tilt",
        ParamButton::DiskTiltDecr,
        ParamButton::DiskTiltIncr,
    );
    spawn_param_row(
        scroll,
        "Rotation",
        "disk_rotation",
        ParamButton::DiskRotationDecr,
        ParamButton::DiskRotationIncr,
    );
    // Remove button — only show if more than one disk.
    if params.disks.len() > 1 {
        scroll
            .spawn((
                Button,
                Node {
                    width: Val::Percent(100.0),
                    height: Val::Px(24.0),
                    justify_content: JustifyContent::Center,
                    align_items: AlignItems::Center,
                    border: UiRect::all(Val::Px(1.0)),
                    ..default()
                },
                BackgroundColor(Color::srgb(0.25, 0.10, 0.10)),
                BorderColor(Color::srgb(0.50, 0.20, 0.20)),
                BorderRadius::all(Val::Px(4.0)),
                ParamButton::DiskRemove,
                DiskRemoveButton,
            ))
            .with_children(|btn| {
                btn.spawn((
                    Text::new("Remove This Disk"),
                    TextFont {
                        font_size: BUTTON_FONT_SIZE,
                        ..default()
                    },
                    TextColor(Color::srgb(0.90, 0.60, 0.60)),
                ));
            });
    }

    // ── Top Beam (+Y) ───────────────────────────────────────────────
    spawn_section(scroll, "Top Beam (+Y)");
    spawn_toggle_row(
        scroll,
        "Enabled",
        "beam_top_enabled",
        ParamButton::BeamTopEnabled,
    );
    spawn_param_row(
        scroll,
        "Thickness",
        "beam_top_thickness",
        ParamButton::BeamTopThicknessDecr,
        ParamButton::BeamTopThicknessIncr,
    );
    spawn_param_row(
        scroll,
        "Length",
        "beam_top_length",
        ParamButton::BeamTopLengthDecr,
        ParamButton::BeamTopLengthIncr,
    );
    spawn_param_row(
        scroll,
        "Density",
        "beam_top_density",
        ParamButton::BeamTopDensityDecr,
        ParamButton::BeamTopDensityIncr,
    );
    spawn_param_row(
        scroll,
        "Taper",
        "beam_top_taper",
        ParamButton::BeamTopTaperDecr,
        ParamButton::BeamTopTaperIncr,
    );
    spawn_param_row(
        scroll,
        "Gravity",
        "beam_top_gravity",
        ParamButton::BeamTopGravityDecr,
        ParamButton::BeamTopGravityIncr,
    );

    // ── Bottom Beam (-Y) ────────────────────────────────────────────
    spawn_section(scroll, "Bottom Beam (-Y)");
    spawn_toggle_row(
        scroll,
        "Enabled",
        "beam_bottom_enabled",
        ParamButton::BeamBottomEnabled,
    );
    spawn_param_row(
        scroll,
        "Thickness",
        "beam_bottom_thickness",
        ParamButton::BeamBottomThicknessDecr,
        ParamButton::BeamBottomThicknessIncr,
    );
    spawn_param_row(
        scroll,
        "Length",
        "beam_bottom_length",
        ParamButton::BeamBottomLengthDecr,
        ParamButton::BeamBottomLengthIncr,
    );
    spawn_param_row(
        scroll,
        "Density",
        "beam_bottom_density",
        ParamButton::BeamBottomDensityDecr,
        ParamButton::BeamBottomDensityIncr,
    );
    spawn_param_row(
        scroll,
        "Taper",
        "beam_bottom_taper",
        ParamButton::BeamBottomTaperDecr,
        ParamButton::BeamBottomTaperIncr,
    );
    spawn_param_row(
        scroll,
        "Gravity",
        "beam_bottom_gravity",
        ParamButton::BeamBottomGravityDecr,
        ParamButton::BeamBottomGravityIncr,
    );

    // ── Action buttons ──────────────────────────────────────────────
    scroll
        .spawn((
            Button,
            Node {
                width: Val::Percent(100.0),
                height: Val::Px(32.0),
                margin: UiRect::top(Val::Px(8.0)),
                justify_content: JustifyContent::Center,
                align_items: AlignItems::Center,
                border: UiRect::all(Val::Px(1.0)),
                ..default()
            },
            BackgroundColor(BUTTON_BG),
            BorderColor(PANEL_BORDER),
            BorderRadius::all(Val::Px(6.0)),
            ParamButton::Regenerate,
        ))
        .with_children(|btn| {
            btn.spawn((
                Text::new("Regenerate"),
                TextFont {
                    font_size: BUTTON_FONT_SIZE,
                    ..default()
                },
                TextColor(TEXT_BRIGHT),
            ));
        });

    scroll
        .spawn((
            Button,
            Node {
                width: Val::Percent(100.0),
                height: Val::Px(32.0),
                margin: UiRect::top(Val::Px(4.0)),
                justify_content: JustifyContent::Center,
                align_items: AlignItems::Center,
                border: UiRect::all(Val::Px(1.0)),
                ..default()
            },
            BackgroundColor(BUTTON_BG),
            BorderColor(PANEL_BORDER),
            BorderRadius::all(Val::Px(6.0)),
            ParamButton::CenterView,
        ))
        .with_children(|btn| {
            btn.spawn((
                Text::new("Center View"),
                TextFont {
                    font_size: BUTTON_FONT_SIZE,
                    ..default()
                },
                TextColor(TEXT_BRIGHT),
            ));
        });

    scroll.spawn((
        Text::new("Drag to orbit (360°) • Scroll to zoom • Click a star • Esc to return"),
        TextFont {
            font_size: HELP_FONT_SIZE,
            ..default()
        },
        TextColor(TEXT_DIM),
        Node {
            margin: UiRect::top(Val::Px(10.0)),
            ..default()
        },
    ));
}

// ── Helpers ─────────────────────────────────────────────────────────────────

/// Randomize all fields of a [`DiskParams`] to random valid values within
/// their respective bounds, rounded to the UI step. Biased toward
/// spiral-friendly combinations: arms are weighted toward 2–4, twist is
/// floored at `arms × 4.0` radians (ensuring at least ~0.6 turns per arm),
/// and outer radius defaults to "auto" most of the time so disks fill the
/// galaxy.
fn randomize_disk(rng: &mut impl rand::Rng) -> DiskParams {
    let mut disk = DiskParams::default();
    disk.count = rng.gen_range(DISK_COUNT_MIN..=DISK_COUNT_MAX);
    disk.count = (disk.count / DISK_COUNT_STEP) * DISK_COUNT_STEP;

    // Weight arms toward 2–4 for clear spiral structure. 1 and 5–6 are
    // allowed but less likely.
    disk.arms = match rng.gen_range(0..10) {
        0 => 1,
        1..=3 => 2,
        4..=6 => 3,
        7..=8 => 4,
        9 => rng.gen_range(5..=DISK_ARMS_MAX),
        _ => DISK_ARMS_MIN,
    };

    // Floor twist at arms × 4.0 so multi-arm disks always wind enough to
    // show distinct spiral structure. The floor ensures at least ~0.6 turns
    // between adjacent arms.
    let twist_floor = disk.arms as f32 * 4.0;
    let effective_min = DISK_TWIST_MIN.max(twist_floor).min(DISK_TWIST_MAX);
    disk.twist = rng.gen_range(effective_min..=DISK_TWIST_MAX);
    disk.twist = (disk.twist / DISK_TWIST_STEP).round() * DISK_TWIST_STEP;

    disk.tilt = rng.gen_range(DISK_TILT_MIN..=DISK_TILT_MAX);
    disk.tilt = (disk.tilt / DISK_TILT_STEP).round() * DISK_TILT_STEP;
    disk.rotation_offset = rng.gen_range(DISK_ROTATION_MIN..=DISK_ROTATION_MAX);
    disk.rotation_offset = (disk.rotation_offset / DISK_ROTATION_STEP).round() * DISK_ROTATION_STEP;
    disk
}

/// Randomize all fields of a [`BeamParams`] to random valid values within
/// their respective bounds, rounded to the UI step.
fn randomize_beam(beam: &mut BeamParams, rng: &mut impl rand::Rng) {
    beam.enabled = rng.gen();
    beam.count = rng.gen_range(BEAM_COUNT_MIN..=BEAM_COUNT_MAX);
    beam.count = (beam.count / BEAM_COUNT_STEP) * BEAM_COUNT_STEP;
    beam.thickness = rng.gen_range(BEAM_THICKNESS_MIN..=BEAM_THICKNESS_MAX);
    beam.thickness = (beam.thickness / BEAM_THICKNESS_STEP).round() * BEAM_THICKNESS_STEP;
    beam.length = rng.gen_range(BEAM_LENGTH_MIN..=BEAM_LENGTH_MAX);
    beam.length = (beam.length / BEAM_LENGTH_STEP).round() * BEAM_LENGTH_STEP;
    beam.taper = rng.gen_range(BEAM_TAPER_MIN..=BEAM_TAPER_MAX);
    beam.taper = (beam.taper / BEAM_TAPER_STEP).round() * BEAM_TAPER_STEP;
    beam.gravity = rng.gen_range(BEAM_GRAVITY_MIN..=BEAM_GRAVITY_MAX);
    beam.gravity = (beam.gravity / BEAM_GRAVITY_STEP).round() * BEAM_GRAVITY_STEP;
}

/// Route mouse-wheel events to the control panel's scroll container when the
/// cursor is over the panel. Bevy 0.16 does not wire `MouseWheel` to
/// `ScrollPosition` automatically — this system bridges the gap.
pub fn scroll_control_panel(
    mut scroll_events: EventReader<MouseWheel>,
    primary_window: Query<&Window, With<PrimaryWindow>>,
    panel_nodes: Query<(&ComputedNode, &GlobalTransform), With<GalaxyControlPanel>>,
    mut scroll_content: Query<&mut ScrollPosition, With<GalaxyScrollContent>>,
) {
    let Ok(window) = primary_window.single() else {
        scroll_events.clear();
        return;
    };

    let Some(cursor_pos) = window.cursor_position() else {
        scroll_events.clear();
        return;
    };

    // Check if cursor is within any control panel node's rect.
    let over_panel = panel_nodes.iter().any(|(node, transform)| {
        let pos = transform.translation().truncate();
        let size = node.size();
        let min = pos - size * 0.5;
        let max = pos + size * 0.5;
        cursor_pos.x >= min.x
            && cursor_pos.x <= max.x
            && cursor_pos.y >= min.y
            && cursor_pos.y <= max.y
    });

    if !over_panel {
        return;
    }

    let Ok(mut scroll_pos) = scroll_content.single_mut() else {
        return;
    };

    for event in scroll_events.read() {
        scroll_pos.offset_y -= event.y * 20.0;
    }
}
