use std::collections::HashMap;

use bevy::{
    prelude::*,
    tasks::{prelude::*, Task},
};
use bevy_rapier2d::{
    na::UnitComplex,
    prelude::*,
    rapier::data::{ComponentSet, ComponentSetOption, Index},
};
use derive_more::{Deref, DerefMut};
use futures_lite::future;
use pathfinding::prelude::*;

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

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

#[derive(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(Clone, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct NoPath;

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

pub fn find_path(
    start: &dyn PointLike,
    destination: &dyn PointLike,
    map: &Map,
) -> Option<(Vec<(i32, i32)>, u32)> {
    astar(
        &start.into(),
        |p| {
            let mut successors: Vec<((i32, i32), u32)> = vec![];
            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(),
    )
}

struct StaticColliderComponentsSet(
    HashMap<Entity, (ColliderPosition, ColliderShape, ColliderFlags)>,
);

impl
    From<(
        &Query<'_, (Entity, &ColliderPosition, &SharedShape, &ColliderFlags)>,
        &Query<'_, &MapObstruction>,
    )> for StaticColliderComponentsSet
{
    fn from(
        query: (
            &Query<(Entity, &ColliderPosition, &SharedShape, &ColliderFlags)>,
            &Query<&MapObstruction>,
        ),
    ) -> Self {
        let entries = query
            .0
            .iter()
            .filter(|(a, _, _, _)| query.1.get(*a).is_ok())
            .map(|(a, b, c, d)| (a, *b, c.clone(), *d))
            .collect::<Vec<(Entity, ColliderPosition, ColliderShape, ColliderFlags)>>();
        let mut m = HashMap::new();
        for (e, a, b, c) in entries {
            m.insert(e, (a, b, c));
        }
        Self(m)
    }
}

impl ComponentSet<SharedShape> for StaticColliderComponentsSet {
    fn size_hint(&self) -> usize {
        self.0.len()
    }

    fn for_each(&self, _f: impl FnMut(Index, &SharedShape)) {
        unimplemented!()
    }
}

impl ComponentSetOption<SharedShape> for StaticColliderComponentsSet {
    fn get(&self, index: Index) -> Option<&SharedShape> {
        self.0.get(&index.entity()).map(|v| &v.1)
    }
}

impl ComponentSet<ColliderFlags> for StaticColliderComponentsSet {
    fn size_hint(&self) -> usize {
        self.0.len()
    }

    fn for_each(&self, _f: impl FnMut(Index, &ColliderFlags)) {
        unimplemented!()
    }
}

impl ComponentSetOption<ColliderFlags> for StaticColliderComponentsSet {
    fn get(&self, index: Index) -> Option<&ColliderFlags> {
        self.0.get(&index.entity()).map(|v| &v.2)
    }
}

impl ComponentSet<ColliderPosition> for StaticColliderComponentsSet {
    fn size_hint(&self) -> usize {
        self.0.len()
    }

    fn for_each(&self, _f: impl FnMut(Index, &ColliderPosition)) {
        unimplemented!()
    }
}

impl ComponentSetOption<ColliderPosition> for StaticColliderComponentsSet {
    fn get(&self, index: Index) -> Option<&ColliderPosition> {
        let v = self.0.get(&index.entity()).map(|v| &v.0);
        v
    }
}

fn find_path_for_shape(
    initiator: Entity,
    start: &dyn PointLike,
    destination: &dyn PointLike,
    query_pipeline: &QueryPipeline,
    map: &Map,
    collider_set: StaticColliderComponentsSet,
    shape: &SharedShape,
) -> Option<Path> {
    let path = astar(
        &start.i32(),
        |p| {
            let mut successors: Vec<((i32, i32), u32)> = vec![];
            for tile in map.get_available_exits(p.0 as usize, p.1 as usize) {
                let mut should_push = true;
                let shape_pos = Isometry::new(vector![tile.0 as f32, tile.1 as f32], 0.);
                query_pipeline.intersections_with_shape(
                    &collider_set,
                    &shape_pos,
                    &**shape,
                    InteractionGroups::all(),
                    Some(&|v| v.entity() != 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(
    mut commands: Commands,
    pool: Res<AsyncComputeTaskPool>,
    query_pipeline: Res<QueryPipeline>,
    obstructions: Query<&MapObstruction>,
    collider_query: QueryPipelineColliderComponentsQuery,
    query: Query<(Entity, &Destination, &Coordinates, &ColliderShape), Changed<Destination>>,
    map: Query<&Map>,
) {
    for (entity, destination, coordinates, shape) in query.iter() {
        if coordinates.i32() == **destination {
            commands
                .entity(entity)
                .remove::<Path>()
                .remove::<NoPath>()
                .remove::<Calculating>()
                .remove::<Destination>();
            continue;
        }
        for map in map.iter() {
            let coordinates_clone = *coordinates;
            let destination_clone = *destination;
            let query_pipeline_clone = query_pipeline.clone();
            let map_clone = map.clone();
            let shape_clone = shape.clone();
            let collider_set: StaticColliderComponentsSet = (&collider_query, &obstructions).into();
            let task = pool.spawn(async move {
                find_path_for_shape(
                    entity,
                    &coordinates_clone,
                    &destination_clone,
                    &query_pipeline_clone,
                    &map_clone,
                    collider_set,
                    &shape_clone,
                )
            });
            commands
                .entity(entity)
                .insert(Calculating(task))
                .remove::<Path>()
                .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, removed: RemovedComponents<Destination>) {
    for entity in removed.iter() {
        commands.entity(entity).remove::<Calculating>();
    }
}

fn negotiate_path(
    mut commands: Commands,
    mut query: Query<(
        Entity,
        &mut Path,
        &mut RigidBodyPosition,
        &mut RigidBodyVelocity,
        &Speed,
        Option<&RotationSpeed>,
    )>,
) {
    for (entity, mut path, mut position, mut velocity, speed, rotation_speed) in query.iter_mut() {
        let mut new_path = path.0.clone();
        let start_i32 = (
            position.position.translation.x,
            position.position.translation.y,
        )
            .i32();
        let new_path_clone = new_path.clone();
        let mut iter = new_path_clone.split(|p| *p == start_i32);
        if iter.next().is_some() {
            if let Some(upcoming) = iter.next() {
                new_path = vec![start_i32];
                new_path.append(&mut upcoming.to_vec());
            } else {
                let start = Vec2::new(
                    position.position.translation.x,
                    position.position.translation.y,
                );
                let next = path[1];
                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.into();
                continue;
            }
        } else {
            commands.entity(entity).remove::<Path>();
            velocity.linvel = Vec2::ZERO.into();
        }
        **path = new_path;
        if path.len() >= 2 {
            let start = Vec2::new(
                position.position.translation.x,
                position.position.translation.y,
            );
            let next = path[1];
            let next = Vec2::new(next.0 as f32 + 0.5, next.1 as f32 + 0.5);
            if rotation_speed.is_some() {
                let v = next - start;
                let angle = v.y.atan2(v.x);
                position.position.rotation = UnitComplex::new(angle);
            }
            let mut direction = next - start;
            direction = direction.normalize();
            direction *= **speed;
            velocity.linvel = direction.into();
        } else {
            velocity.linvel = Vec2::ZERO.into();
            commands
                .entity(entity)
                .remove::<Path>()
                .remove::<Destination>();
        }
    }
}

fn remove_calculating(
    mut commands: Commands,
    query: Query<Entity, (Changed<Destination>, With<Calculating>)>,
) {
    for entity in query.iter() {
        commands.entity(entity).remove::<Calculating>();
    }
}

pub struct PathfindingPlugin;

impl Plugin for PathfindingPlugin {
    fn build(&self, app: &mut AppBuilder) {
        app.add_system(calculate_path.system())
            .add_system_to_stage(CoreStage::PostUpdate, remove_destination.system())
            .add_system(poll_tasks.system())
            .add_system(negotiate_path.system())
            .add_system_to_stage(CoreStage::PostUpdate, remove_calculating.system());
    }
}