//! TestbedApp - the main application runner.

use crate::Camera;
use crate::debug_render::{DebugRenderPipelineResource, debug_render_scene};
use crate::graphics::GraphicsManager;
use crate::harness::Harness;
use crate::mouse::SceneMouse;
use crate::save::SerializableTestbedState;
use crate::testbed::hover::highlight_hovered_body;
use crate::ui;
use kiss3d::color::Color;
use kiss3d::event::{Action, Key, WindowEvent};
use kiss3d::window::Window;
use rapier::dynamics::RigidBodyActivation;
use std::mem;

use super::Plugins;
use super::graphics_context::TestbedGraphics;
use super::keys::KeysState;
use super::state::{RAPIER_BACKEND, RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags};
use super::testbed::{SimulationBuilders, Testbed};

#[cfg(feature = "other-backends")]
use super::OtherBackends;

#[cfg(all(feature = "dim3", feature = "other-backends"))]
use super::state::{PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR};

/// The main testbed application
pub struct TestbedApp {
    builders: SimulationBuilders,
    graphics: GraphicsManager,
    state: TestbedState,
    harness: Harness,
    #[cfg(feature = "other-backends")]
    other_backends: OtherBackends,
    plugins: Plugins,
}

impl TestbedApp {
    pub fn save_file_path() -> String {
        format!("testbed_state_{}.autosave.json", env!("CARGO_CRATE_NAME"))
    }

    pub fn new_empty() -> Self {
        let graphics = GraphicsManager::new();
        let state = TestbedState::default();
        let harness = Harness::new_empty();
        #[cfg(feature = "other-backends")]
        let other_backends = OtherBackends {
            #[cfg(feature = "dim3")]
            physx: None,
        };

        TestbedApp {
            builders: Vec::new(),
            plugins: Plugins(Vec::new()),
            graphics,
            state,
            harness,
            #[cfg(feature = "other-backends")]
            other_backends,
        }
    }

    pub fn from_builders(builders: SimulationBuilders) -> Self {
        let mut res = TestbedApp::new_empty();
        res.set_builders(builders);
        res
    }

    pub fn set_builders(&mut self, builders: SimulationBuilders) {
        use super::state::ExampleEntry;
        use indexmap::IndexSet;

        // Collect unique groups in order of first appearance
        let mut groups: IndexSet<&'static str> = IndexSet::new();
        for example in &builders {
            groups.insert(example.group);
        }

        // Build the display order: group by group, preserving original order within each group
        let mut examples = Vec::new();
        for group in &groups {
            for (builder_index, example) in builders.iter().enumerate() {
                if example.group == *group {
                    examples.push(ExampleEntry {
                        name: example.name,
                        group: example.group,
                        builder_index,
                    });
                }
            }
        }

        self.state.example_groups = groups.into_iter().collect();
        self.state.examples = examples;
        self.builders = builders;
    }

    pub async fn run(self) {
        self.run_with_init(|_| {}).await
    }

    pub async fn run_with_init(mut self, init: impl FnMut(&mut Testbed)) {
        // Install a default logger so warnings emitted by the example
        // builders (e.g. the MJCF loader complaining about missing mesh
        // files or features) actually print. Uses RUST_LOG if set, falls
        // back to `warn` level otherwise. `try_init` yields silently to a
        // user-installed logger so embedders aren't forced into this
        // backend.
        let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn"))
            .try_init();

        #[cfg(feature = "profiler_ui")]
        profiling::puffin::set_scopes_on(true);

        // Check for benchmark mode
        let args: Vec<String> = std::env::args().collect();
        if args.iter().any(|a| a == "--bench") {
            self.run_benchmark();
            return;
        }

        self.run_async(init).await
    }

