use std::marker::PhantomData;

use bevy::prelude::*;
use bevy_rapier2d::{
    na::{Isometry2, Vector2},
    prelude::*,
    rapier::prelude::AABB,
};
pub use here_be_dragons::Map as MapgenMap;
use here_be_dragons::{geometry::Rect as MRect, MapFilter, Tile};
use maze_generator::{prelude::*, recursive_backtracking::RbGenerator};
use rand::prelude::StdRng;

use crate::{
    core::{Player, PointLike},
    exploration::{ExplorationType, Mappable},
    log::Log,
    utils::target_and_other,
    visibility::Visible,
};

#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, PartialEq)]
pub struct Area(pub AABB);

#[derive(Component, Clone, Default, Deref, DerefMut)]
pub struct Map<D: 'static + Clone + Default + Send + Sync>(pub MapgenMap<D>);

impl<D: Clone + Default + Send + Sync> From<MapgenMap<D>> for Map<D> {
    fn from(v: MapgenMap<D>) -> Self {
        Self(v)
    }
}

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

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

#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct SpawnColliders(pub bool);

impl Default for SpawnColliders {
    fn default() -> Self {
        Self(true)
    }
}

impl From<bool> for SpawnColliders {
    fn from(v: bool) -> Self {
        Self(v)
    }
}

#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct SpawnPortals(pub bool);

impl Default for SpawnPortals {
    fn default() -> Self {
        Self(true)
    }
}

pub trait ITileType {
    fn blocks_motion(&self) -> bool;
    fn blocks_visibility(&self) -> bool;
}

impl ITileType for Tile {
    fn blocks_motion(&self) -> bool {
        self.is_blocked()
    }

    fn blocks_visibility(&self) -> bool {
        self.is_blocked()
    }
}

#[derive(Clone, Debug)]
pub struct MapConfig {
    pub describe_undescribed_areas: bool,
    pub speak_area_descriptions: bool,
    pub start_revealed: bool,
}

impl Default for MapConfig {
    fn default() -> Self {
        Self {
            describe_undescribed_areas: false,
            speak_area_descriptions: true,
            start_revealed: false,
        }
    }
}

#[derive(Bundle)]
pub struct PortalBundle {
    pub portal: Portal,
    pub exploration_type: ExplorationType,
    pub mappable: Mappable,
    pub transform: Transform,
    pub global_transform: GlobalTransform,
}

impl Default for PortalBundle {
    fn default() -> Self {
        Self {
            portal: Default::default(),
            exploration_type: ExplorationType::Portal,
            mappable: Default::default(),
            transform: Default::default(),
            global_transform: Default::default(),
        }
    }
}

#[derive(Bundle, Clone, Default)]
pub struct MapBundle<D: 'static + Clone + Default + Send + Sync> {
    pub map: Map<D>,
    pub spawn_colliders: SpawnColliders,
    pub spawn_portals: SpawnPortals,
    pub transform: Transform,
    pub global_transform: GlobalTransform,
}

pub struct GridBuilder {
    width_in_rooms: usize,
    height_in_rooms: usize,
    room_width: usize,
    room_height: usize,
}

impl GridBuilder {
    pub fn new(
        width_in_rooms: usize,
        height_in_rooms: usize,
        room_width: usize,
        room_height: usize,
    ) -> Box<GridBuilder> {
        Box::new(GridBuilder {
            width_in_rooms,
            height_in_rooms,
            room_width,
            room_height,
        })
    }
}

