//! Galaxy inspection scene.
//!
//! Procedural spiral galaxy viewer with editable parameters. The 3D scene is
//! regenerated whenever [`GalaxyParams`] changes (via the `generation` counter);
//! click-to-select is handled in [`super::selection`]; the slider/info panels
//! live in [`super::ui`].

mod axes;
mod contents;
mod orbits;
mod params;
mod poi;
mod selection;
mod ui;

pub use poi::*;

use bevy::prelude::*;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};

use crate::camera::apply_orbit_reset;
use crate::state::AppState;

pub use contents::{SystemContents, SystemContext, SystemSummary};
pub use params::{GalaxyParams, SelectedStar};
use params::{CORE_COUNT, NEAREST_NEIGHBOR_CONNECTIONS, SPACING_ATTEMPTS};

pub struct GalaxyPlugin;

impl Plugin for GalaxyPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<GalaxyParams>()
            .init_resource::<SelectedStar>()
            .add_systems(
                OnEnter(AppState::Galaxy),
                (setup_galaxy_scene, ui::setup_galaxy_ui),
            )
            .add_systems(
                OnExit(AppState::Galaxy),
                (despawn_galaxy, reset_selection),
            )
            .add_systems(
                Update,
                (
                    escape_to_main_menu,
                    ui::param_button_handler,
                    ui::refresh_control_panel_values,
                    ui::reset_view_button_handler,
                    regenerate_galaxy_on_param_change,
                    selection::select_star_on_click,
                    selection::animate_selected_star,
                    selection::refresh_info_panel,
                    apply_orbit_reset,
                    orbits::advance_orbital_paths,
                )
                    .chain()
                    .run_if(in_state(AppState::Galaxy)),
            );
    }
}

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

/// Tag for *anything* spawned during galaxy creation so it can be cleanly
/// despawned on state exit. Applies to both 3D scene roots and UI panel roots.
#[derive(Component)]
pub struct GalaxySpawned;

/// Tag for the 3D scene root only — despawned on regeneration, so we can
/// rebuild the galaxy without disturbing the UI panels.
#[derive(Component)]
pub struct GalaxyScene;

/// A star system in the procedural galaxy. Acts as a parent entity for the
/// system's [`Star`] (visual marker) and all POI children (planets, belts,
/// stations, anomalies, stargates, gas clouds).
#[derive(Component, Debug)]
pub struct StarSystem {
    pub id: String,
    pub name: String,
    pub faction: &'static str,
    pub security: f32,
    /// Cached POI count for quick display in the info panel without walking
    /// the child hierarchy. Updated whenever [`contents::SystemContents`] is
    /// regenerated.
    pub poi_count: usize,
}

/// The anchor celestial of a star system — its primary star. Distinct from
/// POIs (which are destinations within a system): a star is the body the
/// system is named after and the parent that planets/moons/belts orbit.
///
/// Requires components that describe its physical nature; mass-locks nearby
/// warp drives, emits light (used to compute habitable zones for orbiting
/// planets).
#[derive(Component, Debug, Clone, Copy, Reflect)]
#[reflect(Component)]
#[require(Massive, Luminosity, MassLock, BoundingVolume, Identifiable)]
pub struct Star;

// POI components, markers, and events live in [`poi`] (re-exported as `pub use`).

// ── Faction palette (matches docs GalaxyScene) ──────────────────────────────

const FACTIONS: &[(&str, [f32; 3])] = &[
    ("Concord", [0.13, 0.83, 0.93]),  // cyan — core faction
    ("Amarr", [0.96, 0.62, 0.04]),    // amber
    ("Minmatar", [0.94, 0.27, 0.27]), // red
    ("Gallente", [0.66, 0.33, 0.97]), // purple
    ("Caldari", [0.22, 0.74, 0.97]),  // blue
];

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

fn setup_galaxy_scene(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    params: Res<GalaxyParams>,
) {
    let (systems, contents, connections) = generate_galaxy(&params);
    spawn_galaxy_scene(
        &mut commands,
        &mut meshes,
        &mut materials,
        &systems,
        &contents,
        &connections,
    );
}

