use bevy::prelude::*;

use crate::{
    load_project_settings_with_source, save_project_settings_to_string, PhysicsSettings,
    ProjectSettings, DEFAULT_PROJECT_PATH,
};

/// Runtime copy of physics tuning consumed by `sim` systems.
#[derive(Resource, Debug, Clone, Copy)]
pub struct SimTuning {
    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 SimTuning {
    pub fn from_physics(physics: &PhysicsSettings) -> Self {
        Self {
            player_radius: physics.player_radius,
            player_length: physics.player_length,
            eye_offset_stand: physics.eye_offset_stand,
            eye_offset_crouch: physics.eye_offset_crouch,
            gravity: physics.gravity,
            walk_speed: physics.walk_speed,
            run_speed: physics.run_speed,
            crouch_speed: physics.crouch_speed,
            jump_speed: physics.jump_speed,
            ground_accel: physics.ground_accel,
            air_accel: physics.air_accel,
            ground_check_dist: physics.ground_check_dist,
            skin: physics.skin,
            max_slide_iter: physics.max_slide_iter,
            ground_normal_y: physics.ground_normal_y,
            coyote_time: physics.coyote_time,
            jump_buffer: physics.jump_buffer,
        }
    }
}

/// Tracks project settings file path and dirty state (editor persists via UI).
#[derive(Resource, Debug)]
pub struct ProjectSettingsIo {
    pub path: String,
    pub dirty: bool,
    /// Exact source loaded or last saved by this process; collaboration guards hash it.
    pub loaded_source: Option<String>,
}

impl Default for ProjectSettingsIo {
    fn default() -> Self {
        Self {
            path: DEFAULT_PROJECT_PATH.into(),
            dirty: false,
            loaded_source: None,
        }
    }
}

/// Fired after project settings are saved or reloaded from disk.
#[derive(Message, Debug, Clone, Copy, Default)]
pub struct ProjectSettingsChanged;

pub struct ProjectSettingsPlugin;

impl Plugin for ProjectSettingsPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<ProjectSettingsIo>()
            .register_type::<ProjectSettings>()
            .register_type::<crate::ProjectKind>()
            .register_type::<crate::AudioSettings>()
            .register_type::<crate::AudioBusSettings>()
            .register_type::<crate::GiMode>()
            .register_type::<crate::ExposureMode>()
            .register_type::<crate::ActiveCameraRenderProfile>()
            .register_type::<crate::EffectiveRenderStack>()
            .register_type::<crate::RenderFallbackReason>()
            .register_type::<crate::RenderingCapabilities>()
            .add_message::<ProjectSettingsChanged>()
            .add_systems(PreStartup, load_settings);
    }
}

fn load_settings(mut commands: Commands, mut io: ResMut<ProjectSettingsIo>) {
    let (settings, source) = load_project_settings_with_source(&io.path);
    io.loaded_source = source;
    let tuning = SimTuning::from_physics(&settings.physics);
    commands.insert_resource(settings);
    commands.insert_resource(tuning);
}

/// Refreshes [`SimTuning`] from the current [`ProjectSettings`] resource.
pub fn sync_sim_tuning(settings: &ProjectSettings, mut tuning: ResMut<SimTuning>) {
    *tuning = SimTuning::from_physics(&settings.physics);
}

/// Low-level unconditional persistence for non-editor callers.
///
/// The editor uses its guarded authored-file publication path instead so external revisions cannot
/// be overwritten.
pub fn save_project_settings(
    settings: &ProjectSettings,
    io: &mut ProjectSettingsIo,
) -> Result<(), String> {
    let text = save_project_settings_to_string(settings).map_err(|error| error.to_string())?;
    std::fs::write(&io.path, &text).map_err(|error| error.to_string())?;
    io.loaded_source = Some(text);
    io.dirty = false;
    Ok(())
}
