//! Serializable project settings loaded from `assets/project.ron`.

mod audio;
mod plugin;
mod rendering;

pub use audio::*;
pub use plugin::{
    save_project_settings, sync_sim_tuning, ProjectSettingsChanged, ProjectSettingsIo,
    ProjectSettingsPlugin, SimTuning,
};
pub use rendering::{
    effective_auto_exposure, resolve_effective_render_stack, resolve_gi_path,
    write_effective_render_stack, ActiveCameraRenderProfile, ActiveVolumeContribution,
    EffectiveRenderStack, ExposureMode, GiMode, GiPath, RenderFallbackReason,
    RenderingCapabilities, VolumeContribution,
};

use bevy::prelude::*;
use serde::{Deserialize, Serialize};

pub const DEFAULT_PROJECT_PATH: &str = "assets/project.ron";
pub const SETTINGS_VERSION: u32 = 1;
pub const PROJECT_TEMPLATE_VERSION: u32 = 1;

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, Reflect, PartialEq, Eq)]
#[reflect(Clone)]
pub enum ProjectKind {
    #[default]
    Game,
    Sandbox,
}

/// Marker for cameras that receive the shared project rendering stack.
#[derive(Component, Debug, Clone, Copy, Default)]
pub struct ProjectRenderCamera;

/// Root project manifest and tunable sections.
#[derive(Resource, Debug, Clone, Serialize, Deserialize, Reflect)]
#[reflect(Resource, Clone)]
pub struct ProjectSettings {
    pub version: u32,
    #[serde(default)]
    pub project_id: String,
    pub name: String,
    #[serde(default)]
    pub project_kind: ProjectKind,
    #[serde(default = "default_project_template_version")]
    pub template_version: u32,
    #[serde(default)]
    pub capabilities: Vec<String>,
    pub default_level: String,
    pub asset_roots: Vec<String>,
    pub rendering: RenderingSettings,
    #[serde(default)]
    pub audio: AudioSettings,
    pub physics: PhysicsSettings,
    pub input: InputSettings,
}

impl Default for ProjectSettings {
    fn default() -> Self {
        Self {
            version: SETTINGS_VERSION,
            project_id: "blacksite-default".into(),
            name: "Bevy FPS Foundation".into(),
            project_kind: ProjectKind::Game,
            template_version: PROJECT_TEMPLATE_VERSION,
            capabilities: vec![
                "editor".into(),
                "gameplay".into(),
                "rendering".into(),
                "audio".into(),
            ],
            default_level: "assets/levels/editor_scene.scn.ron".into(),
            asset_roots: vec![
                "assets/models".into(),
                "assets/textures".into(),
                "assets/materials".into(),
                "assets/levels".into(),
                "assets/audio".into(),
            ],
            rendering: RenderingSettings::default(),
            audio: AudioSettings::default(),
            physics: PhysicsSettings::default(),
            input: InputSettings::default(),
        }
    }
}

const fn default_project_template_version() -> u32 {
    PROJECT_TEMPLATE_VERSION
}

/// Camera post-processing and world lighting defaults.
#[derive(Debug, Clone, Serialize, Deserialize, Reflect)]
#[reflect(Clone)]
pub struct RenderingSettings {
    #[serde(default)]
    pub gi_mode: GiMode,
    pub hdr: bool,
    #[serde(default)]
    pub exposure_mode: ExposureMode,
    pub exposure_ev100: f32,
    #[serde(default = "default_auto_exposure_speed_brighten")]
    pub auto_exposure_speed_brighten: f32,
    #[serde(default = "default_auto_exposure_speed_darken")]
    pub auto_exposure_speed_darken: f32,
    pub tonemapping_aces: bool,
    pub bloom: bool,
    pub taa: bool,
    pub ssao: bool,
    pub atmosphere: bool,
    pub fog_color: [f32; 3],
    pub fog_density: f32,
    pub ambient_color: [f32; 3],
    pub ambient_brightness: f32,
    pub sun_illuminance: f32,
    pub shadow_cascades: u32,
    pub shadow_first_cascade: f32,
    pub shadow_max_distance: f32,
}

fn default_auto_exposure_speed_brighten() -> f32 {
    3.0
}

fn default_auto_exposure_speed_darken() -> f32 {
    1.0
}