    fn run_benchmark(&mut self) {
        use std::fs::File;
        use std::io::{BufWriter, Write};

        let num_bench_iters = 1000u32;
        let builders = mem::take(&mut self.builders);
        let backend_names = self.state.backend_names.clone();

        for builder in &builders {
            let mut results = Vec::new();
            println!("Running benchmark for {}", builder.name);

            for (backend_id, backend) in backend_names.iter().enumerate() {
                println!("|_ using backend {backend}");
                self.state.selected_backend = backend_id;
                self.harness = Harness::new_empty();

                let mut testbed = Testbed {
                    graphics: None,
                    state: &mut self.state,
                    harness: &mut self.harness,
                    #[cfg(feature = "other-backends")]
                    other_backends: &mut self.other_backends,
                    plugins: &mut self.plugins,
                };
                (builder.builder)(&mut testbed);

                let mut timings = Vec::new();
                for k in 0..num_bench_iters {
                    if self.state.selected_backend == RAPIER_BACKEND {
                        self.harness.step();
                    }

                    #[cfg(all(feature = "dim3", feature = "other-backends"))]
                    {
                        if self.state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
                            || self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR
                        {
                            self.other_backends.physx.as_mut().unwrap().step(
                                &mut self.harness.physics.pipeline.counters,
                                &self.harness.physics.integration_parameters,
                            );
                            self.other_backends.physx.as_mut().unwrap().sync(
                                &mut self.harness.physics.bodies,
                                &mut self.harness.physics.colliders,
                            );
                        }
                    }

                    if k > 0 {
                        timings.push(self.harness.physics.pipeline.counters.step_time.time_ms());
                    }
                }
                results.push(timings);
            }

            use inflector::Inflector;
            let filename = format!("{}.csv", builder.name.to_camel_case());
            let mut file = BufWriter::new(File::create(filename).unwrap());

            write!(file, "{}", backend_names[0]).unwrap();
            for backend in &backend_names[1..] {
                write!(file, ",{backend}").unwrap();
            }
            writeln!(file).unwrap();

            for i in 0..results[0].len() {
                write!(file, "{}", results[0][i]).unwrap();
                for result in &results[1..] {
                    write!(file, ",{}", result[i]).unwrap();
                }
                writeln!(file).unwrap();
            }
        }
    }

    async fn run_async(mut self, mut init: impl FnMut(&mut Testbed)) {
        let title = if cfg!(feature = "dim2") {
            "Rapier: 2D demos"
        } else {
            "Rapier: 3D demos"
        };

        let mut window = Window::new_with_size(title, 1280, 720).await;
        window.set_background_color(Color::new(245.0 / 255.0, 245.0 / 255.0, 236.0 / 255.0, 1.0));
        window.set_ambient(0.1);

        let mut debug_render = DebugRenderPipelineResource::default();
        let mut camera = Camera::default();
        let mut scene_mouse = SceneMouse::new();
        let mut keys = KeysState::default();

        // User init
        let testbed_gfx = TestbedGraphics {
            graphics: &mut self.graphics,
            window: &mut window,
            camera: &mut camera,
            mouse: &mut scene_mouse,
            keys: &mut keys,
            settings: None,
        };

        let mut testbed = Testbed {
            graphics: Some(testbed_gfx),
            state: &mut self.state,
            harness: &mut self.harness,
            #[cfg(feature = "other-backends")]
            other_backends: &mut self.other_backends,
            plugins: &mut self.plugins,
        };

        init(&mut testbed);

        // Main render loop
        #[cfg(feature = "dim3")]
        while window
            .render_3d(self.graphics.scene_mut(), &mut camera)
            .await
        {
            self.run_frame(
                &mut window,
                &mut debug_render,
                &mut camera,
                &mut scene_mouse,
                &mut keys,
            );
        }

        #[cfg(feature = "dim2")]
        while window
            .render_2d(self.graphics.scene_mut(), &mut camera)
            .await
        {
            self.run_frame(
                &mut window,
                &mut debug_render,
                &mut camera,
                &mut scene_mouse,
                &mut keys,
            );
        }
    }

