use ahash::AHashMap;
use bevy::{
    prelude::*,
    tasks::{futures_lite::future, AsyncComputeTaskPool, Task},
};
use de_core::{
    gamestate::GameState,
    objects::MovableSolid,
    schedule::{PostMovement, PreMovement},
    state::AppState,
};
use de_types::{path::Path, projection::ToFlat};
use crate::{
    fplugin::{FinderRes, FinderSet, PathFinderUpdatedEvent},
    path::ScheduledPath,
    PathQueryProps, PathTarget,
};
const TARGET_TOLERANCE: f32 = 2.;
pub struct PathingPlugin;
impl Plugin for PathingPlugin {
    fn build(&self, app: &mut App) {
        app.add_event::<UpdateEntityPathEvent>()
            .add_event::<PathFoundEvent>()
            .add_systems(OnEnter(AppState::InGame), setup)
            .add_systems(OnExit(AppState::InGame), cleanup)
            .add_systems(
                PreMovement,
                (
                    update_existing_paths
                        .run_if(on_event::<PathFinderUpdatedEvent>())
                        .in_set(PathingSet::UpdateExistingPaths)
                        .after(FinderSet::UpdateFinder),
                    update_requested_paths
                        .in_set(PathingSet::UpdateRequestedPaths)
                        .after(PathingSet::UpdateExistingPaths),
                    check_path_results
                        .in_set(PathingSet::PathResults)
                        .after(PathingSet::UpdateExistingPaths),
                    update_path_components
                        .after(PathingSet::PathResults)
                        .before(PathingSet::UpdateRequestedPaths),
                )
                    .run_if(in_state(GameState::Playing)),
            )
            .add_systems(
                PostMovement,
                remove_path_targets.run_if(in_state(AppState::InGame)),
            );
    }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, SystemSet)]
pub(crate) enum PathingSet {
    UpdateRequestedPaths,
    UpdateExistingPaths,
    PathResults,
}
#[derive(Event)]
pub struct UpdateEntityPathEvent {
    entity: Entity,
    target: PathTarget,
}
impl UpdateEntityPathEvent {
    pub fn new(entity: Entity, target: PathTarget) -> Self {
        Self { entity, target }
    }
    fn entity(&self) -> Entity {
        self.entity
    }
    fn target(&self) -> PathTarget {
        self.target
    }
}
#[derive(Event)]
pub(crate) struct PathFoundEvent {
    entity: Entity,
    path: Option<Path>,
}
impl PathFoundEvent {
    fn new(entity: Entity, path: Option<Path>) -> Self {
        Self { entity, path }
    }
    pub(crate) fn entity(&self) -> Entity {
        self.entity
    }
    pub(crate) fn path(&self) -> Option<&Path> {
        self.path.as_ref()
    }
}
#[derive(Default, Resource)]
struct UpdatePathsState {
    tasks: AHashMap<Entity, UpdatePathTask>,
}
impl UpdatePathsState {
    fn contains(&self, entity: Entity) -> bool {
        self.tasks.contains_key(&entity)
    }
    fn spawn_new(&mut self, finder: FinderRes, entity: Entity, source: Vec2, target: PathTarget) {
        let pool = AsyncComputeTaskPool::get();
        let task = pool.spawn(async move { finder.find_path(source, target) });
        self.tasks.insert(entity, UpdatePathTask::new(task));
    }
    fn check_results(&mut self) -> Vec<(Entity, Option<Path>)> {
        let mut results = Vec::new();
        self.tasks.retain(|&entity, task| match task.check() {
            UpdatePathState::Resolved(path) => {
                results.push((entity, path));
                false
            }
            UpdatePathState::Processing => true,
        });
        results
    }
}
struct UpdatePathTask(Task<Option<Path>>);
impl UpdatePathTask {
    fn new(task: Task<Option<Path>>) -> Self {
        Self(task)
    }
    fn check(&mut self) -> UpdatePathState {
        match future::block_on(future::poll_once(&mut self.0)) {
            Some(path) => UpdatePathState::Resolved(path),
            None => UpdatePathState::Processing,
        }
    }
}
enum UpdatePathState {
    Resolved(Option<Path>),
    Processing,
}
fn setup(mut commands: Commands) {
    commands.init_resource::<UpdatePathsState>()
}
fn cleanup(mut commands: Commands) {
    commands.remove_resource::<UpdatePathsState>();
}
fn update_existing_paths(
    finder: Res<FinderRes>,
    mut state: ResMut<UpdatePathsState>,
    entities: Query<(Entity, &Transform, &PathTarget, Has<ScheduledPath>)>,
) {
    for (entity, transform, target, has_path) in entities.iter() {
        let position = transform.translation.to_flat();
        if !has_path && !state.contains(entity) {
            let current_distance = position.distance(target.location());
            let desired_distance = target.properties().distance();
            if (current_distance - desired_distance).abs() <= TARGET_TOLERANCE {
                continue;
            }
        }
        let new_target = PathTarget::new(
            target.location(),
            PathQueryProps::new(target.properties().distance(), f32::INFINITY),
            target.permanent(),
        );
        state.spawn_new(finder.clone(), entity, position, new_target);
    }
}
fn update_requested_paths(
    mut commands: Commands,
    finder: Res<FinderRes>,
    mut state: ResMut<UpdatePathsState>,
    mut events: EventReader<UpdateEntityPathEvent>,
    entities: Query<&Transform, With<MovableSolid>>,
) {
    for event in events.read() {
        if let Ok(transform) = entities.get(event.entity()) {
            commands.entity(event.entity()).insert(event.target());
            state.spawn_new(
                finder.clone(),
                event.entity(),
                transform.translation.to_flat(),
                event.target(),
            );
        }
    }
}
fn check_path_results(
    mut state: ResMut<UpdatePathsState>,
    mut events: EventWriter<PathFoundEvent>,
) {
    for (entity, path) in state.check_results() {
        events.send(PathFoundEvent::new(entity, path));
    }
}
fn update_path_components(
    mut commands: Commands,
    targets: Query<&PathTarget>,
    mut events: EventReader<PathFoundEvent>,
) {
    for event in events.read() {
        let mut entity_commands = commands.entity(event.entity());
        match event.path() {
            Some(path) => {
                entity_commands.insert(ScheduledPath::new(path.clone()));
            }
            None => {
                entity_commands.remove::<ScheduledPath>();
                if let Ok(target) = targets.get(event.entity()) {
                    if !target.permanent() {
                        entity_commands.remove::<PathTarget>();
                    }
                }
            }
        }
    }
}
fn remove_path_targets(
    mut commands: Commands,
    targets: Query<&PathTarget>,
    mut removed: RemovedComponents<ScheduledPath>,
) {
    for entity in removed.read() {
        if let Ok(target) = targets.get(entity) {
            if !target.permanent() {
                commands.entity(entity).remove::<PathTarget>();
            }
        }
    }
}