use std::collections::HashMap;

use bevy::{prelude::*, tasks::prelude::*};
use bevy_rapier2d::{na::UnitComplex, prelude::*};
use crossbeam_channel::{unbounded, Receiver};
use derive_more::{Deref, DerefMut};
use pathfinding::prelude::*;

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

#[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, 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(),
    )
}

fn calculate_path(
    mut commands: Commands,
    pool: Res<AsyncComputeTaskPool>,
    mut calculating: Local<HashMap<Entity, Receiver<Path>>>,
    query: Query<(Entity, &Destination, &Coordinates), Changed<Destination>>,
    destinations: Query<&Destination>,
    map: Query<&Map>,
) {
    let calculating_clone = calculating.clone();
    for (entity, rx) in calculating_clone.iter() {
        if destinations.get(*entity).is_ok() {
            if let Ok(path) = rx.try_recv() {
                commands.entity(*entity).insert(path);
                calculating.remove(&entity);
            }
        } else {
            calculating.remove(&entity);
        }
    }
    for (entity, destination, coordinates) in query.iter() {
        if !calculating.contains_key(&entity) {
            let (tx, rx) = unbounded();
            calculating.insert(entity, rx);
            for map in map.iter() {
                let start_clone = *coordinates;
                let destination_clone = *destination;
                let map_clone = map.clone();
                let tx_clone = tx.clone();
                pool.spawn(async move {
                    if let Some(result) = find_path(&start_clone, &destination_clone, &map_clone) {
                        tx_clone.send(Path(result.0)).expect("Channel should exist");
                    }
                })
                .detach();
            }
        }
    }
}

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());
            }
        }
        **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, next.1 as f32);
            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.0;
            velocity.linvel = direction.into();
        } else {
            commands.entity(entity).remove::<Path>();
            commands.entity(entity).remove::<Destination>();
            velocity.linvel = Vec2::ZERO.into();
        }
    }
}

pub struct PathfindingPlugin;

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