impl<D: Clone + Default> MapFilter<D> for GridBuilder {
    fn modify_map(&self, _rng: &mut StdRng, map: &MapgenMap<D>) -> MapgenMap<D> {
        let mut map = map.clone();
        let mut generator = RbGenerator::new(None);
        if let Ok(maze) =
            generator.generate(self.width_in_rooms as i32, self.height_in_rooms as i32)
        {
            let total_height = (self.room_height + 1) * self.height_in_rooms + 1;
            let half_width = self.room_width / 2;
            let half_height = self.room_height / 2;
            for y in 0..self.height_in_rooms {
                for x in 0..self.width_in_rooms {
                    let x_offset = x * (self.room_width + 1);
                    let y_offset =
                        total_height - (y * (self.room_height + 1)) - self.room_height - 2;
                    let room = MRect::new_i32(
                        x_offset as i32 + 1,
                        y_offset as i32 + 1,
                        self.room_width as i32,
                        self.room_height as i32,
                    );
                    map.add_room(room);
                    let coords = maze_generator::prelude::Coordinates::new(x as i32, y as i32);
                    if let Some(field) = maze.get_field(&coords) {
                        use maze_generator::prelude::Direction::*;
                        if field.has_passage(&North) {
                            let x = x_offset + half_width;
                            let y = y_offset + self.room_height;
                            map.set_tile(x as usize, y as usize, Tile::floor());
                        }
                        if field.has_passage(&South) {
                            let x = x_offset + half_width;
                            let y = y_offset;
                            map.set_tile(x as usize, y as usize, Tile::floor());
                        }
                        if field.has_passage(&East) {
                            let x = x_offset + self.room_width;
                            let y = y_offset + half_height;
                            map.set_tile(x as usize, y as usize, Tile::floor());
                        }
                        if field.has_passage(&West) {
                            let x = x_offset;
                            let y = y_offset + half_height;
                            map.set_tile(x as usize, y as usize, Tile::floor());
                        }
                    }
                }
            }
            map.starting_point = Some(here_be_dragons::geometry::Point::new(
                maze.start.x as usize + 1,
                maze.start.y as usize + 1,
            ));
            map.exit_point = Some(here_be_dragons::geometry::Point::new(
                (maze.goal.x as usize) * self.room_width + half_width,
                (maze.goal.y as usize) * self.room_height + half_height,
            ));
        }
        map
    }
}

fn spawn_colliders<D: 'static + Clone + Default + Send + Sync>(
    mut commands: Commands,
    maps: Query<(Entity, &Map<D>, &SpawnColliders), Changed<SpawnColliders>>,
) {
    for (map_entity, map, spawn_colliders) in maps.iter() {
        if **spawn_colliders {
            commands
                .entity(map_entity)
                .remove::<SpawnColliders>()
                .insert(RigidBody::Fixed);
            for y in 0..map.height {
                for x in 0..map.width {
                    let tile = map.at(x, y);
                    if tile.blocks_motion() {
                        let id = commands
                            .spawn()
                            .insert(Collider::cuboid(0.5, 0.5))
                            .insert(Transform::from_xyz(x as f32 + 0.5, y as f32 + 0.5, 0.))
                            .insert(GlobalTransform::default())
                            .insert(MapObstruction)
                            .id();
                        if tile.blocks_visibility() {
                            commands.entity(id).insert(Visible::opaque());
                        }
                        commands.entity(map_entity).push_children(&[id]);
                    }
                }
            }
            for room in &map.rooms {
                let shape = Collider::cuboid((room.width() / 2) as f32, (room.height() / 2) as f32);
                let position =
                    Isometry2::new(Vector2::new(room.center().x(), room.center().y()), 0.);
                let aabb = shape.raw.compute_aabb(&position);
                let id = commands
                    .spawn()
                    .insert(shape)
                    .insert(Sensor)
                    .insert(ActiveEvents::COLLISION_EVENTS)
                    .insert(Transform::from_xyz(
                        position.translation.x,
                        position.translation.y,
                        0.,
                    ))
                    .insert(GlobalTransform::default())
                    .insert(Area(aabb))
                    .id();
                commands.entity(map_entity).push_children(&[id]);
            }
        }
    }
}

