//! Spawn the main level.

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

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(LevelSelection::iid("34f51d20-8990-11ee-b0d1-cfeb0e9e30f6"))
        .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,
        )
            .chain(),
    );
}

#[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/tile-based-game.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:?}");
        debug!("Previous traslation: {:?}", transform.translation);
        transform.translation = (bevy_ecs_ldtk::utils::grid_coords_to_translation(
            *grid_coords,
            IVec2::splat(GRID_SIZE),
        ))
        .extend(transform.translation.z);
        debug!("Updated traslation: {:?}", transform.translation);
    }
}

#[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_tile_pos_x: i32,
    level_tile_pos_y: i32,
    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) {
        debug!(
            "map for a level that is x: {} by y: {}",
            self.level_width, self.level_height
        );
        debug!("player pos: {:?}", player_pos);
        debug!(
            "level world coordinates: x: {} y: {}",
            self.level_tile_pos_x, self.level_tile_pos_y
        );
        // FIXME: kwwp aligned with MultiLevelWalls.in_wall.translated_coords
        let player_translated_coords = GridCoords::new(
            player_pos.x - self.level_tile_pos_x,
            player_pos.y - (-self.level_tile_pos_y - self.level_height),
        );
        debug!("Player absolute coords: {:?}", *player_pos);
        info!("Player relative coords: {player_translated_coords:?}");
        for y in (0..self.level_height).rev() {
            let mut lineoutput = String::from("");
            for x in 0..self.level_width {
                let coords = GridCoords::new(x, y);
                if coords.x == 0 {
                    // print!("[X :{:03} Y: {:03}]  ", coords.x, coords.y);
                    lineoutput = format!("{lineoutput}[X :{:03} Y: {:03}]  ", coords.x, coords.y);
                }
                if coords == player_translated_coords {
                    lineoutput = format!("{lineoutput}@");
                } else if self.in_wall(&coords) {
                    lineoutput = format!("{lineoutput}X");
                } else {
                    lineoutput = format!("{lineoutput}_");
                }
                if coords.x == (self.level_width - 1) {
                    // println!("[X :{:03} Y: {:03}]  ", coords.x, coords.y);
                    lineoutput = format!("{lineoutput}  [X :{:03} Y: {:03}]", coords.x, coords.y);
                    info!("{lineoutput}");
                }
            }
        }
    }
}

#[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 {
        let translated_coords = GridCoords::new(
            // FIXME: x seems to work, y... not so much.
            // Level UNDER the "zero level strip" by a level_height has:
            // level world coordinates: x: -16 y: 0
            // Player absolute coords: GridCoords { x: -8, y: -1 } y should be 15
            // Player relative coords: GridCoords { x: 8, y: -1 }
            // Level ABOVE the "zero level strip" by a level_height has:
            // level world coordinates: x: 32 y: -32
            // Player absolute coords: GridCoords { x: 42, y: 17 }
            // Player relative coords: GridCoords { x: 10, y: 17 } y should be 1
            // level world coordinates: x: 32 y: -16
            // Player absolute coords: GridCoords { x: 41, y: 16 }
            // Player relative coords: GridCoords { x: 9, y: 16 } y should be 0
            grid_coords.x - self.cache[level].level_tile_pos_x,
            grid_coords.y - (-self.cache[level].level_tile_pos_y - self.cache[level].level_height),
        );
        self.cache[level].in_wall(&translated_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 cache_wall_locations(
    mut levels_wall_cache: ResMut<MultiLevelWalls>,
    mut level_events: EventReader<LevelEvent>,
    ldtk_project_entities: Query<&LdtkProjectHandle>,
    ldtk_project_assets: Res<Assets<LdtkProject>>,
) -> Result {
    let multi_level_walls = levels_wall_cache.into_inner();
    let pippo: TileInstance = TileInstance::default();

    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();
            let mut last_coord: GridCoords = GridCoords::default();
            trace!("current level neighbours: {:?}", level.neighbours);
            trace!(
                "Level world coordinates: x [{}], y[{}]",
                level.world_x, level.world_y
            );

            let level_tile_pos_x = level.world_x / GRID_SIZE;
            let level_tile_pos_y = level.world_y / GRID_SIZE;
            let level_tile_width = level.px_wid / GRID_SIZE;
            trace!(
                "Level tile coordinates: x [{}], y[{}]",
                level_tile_pos_x, level_tile_pos_y
            );
            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);
                        for (i, value) in field.int_grid_csv.iter().enumerate() {
                            let gc = int_grid_index_to_grid_coords(
                                i,
                                (level.px_wid / GRID_SIZE) as u32,
                                (level.px_hei / GRID_SIZE) as u32,
                            );
                            let Some(gc) = gc else {
                                warn!("Invalid GridCoords for index {i}");
                                continue;
                            };
                            last_coord = GridCoords::new(gc.x, gc.y);
                            if last_coord.x == 0 {
                                print!("[X :{:03} Y: {:03}]  ", last_coord.x, last_coord.y);
                            }
                            if *value == 1 {
                                print!("X");
                                wall_locations.insert(last_coord);
                            } else {
                                print!("_");
                            }
                            if last_coord.x == (level_tile_width - 1) {
                                println!("  [X :{:03} Y: {:03}]", last_coord.x, last_coord.y);
                            }
                        }
                    }
                });
            }

            multi_level_walls.cache.insert(
                level_iid.clone(), // You'll need to clone the key since HashMap takes ownership
                LevelWalls {
                    wall_locations,
                    level_tile_pos_x,
                    level_tile_pos_y,
                    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,
            );
        }
    }
    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(())
}
