use serde::{Deserialize, Serialize};

use crate::math::Transform;
use crate::physics::ColliderConfig;

// ---------------------------------------------------------------------------
// Format de fichier de scène
// ---------------------------------------------------------------------------

/// Données d'une primitive mesh sauvegardée (Cuboid, Sphere, …).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MeshPrimitiveData {
    pub shape: String,
    pub color: [f32; 4],
    /// Chemin relatif à la racine projet pour chaque slot texture (None = pas de texture).
    #[serde(default)]
    pub base_color_texture: Option<String>,
    #[serde(default)]
    pub emissive_texture: Option<String>,
    #[serde(default)]
    pub normal_map_texture: Option<String>,
    #[serde(default)]
    pub metallic_roughness_texture: Option<String>,
    #[serde(default)]
    pub occlusion_texture: Option<String>,
}

/// Slots texture d'une primitive — chemins absolus ou relatifs selon le contexte.
/// Utilisé par `SetTextureData` pour l'undo/redo des textures.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct TextureSlots {
    #[serde(default)]
    pub base_color: Option<String>,
    #[serde(default)]
    pub emissive: Option<String>,
    #[serde(default)]
    pub normal_map: Option<String>,
    #[serde(default)]
    pub metallic_roughness: Option<String>,
    #[serde(default)]
    pub occlusion: Option<String>,
}

/// Type d'entité spécialisée dans la scène.
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub enum EntityKind {
    #[default]
    Empty,
    Camera3d,
    DirectionalLight,
    PointLight,
    SpotLight,
    AudioSource,
    /// Entité océan — rendu par `bevy_water` (WaterPlugin) dans le jeu exporté.
    /// Dans l'éditeur : plan bleu semi-transparent de prévisualisation.
    Ocean,
    /// Élément d'interface 2D (HUD) — rendu en overlay par Bevy UI dans le jeu exporté.
    /// Dans l'éditeur : rectangle coloré de prévisualisation.
    Hud,
    /// Zone de déclenchement invisible — `avian3d::Sensor` dans le jeu exporté.
    /// Dans l'éditeur : wireframe coloré de prévisualisation.
    Trigger,
    /// Chemin (spline/waypoints) suivi par l'entité dans le jeu exporté.
    /// Dans l'éditeur : gizmo polyline reliant les waypoints.
    Path,
    /// Émetteur de particules CPU dans le jeu exporté.
    /// Dans l'éditeur : sphère de prévisualisation.
    Particle,
    /// Terrain généré depuis une heightmap PNG.
    /// Dans l'éditeur : mesh plan vert (ou mesh réel si heightmap chargée).
    Terrain,
    /// Nuage de points Gaussian Splat affiché dans le jeu exporté.
    /// Dans l'éditeur : cube violet de prévisualisation (ou nuage réel si .splat chargé).
    GaussianSplat,
}

// ---------------------------------------------------------------------------
// Terrain heightmap (J58)
// ---------------------------------------------------------------------------

/// Données d'un terrain généré depuis une heightmap PNG.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerrainData {
    /// Chemin relatif au projet du PNG niveaux de gris 8-bit (optionnel).
    pub heightmap_path: Option<String>,
    /// Largeur monde en X (mètres).
    pub size_x: f32,
    /// Profondeur monde en Z (mètres).
    pub size_z: f32,
    /// Hauteur maximale du terrain (mètres, correspond à la valeur 255).
    pub height_scale: f32,
    /// Nombre de subdivisions du mesh (16–256).
    pub subdivisions: u32,
    /// Chemin relatif au projet de la texture sol diffuse (optionnel).
    pub texture_path: Option<String>,
    /// Échelle UV de la texture (répétitions sur la surface).
    pub texture_scale: f32,
}

impl Default for TerrainData {
    fn default() -> Self {
        Self {
            heightmap_path: None,
            size_x: 200.0,
            size_z: 200.0,
            height_scale: 30.0,
            subdivisions: 128,
            texture_path: None,
            texture_scale: 20.0,
        }
    }
}

// ---------------------------------------------------------------------------
// Gaussian Splat (J57)
// ---------------------------------------------------------------------------

/// Données d'un nuage de points Gaussian Splat sérialisées.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GaussianSplatData {
    /// Chemin relatif au projet du fichier `.splat` (optionnel).
    pub splat_path: Option<String>,
    /// Taille apparente de chaque point (en unités monde).
    #[serde(default = "GaussianSplatData::default_point_size")]
    pub point_size: f32,
}

impl GaussianSplatData {
    fn default_point_size() -> f32 { 1.0 }
}

impl Default for GaussianSplatData {
    fn default() -> Self {
        Self {
            splat_path: None,
            point_size: Self::default_point_size(),
        }
    }
}

// ---------------------------------------------------------------------------
// Chemin / Spline (J52)
// ---------------------------------------------------------------------------

/// Données d'un chemin waypoints sérialisées.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathData {
    /// Points du chemin en coordonnées monde (`[x, y, z]`).
    pub points: Vec<[f32; 3]>,
    /// Boucle fermée (revient au point 0 après le dernier).
    #[serde(default)]
    pub closed: bool,
    /// Vitesse de déplacement (unités/seconde).
    #[serde(default = "PathData::default_speed")]
    pub speed: f32,
}