    fn run_frame(
        &mut self,
        window: &mut Window,
        debug_render: &mut DebugRenderPipelineResource,
        camera: &mut Camera,
        scene_mouse: &mut SceneMouse,
        keys: &mut KeysState,
    ) {
        profiling::finish_frame!();

        // Handle input events
        self.handle_events(window, keys);

        // Handle the vehicle controller if there is one.
        #[cfg(feature = "dim3")]
        {
            self.update_vehicle_controller(keys);
        }

        // Update mouse state
        let cursor_pos = window.cursor_pos();
        scene_mouse.update_from_window(cursor_pos, window.size().into(), camera);

        // Handle action flags
        self.handle_action_flags(window, camera, scene_mouse, keys);

        // Handle sleep settings
        self.handle_sleep_settings();

        // Run simulation
        if self.state.running != RunMode::Stop {
            for _ in 0..self.state.nsteps {
                if self.state.selected_backend == RAPIER_BACKEND {
                    let mut testbed_gfx = TestbedGraphics {
                        graphics: &mut self.graphics,
                        window,
                        camera,
                        mouse: scene_mouse,
                        keys,
                        settings: Some(&mut self.state.example_settings),
                    };
                    self.harness.step_with_graphics(Some(&mut testbed_gfx));

                    for plugin in &mut self.plugins.0 {
                        plugin.step(&mut self.harness.physics);
                    }
                }

                #[cfg(all(feature = "dim3", feature = "other-backends"))]
                {
                    if self.state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
                        || self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR
                    {
                        self.other_backends.physx.as_mut().unwrap().step(
                            &mut self.harness.physics.pipeline.counters,
                            &self.harness.physics.integration_parameters,
                        );
                        self.other_backends.physx.as_mut().unwrap().sync(
                            &mut self.harness.physics.bodies,
                            &mut self.harness.physics.colliders,
                        );
                    }
                }

                for plugin in &mut self.plugins.0 {
                    plugin.run_callbacks(&mut self.harness);
                }
            }

            if self.state.running == RunMode::Step {
                self.state.running = RunMode::Stop;
            }
        }

        // Autosave state.
        #[cfg(not(target_arch = "wasm32"))]
        {
            let new_save_data = self.state.save_data(*camera);
            if self.state.prev_save_data != new_save_data {
                // Save the data in a file.
                let data = serde_json::to_string_pretty(&new_save_data).unwrap();
                if let Err(e) = std::fs::write(Self::save_file_path(), &data) {
                    eprintln!("Failed to write autosave file: {}", e);
                }
                self.state.prev_save_data = new_save_data;
            }
        }

        highlight_hovered_body(&mut self.graphics, scene_mouse, &self.harness.physics);

        // Update graphics
        self.graphics.draw(
            self.state.flags,
            &self.harness.physics.bodies,
            &self.harness.physics.colliders,
        );

        // Draw debug render
        debug_render_scene(window, debug_render, &self.harness);

        // Draw UI
        window.draw_ui(|ctx| {
            ui::update_ui(ctx, &mut self.state, &mut self.harness, debug_render);
        });

        self.state.prev_flags = self.state.flags;
    }

    fn handle_events(&mut self, window: &mut Window, keys: &mut KeysState) {
        for event in window.events().iter() {
            match event.value {
                WindowEvent::Key(key, Action::Press, _) => {
                    // Track pressed keys
                    if !keys.pressed_keys.contains(&key) {
                        keys.pressed_keys.push(key);
                    }
                    // Update modifier states
                    match key {
                        Key::LShift | Key::RShift => keys.shift = true,
                        Key::LControl | Key::RControl => keys.ctrl = true,
                        Key::LAlt | Key::RAlt => keys.alt = true,
                        _ => {}
                    }
                }
                WindowEvent::Key(key, Action::Release, _) => {
                    // Remove from pressed keys
                    keys.pressed_keys.retain(|k| *k != key);
                    // Handle special keys
                    match key {
                        Key::T => {
                            if self.state.running == RunMode::Stop {
                                self.state.running = RunMode::Running;
                            } else {
                                self.state.running = RunMode::Stop;
                            }
                        }
                        Key::S => {
                            self.state.running = RunMode::Step;
                        }
                        Key::R => {
                            self.state
                                .action_flags
                                .set(TestbedActionFlags::EXAMPLE_CHANGED, true);
                        }
                        Key::LShift | Key::RShift => keys.shift = false,
                        Key::LControl | Key::RControl => keys.ctrl = false,
                        Key::LAlt | Key::RAlt => keys.alt = false,
                        _ => {}
                    }
                }
                _ => {}
            }
        }
    }