/// Generate the full galaxy layout (pure data — no Bevy types).
///
/// Faithful port of the TS reference in
/// `apps/docs/src/prototypes/r3f/galaxy/GalaxyScene.tsx::generateGalaxy`,
/// extended with per-system POI generation (planets, belts, stations,
/// anomalies, gas clouds, stargates).
///
/// Returns `(systems, contents_per_system, connections)`. The contents are
/// returned in parallel with the systems (same index) so the spawner can
/// attach them as children of each [`StarSystem`] entity.
fn generate_galaxy(
    params: &GalaxyParams,
) -> (
    Vec<GeneratedSystem>,
    Vec<SystemContents>,
    Vec<(usize, usize)>,
) {
    let mut rng = StdRng::seed_from_u64(params.seed);
    let systems = generate_system_positions(params, &mut rng);
    let connections = build_connections(&systems);

    // Per-system contents (planets, belts, stations, anomalies, gas clouds).
    let mut contents: Vec<SystemContents> = systems
        .iter()
        .map(|sys| {
            let ctx = SystemContext {
                id: &sys.id,
                name: &sys.name,
                faction: sys.faction,
                security: sys.security,
                is_core: sys.is_core,
            };
            contents::generate_system_contents(&mut rng, &ctx)
        })
        .collect();

    // Stargates — paired across each connection.
    let summaries: Vec<SystemSummary> = systems
        .iter()
        .map(|sys| SystemSummary {
            id: sys.id.clone(),
            name: sys.name.clone(),
        })
        .collect();
    contents::generate_stargates(&summaries, &mut contents, &connections);

    (systems, contents, connections)
}

/// Position-only galaxy generation. Pure faithful port of the TS reference
/// in `apps/docs/src/prototypes/r3f/galaxy/GalaxyScene.tsx::generateGalaxy`.
/// Split out so [`generate_galaxy`] can compose it with POI generation.
fn generate_system_positions(params: &GalaxyParams, rng: &mut StdRng) -> Vec<GeneratedSystem> {
    let mut systems: Vec<GeneratedSystem> = Vec::with_capacity(params.count);

    // Spacing scales with density: smaller for tight cores, larger spread overall.
    let base_spacing = (params.size / (params.count as f32).sqrt() * 1.25).clamp(9.0, 24.0);
    let horizontal_arms = params.arms.max(1);
    let vertical_arms = params.vertical_arms;
    let total_arm_slots = horizontal_arms + vertical_arms;

    for index in 0..params.count {
        let core = index < CORE_COUNT;
        let arm_slot = (index as u32) % total_arm_slots.max(1);
        let vertical = !core && arm_slot >= horizontal_arms;
        let arm = if vertical {
            arm_slot - horizontal_arms
        } else {
            arm_slot
        };
        let faction_index = if core {
            0 // Concord
        } else {
            ((arm_slot as usize) % (FACTIONS.len() - 1)) + 1
        };
        let (faction, color) = FACTIONS[faction_index];

        let mut position = Vec3::ZERO;
        let mut final_radius = 0.0f32;
        for attempt in 0..SPACING_ATTEMPTS {
            // `pow(0.62)` produces a density bias toward the core.
            let r = if core {
                8.0 + rng.gen::<f32>() * 30.0
            } else {
                rng.gen::<f32>().powf(0.62) * params.size
            };
            let arm_count = if vertical {
                vertical_arms.max(1)
            } else {
                horizontal_arms
            };
            let arm_twist = if vertical {
                params.vertical_twist
            } else {
                params.twist
            };
            let angle = if core {
                rng.gen::<f32>() * std::f32::consts::TAU
            } else {
                std::f32::consts::TAU * arm as f32 / arm_count as f32
                    + (r / params.size) * arm_twist
                    + (rng.gen::<f32>() - 0.5) * 0.72
            };
            let (x, y, z) = if vertical {
                // Vertical arms: stars orbit in a vertical plane rotated around the Y axis.
                let plane_angle = std::f32::consts::TAU * arm as f32 / vertical_arms.max(1) as f32;
                let arm_sweep = angle.cos() * r;
                let vx = plane_angle.cos() * arm_sweep;
                let vz = plane_angle.sin() * arm_sweep;
                let vy = angle.sin() * r * 0.72 + (rng.gen::<f32>() - 0.5) * 12.0;
                (vx, vy, vz)
            } else {
                (
                    angle.cos() * r,
                    (rng.gen::<f32>() - 0.5) * 20.0,
                    angle.sin() * r,
                )
            };
            let candidate = Vec3::new(x, y, z);
            final_radius = r;

            // Spacing relaxation: shrink required distance after many failed attempts.
            let relaxed = if attempt > 120 {
                base_spacing * 0.68
            } else if attempt > 60 {
                base_spacing * 0.82
            } else {
                base_spacing
            };
            let local_spacing = if core { relaxed * 0.9 } else { relaxed };
            let clear = systems
                .iter()
                .all(|s| s.position.distance(candidate) >= local_spacing);
            if clear {
                position = candidate;
                break;
            }
        }

        let security = ((1.0 - (final_radius / params.size) * 2.0) * 100.0).round() / 100.0;
        let name = if core {
            format!("COR-{}", 100 + index)
        } else {
            format!("{}-{}", &faction[..3].to_uppercase(), 100 + index)
        };

        systems.push(GeneratedSystem {
            id: format!("g-{index}"),
            name,
            position,
            faction,
            faction_index,
            color,
            security,
            is_core: core,
        });
    }

    systems
}