impl PathData {
    fn default_speed() -> f32 { 3.0 }
}

impl Default for PathData {
    fn default() -> Self {
        Self {
            points: vec![
                [0.0, 0.0, 0.0],
                [3.0, 0.0, 0.0],
                [3.0, 0.0, 3.0],
                [0.0, 0.0, 3.0],
            ],
            closed: true,
            speed: Self::default_speed(),
        }
    }
}

// ---------------------------------------------------------------------------
// Émetteur de particules (J53)
// ---------------------------------------------------------------------------

/// Données d'un émetteur de particules CPU sérialisées.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParticleData {
    /// Nombre de particules émises par seconde.
    #[serde(default = "ParticleData::default_rate")]
    pub rate: f32,
    /// Durée de vie d'une particule (secondes).
    #[serde(default = "ParticleData::default_lifetime")]
    pub lifetime: f32,
    /// Vitesse initiale (unités/seconde).
    #[serde(default = "ParticleData::default_speed")]
    pub speed: f32,
    /// Demi-angle du cône d'émission (degrés, 0 = vertical).
    #[serde(default = "ParticleData::default_spread")]
    pub spread: f32,
    /// Couleur de départ (RGBA linéaire).
    #[serde(default = "ParticleData::default_color_start")]
    pub color_start: [f32; 4],
    /// Couleur de fin (RGBA linéaire).
    #[serde(default = "ParticleData::default_color_end")]
    pub color_end: [f32; 4],
    /// Taille initiale.
    #[serde(default = "ParticleData::default_size_start")]
    pub size_start: f32,
    /// Taille finale.
    #[serde(default = "ParticleData::default_size_end")]
    pub size_end: f32,
    /// Gravité additionnelle (m/s², négatif = vers le bas).
    #[serde(default)]
    pub gravity: f32,
}

impl ParticleData {
    fn default_rate()        -> f32 { 20.0 }
    fn default_lifetime()    -> f32 { 1.5 }
    fn default_speed()       -> f32 { 3.0 }
    fn default_spread()      -> f32 { 30.0 }
    fn default_color_start() -> [f32; 4] { [1.0, 0.6, 0.1, 1.0] }
    fn default_color_end()   -> [f32; 4] { [1.0, 0.1, 0.0, 0.0] }
    fn default_size_start()  -> f32 { 0.15 }
    fn default_size_end()    -> f32 { 0.0 }
}

impl Default for ParticleData {
    fn default() -> Self {
        Self {
            rate:        Self::default_rate(),
            lifetime:    Self::default_lifetime(),
            speed:       Self::default_speed(),
            spread:      Self::default_spread(),
            color_start: Self::default_color_start(),
            color_end:   Self::default_color_end(),
            size_start:  Self::default_size_start(),
            size_end:    Self::default_size_end(),
            gravity:     -2.0,
        }
    }
}

// ---------------------------------------------------------------------------
// Océan
// ---------------------------------------------------------------------------

/// Paramètres de l'entité océan, sérialisés dans la scène.
///
/// La hauteur de l'eau correspond au `Transform.Y` de l'entité.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OceanData {
    /// Hauteur des vagues (amplitude Gerstner).
    pub amplitude: f32,
    /// Grille de tuiles `[colonnes, lignes]` — ex. `[6, 6]` = 36 tuiles.
    pub spawn_tiles: [u32; 2],
    /// Couleur de l'eau profonde (RGBA linéaire).
    pub deep_color: [f32; 4],
    /// Couleur de l'eau peu profonde (RGBA linéaire).
    pub shallow_color: [f32; 4],
    /// Clarté de l'eau (0 = opaque, 1 = transparent).
    pub clarity: f32,
}

impl Default for OceanData {
    fn default() -> Self {
        Self {
            amplitude: 0.5,
            spawn_tiles: [6, 6],
            deep_color: [0.2, 0.41, 0.54, 1.0],
            shallow_color: [0.45, 0.78, 0.81, 1.0],
            clarity: 0.25,
        }
    }
}

/// Paramètres de navigation d'un agent (NPC, ennemi, etc.).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NavigationData {
    /// Vitesse de déplacement (m/s).
    pub speed: f32,
    /// Distance à laquelle l'agent s'arrête (m).
    pub stop_distance: f32,
    /// Nom de l'entité cible (vide = inactive).
    pub target_entity: String,
    /// Tourne l'agent vers sa cible.
    pub rotate_toward_target: bool,
}

impl Default for NavigationData {
    fn default() -> Self {
        Self {
            speed: 3.0,
            stop_distance: 0.5,
            target_entity: String::new(),
            rotate_toward_target: true,
        }
    }
}

/// Mode de fond de la caméra.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CameraClearMode {
    /// Couleur unie RGBA.
    Color([f32; 4]),
    /// Texture skybox (stub — J37).
    Skybox,
    /// Transparent / aucun fond.
    None,
}