    #[cfg(feature = "dim3")]
    fn update_vehicle_controller(&mut self, keys: &mut KeysState) {
        use rapier::prelude::QueryFilter;

        if self.state.running == RunMode::Stop {
            return;
        }

        if let Some(vehicle) = &mut self.state.vehicle_controller {
            let mut engine_force = 0.0;
            let mut steering_angle = 0.0;

            println!("Pressed: {:?}", keys);
            if keys.pressed(Key::Right) {
                steering_angle += -0.7;
            }
            if keys.pressed(Key::Left) {
                steering_angle += 0.7;
            }
            if keys.pressed(Key::Up) {
                engine_force += 30.0;
            }
            if keys.pressed(Key::Down) {
                engine_force += -30.0;
            }

            let wheels = vehicle.wheels_mut();
            wheels[0].engine_force = engine_force;
            wheels[0].steering = steering_angle;
            wheels[1].engine_force = engine_force;
            wheels[1].steering = steering_angle;

            let query_pipeline = self.harness.physics.broad_phase.as_query_pipeline_mut(
                self.harness.physics.narrow_phase.query_dispatcher(),
                &mut self.harness.physics.bodies,
                &mut self.harness.physics.colliders,
                QueryFilter::exclude_dynamic().exclude_rigid_body(vehicle.chassis),
            );

            vehicle.update_vehicle(
                self.harness.physics.integration_parameters.dt,
                query_pipeline,
            );
        }
    }