fn spawn_portals<D: 'static + Clone + Default + Send + Sync>(
    mut commands: Commands,
    map: Query<(Entity, &Map<D>, &SpawnPortals), Changed<SpawnPortals>>,
) {
    for (map_entity, map, spawn_portals) in map.iter() {
        if **spawn_portals {
            commands.entity(map_entity).remove::<SpawnPortals>();
            let mut portals: Vec<(f32, f32)> = vec![];
            for x in 1..map.width {
                for y in 1..map.height {
                    let mut spawn_portal = false;
                    if map.get_available_exits(x, y).len() > 2 {
                        let idx = (x, y).to_index(map.width);
                        if map.tiles[idx].is_walkable()
                            && (x > 1 && map.tiles[idx - 1].is_walkable())
                            && (x < map.width - 2 && map.tiles[idx + 1].is_walkable())
                            && (y > 1 && map.tiles[idx - map.width].is_blocked())
                            && (y < map.height - 2 && map.tiles[idx + map.width].is_blocked())
                        {
                            spawn_portal = true;
                        }
                        if map.tiles[idx].is_walkable()
                            && (x > 1 && map.tiles[idx - 1].is_blocked())
                            && (x < map.width - 2 && map.tiles[idx + 1].is_blocked())
                            && (y > 1 && map.tiles[idx - map.width].is_walkable())
                            && (y < map.height - 2 && map.tiles[idx + map.width].is_walkable())
                        {
                            spawn_portal = true;
                        }
                    }
                    if spawn_portal {
                        let x = x as f32 + 0.5;
                        let y = y as f32 + 0.5;
                        if !portals.contains(&(x, y)) {
                            portals.push((x, y));
                        }
                    }
                }
            }
            for (x, y) in portals {
                let portal = commands
                    .spawn_bundle(PortalBundle {
                        transform: Transform::from_translation(Vec3::new(x, y, 0.)),
                        ..default()
                    })
                    .id();
                commands.entity(map_entity).push_children(&[portal]);
            }
        }
    }
}

fn spawn_portal_colliders<D: 'static + Clone + Default + Send + Sync>(
    mut commands: Commands,
    map: Query<(Entity, &SpawnColliders), With<Map<D>>>,
    portals: Query<Entity, (With<Portal>, Without<Collider>)>,
) {
    for (entity, spawn_colliders) in map.iter() {
        if **spawn_colliders {
            for portal_entity in portals.iter() {
                commands
                    .entity(portal_entity)
                    .insert(Collider::cuboid(0.5, 0.5))
                    .insert(Sensor);
            }
            commands.entity(entity).remove::<SpawnColliders>();
        }
    }
}

fn area_description(
    mut events: EventReader<CollisionEvent>,
    areas: Query<(&Area, Option<&Name>)>,
    players: Query<&Player>,
    config: Res<MapConfig>,
    mut log: Query<&mut Log>,
) {
    for event in events.iter() {
        let (entity1, entity2, started) = match event {
            CollisionEvent::Started(collider1, collider2, _) => (collider1, collider2, true),
            CollisionEvent::Stopped(collider1, collider2, _) => (collider1, collider2, false),
        };
        if let Some((area, other)) = target_and_other(*entity1, *entity2, &|v| areas.get(v).is_ok())
        {
            if players.get(other).is_ok() {
                if let Ok((aabb, area_name)) = areas.get(area) {
                    let name = if let Some(name) = area_name {
                        Some(name.to_string())
                    } else if config.describe_undescribed_areas {
                        Some(format!("{}-by-{} area", aabb.extents().x, aabb.extents().y))
                    } else {
                        None
                    };
                    if let Some(name) = name {
                        if let Ok(mut log) = log.get_single_mut() {
                            if started {
                                log.push(format!("Entering {name}."));
                            } else {
                                log.push(format!("Leaving {name}."));
                            }
                        }
                    }
                }
            }
        }
    }
}

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

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

impl<D: 'static + Clone + Default + Send + Sync> Plugin for MapPlugin<D> {
    fn build(&self, app: &mut App) {
        if !app.world.contains_resource::<MapConfig>() {
            app.insert_resource(MapConfig::default());
        }
        let config = app.world.get_resource::<MapConfig>().unwrap().clone();
        app.register_type::<Portal>()
            .add_system_to_stage(CoreStage::PreUpdate, spawn_colliders::<D>)
            .add_system_to_stage(CoreStage::PreUpdate, spawn_portals::<D>)
            .add_system_to_stage(CoreStage::PreUpdate, spawn_portal_colliders::<D>);
        if config.speak_area_descriptions {
            app.add_system_to_stage(CoreStage::PostUpdate, area_description);
        }
    }
}