/// Paramètres d'une caméra 3D sérialisés.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CameraSettings {
    /// Champ de vision vertical en degrés (5–175).
    pub fov_degrees: f32,
    /// Plan de clipping proche (> 0).
    pub near: f32,
    /// Plan de clipping éloigné (> near).
    pub far: f32,
    /// Mode de fond.
    pub clear_mode: CameraClearMode,
    /// Priorité de rendu (caméras multiples).
    pub priority: i32,
    /// Chemin relatif à la racine projet pour le cubemap/HDR skybox.
    /// Utilisé quand `clear_mode == CameraClearMode::Skybox`.
    #[serde(default)]
    pub skybox_path: Option<String>,
    /// Luminosité du skybox (cd/m²). Ignoré si `skybox_path` est None.
    #[serde(default = "CameraSettings::default_skybox_brightness")]
    pub skybox_brightness: f32,

    // ── Post-traitement ──────────────────────────────────────────────────────
    /// Intensité Bloom (0.0 = désactivé).
    #[serde(default)]
    pub bloom_intensity: f32,
    /// Tonemapping : "BlenderFilmic" | "AgX" | "AcesFitted" | "Reinhard" | "ReinhardLuminance" | "TonyMcMapface" | "None"
    #[serde(default = "CameraSettings::default_tonemapping")]
    pub tonemapping: String,
    /// Exposition EV100 (0.0 = valeur Bevy par défaut ≈ 9.7).
    #[serde(default)]
    pub exposure_ev100: f32,
    /// Brouillard activé.
    #[serde(default)]
    pub fog_enabled: bool,
    /// Couleur du brouillard RGBA.
    #[serde(default = "CameraSettings::default_fog_color")]
    pub fog_color: [f32; 4],
    /// Mode de décroissance : "Linear" | "Exponential" | "ExponentialSquared"
    #[serde(default = "CameraSettings::default_fog_falloff")]
    pub fog_falloff: String,
    /// Densité (modes Exponential / ExponentialSquared).
    #[serde(default = "CameraSettings::default_fog_density")]
    pub fog_density: f32,
    /// Distance de début (mode Linear).
    #[serde(default)]
    pub fog_start: f32,
    /// Distance de fin (mode Linear).
    #[serde(default = "CameraSettings::default_fog_end")]
    pub fog_end: f32,

    // ── Profondeur de champ ──────────────────────────────────────────────────
    /// Profondeur de champ activée.
    #[serde(default)]
    pub dof_enabled: bool,
    /// Distance de mise au point (en mètres).
    #[serde(default = "CameraSettings::default_dof_focus")]
    pub dof_focus_distance: f32,
    /// Ouverture (nombre f, ex. 1.0 = très flou, 32.0 = tout net).
    #[serde(default = "CameraSettings::default_dof_aperture")]
    pub dof_aperture_f_stops: f32,
}

impl CameraSettings {
    fn default_skybox_brightness() -> f32 { 1000.0 }
    fn default_tonemapping()       -> String { "BlenderFilmic".into() }
    fn default_fog_color()         -> [f32; 4] { [0.5, 0.5, 0.5, 1.0] }
    fn default_fog_falloff()       -> String { "Exponential".into() }
    fn default_fog_density()       -> f32 { 0.05 }
    fn default_fog_end()           -> f32 { 100.0 }
    fn default_dof_focus()         -> f32 { 10.0 }
    fn default_dof_aperture()      -> f32 { 5.6 }
}

impl Default for CameraSettings {
    fn default() -> Self {
        Self {
            fov_degrees: 60.0,
            near: 0.1,
            far: 1000.0,
            clear_mode: CameraClearMode::Color([0.1, 0.1, 0.1, 1.0]),
            priority: 0,
            skybox_path: None,
            skybox_brightness: Self::default_skybox_brightness(),
            bloom_intensity: 0.0,
            tonemapping: Self::default_tonemapping(),
            exposure_ev100: 0.0,
            fog_enabled: false,
            fog_color: Self::default_fog_color(),
            fog_falloff: Self::default_fog_falloff(),
            fog_density: Self::default_fog_density(),
            fog_start: 0.0,
            fog_end: Self::default_fog_end(),
            dof_enabled: false,
            dof_focus_distance: Self::default_dof_focus(),
            dof_aperture_f_stops: Self::default_dof_aperture(),
        }
    }
}

/// Données de lumière sérialisées (DirectionalLight, PointLight ou SpotLight).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LightData {
    /// Couleur RGBA linéaire.
    pub color: [f32; 4],
    /// Illuminance en lux (DirectionalLight) ou intensité en lumens (PointLight/SpotLight).
    pub intensity: f32,
    pub shadows: bool,
    /// Portée en mètres (PointLight + SpotLight).
    #[serde(default = "LightData::default_range")]
    pub range: f32,
    /// Rayon physique de la source (PointLight) — produit des ombres douces si > 0.
    #[serde(default)]
    pub radius: f32,
    /// Ombres douces (PointLight) — nécessite `shadows = true`.
    #[serde(default)]
    pub soft_shadows: bool,
    /// Angle extérieur en radians — SpotLight uniquement.
    #[serde(default = "LightData::default_outer_angle")]
    pub outer_angle: f32,
    /// Angle intérieur en radians — SpotLight uniquement.
    #[serde(default = "LightData::default_inner_angle")]
    pub inner_angle: f32,
}

impl LightData {
    fn default_range() -> f32 { 20.0 }
    fn default_outer_angle() -> f32 { std::f32::consts::FRAC_PI_4 }
    fn default_inner_angle() -> f32 { std::f32::consts::FRAC_PI_6 }
}

