use std::{
    cell::RefCell,
    collections::{HashMap, HashSet},
    marker::PhantomData,
};

use bevy::prelude::*;

use bevy_rapier2d::{
    na::{Isometry2, Vector2},
    parry::bounding_volume::BoundingVolume,
};
use coord_2d::{Coord, Size};
use shadowcast::{vision_distance, Context, InputGrid};

use crate::{
    bevy_rapier2d::prelude::*,
    core::{GlobalTransformExt, Player, PointLike},
    log::Log,
    map::{Map, MapConfig, MapObstruction},
};

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

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

#[derive(Component, Clone, Debug, Eq, PartialEq)]
pub struct Viewshed {
    pub range: u32,
    pub visible_points: HashSet<(i32, i32)>,
}

impl Default for Viewshed {
    fn default() -> Self {
        Self {
            range: 15,
            visible_points: HashSet::new(),
        }
    }
}

impl Viewshed {
    pub fn is_point_visible(&self, point: &dyn PointLike) -> bool {
        self.visible_points.contains(&point.into())
    }

    fn update(
        &mut self,
        viewer_entity: &Entity,
        visible_entities: &mut VisibleEntities,
        start: &GlobalTransform,
        rapier_context: &RapierContext,
        visible_query: &Query<(&Visible, &Collider, &GlobalTransform)>,
        obstructions_query: &Query<&MapObstruction>,
        events: &mut EventWriter<VisibilityChanged>,
        cache: &mut HashMap<(i32, i32), (u8, HashSet<Entity>)>,
    ) {
        // println!("Start");
        let mut boxes = vec![];
        let shape = Collider::cuboid(self.range as f32, self.range as f32);
        rapier_context.intersections_with_shape(
            start.translation().truncate(),
            0.,
            &shape,
            QueryFilter::new().predicate(&|e| visible_query.get(e).is_ok()),
            |entity| {
                if let Ok((_, collider, transform)) = visible_query.get(entity) {
                    let position = Isometry2::new(
                        Vector2::new(transform.translation().x, transform.translation().y),
                        0.,
                    );
                    // println!(
                    // "Hit {:?}, pushing {:?}",
                    // entity,
                    // collider.raw.compute_aabb(&position)
                    // );
                    boxes.push(collider.raw.compute_aabb(&position));
                }
                true
            },
        );
        let mut context: Context<u8> = Context::default();
        let vision_distance = vision_distance::Circle::new(self.range);
        let shape = Collider::cuboid(0.49, 0.49);
        let mut new_visible_entities = HashSet::new();
        let size = (
            (start.translation().x.abs() + self.range as f32) as u32,
            (start.translation().y.abs() + self.range as f32) as u32,
        );
        let visibility_grid = VisibilityGrid(
            size,
            RefCell::new(Box::new(|coord: Coord| {
                let shape_pos = Vec2::new(coord.x as f32 + 0.5, coord.y as f32 + 0.5);
                // println!("Checking {:?}", shape_pos);
                if start.distance(&shape_pos) > self.range as f32 {
                    // println!("Out of range");
                    return u8::MAX;
                }
                let result = if let Some((opacity, coord_entities)) = cache.get(&(coord.x, coord.y))
                {
                    Some((*opacity, coord_entities.clone()))
                } else {
                    let position = Isometry2::new(Vector2::new(shape_pos.x, shape_pos.y), 0.);
                    let aabb = shape.raw.compute_aabb(&position);
                    let mut hit = false;
                    for b in &boxes {
                        if b.intersects(&aabb) {
                            // println!("Hit at {:?}", b);
                            hit = true;
                            break;
                        }
                    }
                    if hit {
                        // println!("Hit test");
                        let mut opacity = 0;
                        let mut coord_entities = HashSet::new();
                        rapier_context.intersections_with_shape(
                            shape_pos,
                            0.,
                            &shape,
                            QueryFilter::new().predicate(&|v| visible_query.get(v).is_ok()),
                            |entity| {
                                // println!("{:?}", entity);
                                let obstruction = obstructions_query.get(entity).is_ok();
                                if obstruction {
                                    // println!("Obstruction");
                                    coord_entities.clear();
                                }
                                coord_entities.insert(entity);
                                if let Ok((visible, _, _)) = visible_query.get(entity) {
                                    opacity = opacity.max(**visible);
                                }
                                !obstruction
                            },
                        );
                        cache.insert((coord.x, coord.y), (opacity, coord_entities.clone()));
                        Some((opacity, coord_entities))
                    } else {
                        // println!("No hits, 0");
                        let coord_entities = HashSet::new();
                        cache.insert((coord.x, coord.y), (0, coord_entities.clone()));
                        Some((0, coord_entities))
                    }
                };
                if let Some((opacity, coord_entities)) = result {
                    for e in &coord_entities {
                        new_visible_entities.insert(*e);
                    }
                    if coord_entities.contains(viewer_entity) {
                        // println!("Self hit, 0");
                        0
                    } else {
                        // println!("{}", opacity);
                        opacity
                    }
                } else {
                    0
                }
            })),
        );
        let mut new_visible = HashSet::new();
        context.for_each_visible(
            Coord::new(start.x_i32(), start.y_i32()),
            &visibility_grid,
            &size,
            vision_distance,
            u8::MAX,
            |coord, _directions, _visibility| {
                new_visible.insert((coord.x, coord.y));
            },
        );
        if self.visible_points != new_visible {
            self.visible_points = new_visible;
        }
        if new_visible_entities != **visible_entities {
            for lost in visible_entities.difference(&new_visible_entities) {
                events.send(VisibilityChanged::Lost {
                    viewer: *viewer_entity,
                    viewed: *lost,
                });
            }
            for gained in new_visible_entities.difference(visible_entities) {
                // println!("transition: {:?} gains {:?}", viewer_entity, gained);
                events.send(VisibilityChanged::Gained {
                    viewer: *viewer_entity,
                    viewed: *gained,
                });
            }
            **visible_entities = new_visible_entities;
        }
    }
}

