use std::ops::{Deref, DerefMut};

use bevy_app::{App, Plugin, SubApp};
use bevy_ecs::{
    resource::Resource,
    schedule::{IntoScheduleConfigs, Schedule, ScheduleBuildSettings, ScheduleLabel, Schedules},
    world::{Mut, World},
};
use bevy_utils::default;

use crate::{
    Render, RenderApp, RenderSystems, ScratchMainWorld, SyncWorldPlugin,
    sync_world::{despawn_temporary_render_entities, entity_sync_system},
};

/// The simulation [`World`] of the application, stored as a resource.
///
/// This resource is only available during [`ExtractSchedule`] and not
/// during command application of that schedule.
/// See [`Extract`] for more details.
#[derive(Resource, Default)]
pub struct MainWorld(World);

impl Deref for MainWorld {
    type Target = World;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for MainWorld {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

/// Schedule which extract data from the main world and inserts it into the render world.
///
/// This step should be kept as short as possible to increase the "pipelining potential" for
/// running the next frame while rendering the current frame.
///
/// This schedule is run on the main world, but its buffers are not applied
/// until it is returned to the render world.
#[derive(ScheduleLabel, PartialEq, Eq, Debug, Clone, Hash, Default)]
pub struct ExtractSchedule;

/// Executes the [`ExtractSchedule`] step of the renderer.
/// This updates the render world with the extracted ECS data of the current frame.
fn extract(main_world: &mut World, render_world: &mut World) {
    // temporarily add the app world to the render world as a resource
    let scratch_world = main_world.remove_resource::<ScratchMainWorld>().unwrap();
    let inserted_world = core::mem::replace(main_world, scratch_world.0);
    render_world.insert_resource(MainWorld(inserted_world));
    render_world.run_schedule(ExtractSchedule);

    // move the app world back, as if nothing happened.
    let inserted_world = render_world.remove_resource::<MainWorld>().unwrap();
    let scratch_world = core::mem::replace(main_world, inserted_world.0);
    main_world.insert_resource(ScratchMainWorld(scratch_world));
}

/// Applies the commands from the extract schedule. This happens during
/// the render schedule rather than during extraction to allow the commands to run in parallel with the
/// main app when pipelined rendering is enabled.
fn apply_extract_commands(render_world: &mut World) {
    render_world.resource_scope(|render_world, mut schedules: Mut<Schedules>| {
        schedules
            .get_mut(ExtractSchedule)
            .unwrap()
            .apply_deferred(render_world);
    });
}

/// Plugin that sets up the [`RenderApp`] and handles extracting data from the
/// main world to the render world.
pub struct ExtractPlugin {
    /// Function that gets run at the beginning of each extraction.
    ///
    /// Gets the main world and render world as arguments (in that order).
    pub pre_extract: fn(&mut World, &mut World),
}

impl Default for ExtractPlugin {
    fn default() -> Self {
        Self {
            pre_extract: |_, _| {},
        }
    }
}

impl Plugin for ExtractPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins(SyncWorldPlugin);
        app.init_resource::<ScratchMainWorld>();

        let mut render_app = SubApp::new();

        let mut extract_schedule = Schedule::new(ExtractSchedule);
        // We skip applying any commands during the ExtractSchedule
        // so commands can be applied on the render thread.
        extract_schedule.set_build_settings(ScheduleBuildSettings {
            auto_insert_apply_deferred: false,
            ..default()
        });
        extract_schedule.set_apply_final_deferred(false);

        render_app.add_schedule(Render::base_schedule());
        render_app.add_schedule(extract_schedule);
        render_app.add_systems(
            Render,
            (
                // This set applies the commands from the extract schedule while the render schedule
                // is running in parallel with the main app.
                apply_extract_commands.in_set(RenderSystems::ExtractCommands),
                despawn_temporary_render_entities.in_set(RenderSystems::PostCleanup),
            ),
        );

        let pre_extract = self.pre_extract;
        render_app.set_extract(move |main_world, render_world| {
            pre_extract(main_world, render_world);

            {
                #[cfg(feature = "trace")]
                let _stage_span = bevy_log::info_span!("entity_sync").entered();
                entity_sync_system(main_world, render_world);
            }

            // run extract schedule
            extract(main_world, render_world);
        });

        let (sender, receiver) = bevy_time::create_time_channels();
        render_app.insert_resource(sender);
        app.insert_resource(receiver);
        app.insert_sub_app(RenderApp, render_app);
    }
}