impl Default for RenderingSettings {
    fn default() -> Self {
        Self {
            gi_mode: GiMode::default(),
            hdr: true,
            exposure_mode: ExposureMode::default(),
            exposure_ev100: 13.0,
            auto_exposure_speed_brighten: default_auto_exposure_speed_brighten(),
            auto_exposure_speed_darken: default_auto_exposure_speed_darken(),
            tonemapping_aces: true,
            bloom: true,
            taa: true,
            ssao: true,
            atmosphere: true,
            fog_color: [0.66, 0.75, 0.86],
            fog_density: 0.0012,
            ambient_color: [0.6, 0.7, 0.85],
            ambient_brightness: 60.0,
            sun_illuminance: 100_000.0,
            shadow_cascades: 4,
            shadow_first_cascade: 6.0,
            shadow_max_distance: 220.0,
        }
    }
}

/// Gameplay movement and capsule constants (mirrors former `sim::tuning`).
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Reflect)]
#[reflect(Clone)]
pub struct PhysicsSettings {
    pub player_radius: f32,
    pub player_length: f32,
    pub eye_offset_stand: f32,
    pub eye_offset_crouch: f32,
    pub gravity: f32,
    pub walk_speed: f32,
    pub run_speed: f32,
    pub crouch_speed: f32,
    pub jump_speed: f32,
    pub ground_accel: f32,
    pub air_accel: f32,
    pub ground_check_dist: f32,
    pub skin: f32,
    pub max_slide_iter: u32,
    pub ground_normal_y: f32,
    pub coyote_time: f32,
    pub jump_buffer: f32,
}

impl Default for PhysicsSettings {
    fn default() -> Self {
        Self {
            player_radius: 0.4,
            player_length: 1.0,
            eye_offset_stand: 0.6,
            eye_offset_crouch: 0.1,
            gravity: 22.0,
            walk_speed: 6.0,
            run_speed: 9.5,
            crouch_speed: 3.0,
            jump_speed: 7.5,
            ground_accel: 70.0,
            air_accel: 16.0,
            ground_check_dist: 0.2,
            skin: 0.02,
            max_slide_iter: 4,
            ground_normal_y: 0.5,
            coyote_time: 0.12,
            jump_buffer: 0.12,
        }
    }
}

/// Input sensitivities and editor fly camera speeds.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Reflect)]
#[reflect(Clone)]
pub struct InputSettings {
    pub yaw_sensitivity: f32,
    pub pitch_sensitivity: f32,
    pub editor_fly_speed: f32,
    pub editor_look_sensitivity: f32,
    pub editor_pan_speed: f32,
}

impl Default for InputSettings {
    fn default() -> Self {
        Self {
            yaw_sensitivity: 0.003,
            pitch_sensitivity: 0.0022,
            editor_fly_speed: 14.0,
            editor_look_sensitivity: 0.003,
            editor_pan_speed: 0.015,
        }
    }
}

/// Loads settings from disk, falling back to defaults.
pub fn load_project_settings_from_path(path: &str) -> ProjectSettings {
    load_project_settings_with_source(path).0
}

/// Loads settings plus the exact source text used when the file was readable.
pub fn load_project_settings_with_source(path: &str) -> (ProjectSettings, Option<String>) {
    match std::fs::read_to_string(path) {
        Ok(contents) => match ron::from_str(&contents) {
            Ok(settings) => (settings, Some(contents)),
            Err(error) => {
                warn!("Failed to parse {path}: {error}; using defaults");
                (ProjectSettings::default(), Some(contents))
            }
        },
        Err(error) => {
            warn!("Could not read {path}: {error}; using defaults");
            (ProjectSettings::default(), None)
        }
    }
}