#[derive(Component, Clone, Copy, Debug, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct Visible(pub u8);

impl Default for Visible {
    fn default() -> Self {
        Self::opaque()
    }
}

impl Visible {
    pub fn transparent() -> Self {
        Self(0)
    }

    pub fn opaque() -> Self {
        Self(u8::MAX)
    }
}

#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
pub struct VisibleEntities(HashSet<Entity>);

#[derive(Bundle, Default)]
pub struct ViewshedBundle {
    pub viewshed: Viewshed,
    pub visible_entities: VisibleEntities,
}

impl ViewshedBundle {
    pub fn new(range: u32) -> Self {
        Self {
            viewshed: Viewshed { range, ..default() },
            ..default()
        }
    }
}

pub enum VisibilityChanged {
    Gained { viewer: Entity, viewed: Entity },
    Lost { viewer: Entity, viewed: Entity },
}

impl PointLike for Coord {
    fn x(&self) -> f32 {
        self.x as f32
    }

    fn y(&self) -> f32 {
        self.y as f32
    }
}

fn add_visibility_indices<D: 'static + Clone + Default + Send + Sync>(
    mut commands: Commands,
    query: Query<(Entity, &Map<D>), (Added<Map<D>>, Without<RevealedTiles>)>,
    map_config: Res<MapConfig>,
) {
    for (entity, map) in query.iter() {
        let count = map.width * map.height;
        commands
            .entity(entity)
            .insert(RevealedTiles(vec![map_config.start_revealed; count]));
    }
}

fn viewshed_removed(
    query: RemovedComponents<Viewshed>,
    visible_entities: Query<&VisibleEntities>,
    mut events: EventWriter<VisibilityChanged>,
) {
    for entity in query.iter() {
        if let Ok(visible) = visible_entities.get(entity) {
            for e in visible.iter() {
                events.send(VisibilityChanged::Lost {
                    viewer: entity,
                    viewed: *e,
                })
            }
        }
    }
}

pub struct VisibilityGrid<F>(pub (u32, u32), pub RefCell<Box<F>>);

impl<F> InputGrid for VisibilityGrid<F>
where
    F: FnMut(Coord) -> u8,
{
    type Grid = (u32, u32);

    type Opacity = u8;

    fn size(&self, grid: &Self::Grid) -> Size {
        Size::new(grid.0, grid.1)
    }

    fn get_opacity(&self, _grid: &Self::Grid, coord: Coord) -> Self::Opacity {
        (self.1.borrow_mut())(coord)
    }
}

fn update_viewshed(
    config: Res<RapierConfiguration>,
    visible: Query<(&Visible, &Collider, &GlobalTransform)>,
    obstructions: Query<&MapObstruction>,
    mut viewers: Query<(
        Entity,
        &mut Viewshed,
        &mut VisibleEntities,
        &GlobalTransform,
    )>,
    rapier_context: Res<RapierContext>,
    mut changed: EventWriter<VisibilityChanged>,
) {
    if !config.query_pipeline_active {
        return;
    }
    let mut cache = HashMap::new();
    for (viewer_entity, mut viewshed, mut visible_entities, viewer_transform) in viewers.iter_mut()
    {
        viewshed.update(
            &viewer_entity,
            &mut visible_entities,
            viewer_transform,
            &rapier_context,
            &visible,
            &obstructions,
            &mut changed,
            &mut cache,
        );
    }
}

