//! Spawn the main level.

use bevy::{
    input::mouse::AccumulatedMouseMotion,
    platform::collections::{HashMap, HashSet},
    prelude::*,
    window::PrimaryWindow,
};
use bevy_ecs_ldtk::{ldtk::NeighbourLevel, prelude::*};

use crate::{
    asset_tracking::LoadResource, audio::music, demo::player::PlayerAssets, screens::Screen,
};

use super::player::Player;

pub(super) fn plugin(app: &mut App) {
    app.add_plugins(LdtkPlugin)
        .insert_resource(LevelSelection::iid("d53f9950-c640-11ed-8430-4942c04951ff"))
        .insert_resource(LdtkSettings {
            level_spawn_behavior: LevelSpawnBehavior::UseWorldTranslation {
                load_level_neighbors: true,
            },
            ..default()
        })
        .init_resource::<LevelWalls>()
        .init_resource::<MultiLevelWalls>();

    app.register_type::<PanLevel>();
    app.insert_resource(PanLevel::default());
    app.register_type::<LevelAssets>();
    app.load_resource::<LevelAssets>();
    app.register_type::<LevelWalls>();
    app.register_type::<MultiLevelWalls>();
    app.register_ldtk_int_cell::<WallBundle>(1);
    app.add_systems(
        Update,
        (
            translate_grid_coords_entities,
            level_selection_follow_player,
            cache_wall_locations,
            pan_camera,
        ),
    );
}

#[derive(Resource, Default, Asset, Clone, Reflect)]
#[reflect(Resource)]
struct PanLevel {
    offset: Vec2,
}

#[derive(Resource, Asset, Clone, Reflect)]
#[reflect(Resource)]
pub struct LevelAssets {
    #[dependency]
    music: Handle<AudioSource>,
    world: LdtkProjectHandle,
}

impl FromWorld for LevelAssets {
    fn from_world(world: &mut World) -> Self {
        let assets = world.resource::<AssetServer>();
        Self {
            music: assets.load("audio/music/Fluffing A Duck.ogg"),
            world: assets.load("levels/world.ldtk").into(),
            // world: assets.load("levels/collectathon.ldtk").into(),
        }
    }
}

const GRID_SIZE: i32 = 16;

fn pan_camera(
    mut pan: ResMut<PanLevel>,
    mouse_buttons: Res<ButtonInput<MouseButton>>,
    mouse_motion: Res<AccumulatedMouseMotion>,
    mut camera: Query<&mut Transform, With<Camera>>,
) -> Result {
    let delta = mouse_motion.delta;

    if mouse_buttons.pressed(MouseButton::Middle) {
        pan.offset += delta;
        let mut gino = camera.single_mut()?;
        gino.translation += Vec3::new(-delta.x, delta.y, 0.0) / 5.0;
    }
    Ok(())
}

fn translate_grid_coords_entities(
    mut grid_coords_entities: Query<
        (&mut Transform, &GridCoords),
        (With<Player>, Changed<GridCoords>),
    >,
    pan: Res<PanLevel>,
) {
    // TODO: what is this used for? Why it doesn't work for a moving Player?
    for (mut transform, grid_coords) in grid_coords_entities.iter_mut() {
        info!("Changed GridCoords: {grid_coords:?}");
        transform.translation = (bevy_ecs_ldtk::utils::grid_coords_to_translation(
            *grid_coords,
            IVec2::splat(GRID_SIZE),
        ))
        .extend(transform.translation.z);
    }
}

#[derive(Default, Component)]
struct Wall;

#[derive(Default, Bundle, LdtkIntCell)]
struct WallBundle {
    wall: Wall,
}

#[derive(Debug, Default, Eq, Hash, PartialEq, Reflect)]
pub enum Direction {
    #[default]
    N,
    E,
    S,
    W,
    // we ignore the diagonals NE SE NW SW
}

impl TryFrom<&str> for Direction {
    type Error = &'static str;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value {
            "n" => Ok(Direction::N),
            "e" => Ok(Direction::E),
            "s" => Ok(Direction::S),
            "w" => Ok(Direction::W),
            _ => Err("Invalid direction string"),
        }
    }
}