impl Default for LightData {
    fn default() -> Self {
        Self {
            color: [1.0, 1.0, 1.0, 1.0],
            intensity: 10_000.0,
            shadows: true,
            range: Self::default_range(),
            radius: 0.0,
            soft_shadows: false,
            outer_angle: Self::default_outer_angle(),
            inner_angle: Self::default_inner_angle(),
        }
    }
}

// ---------------------------------------------------------------------------
// Animation
// ---------------------------------------------------------------------------

/// Image-clé d'un clip d'animation.
///
/// Les canaux non renseignés (None) ne sont pas animés à ce temps.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AnimationKeyframe {
    /// Temps en secondes depuis le début du clip.
    pub time: f32,
    /// Position XYZ (None = canal non animé).
    #[serde(default)]
    pub translation: Option<[f32; 3]>,
    /// Rotation quaternion [x, y, z, w] (None = canal non animé).
    #[serde(default)]
    pub rotation: Option<[f32; 4]>,
    /// Échelle XYZ (None = canal non animé).
    #[serde(default)]
    pub scale: Option<[f32; 3]>,
}

impl AnimationKeyframe {
    /// Crée un keyframe depuis un `Transform` Bevy (tous les canaux).
    pub fn from_transform(time: f32, t: &[f32; 3], r: &[f32; 4], s: &[f32; 3]) -> Self {
        Self {
            time,
            translation: Some(*t),
            rotation: Some(*r),
            scale: Some(*s),
        }
    }
}

/// Clip d'animation attaché à une entité (données éditeur).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AnimationClipData {
    /// Nom du clip (identifiant dans l'`AnimationPlayer`).
    pub name: String,
    /// Durée totale en secondes.
    pub duration: f32,
    pub keyframes: Vec<AnimationKeyframe>,
    /// Lecture en boucle.
    #[serde(default)]
    pub looping: bool,
}

impl Default for AnimationClipData {
    fn default() -> Self {
        Self {
            name: "Clip".into(),
            duration: 1.0,
            keyframes: Vec::new(),
            looping: true,
        }
    }
}

/// Référence à un clip d'animation embarqué dans un fichier GLTF/GLB.
///
/// Contrairement aux [`AnimationClipData`] (keyframes créés dans l'éditeur),
/// ces clips référencent des animations déjà présentes dans le mesh `.glb`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GltfAnimation {
    /// Nom affiché dans l'éditeur (ex : "Idle", "Walk", "Run").
    pub name: String,
    /// Index de l'animation dans le fichier GLB (0-based).
    pub gltf_index: usize,
    /// Joue automatiquement au démarrage.
    #[serde(default)]
    pub autoplay: bool,
    /// Lecture en boucle.
    #[serde(default = "GltfAnimation::default_looping")]
    pub looping: bool,
    /// Vitesse de lecture (1.0 = normale).
    #[serde(default = "GltfAnimation::default_speed")]
    pub speed: f32,
}

impl GltfAnimation {
    fn default_looping() -> bool { true }
    fn default_speed() -> f32 { 1.0 }
}

impl Default for GltfAnimation {
    fn default() -> Self {
        Self {
            name: "Clip 0".into(),
            gltf_index: 0,
            autoplay: false,
            looping: true,
            speed: 1.0,
        }
    }
}

// ---------------------------------------------------------------------------
// Audio
// ---------------------------------------------------------------------------

/// Données d'une source audio sérialisées.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AudioData {
    /// Volume linéaire (0.0 = silencieux, 1.0 = nominal).
    pub volume: f32,
    /// Portée spatiale en mètres (0.0 = son global non spatial).
    #[serde(default = "AudioData::default_range")]
    pub range: f32,
    /// Chemin absolu de l'asset audio (ex : `"/projet/assets/sounds/pickup.ogg"`).
    #[serde(default)]
    pub asset_path: Option<String>,
    /// Lecture en boucle.
    #[serde(default)]
    pub looping: bool,
    /// Démarrer automatiquement à la création de la scène.
    #[serde(default = "AudioData::default_autoplay")]
    pub autoplay: bool,
}

impl AudioData {
    fn default_range() -> f32 { 50.0 }
    fn default_autoplay() -> bool { true }
}

impl Default for AudioData {
    fn default() -> Self {
        Self {
            volume: 1.0,
            range: Self::default_range(),
            asset_path: None,
            looping: false,
            autoplay: Self::default_autoplay(),
        }
    }
}

// ---------------------------------------------------------------------------
// Matériau
// ---------------------------------------------------------------------------

/// Paramètres PBR d'un matériau sérialisés.
///
/// Couvre tous les champs courants de `StandardMaterial` de Bevy.
/// `custom_shader_path` est stocké uniquement pour le codegen (non chargé en éditeur).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MaterialData {
    pub metallic: f32,
    pub roughness: f32,
    pub base_color: [f32; 4],
    pub emissive_color: [f32; 4],
    pub emissive_strength: f32,
    /// "Opaque" | "Blend" | "Mask"
    pub alpha_mode: String,
    pub reflectance: f32,
    /// Chemin relatif vers un shader WGSL custom (stocké pour codegen).
    #[serde(default)]
    pub custom_shader_path: Option<String>,
    /// Nombre de répétitions UV (tiling) en X et Y. Défaut [1.0, 1.0].
    #[serde(default = "MaterialData::default_uv_scale")]
    pub uv_scale: [f32; 2],
    /// Décalage UV (offset) en X et Y. Défaut [0.0, 0.0].
    #[serde(default)]
    pub uv_offset: [f32; 2],
    /// Si vrai, les textures sont répétées (sampler Repeat) quand uv_scale > 1.
    #[serde(default)]
    pub texture_repeat: bool,
}