    fn handle_action_flags(
        &mut self,
        window: &mut Window,
        camera: &mut Camera,
        scene_mouse: &mut SceneMouse,
        keys: &mut KeysState,
    ) {
        #[cfg(not(target_arch = "wasm32"))]
        {
            let app_started = self
                .state
                .action_flags
                .contains(TestbedActionFlags::APP_STARTED);

            if app_started {
                self.state
                    .action_flags
                    .set(TestbedActionFlags::APP_STARTED, false);
                if let Some(saved_state) = std::fs::read(Self::save_file_path())
                    .ok()
                    .and_then(|data| serde_json::from_slice::<SerializableTestbedState>(&data).ok())
                {
                    self.state.apply_saved_data(saved_state, camera);
                    self.state.camera_locked = true;
                }
            }
        }

        let backend_changed = self
            .state
            .action_flags
            .contains(TestbedActionFlags::BACKEND_CHANGED);
        if backend_changed {
            self.state
                .action_flags
                .set(TestbedActionFlags::BACKEND_CHANGED, false);
            self.state
                .action_flags
                .set(TestbedActionFlags::EXAMPLE_CHANGED, true);
            self.state.camera_locked = true;
        }

        let restarted = self
            .state
            .action_flags
            .contains(TestbedActionFlags::RESTART);
        if restarted {
            self.state
                .action_flags
                .set(TestbedActionFlags::RESTART, false);
            self.state.camera_locked = true;
            self.state
                .action_flags
                .set(TestbedActionFlags::EXAMPLE_CHANGED, true);
        }

        let example_changed = self
            .state
            .action_flags
            .contains(TestbedActionFlags::EXAMPLE_CHANGED);
        if example_changed {
            self.state
                .action_flags
                .set(TestbedActionFlags::EXAMPLE_CHANGED, false);
            self.clear(window);
            self.harness.clear_callbacks();

            if !self.state.camera_locked {
                *camera = Camera::default();
            }

            if !restarted {
                self.state.example_settings.clear();
            }

            // Clamp selected_display_index to valid range
            let max_index = self.state.examples.len().saturating_sub(1);
            self.state.selected_display_index = self.state.selected_display_index.min(max_index);

            if !self.builders.is_empty() {
                let builder_index = self.state.selected_builder_index();
                let builder = self.builders[builder_index].builder;
                let testbed_gfx = TestbedGraphics {
                    graphics: &mut self.graphics,
                    window,
                    camera,
                    mouse: scene_mouse,
                    keys,
                    settings: None,
                };

                let mut testbed = Testbed {
                    graphics: Some(testbed_gfx),
                    state: &mut self.state,
                    harness: &mut self.harness,
                    #[cfg(feature = "other-backends")]
                    other_backends: &mut self.other_backends,
                    plugins: &mut self.plugins,
                };
                builder(&mut testbed);
            }

            self.state.camera_locked = false;
        }

        if self
            .state
            .action_flags
            .contains(TestbedActionFlags::RESET_WORLD_GRAPHICS)
        {
            self.state
                .action_flags
                .set(TestbedActionFlags::RESET_WORLD_GRAPHICS, false);
            for (handle, _) in self.harness.physics.bodies.iter() {
                self.graphics.add_body_colliders(
                    window,
                    handle,
                    &self.harness.physics.bodies,
                    &self.harness.physics.colliders,
                );
            }

            for (handle, co) in self.harness.physics.colliders.iter() {
                if co.parent().is_none() {
                    self.graphics
                        .add_collider(window, handle, &self.harness.physics.colliders);
                }
            }
        }

        if self
            .state
            .action_flags
            .contains(TestbedActionFlags::TAKE_SNAPSHOT)
        {
            self.state
                .action_flags
                .set(TestbedActionFlags::TAKE_SNAPSHOT, false);
            self.state.snapshot = Some(self.harness.physics.snapshot());
        }

        if self
            .state
            .action_flags
            .contains(TestbedActionFlags::RESTORE_SNAPSHOT)
        {
            self.state
                .action_flags
                .set(TestbedActionFlags::RESTORE_SNAPSHOT, false);
            if let Some(snapshot) = &self.state.snapshot {
                self.harness.physics.restore_snapshot(snapshot.clone());
                self.state
                    .action_flags
                    .set(TestbedActionFlags::RESET_WORLD_GRAPHICS, true);
            }
        }

        if example_changed
            || self.state.prev_flags.contains(TestbedStateFlags::WIREFRAME)
                != self.state.flags.contains(TestbedStateFlags::WIREFRAME)
        {
            self.graphics.toggle_wireframe_mode(
                &self.harness.physics.colliders,
                self.state.flags.contains(TestbedStateFlags::WIREFRAME),
            );
        }

        if self
            .state
            .action_flags
            .contains(TestbedActionFlags::FRAME_SCENE)
        {
            self.state
                .action_flags
                .set(TestbedActionFlags::FRAME_SCENE, false);
            frame_scene(
                &self.harness.physics.colliders,
                &self.harness.physics.bodies,
                &self.graphics,
                camera,
            );
        }
    }

    fn handle_sleep_settings(&mut self) {
        if self.state.prev_flags.contains(TestbedStateFlags::SLEEP)
            != self.state.flags.contains(TestbedStateFlags::SLEEP)
        {
            if self.state.flags.contains(TestbedStateFlags::SLEEP) {
                for (_, body) in self.harness.physics.bodies.iter_mut() {
                    body.activation_mut().normalized_linear_threshold =
                        RigidBodyActivation::default_normalized_linear_threshold();
                    body.activation_mut().angular_threshold =
                        RigidBodyActivation::default_angular_threshold();
                }
            } else {
                for (_, body) in self.harness.physics.bodies.iter_mut() {
                    body.wake_up(true);
                    body.activation_mut().normalized_linear_threshold = -1.0;
                }
            }
        }
    }