struct GeneratedSystem {
    id: String,
    name: String,
    position: Vec3,
    faction: &'static str,
    faction_index: usize,
    /// Per-system tint; currently the renderer uses per-faction materials indexed
    /// by `faction_index`, but this is kept for future per-system variation.
    color: [f32; 3],
    security: f32,
    /// True for the [`CORE_COUNT`] Concord systems clustered near the origin.
    /// Used by [`contents::generate_system_contents`] to bias planet / station
    /// counts toward core systems.
    is_core: bool,
}

fn spawn_galaxy_scene(
    commands: &mut Commands,
    meshes: &mut Assets<Mesh>,
    materials: &mut Assets<StandardMaterial>,
    systems: &[GeneratedSystem],
    contents: &[SystemContents],
    connections: &[(usize, usize)],
) {
    debug_assert_eq!(
        systems.len(),
        contents.len(),
        "systems and contents must be parallel arrays of the same length"
    );

    // Shared star visuals (Bevy 0.16 required-components model — no bundle needed).
    // Unit spheres, scaled per-faction at the spawn site.
    let star_mesh = meshes.add(Sphere::new(1.0).mesh().ico(3).unwrap());
    let core_star_mesh = meshes.add(Sphere::new(1.0).mesh().ico(4).unwrap());

    let faction_materials: Vec<Handle<StandardMaterial>> = FACTIONS
        .iter()
        .map(|(_, rgb)| {
            materials.add(StandardMaterial {
                base_color: Color::srgb(rgb[0], rgb[1], rgb[2]),
                emissive: LinearRgba::new(rgb[0] * 1.6, rgb[1] * 1.6, rgb[2] * 1.6, 1.0),
                unlit: true,
                ..default()
            })
        })
        .collect();
    let connection_material = materials.add(StandardMaterial {
        base_color: Color::srgb(0.11, 0.16, 0.25),
        emissive: LinearRgba::new(0.08, 0.13, 0.22, 1.0),
        unlit: true,
        ..default()
    });

    // Cached POI meshes/materials (one per type — entities scale via Transform).
    let content_assets = contents::ContentAssets::new(meshes, materials);

    // Parent group so all galaxy contents despawn together.
    commands
        .spawn((Transform::default(), GalaxyScene, GalaxySpawned))
        .with_children(|parent| {
            // XYZ reference axes through the origin.
            axes::spawn_axes(parent, meshes, materials);

            // Star systems — each is a parent entity that owns its Star + POI children.
            for (sys, sys_contents) in systems.iter().zip(contents.iter()) {
                let is_core = sys.faction_index == 0 && sys.is_core;
                let (mesh, material, scale) = if is_core {
                    // Visual differentiation for the 7 Concord core systems.
                    (core_star_mesh.clone(), faction_materials[0].clone(), 1.1)
                } else {
                    (
                        star_mesh.clone(),
                        faction_materials[sys.faction_index].clone(),
                        1.0,
                    )
                };

                let poi_count = sys_contents.total();

                parent
                    .spawn((
                        Transform::from_translation(sys.position),
                        StarSystem {
                            id: sys.id.clone(),
                            name: sys.name.clone(),
                            faction: sys.faction,
                            security: sys.security,
                            poi_count,
                        },
                    ))
                    .with_children(|sys_parent| {
                        // The anchor celestial — system's primary star.
                        let star_entity = sys_parent
                            .spawn((
                                Star,
                                Massive {
                                    mass: if sys.is_core { 10000.0 } else { 5000.0 },
                                },
                                Luminosity {
                                    value: if sys.is_core { 100.0 } else { 50.0 },
                                },
                                MassLock { radius: 5.0 },
                                BoundingVolume { radius: 1.5 },
                                Identifiable {
                                    id: format!("{}-star", sys.id),
                                    display_name: sys.name.clone(),
                                    classification: Classification::Celestial,
                                },
                                Mesh3d(mesh),
                                MeshMaterial3d(material),
                                Transform::default().with_scale(Vec3::splat(scale)),
                            ))
                            .id();

                        // POI children (planets, belts, stations, anomalies, stargates, gas).
                        let ctx = SystemContext {
                            id: &sys.id,
                            name: &sys.name,
                            faction: sys.faction,
                            security: sys.security,
                            is_core: sys.is_core,
                        };
                        contents::spawn_system_contents(
                            sys_parent,
                            &ctx,
                            sys_contents,
                            star_entity,
                            &content_assets,
                        );
                    });
            }

            // Connections rendered as thin cylinders between linked systems.
            for (a, b) in connections {
                let from = systems[*a].position;
                let to = systems[*b].position;
                let delta = to - from;
                let length = delta.length();
                if length < 0.01 {
                    continue;
                }
                let midpoint = (from + to) * 0.5;
                let direction = delta / length;
                // Cylinder default axis is +Y; rotate so +Y aligns with the link direction.
                let rotation = Quat::from_rotation_arc(Vec3::Y, direction);
                parent.spawn((
                    Mesh3d(meshes.add(Cylinder::new(0.15, length).mesh())),
                    MeshMaterial3d(connection_material.clone()),
                    Transform::from_translation(midpoint).with_rotation(rotation),
                ));
            }
        });
}