impl Default for MaterialData {
    fn default() -> Self {
        Self {
            metallic: 0.0,
            roughness: 0.5,
            base_color: [0.7, 0.7, 0.7, 1.0],
            emissive_color: [0.0, 0.0, 0.0, 1.0],
            emissive_strength: 0.0,
            alpha_mode: "Opaque".into(),
            reflectance: 0.5,
            custom_shader_path: None,
            uv_scale: [1.0, 1.0],
            uv_offset: [0.0, 0.0],
            texture_repeat: false,
        }
    }
}

impl MaterialData {
    fn default_uv_scale() -> [f32; 2] { [1.0, 1.0] }
}

/// État de la caméra éditeur sauvegardé avec la scène.
///
/// Permet de restaurer exactement le point de vue lors du rechargement.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditorCameraData {
    /// Rotation horizontale (radians).
    pub yaw: f32,
    /// Rotation verticale (radians).
    pub pitch: f32,
    /// Distance de la caméra au pivot.
    pub distance: f32,
    /// Point de pivot (centre de l'orbite) [x, y, z].
    pub pivot: [f32; 3],
}

/// Lumière ambiante globale de la scène.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AmbientLightData {
    pub color: [f32; 4],
    pub brightness: f32,
}

impl Default for AmbientLightData {
    fn default() -> Self {
        Self { color: [1.0, 1.0, 1.0, 1.0], brightness: 200.0 }
    }
}

/// Préfab sérialisé : template d'entité réutilisable.
///
// ---------------------------------------------------------------------------
// Gameplay
// ---------------------------------------------------------------------------

/// Contrôleur de personnage joueur (déplacement personnalisable + saut).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlayerController {
    /// "Kinematic" (Transform direct, pas de dépendance) | "Physics" (avian3d CharacterController).
    pub mode: String,
    /// Vitesse de marche (m/s).
    pub speed: f32,
    /// Vitesse de sprint (m/s) — maintenir la touche sprint.
    pub sprint_speed: f32,
    /// Hauteur de saut (m).
    pub jump_height: f32,
    /// Gravité kinématique (m/s²) — ignorée si mode = "Physics".
    #[serde(default = "PlayerController::default_gravity")]
    pub gravity: f32,
    /// Touche avancer (ex: "KeyZ", "KeyW"). Nom de variante `bevy::prelude::KeyCode`.
    #[serde(default = "PlayerController::default_key_forward")]
    pub key_forward: String,
    /// Touche reculer.
    #[serde(default = "PlayerController::default_key_backward")]
    pub key_backward: String,
    /// Touche aller à gauche (strafe).
    #[serde(default = "PlayerController::default_key_left")]
    pub key_left: String,
    /// Touche aller à droite (strafe).
    #[serde(default = "PlayerController::default_key_right")]
    pub key_right: String,
    /// Touche saut.
    #[serde(default = "PlayerController::default_key_jump")]
    pub key_jump: String,
    /// Touche sprint (modificateur maintenu).
    #[serde(default = "PlayerController::default_key_sprint")]
    pub key_sprint: String,
    /// Vitesse de rotation gauche/droite (rad/s). 0 = désactivé.
    #[serde(default = "PlayerController::default_rotation_speed")]
    pub rotation_speed: f32,
    /// Distance pieds→racine (m) pour la détection de sol par rayon (mode Physics).
    /// 0.0 = calculé automatiquement depuis la géométrie du collider enfant.
    #[serde(default)]
    pub ground_sensor_distance: f32,
    /// Nom du clip GLTF joué quand le personnage est immobile. Vide = pas de commutation.
    #[serde(default)]
    pub anim_idle: String,
    /// Nom du clip GLTF joué pendant la marche/course. Vide = pas de commutation.
    #[serde(default)]
    pub anim_walk: String,
    /// Nom du clip GLTF joué pendant le saut. Vide = pas de commutation.
    #[serde(default)]
    pub anim_jump: String,
    /// Distance de sonde horizontale pour la détection de mur (m). Défaut : 0.45.
    #[serde(default = "PlayerController::default_wall_probe_dist")]
    pub wall_probe_dist: f32,
    /// Seuil |normal.y| en dessous duquel une surface est considérée mur (au sol). Défaut : 0.4.
    #[serde(default = "PlayerController::default_wall_slope_threshold")]
    pub wall_slope_threshold: f32,
    /// Seuil |normal.y| en dessous duquel une surface est considérée mur (en l'air). Défaut : 0.7.
    #[serde(default = "PlayerController::default_air_slope_threshold")]
    pub air_slope_threshold: f32,
}

