use std::marker::PhantomData;

use bevy::{
    ecs::entity::Entities,
    prelude::*,
    tasks::{prelude::*, Task},
};
use bevy_rapier2d::{
    na::{Isometry2, Vector2},
    prelude::*,
    rapier::prelude::{ColliderHandle, ColliderSet, QueryPipeline},
};
use futures_lite::future;

use pathfinding::prelude::*;

use crate::{
    commands::RunIfExistsExt,
    core::{Coordinates, PointLike},
    map::{Map, MapObstruction},
    navigation::{RotationSpeed, Speed},
};

#[derive(Component, Debug, Deref, DerefMut)]
struct Calculating(Task<Option<Path>>);

#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Eq, Hash, PartialEq, Reflect)]
#[reflect(Component)]
pub struct Destination(pub (i32, i32));

impl_pointlike_for_tuple_component!(Destination);

#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct NoPath;

#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct Path(pub Vec<(i32, i32)>);

pub fn find_path<D: 'static + Clone + Default + Send + Sync>(
    start: &dyn PointLike,
    destination: &dyn PointLike,
    map: &Map<D>,
) -> Option<(Vec<(i32, i32)>, u32)> {
    astar(
        &start.into(),
        |p| {
            let mut successors: Vec<((i32, i32), u32)> = vec![];
            if map.at(p.0 as usize, p.1 as usize).is_walkable() {
                for tile in map.get_available_exits(p.0 as usize, p.1 as usize) {
                    successors.push(((tile.0 as i32, tile.1 as i32), (tile.2 * 100.) as u32));
                }
            }
            successors
        },
        |p| (p.distance_squared(destination) * 100.) as u32,
        |p| *p == destination.into(),
    )
}

fn find_path_for_shape<D: 'static + Clone + Default + Send + Sync>(
    initiator: ColliderHandle,
    start: Coordinates,
    destination: Destination,
    map: Map<D>,
    query_pipeline: QueryPipeline,
    collider_set: ColliderSet,
    shape: Collider,
) -> Option<Path> {
    let path = astar(
        &start.i32(),
        |p| {
            let mut successors: Vec<((i32, i32), u32)> = vec![];
            if map.at(p.0 as usize, p.1 as usize).is_walkable() {
                for tile in map.get_available_exits(p.0 as usize, p.1 as usize) {
                    let mut should_push = true;
                    let shape_pos = Isometry2::new(Vector2::new(tile.0 as f32, tile.1 as f32), 0.);
                    query_pipeline.intersections_with_shape(
                        &collider_set,
                        &shape_pos,
                        &*shape.raw,
                        InteractionGroups::all(),
                        Some(&|v| v != initiator),
                        |_handle| {
                            should_push = false;
                            false
                        },
                    );
                    if should_push {
                        successors.push(((tile.0 as i32, tile.1 as i32), (tile.2 * 100.) as u32));
                    }
                }
            }
            successors
        },
        |p| (p.distance_squared(&destination) * 100.) as u32,
        |p| *p == destination.i32(),
    );
    if let Some(path) = path {
        Some(Path(path.0))
    } else {
        None
    }
}

fn calculate_path<D: 'static + Clone + Default + Send + Sync>(
    mut commands: Commands,
    pool: Res<AsyncComputeTaskPool>,
    rapier_context: Res<RapierContext>,
    obstructions: Query<&MapObstruction>,
    query: Query<
        (
            Entity,
            &RapierColliderHandle,
            &Destination,
            &Coordinates,
            &Collider,
        ),
        Changed<Destination>,
    >,
    map: Query<&Map<D>>,
) {
    for (entity, handle, destination, coordinates, shape) in query.iter() {
        if coordinates.i32() == **destination {
            commands
                .entity(entity)
                .remove::<Path>()
                .remove::<NoPath>()
                .remove::<Calculating>()
                .remove::<Destination>()
                .remove::<Speed>();
            continue;
        }
        if let Ok(map) = map.get_single() {
            let coordinates_clone = *coordinates;
            let destination_clone = *destination;
            let query_pipeline = rapier_context.query_pipeline.clone();
            let map_clone = map.clone();
            let handle_clone = *handle;
            let collider_set = rapier_context.colliders.clone();
            let shape_clone = (*shape).clone();
            let task = pool.spawn(async move {
                find_path_for_shape(
                    handle_clone.0,
                    coordinates_clone,
                    destination_clone,
                    map_clone,
                    query_pipeline,
                    collider_set,
                    shape_clone,
                )
            });
            commands.run_if_exists(entity, |mut entity| {
                entity.insert(Calculating(task));
                entity.remove::<Path>();
                entity.remove::<NoPath>();
            });
        }
    }
}

fn poll_tasks(mut commands: Commands, mut query: Query<(Entity, &mut Calculating)>) {
    for (entity, mut calculating) in query.iter_mut() {
        if let Some(result) = future::block_on(future::poll_once(&mut **calculating)) {
            if let Some(path) = result {
                commands.entity(entity).insert(path);
            } else {
                commands.entity(entity).insert(NoPath);
            }
            commands.entity(entity).remove::<Calculating>();
        }
    }
}

fn remove_destination(
    mut commands: Commands,
    entities: &Entities,
    removed: RemovedComponents<Destination>,
) {
    for entity in removed.iter() {
        if entities.contains(entity) {
            commands.entity(entity).remove::<Calculating>();
        }
    }
}

fn negotiate_path(
    mut commands: Commands,
    mut query: Query<(
        Entity,
        &mut Path,
        &mut Transform,
        &mut Velocity,
        &Speed,
        Option<&RotationSpeed>,
    )>,
) {
    for (entity, mut path, mut transform, mut velocity, speed, rotation_speed) in query.iter_mut() {
        let start_i32 = (transform.translation.x, transform.translation.y).i32();
        let mut new_path = path
            .iter()
            .cloned()
            .skip_while(|v| *v == start_i32)
            .collect::<Vec<(i32, i32)>>();
        new_path.retain(|v| *v != start_i32);
        **path = new_path;
        if let Some(next) = path.first() {
            let start = Vec2::new(transform.translation.x, transform.translation.y);
            let next = Vec2::new(next.0 as f32 + 0.5, next.1 as f32 + 0.5);
            let mut direction = next - start;
            direction = direction.normalize();
            direction *= **speed;
            velocity.linvel = direction;
            if rotation_speed.is_some() {
                let v = next - start;
                let angle = v.y.atan2(v.x);
                transform.rotation = Quat::from_rotation_z(angle);
            }
            continue;
        } else {
            commands
                .entity(entity)
                .remove::<Path>()
                .remove::<NoPath>()
                .remove::<Destination>()
                .remove::<Speed>();
            velocity.linvel = Vec2::ZERO;
        }
    }
}

pub struct PathfindingPlugin<D: 'static + Clone + Default + Send + Sync>(PhantomData<D>);

impl<D: 'static + Clone + Default + Send + Sync> Default for PathfindingPlugin<D> {
    fn default() -> Self {
        Self(Default::default())
    }
}

impl<D: 'static + Clone + Default + Send + Sync> Plugin for PathfindingPlugin<D> {
    fn build(&self, app: &mut App) {
        app.add_system(calculate_path::<D>)
            .add_system_to_stage(CoreStage::PostUpdate, remove_destination)
            .add_system(poll_tasks)
            .add_system(negotiate_path);
    }
}