fn remove_visible(
    removed: RemovedComponents<Visible>,
    mut viewers: Query<(
        Entity,
        &mut Viewshed,
        &mut VisibleEntities,
        &GlobalTransform,
    )>,
    rapier_context: Res<RapierContext>,
    visible: Query<(&Visible, &Collider, &GlobalTransform)>,
    obstructions: Query<&MapObstruction>,
    mut changed: EventWriter<VisibilityChanged>,
) {
    if removed.iter().len() != 0 {
        let mut cache = HashMap::new();
        for removed in removed.iter() {
            for (viewer_entity, mut viewshed, mut visible_entities, start) in viewers.iter_mut() {
                if !visible_entities.contains(&removed) {
                    continue;
                }
                visible_entities.remove(&removed);
                viewshed.update(
                    &viewer_entity,
                    &mut visible_entities,
                    start,
                    &*rapier_context,
                    &visible,
                    &obstructions,
                    &mut changed,
                    &mut cache,
                );
            }
        }
    }
}

fn update_revealed_tiles<D: 'static + Clone + Default + Send + Sync>(
    mut map: Query<(&Map<D>, &mut RevealedTiles)>,
    viewers: Query<&Viewshed, (With<Player>, Changed<Viewshed>)>,
) {
    for viewshed in viewers.iter() {
        for (map, mut revealed_tiles) in map.iter_mut() {
            for v in viewshed.visible_points.iter() {
                let idx = v.to_index(map.width);
                if idx >= revealed_tiles.len() {
                    continue;
                }
                revealed_tiles[idx] = true;
            }
        }
    }
}

fn log_visible(
    time: Res<Time>,
    mut recently_lost: Local<HashMap<Entity, Timer>>,
    mut events: EventReader<VisibilityChanged>,
    mut log: Query<&mut Log>,
    viewers: Query<(Entity, &GlobalTransform, Option<&Collider>), (With<Player>, With<Viewshed>)>,
    visible: Query<(&Name, &GlobalTransform, Option<&Collider>), Without<DontLogWhenVisible>>,
) {
    for timer in recently_lost.values_mut() {
        timer.tick(time.delta());
    }
    recently_lost.retain(|_entity, timer| !timer.finished());
    for event in events.iter() {
        let viewer = match event {
            VisibilityChanged::Gained { viewer, .. } => viewer,
            VisibilityChanged::Lost { viewer, .. } => viewer,
        };
        if let Ok((viewer_entity, viewer_transform, viewer_collider)) = viewers.get(*viewer) {
            if let VisibilityChanged::Gained { viewed, .. } = event {
                if *viewed == viewer_entity || recently_lost.contains_key(viewed) {
                    continue;
                }
                if let Ok((name, viewed_transform, viewed_collider)) = visible.get(*viewed) {
                    let location = if let (Some(viewer_collider), Some(viewed_collider)) =
                        (viewer_collider, viewed_collider)
                    {
                        viewer_transform.collider_direction_and_distance(
                            viewer_collider,
                            viewed_transform,
                            viewed_collider,
                        )
                    } else {
                        let yaw = viewer_transform.yaw();
                        viewer_transform.direction_and_distance(viewed_transform, Some(yaw))
                    };
                    if let Ok(mut log) = log.get_single_mut() {
                        log.push(format!("{}: {location}", name));
                    }
                }
            } else if let VisibilityChanged::Lost { viewed, .. } = event {
                recently_lost.insert(*viewed, Timer::from_seconds(2., false));
            }
        }
    }
}

pub const LOG_VISIBLE_LABEL: &str = "LOG_VISIBLE";

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

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

impl<D: 'static + Clone + Default + Send + Sync> Plugin for VisibilityPlugin<D> {
    fn build(&self, app: &mut App) {
        app.add_event::<VisibilityChanged>()
            .add_system_to_stage(CoreStage::PreUpdate, add_visibility_indices::<D>)
            .add_system_to_stage(CoreStage::PreUpdate, update_viewshed)
            .add_system_to_stage(CoreStage::PostUpdate, viewshed_removed)
            .add_system_to_stage(CoreStage::PostUpdate, remove_visible)
            .add_system_to_stage(CoreStage::PreUpdate, update_revealed_tiles::<D>)
            .add_system_to_stage(CoreStage::PreUpdate, log_visible);
    }
}