impl PlayerController {
    fn default_gravity() -> f32 { 20.0 }
    fn default_key_forward()  -> String { "KeyW".into() }
    fn default_key_backward() -> String { "KeyS".into() }
    fn default_key_left()     -> String { "KeyA".into() }
    fn default_key_right()    -> String { "KeyD".into() }
    fn default_key_jump()     -> String { "Space".into() }
    fn default_key_sprint()   -> String { "ShiftLeft".into() }
    fn default_rotation_speed() -> f32 { 3.0 }
    fn default_wall_probe_dist() -> f32 { 0.45 }
    fn default_wall_slope_threshold() -> f32 { 0.4 }
    fn default_air_slope_threshold() -> f32 { 0.7 }
}

impl Default for PlayerController {
    fn default() -> Self {
        Self {
            mode: "Kinematic".into(),
            speed: 5.0,
            sprint_speed: 10.0,
            jump_height: 2.0,
            gravity: Self::default_gravity(),
            key_forward:  Self::default_key_forward(),
            key_backward: Self::default_key_backward(),
            key_left:     Self::default_key_left(),
            key_right:    Self::default_key_right(),
            key_jump:     Self::default_key_jump(),
            key_sprint:   Self::default_key_sprint(),
            rotation_speed: Self::default_rotation_speed(),
            ground_sensor_distance: 0.0,
            anim_idle: String::new(),
            anim_walk: String::new(),
            anim_jump: String::new(),
            wall_probe_dist: Self::default_wall_probe_dist(),
            wall_slope_threshold: Self::default_wall_slope_threshold(),
            air_slope_threshold: Self::default_air_slope_threshold(),
        }
    }
}

/// Suivi de caméra (third-person, top-down…).
/// La caméra suit automatiquement l'entité avec `PlayerMarker` dans le jeu généré.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CameraFollow {
    /// Décalage position [x, y, z] par rapport à la cible.
    pub offset: [f32; 3],
    /// Vitesse de lissage (lerp/s). 0.0 = instantané.
    pub smooth: f32,
    /// La caméra oriente son regard vers la cible (look_at).
    pub look_at_target: bool,
    /// Marge de sécurité anti-clipping (m) : distance minimale entre la caméra et la surface touchée. Défaut : 0.5.
    #[serde(default = "CameraFollow::default_camera_margin")]
    pub camera_margin: f32,
    /// Ancien champ supprimé — ignoré à la désérialisation.
    #[serde(default, skip_serializing)]
    pub target_name: String,
}

impl CameraFollow {
    fn default_camera_margin() -> f32 { 0.5 }
}

impl Default for CameraFollow {
    fn default() -> Self {
        Self {
            offset: [0.0, 5.0, -10.0],
            smooth: 5.0,
            look_at_target: true,
            camera_margin: Self::default_camera_margin(),
            target_name: String::new(),
        }
    }
}

/// Format de fichier : `.prefab.ron`
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavedPrefab {
    /// Nom du préfab (= nom de l'entité au moment de la sauvegarde).
    pub name: String,
    /// Données de l'entité template.
    pub entity: SavedEntity,
}

/// Entité sérialisée dans un fichier `.scn.ron`.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SavedEntity {
    pub name: Option<String>,
    pub transform: Option<Transform>,
    /// Index du parent dans `SavedScene::entities` (`None` = racine).
    #[serde(default)]
    pub parent_index: Option<usize>,
    /// Chemin asset relatif à `assets/` pour les meshes GLB/GLTF (ex : `"demo/models/player.glb#Scene0"`).
    #[serde(default)]
    pub mesh_path: Option<String>,
    /// Identifiant stable de l'asset dans l'AssetIndex (prioritaire sur `mesh_path`).
    #[serde(default)]
    pub mesh_asset_id: Option<String>,
    /// Primitive mesh procédurale (Cuboid, etc.) avec sa couleur.
    #[serde(default)]
    pub mesh_primitive: Option<MeshPrimitiveData>,
    /// Type d'entité (caméra, lumière directionnelle, lumière ponctuelle, …).
    #[serde(default)]
    pub kind: EntityKind,
    /// Données de lumière (présentes si kind est DirectionalLight ou PointLight).
    #[serde(default)]
    pub light: Option<LightData>,
    /// Paramètres caméra (présents si kind est Camera3d).
    #[serde(default)]
    pub camera: Option<CameraSettings>,
    /// Données audio (présentes si kind est AudioSource).
    #[serde(default)]
    pub audio: Option<AudioData>,
    /// Clips d'animation attachés à cette entité (keyframes éditeur).
    #[serde(default)]
    pub animations: Vec<AnimationClipData>,
    /// Animations embarquées dans le mesh GLTF/GLB de l'entité.
    #[serde(default)]
    pub gltf_animations: Vec<GltfAnimation>,
    /// Paramètres PBR du matériau (présents si l'entité a un `PrimitiveMesh`).
    #[serde(default)]
    pub material: Option<MaterialData>,
    /// Chemin relatif au projet du préfab source (si cette entité est une instance de préfab).
    /// Persisté dans `.scn.ron` pour afficher le lien préfab dans l'inspector après reload.
    #[serde(default)]
    pub prefab_source: Option<String>,
    /// Entité masquée (`Visibility::Hidden`). `false` par défaut (hérité = visible).
    #[serde(default)]
    pub hidden: bool,
    /// Contrôleur de personnage (déplacement + saut).
    #[serde(default)]
    pub player_controller: Option<PlayerController>,
    /// Suivi de caméra (pour les entités caméra).
    #[serde(default)]
    pub camera_follow: Option<CameraFollow>,
    /// Configuration de collider physique.
    #[serde(default)]
    pub collider: Option<ColliderConfig>,
    /// Vrai si cette entité est un nœud collider enfant (pas une entité jeu directe).
    /// `#[serde(default)]` → vaut `false` pour tous les anciens fichiers `.scn.ron`.
    #[serde(default)]
    pub is_collider_node: bool,
    /// Paramètres ocean (présents si `kind == EntityKind::Ocean`).
    #[serde(default)]
    pub ocean: Option<OceanData>,
    /// Paramètres HUD (présents si `kind == EntityKind::Hud`).
    #[serde(default)]
    pub hud: Option<HudData>,
    /// Paramètres trigger zone (présents si `kind == EntityKind::Trigger`).
    #[serde(default)]
    pub trigger: Option<TriggerData>,
    /// Données chemin waypoints (présents si `kind == EntityKind::Path`).
    #[serde(default)]
    pub path: Option<PathData>,
    /// Données émetteur de particules (présents si `kind == EntityKind::Particle`).
    #[serde(default)]
    pub particle: Option<ParticleData>,
    /// Paramètres de navigation (NPC qui suit une cible).
    #[serde(default)]
    pub navigation: Option<NavigationData>,
    /// Données terrain heightmap (présentes si `kind == EntityKind::Terrain`).
    #[serde(default)]
    pub terrain: Option<TerrainData>,
    /// Données Gaussian Splat (présentes si `kind == EntityKind::GaussianSplat`).
    #[serde(default)]
    pub splat: Option<GaussianSplatData>,
    /// Étiquettes utilisateur libres, utilisées pour retrouver les entités
    /// entre sessions sans dépendre d'un `EntityId` Bevy (qui change à chaque
    /// reload). Exposé via `GET /api/entities?tag=X` et `POST AddTag/RemoveTag`.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
}

