/// TODO: Pick up and move pieces!
/// TODO: Custom Asset: SpriteSheetAtlas Mapper
/// TODO: Handle Cursor!
use bevy::window::{PrimaryWindow, WindowResized};

use crate::{
    game::{Board, BoardIndex, Piece, Side, Tile},
    prelude::*,
    tweak::Tweaks,
};
use serde::Deserialize;

const SCALE: f32 = 4.0;

pub(crate) struct Display2dPlugin;

impl Plugin for Display2dPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, (initialize_camera, set_background))
            .add_systems(OnExit(GameState::Loading), initialize_board)
            .add_systems(
                Update,
                (
                    move_piece
                        .run_if(in_state(GameState::Play))
                        .run_if(in_state(DisplayState::Display2d))
                        .run_if(any_with_component::<game::Selected>()),
                    select
                        .run_if(in_state(GameState::Play))
                        .run_if(in_state(DisplayState::Display2d))
                        .run_if(|buttons: Res<Input<MouseButton>>| -> bool {
                            buttons.just_pressed(MouseButton::Left)
                        }),
                    update_background.run_if(on_event::<WindowResized>()),
                    set_transform
                        .after(game::update_board)
                        .run_if(any_component_changed::<BoardIndex>),
                    sync_sprite.run_if(any_component_changed::<Side>),
                    load_spritesheet.run_if(on_event::<AssetEvent<Tweaks>>()),
                    // Set Sprite for Pieces
                    set_sprite.run_if(any_component_changed::<GameSprite>),
                    // When tweakfile is updated
                    set_sprite.run_if(resource_exists_and_changed::<SpriteSheet>()),
                ),
            )
            .add_systems(OnEnter(DisplayState::Display2d), activate::<Display2d>)
            .add_systems(OnExit(DisplayState::Display2d), deactivate::<Display2d>)
            .add_systems(
                OnEnter(GameState::Play),
                activate::<Display2d>.run_if(in_state(DisplayState::Display2d)),
            );
    }
}

/// Sprite sheet Resource for later reference
#[derive(Debug, Resource)]
struct SpriteSheet {
    handle: Handle<TextureAtlas>,
}

/// Marker component for the 2d entitys
#[derive(Debug, Component)]
pub(crate) struct Display2d;

#[derive(Debug, Component)]
struct BackgroundImage;

/// All possible sprites
/// Necessary because individual components of Piece, Side, and Light/Dark are not homogeneous
#[derive(Debug, Deserialize, Component, Clone, PartialEq)]
pub(crate) enum GameSprite {
    RedQueen,
    RedDrone,
    RedPawn,
    BlueQueen,
    BlueDrone,
    BluePawn,
    LightTile,
    DarkTile,
}

impl From<(game::Piece, game::Side)> for GameSprite {
    fn from((piece, side): (game::Piece, game::Side)) -> GameSprite {
        match (piece, side) {
            (Piece::Queen, Side::A) => GameSprite::RedQueen,
            (Piece::Drone, Side::A) => GameSprite::RedDrone,
            (Piece::Pawn, Side::A) => GameSprite::RedPawn,
            (Piece::Queen, Side::B) => GameSprite::BlueQueen,
            (Piece::Drone, Side::B) => GameSprite::BlueDrone,
            (Piece::Pawn, Side::B) => GameSprite::BluePawn,
        }
    }
}

impl From<Tile> for GameSprite {
    fn from(tile: game::Tile) -> GameSprite {
        match tile {
            Tile::Light => GameSprite::LightTile,
            Tile::Dark => GameSprite::DarkTile,
        }
    }
}

/// STARTUP: Initialize 2d gameplay Camera
fn initialize_camera(mut commands: Commands) {
    commands.spawn((
        Display2d,
        DisplayState::Display2d,
        Camera2dBundle {
            camera: Camera {
                is_active: false,
                ..default()
            },
            ..default()
        },
        UiCameraConfig { show_ui: true },
        Name::new("2D Camera"),
    ));
}