pub fn convert_neighbors(
    neighbors: &[NeighbourLevel],
) -> Result<HashMap<Direction, LevelIid>, &'static str> {
    let neighbours = neighbors
        .iter()
        .filter_map(|neighbor| {
            Direction::try_from(neighbor.dir.as_str())
                .ok()
                .map(|dir| (dir, LevelIid::new(neighbor.level_iid.clone())))
        })
        .collect();
    Ok(neighbours)
}

#[derive(Debug, Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct LevelWalls {
    wall_locations: HashSet<GridCoords>,
    level_width: i32,
    level_height: i32,
    level_neighbours: HashMap<Direction, LevelIid>,
}

impl LevelWalls {
    pub fn leave_level(&self, grid_coords: &GridCoords) -> bool {
        grid_coords.x < 0
            || grid_coords.y < 0
            || grid_coords.x >= self.level_width
            || grid_coords.y >= self.level_height
    }

    pub fn in_wall(&self, grid_coords: &GridCoords) -> bool {
        self.wall_locations.contains(grid_coords)
    }

    pub fn debug_collisions(&self, player_pos: &GridCoords) {
        info!(
            "map for a level that is x: {} by y: {}",
            self.level_width, self.level_height
        );
        for y in (0..self.level_height) {
            for x in 0..self.level_width {
                let coords = GridCoords::new(x, y);
                if coords == *player_pos {
                    print!("@");
                } else if self.in_wall(&coords) {
                    print!("X");
                } else {
                    print!("_");
                }
            }
            println!(" [y: {y:02}]");
        }
    }
}

#[derive(Debug, Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct MultiLevelWalls {
    cache: HashMap<LevelIid, LevelWalls>,
}

impl MultiLevelWalls {
    pub fn in_wall(&self, level: &LevelIid, grid_coords: &GridCoords) -> bool {
        self.cache[level].in_wall(grid_coords)
    }
    pub fn debug_collisions(&self, level: &LevelIid, player_pos: &GridCoords) {
        if let Some(level) = self.cache.get(level) {
            level.debug_collisions(player_pos);
        } else {
            warn!("No walls cached for level: {:?}", level);
        }
    }
}

/// A system that spawns the main level.
pub fn spawn_level(
    mut commands: Commands,
    window: Single<&Window, With<PrimaryWindow>>,
    level_assets: Res<LevelAssets>,
) {
    let half_size = window.size() / 2.0;

    commands.spawn((
        Name::new("Ldtk level"),
        StateScoped(Screen::Gameplay),
        LdtkWorldBundle {
            ldtk_handle: level_assets.world.clone(),
            transform: Transform::from_xyz(-half_size.x, half_size.y, 0.0),
            ..Default::default()
        },
    ));
}

fn _old_cache_wall_locations(
    level_selection: Res<LevelSelection>,
    mut level_walls: ResMut<LevelWalls>,
    mut level_events: EventReader<LevelEvent>,
    walls: Query<&GridCoords, With<Wall>>,
    ldtk_project_entities: Query<&LdtkProjectHandle>,
    ldtk_project_assets: Res<Assets<LdtkProject>>,
) -> Result {
    for level_event in level_events.read() {
        if let LevelEvent::Spawned(level_iid) = level_event {
            let ldtk_project = ldtk_project_assets
                .get(ldtk_project_entities.single()?)
                .expect("LdtkProject should be loaded when level is spawned");
            let level = ldtk_project
                .get_raw_level_by_iid(level_iid.get())
                .expect("spawned level should exist in project");

            let wall_locations = walls.iter().copied().collect();

            info!(
                "loading level of dimension x: {} by y: {}",
                level.px_wid, level.px_hei
            );
            let new_level_walls = LevelWalls {
                wall_locations,
                level_width: level.px_wid / GRID_SIZE,
                level_height: level.px_hei / GRID_SIZE,
                ..Default::default()
            };

            *level_walls = new_level_walls;
            info!(
                "new level tile dimensions are x: {} y {}",
                level_walls.level_width, level_walls.level_height
            );
            level_walls.debug_collisions(&GridCoords::default());
        }
    }
    Ok(())
}