/// Serializes settings to RON.
pub fn save_project_settings_to_string(settings: &ProjectSettings) -> Result<String, ron::Error> {
    ron::ser::to_string_pretty(settings, ron::ser::PrettyConfig::default())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn settings_round_trip() {
        let settings = ProjectSettings::default();
        let text = save_project_settings_to_string(&settings).expect("serialize");
        let parsed: ProjectSettings = ron::from_str(&text).expect("deserialize");
        assert_eq!(parsed.name, settings.name);
        assert_eq!(parsed.project_id, settings.project_id);
        assert_eq!(parsed.project_kind, settings.project_kind);
        assert_eq!(parsed.template_version, settings.template_version);
        assert_eq!(parsed.capabilities, settings.capabilities);
        assert_eq!(parsed.audio, settings.audio);
        assert_eq!(parsed.physics.walk_speed, settings.physics.walk_speed);
        assert_eq!(parsed.rendering.gi_mode, settings.rendering.gi_mode);
    }

    #[test]
    fn settings_load_returns_the_exact_source_for_revision_guards() {
        let root = std::env::temp_dir().join(format!(
            "blacksite-settings-source-{}",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        let path = root.join("project.ron");
        std::fs::create_dir_all(&root).unwrap();
        let source = save_project_settings_to_string(&ProjectSettings::default()).unwrap();
        std::fs::write(&path, &source).unwrap();

        let (settings, loaded_source) = load_project_settings_with_source(path.to_str().unwrap());

        assert_eq!(settings.name, ProjectSettings::default().name);
        assert_eq!(loaded_source.as_deref(), Some(source.as_str()));
        std::fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn legacy_manifest_defaults_project_identity_fields() {
        let legacy = r#"(
            version: 1,
            name: "Legacy",
            default_level: "assets/levels/main.scn.ron",
            asset_roots: [],
            rendering: (
                gi_mode: Auto, hdr: true, exposure_mode: Manual, exposure_ev100: 13.0,
                auto_exposure_speed_brighten: 3.0, auto_exposure_speed_darken: 1.0,
                tonemapping_aces: true, bloom: true, taa: true, ssao: true,
                atmosphere: true, fog_color: (0.0, 0.0, 0.0), fog_density: 0.0,
                ambient_color: (0.0, 0.0, 0.0), ambient_brightness: 0.0,
                sun_illuminance: 100000.0, shadow_cascades: 4,
                shadow_first_cascade: 6.0, shadow_max_distance: 220.0,
            ),
            physics: (
                player_radius: 0.4, player_length: 1.0, eye_offset_stand: 0.6,
                eye_offset_crouch: 0.1, gravity: 22.0, walk_speed: 6.0,
                run_speed: 9.5, crouch_speed: 3.0, jump_speed: 7.5,
                ground_accel: 70.0, air_accel: 16.0, ground_check_dist: 0.2,
                skin: 0.02, max_slide_iter: 4, ground_normal_y: 0.5,
                coyote_time: 0.12, jump_buffer: 0.12,
            ),
            input: (
                yaw_sensitivity: 0.003, pitch_sensitivity: 0.0022,
                editor_fly_speed: 14.0, editor_look_sensitivity: 0.003,
                editor_pan_speed: 0.015,
            ),
        )"#;

        let parsed: ProjectSettings = ron::from_str(legacy).unwrap();

        assert!(parsed.project_id.is_empty());
        assert_eq!(parsed.project_kind, ProjectKind::Game);
        assert_eq!(parsed.template_version, PROJECT_TEMPLATE_VERSION);
        assert!(parsed.capabilities.is_empty());
        assert_eq!(parsed.audio, AudioSettings::default());
        assert!(parsed.audio.validate().is_ok());
    }

    #[test]
    fn project_defaults_include_audio_capability_and_asset_root() {
        let settings = ProjectSettings::default();

        assert!(settings.capabilities.iter().any(|item| item == "audio"));
        assert!(settings
            .asset_roots
            .iter()
            .any(|item| item == "assets/audio"));
        assert!(settings.audio.validate().is_ok());
    }

    #[test]
    fn gi_mode_defaults_auto() {
        assert_eq!(RenderingSettings::default().gi_mode, GiMode::Auto);
    }

    #[test]
    fn exposure_mode_auto_round_trip() {
        let mut settings = ProjectSettings::default();
        settings.rendering.exposure_mode = ExposureMode::Auto;
        settings.rendering.auto_exposure_speed_brighten = 4.0;
        settings.rendering.auto_exposure_speed_darken = 0.5;
        let text = save_project_settings_to_string(&settings).expect("serialize");
        let parsed: ProjectSettings = ron::from_str(&text).expect("deserialize");
        assert_eq!(parsed.rendering.exposure_mode, ExposureMode::Auto);
        assert_eq!(parsed.rendering.auto_exposure_speed_brighten, 4.0);
        assert_eq!(parsed.rendering.auto_exposure_speed_darken, 0.5);
    }

    #[test]
    fn effective_auto_exposure_requires_hdr_and_compute() {
        let mut profile = ActiveCameraRenderProfile {
            exposure_mode: ExposureMode::Auto,
            hdr: true,
            ..Default::default()
        };
        let caps = RenderingCapabilities {
            auto_exposure_supported: true,
            ..Default::default()
        };
        assert!(effective_auto_exposure(&profile, &caps));

        profile.hdr = false;
        assert!(!effective_auto_exposure(&profile, &caps));

        profile.hdr = true;
        let caps = RenderingCapabilities::default();
        assert!(!effective_auto_exposure(&profile, &caps));
    }
}