/// STARTUP: Load sprite sheet and insert texture atlas
fn load_spritesheet(
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
    tweaks: Res<Assets<tweak::Tweaks>>,
    mut commands: Commands,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    info!("Loading spritesheet");
    let tweak = tweaks
        .get(&tweaks_file.handle.clone())
        .expect("Load tweakfiles");
    let atlas = TextureAtlas::from_grid(
        tweak
            .get_handle_unchecked::<Image>("display2d_sprites_file")
            .unwrap(),
        Vec2::new(
            tweak.get::<f32>("display2d_sprites_tile_size_x").unwrap(),
            tweak.get::<f32>("display2d_sprites_tile_size_y").unwrap(),
        ),
        tweak.get::<usize>("display2d_sprites_columns").unwrap(),
        tweak.get::<usize>("display2d_sprites_rows").unwrap(),
        None,
        None,
    );
    commands.insert_resource(SpriteSheet {
        handle: texture_atlases.add(atlas),
    });
}

fn set_background(
    server: Res<AssetServer>,
    mut commands: Commands,
    window: Query<&Window, With<PrimaryWindow>>,
) {
    commands.spawn((
        BackgroundImage,
        Display2d,
        SpriteBundle {
            texture: server.load("images/mars-daybreak.png"),
            sprite: Sprite {
                custom_size: Some(Vec2 {
                    x: window.single().width(),
                    y: window.single().height(),
                }),
                ..default()
            },
            transform: Transform {
                translation: Vec3::NEG_Z,
                ..default()
            },
            visibility: Visibility::Hidden,
            ..default()
        },
    ));
}

fn update_background(
    mut sprites: Query<&mut Sprite, With<BackgroundImage>>,
    mut events: EventReader<WindowResized>,
) {
    events
        .read()
        .for_each(|&WindowResized { width, height, .. }| {
            sprites.iter_mut().for_each(|mut sprite| {
                sprite.custom_size = Some(Vec2 {
                    x: width,
                    y: height,
                });
            });
        });
}

/// STARTUP: Initialize the board for representation
fn initialize_board(board: Option<Res<Board>>, mut commands: Commands) {
    if let Some(board) = board {
        commands
            .spawn((
                Display2d,
                SpatialBundle {
                    visibility: Visibility::Hidden,
                    ..default()
                },
            ))
            .with_children(|parent| {
                // Spawn tiles
                game::tiles().for_each(|(index, tile)| {
                    let game_sprite: GameSprite = tile.clone().into();

                    parent.spawn((
                        game_sprite,
                        tile.clone(),
                        index,
                        Display2d,
                        SpriteSheetBundle { ..default() },
                        game::Selectable,
                    ));
                });

                // Spawn pieces
                board.pieces().iter().for_each(|(index, piece)| {
                    let side = Board::side(*index).expect("Spawn valid side");

                    let game_sprite: GameSprite = (*piece, side).into();

                    parent.spawn((
                        game_sprite,
                        piece.clone(),
                        Display2d,
                        index.clone(),
                        side.clone(),
                        SpriteSheetBundle { ..default() },
                        game::Selectable,
                    ));
                });
            });
    }
}

fn sync_sprite(mut events: Query<(&mut GameSprite, &Piece, &Side), Changed<Side>>) {
    events
        .iter_mut()
        .for_each(|(mut game_sprite, piece, side)| {
            *game_sprite = (*piece, *side).into();
        });
}

fn set_sprite(
    mut entities: Query<
        (
            &mut TextureAtlasSprite,
            &mut Handle<TextureAtlas>,
            &GameSprite,
        ),
        With<Display2d>,
    >,
    sprite_sheet: Option<Res<SpriteSheet>>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    let tweak = tweaks
        .get(tweaks_file.handle.clone())
        .expect("Load tweaksfile in set piece sprite");
    let sprite_sheet = sprite_sheet.expect("Sprite sheet");
    entities
        .iter_mut()
        .for_each(|(mut sprite, mut texture_atlas, game_sprite)| {
            if let Some(index) = tweak
                .get::<Vec<GameSprite>>("display2d_sprites_sprite_order")
                .unwrap()
                .iter()
                .position(|s| s == game_sprite)
            {
                if *texture_atlas != sprite_sheet.handle {
                    *texture_atlas = sprite_sheet.handle.clone();
                }
                sprite.index = index;
            }
        });
}