    fn clear(&mut self, window: &mut Window) {
        self.state.can_grab_behind_ground = false;
        self.graphics.clear();

        for mut plugin in self.plugins.0.drain(..) {
            plugin.clear_graphics(&mut self.graphics, window);
        }
    }
}

/// Recenter the camera so the AABB of all the collider and visual-mesh
/// shapes in the scene fills the viewport. Acts as a "frame all" /
/// "best view" command.
fn frame_scene(
    colliders: &rapier::geometry::ColliderSet,
    bodies: &rapier::dynamics::RigidBodySet,
    graphics: &crate::graphics::GraphicsManager,
    camera: &mut Camera,
) {
    use rapier::parry::bounding_volume::BoundingVolume;
    use rapier::parry::math::Real;

    // Helper: skip AABBs with non-finite extents (half-spaces, broken
    // shapes) so they don't blow up the union.
    fn is_finite_aabb(a: &rapier::parry::bounding_volume::Aabb) -> bool {
        if !a.mins.x.is_finite()
            || !a.mins.y.is_finite()
            || !a.maxs.x.is_finite()
            || !a.maxs.y.is_finite()
        {
            return false;
        }
        #[cfg(feature = "dim3")]
        if !a.mins.z.is_finite() || !a.maxs.z.is_finite() {
            return false;
        }
        true
    }

    let mut aabb: Option<rapier::parry::bounding_volume::Aabb> = None;
    let mut merge = |a: rapier::parry::bounding_volume::Aabb| {
        if !is_finite_aabb(&a) {
            return;
        }
        aabb = Some(match aabb {
            None => a,
            Some(prev) => prev.merged(&a),
        });
    };
    // 1. Physical colliders.
    for (_, co) in colliders.iter() {
        merge(co.compute_aabb());
    }
    // 2. Render-only meshes attached to a body — they're not in the
    //    collider set but still visible, and their extent can exceed
    //    the colliders' (e.g. simplified primitive colliders + full
    //    visual mesh). Compute each one's AABB at its current world
    //    pose: `body.position * vm.delta`.
    for vm in graphics.body_attached_nodes() {
        let Some(rb) = bodies.get(vm.body) else {
            continue;
        };
        let world_pose = *rb.position() * vm.delta;
        merge(vm.shape.compute_aabb(&world_pose));
    }
    let Some(aabb) = aabb else {
        return;
    };

    let center = aabb.center();
    let extents = aabb.extents();

    #[cfg(feature = "dim3")]
    {
        // Bounding-sphere radius around the AABB center.
        let half = extents * 0.5 as Real;
        let radius = (half.x * half.x + half.y * half.y + half.z * half.z)
            .sqrt()
            .max(0.5);
        let fov = camera.fov();
        // Distance such that the bounding sphere subtends the camera's
        // vertical FoV exactly. A small margin (1.1×) prevents the AABB
        // edges from being clipped by the frustum / window aspect.
        let dist = radius / (fov as Real * 0.5).sin().max(1e-3) * 1.1;

        let kiss_center =
            kiss3d::glamx::Vec3::new(center.x as f32, center.y as f32, center.z as f32);
        // Approach from along +X / +Y / +Z roughly equally so any axis-up
        // convention reads naturally.
        let dir = kiss3d::glamx::Vec3::new(1.0, 1.0, 1.0).normalize();
        let eye = kiss_center + dir * dist as f32;
        camera.look_at(eye, kiss_center);
    }

    #[cfg(feature = "dim2")]
    {
        let kiss_center = kiss3d::glamx::Vec2::new(center.x as f32, center.y as f32);
        // PanZoomCamera2d's "zoom" is pixels per world unit. Pick a zoom
        // that fits the larger of the two extents in the smallest framebuffer
        // dimension we're aware of (assume 800×600 default).
        let max_extent = extents.x.max(extents.y).max(0.5);
        let zoom = (600.0 as Real / max_extent / 1.2) as f32;
        camera.look_at(kiss_center, zoom);
    }
}