fn cache_wall_locations(
    mut levels_wall_cache: ResMut<MultiLevelWalls>,
    mut level_events: EventReader<LevelEvent>,
    walls: Query<(&ChildOf, &GridCoords), With<Wall>>,
    ldtk_project_entities: Query<&LdtkProjectHandle>,
    ldtk_project_assets: Res<Assets<LdtkProject>>,
) -> Result {
    let multi_level_walls = levels_wall_cache.into_inner();

    for level_event in level_events.read() {
        if let LevelEvent::Spawned(level_iid) = level_event {
            let ldtk_project = ldtk_project_assets
                .get(ldtk_project_entities.single()?)
                .expect("LdtkProject should be loaded when level is spawned");
            let level = ldtk_project
                .get_raw_level_by_iid(level_iid.get())
                .expect("spawned level should exist in project");

            let mut wall_locations = HashSet::<GridCoords>::default();
            info!("current level neighbours: {:?}", level.neighbours);
            if let Some(layers) = level.layer_instances.clone() {
                // info!("layers: {:?}", layers);
                layers.iter().for_each(|field| {
                    info!("Layer field: {:?}", field.identifier);
                    if field.identifier == "Walls" {
                        info!("Found walls layer: {:?}", field.int_grid_csv);
                        info!("Trying to format it");
                        // FIXME: a .rev() here? It doesn't look necessary from what gets printed
                        // remember to fix the supposed "map dragging" too
                        for y in (0..(level.px_hei / GRID_SIZE)) {
                            for x in (0..(level.px_wid / GRID_SIZE)) {
                                let index = (y * level.px_wid / GRID_SIZE + x) as usize;
                                if let Some(value) = field.int_grid_csv.get(index) {
                                    if *value == 1 {
                                        print!("X");
                                        wall_locations.insert(GridCoords::new(x, y));
                                    } else {
                                        print!("_");
                                    }
                                }
                            }
                            println!(" [y: {y:02}]");
                        }
                    }
                });
            }
            // level.iter_fields().for_each(|field| {
            //     info!("Field: {:?}", field);
            // });

            // let wall_locations = walls.iter().map(|e| e.1).copied().collect();

            info!(
                "loading level of dimension x: {} by y: {}",
                level.px_wid, level.px_hei
            );
            multi_level_walls.cache.insert(
                level_iid.clone(), // You'll need to clone the key since HashMap takes ownership
                LevelWalls {
                    wall_locations,
                    level_width: level.px_wid / GRID_SIZE,
                    level_height: level.px_hei / GRID_SIZE,
                    level_neighbours: convert_neighbors(&level.neighbours).unwrap_or_default(), // Convert neighbours to a HashMap
                },
            );

            info!(
                "new level tile dimensions are x: {} y {}, neighbours: {:?}",
                multi_level_walls.cache[level_iid].level_width,
                multi_level_walls.cache[level_iid].level_height,
                multi_level_walls.cache[level_iid].level_neighbours,
            );
            multi_level_walls.cache[level_iid].debug_collisions(&GridCoords::default());
        }
    }
    Ok(())
}

fn level_selection_follow_player(
    players: Query<&GlobalTransform, With<Player>>,
    levels: Query<(&LevelIid, &GlobalTransform)>,
    ldtk_projects: Query<&LdtkProjectHandle>,
    ldtk_project_assets: Res<Assets<LdtkProject>>,
    mut level_selection: ResMut<LevelSelection>,
) -> Result {
    if let Ok(player_transform) = players.single() {
        let ldtk_project = ldtk_project_assets
            .get(ldtk_projects.single()?)
            .expect("ldtk project should be loaded before player is spawned");

        for (level_iid, level_transform) in levels.iter() {
            let level = ldtk_project
                .get_raw_level_by_iid(level_iid.get())
                .expect("level should exist in only project");

            let level_bounds = Rect {
                min: Vec2::new(
                    level_transform.translation().x,
                    level_transform.translation().y,
                ),
                max: Vec2::new(
                    level_transform.translation().x + level.px_wid as f32,
                    level_transform.translation().y + level.px_hei as f32,
                ),
            };

            if level_bounds.contains(player_transform.translation().truncate()) {
                if *level_selection == LevelSelection::Iid(level_iid.clone()) {
                    // Player is already in the current level, no need to change
                    return Ok(());
                }
                info!("Setting current level to {level_iid}");
                *level_selection = LevelSelection::Iid(level_iid.clone());
            }
        }
    }
    Ok(())
}