/// Sets a piece location given it's board index
fn set_transform(
    mut events: Query<
        (&mut Transform, &BoardIndex, Option<&Piece>, Option<&Tile>),
        (
            With<Display2d>,
            Or<(Changed<BoardIndex>, Added<BoardIndex>)>,
        ),
    >,
) {
    events.iter_mut().for_each(|(mut t, i, piece, tile)| {
        let x = SCALE * 16.0 * ((i.x as f32) - 3.5);
        let y = SCALE * 16.0 * ((i.y as f32) - 1.5);
        let z = match (piece, tile) {
            (None, None) | (Some(_), Some(_)) => {
                error!("Entity with BoardIndex is neither a Piece nor a Tile!");
                0.0
            }
            // Piece
            (Some(_), None) => 2.0,
            // Tile
            (None, Some(_)) => 1.0,
        };
        *t = Transform::from_scale(Vec3::splat(SCALE)).with_translation(Vec3::new(x, y, z));
        debug!("setting position of {:?} to {:?}", i, t);
    });
}

/// Select pieces and tiles in 2d
fn select(
    candidates: Query<
        (
            Entity,
            &TextureAtlasSprite,
            &Handle<TextureAtlas>,
            &GlobalTransform,
            &BoardIndex,
        ),
        (
            With<game::Selectable>,
            Without<game::Selected>,
            With<Display2d>,
        ),
    >,
    windows: Query<&Window, With<PrimaryWindow>>,
    cameras: Query<(&Camera, &GlobalTransform), With<Display2d>>,
    atlases: Res<Assets<TextureAtlas>>,
    mut selections: EventWriter<game::Selection>,
) {
    // For each window (there is only one)
    windows
        .iter()
        .filter_map(|window| window.cursor_position())
        .for_each(|position| {
            // For each 2d Camera (there is only one)
            cameras
                .iter()
                .filter_map(|(camera, transform)| camera.viewport_to_world_2d(transform, position))
                .for_each(|pos| {
                    // For each selectable sprite (there are many)
                    // Filter down to the list of hit objects
                    candidates
                        .iter()
                        .filter_map(
                            |(
                                _,
                                TextureAtlasSprite { index, anchor, .. },
                                handle,
                                transform,
                                board_index,
                            )| {
                                let sprite_size = atlases
                                    .get(handle)
                                    .map(|atlas| {
                                        atlas.textures.get(*index).expect("Atlas Sprite Texture")
                                    })
                                    .expect("Atlas Sprite Rectangle")
                                    .size();
                                hit::intersects2d(sprite_size, anchor, transform, pos)
                                    .then_some((transform, board_index))
                            },
                        )
                        .max_by(|(a, _), (b, _)| {
                            a.translation().z.partial_cmp(&b.translation().z).unwrap()
                        })
                        .map(|(_, board_index)| board_index)
                        .iter()
                        .for_each(|&board_index| {
                            selections.send(game::Selection(board_index.clone()));
                        });
                });
        });
}

fn move_piece(
    window: Query<&Window, With<PrimaryWindow>>,
    mut query: Query<&mut Transform, (With<game::Selected>, With<game::Piece>, With<Display2d>)>,
    camera_query: Query<(&Camera, &GlobalTransform), With<Display2d>>,
) {
    query.iter_mut().for_each(|mut t| {
        let (camera, camera_t) = camera_query.single();
        if let Some(pos) = window
            .single()
            .cursor_position()
            .and_then(|cursor| camera.viewport_to_world_2d(camera_t, cursor))
        {
            t.translation.x = pos.x;
            t.translation.y = pos.y;
        }
    })
}