fn build_connections(systems: &[GeneratedSystem]) -> Vec<(usize, usize)> {
    let mut connections: Vec<(usize, usize)> = Vec::new();
    for (i, system) in systems.iter().enumerate() {
        let mut distances: Vec<(usize, f32)> = systems
            .iter()
            .enumerate()
            .filter(|(j, _)| *j != i)
            .map(|(j, other)| (j, system.position.distance(other.position)))
            .collect();
        distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
        for &(j, _) in distances.iter().take(NEAREST_NEIGHBOR_CONNECTIONS) {
            let key = if i < j { (i, j) } else { (j, i) };
            if !connections.contains(&key) {
                connections.push(key);
            }
        }
    }
    connections
}

// ── Lifecycle systems ───────────────────────────────────────────────────────

fn despawn_galaxy(
    mut commands: Commands,
    query: Query<Entity, With<GalaxySpawned>>,
) {
    for entity in &query {
        // Bevy 0.16: despawn() is recursive by default.
        commands.entity(entity).despawn();
    }
}

fn reset_selection(mut selected: ResMut<SelectedStar>) {
    selected.0 = None;
}

fn escape_to_main_menu(
    keys: Res<ButtonInput<KeyCode>>,
    mut next_state: ResMut<NextState<AppState>>,
) {
    if keys.just_pressed(KeyCode::Escape) {
        next_state.set(AppState::MainMenu);
    }
}

// ── Regeneration ────────────────────────────────────────────────────────────

/// Detects [`GalaxyParams`] generation changes and rebuilds the 3D scene.
///
/// On first run, the OnEnter handler has already spawned the initial scene —
/// we just record the generation and bail. On subsequent changes we despawn
/// every `GalaxyScene` root (recursive by default — kills all star/connection
/// children) and respawn a fresh galaxy.
fn regenerate_galaxy_on_param_change(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    params: Res<GalaxyParams>,
    scene_roots: Query<Entity, With<GalaxyScene>>,
    mut selected: ResMut<SelectedStar>,
    mut last_seen: Local<Option<u64>>,
) {
    match *last_seen {
        Some(g) if g == params.generation => return,
        None => {
            // First run: OnEnter already spawned the scene. Just record + bail.
            *last_seen = Some(params.generation);
            return;
        }
        _ => {}
    }
    *last_seen = Some(params.generation);

    // The previously-selected entity is about to be despawned; clear it so
    // the info panel and animation system don't reference a dangling Entity.
    selected.0 = None;

    for entity in &scene_roots {
        commands.entity(entity).despawn();
    }

    let (systems, contents, connections) = generate_galaxy(&params);
    spawn_galaxy_scene(
        &mut commands,
        &mut meshes,
        &mut materials,
        &systems,
        &contents,
        &connections,
    );
}