/// Scène complète sérialisée en RON.
///
/// Exemple de fichier :
/// ```ron
/// (entities: [(name: Some("Fripouille"), transform: Some((position: (x:0, y:0, z:0), ...)))])
/// ```
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SavedScene {
    pub entities: Vec<SavedEntity>,
    /// Lumière ambiante globale (`None` = laisser le défaut Bevy).
    #[serde(default)]
    pub ambient_light: Option<AmbientLightData>,
    /// Position de la caméra éditeur au moment de la sauvegarde.
    /// Restaurée automatiquement au chargement de la scène.
    #[serde(default)]
    pub editor_camera: Option<EditorCameraData>,
    /// États du jeu (State Machine Bevy). `None` = pas de state machine générée.
    #[serde(default)]
    pub game_states: Option<GameStatesConfig>,
    /// Génère un système Save/Load (serde_json) dans le jeu exporté.
    #[serde(default)]
    pub save_load_enabled: bool,
}

/// Configuration de la state machine du jeu.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameStatesConfig {
    /// Liste des états (ex. `["Menu", "Playing", "Paused", "GameOver"]`).
    pub states: Vec<String>,
    /// Nom de l'état initial (doit être dans `states`).
    pub initial: String,
}

// ---------------------------------------------------------------------------
// HUD
// ---------------------------------------------------------------------------

/// Point d'ancrage d'un élément HUD dans l'écran.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub enum HudAnchor {
    #[default]
    TopLeft,
    TopCenter,
    TopRight,
    BottomLeft,
    BottomCenter,
    BottomRight,
    Center,
}

/// Type d'élément HUD.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub enum HudKind {
    /// Texte statique ou avec variables bindées (`{score}`, `{health}`…).
    #[default]
    Label,
    /// Barre de progression (vie, mana, stamina…).
    ProgressBar,
    /// Image 2D fixe (icône, logo…).
    Image,
    /// Grille d'icônes (inventaire, compétences…).
    IconGrid,
}

/// Données d'un élément HUD sérialisées dans la scène.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HudData {
    /// Type d'élément.
    #[serde(default)]
    pub hud_kind: HudKind,
    /// Point d'ancrage dans l'écran.
    #[serde(default)]
    pub anchor: HudAnchor,
    /// Décalage en pixels depuis l'ancrage `[x, y]`.
    #[serde(default = "HudData::default_offset")]
    pub offset: [f32; 2],
    /// Taille en pixels `[largeur, hauteur]`.
    #[serde(default = "HudData::default_size")]
    pub size: [f32; 2],
    // ── Label ──
    /// Texte affiché — supporte `{score}`, `{health}`, etc.
    #[serde(default = "HudData::default_text")]
    pub text: String,
    /// Taille de la police en points.
    #[serde(default = "HudData::default_font_size")]
    pub font_size: f32,
    /// Chemin relatif à `assets/` de la police (ex: `"FiraSans-Regular.ttf"`).
    /// Si `None`, utilise la police par défaut Bevy (ASCII uniquement).
    #[serde(default)]
    pub font_path: Option<String>,
    /// Couleur du texte RGBA.
    #[serde(default = "HudData::default_color")]
    pub color: [f32; 4],
    // ── ProgressBar ──
    /// Couleur de la barre RGBA.
    #[serde(default = "HudData::default_bar_color")]
    pub bar_color: [f32; 4],
    /// Couleur de fond de la barre RGBA.
    #[serde(default = "HudData::default_bg_color")]
    pub background_color: [f32; 4],
    /// Valeur maximale fixe (si `bind_max` est vide).
    #[serde(default = "HudData::default_max_value")]
    pub max_value: f32,
    /// Nom de la variable `HudState` pour la valeur courante (ex : `"health"`).
    #[serde(default)]
    pub bind_value: String,
    /// Nom de la variable `HudState` pour la valeur max (vide = utilise `max_value`).
    #[serde(default)]
    pub bind_max: String,
    // ── Image ──
    /// Chemin relatif à `assets/` de l'image.
    #[serde(default)]
    pub image_path: Option<String>,
    // ── IconGrid ──
    /// Nombre de colonnes.
    #[serde(default = "HudData::default_columns")]
    pub columns: u32,
    /// Nombre de lignes.
    #[serde(default = "HudData::default_rows")]
    pub rows: u32,
    /// Chemin relatif à `assets/` de l'icône.
    #[serde(default)]
    pub icon_path: Option<String>,
}

impl HudData {
    fn default_offset()     -> [f32; 2] { [10.0, 10.0] }
    fn default_size()       -> [f32; 2] { [200.0, 40.0] }
    fn default_text()       -> String   { "Score: {score}".into() }
    fn default_font_size()  -> f32      { 24.0 }
    fn default_color()      -> [f32; 4] { [1.0, 1.0, 1.0, 1.0] }
    fn default_bar_color()  -> [f32; 4] { [0.2, 0.8, 0.2, 1.0] }
    fn default_bg_color()   -> [f32; 4] { [0.1, 0.1, 0.1, 0.8] }
    fn default_max_value()  -> f32      { 100.0 }
    fn default_columns()    -> u32      { 5 }
    fn default_rows()       -> u32      { 1 }
}

impl Default for HudData {
    fn default() -> Self {
        Self {
            hud_kind:         HudKind::Label,
            anchor:           HudAnchor::TopLeft,
            offset:           Self::default_offset(),
            size:             Self::default_size(),
            text:             Self::default_text(),
            font_size:        Self::default_font_size(),
            font_path:        None,
            color:            Self::default_color(),
            bar_color:        Self::default_bar_color(),
            background_color: Self::default_bg_color(),
            max_value:        Self::default_max_value(),
            bind_value:       String::new(),
            bind_max:         String::new(),
            image_path:       None,
            columns:          Self::default_columns(),
            rows:             Self::default_rows(),
            icon_path:        None,
        }
    }
}

// ---------------------------------------------------------------------------
// Trigger Zone
// ---------------------------------------------------------------------------

/// Forme du volume de déclenchement.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum TriggerShape {
    #[default]
    Box,
    Sphere,
    Cylinder,
}

/// Paramètres d'une zone de déclenchement sérialisés dans la scène.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TriggerData {
    /// Forme du collider sensor.
    #[serde(default)]
    pub shape: TriggerShape,
    /// Demi-dimensions du volume (half-extents pour Box, [radius, height, 0] pour Sphere/Cylinder).
    #[serde(default = "TriggerData::default_size")]
    pub size: [f32; 3],
    /// Nom du hook à appeler quand un objet entre dans la zone (ex. `"on_enter_zone_a"`).
    #[serde(default)]
    pub on_enter_hook: String,
    /// Nom du hook à appeler quand un objet sort de la zone.
    #[serde(default)]
    pub on_exit_hook: String,
    /// Couleur de prévisualisation wireframe dans l'éditeur (RGBA).
    #[serde(default = "TriggerData::default_color")]
    pub preview_color: [f32; 4],
}

impl TriggerData {
    fn default_size()  -> [f32; 3] { [1.0, 1.0, 1.0] }
    fn default_color() -> [f32; 4] { [0.2, 1.0, 0.4, 0.8] }
}

impl Default for TriggerData {
    fn default() -> Self {
        Self {
            shape:         TriggerShape::Box,
            size:          Self::default_size(),
            on_enter_hook: String::new(),
            on_exit_hook:  String::new(),
            preview_color: Self::default_color(),
        }
    }
}

/// Référence à une scène (chemin de fichier relatif).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SceneHandle(pub String);

impl SceneHandle {
    pub fn new(path: impl Into<String>) -> Self {
        Self(path.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl std::fmt::Display for SceneHandle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Opérations sur une scène.
///
/// Implémenté par `BevyEngine` (opère sur les `SceneEntity`) en Jalon 2.
pub trait Scene {
    /// Handle (chemin) de cette scène.
    fn handle(&self) -> &SceneHandle;

    /// Sérialise et sauvegarde la scène vers un fichier `.scn.ron`.
    fn save(&self, path: &str) -> Result<(), String>;

    /// Charge une scène depuis un fichier `.scn.ron`.
    fn load(path: &str) -> Result<Self, String>
    where
        Self: Sized;

    /// Vide la scène (supprime toutes les `SceneEntity`).
    fn clear(&mut self);
}
