use bevy::core_pipeline::{
    contrast_adaptive_sharpening::ContrastAdaptiveSharpeningSettings, fxaa::Fxaa,
};

use crate::prelude::*;

pub(crate) struct Display3dPlugin;

impl Plugin for Display3dPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins((
            TemporalAntiAliasPlugin,
            MaterialPlugin::<DissolveMaterial>::default(),
        ))
        .init_state::<DissolvingAnimation>()
        .insert_resource(Msaa::Off)
        .insert_resource(AmbientLight {
            color: Color::WHITE,
            brightness: 100.0,
        })
        .add_systems(
            OnExit(GameState::Loading),
            (
                fix_skybox.before(initialize),
                initialize,
                update_tweaks.run_if(resource_exists::<tweak::GameTweaks>),
            ),
        )
        // Systems related to color and camera
        .add_systems(
            Update,
            (
                color_grading_tweak
                    .run_if(resource_exists::<tweak::GameTweaks>)
                    .run_if(on_event::<AssetEvent<Tweaks>>()),
                fog_tweak
                    .run_if(resource_exists::<tweak::GameTweaks>)
                    .run_if(on_event::<AssetEvent<Tweaks>>()),
                bloom_tweak
                    .run_if(resource_exists::<tweak::GameTweaks>)
                    .run_if(on_event::<AssetEvent<Tweaks>>()),
            ),
        )
        .add_systems(
            Update,
            (
                debug_textures,
                load_assets
                    .run_if(in_state(GameState::Loading))
                    .run_if(on_event::<AssetEvent<Tweaks>>()),
                hydrate_camera.run_if(any_component_added::<Camera3d>()),
                update_tweaks
                    .run_if(on_event::<AssetEvent<Tweaks>>())
                    .run_if(resource_exists::<tweak::GameTweaks>),
                switch_sides
                    .run_if(in_state(GameState::Play))
                    .run_if(state_changed::<game::TurnState>),
                update_pieces
                    .run_if(resource_exists::<tweak::GameTweaks>)
                    .run_if(in_state(GameState::Play))
                    .run_if(
                        any_component_changed::<Piece>()
                            .or_else(any_component_changed::<Side>())
                            .or_else(any_component_changed::<BoardIndex>())
                            .or_else(any_component_removed::<Animating>())
                            .or_else(any_component_removed::<Captured>()),
                    ),
                set_board_model.run_if(any_component_added::<game::BoardComponent>()),
                set_title_model.run_if(any_component_added::<TitleText>()),
                set_valid_move_model.run_if(any_component_added::<game::ValidMove>()),
                set_tile_hitbox.run_if(any_component_added::<game::Tile>()),
                select
                    .run_if(in_state(GameState::Play))
                    .run_if(in_state(DisplayState::Display3d))
                    .run_if(just_pressed(MouseButton::Left)),
                pick_up.run_if(any_component_added::<game::Selected>()),
                put_down.run_if(any_component_removed::<game::Selected>()),
                setup_dissolve_materials.run_if(
                    any_component_added::<Handle<StandardMaterial>>()
                        .or_else(any_component_changed::<Handle<StandardMaterial>>()),
                ),
                dissolve_animation.run_if(any_with_component::<Dissolving>),
                capture_piece.run_if(any_with_component::<game::Captured>),
                skip_animation
                    .run_if(in_state(GameState::Play))
                    .run_if(pressed(KeyCode::Enter).or_else(pressed(MouseButton::Left))),
                un_skip_animation.run_if(in_state(GameState::Play)).run_if(
                    just_released(KeyCode::Enter).or_else(just_released(MouseButton::Left)),
                ),
                monitor_animations
                    .run_if(in_state(GameState::Play))
                    .run_if(any_component_changed::<AnimationPlayer>()),
            ),
        )
        .add_systems(
            Update,
            (
                move_camera.run_if(on_event::<MouseMotion>()),
                mouse_zoom.run_if(on_event::<MouseWheel>()),
                gizmo_system,
                selected_gizmo,
                moves_gizmo,
                debug_selected.run_if(any_with_component::<game::Selected>),
            )
                .run_if(in_state(debug::DebugState::Enabled))
                .run_if(in_state(GameState::Play))
                .run_if(in_state(DisplayState::Display3d)),
        )
        .add_systems(
            Update,
            (
                animate_title_light_in.run_if(in_state(DissolvingAnimation::In)),
                animate_title_light_out.run_if(in_state(DissolvingAnimation::Out)),
            )
        )
        .add_systems(
            OnEnter(GameState::Intro),
            (
                // Toggle hidden/visible 3d entities
                manage_state_entities::<DisplayState>(),
                // fixup_shadows,
                color_grading_tweak.run_if(resource_exists::<tweak::GameTweaks>),
                fog_tweak.run_if(resource_exists::<tweak::GameTweaks>),
                bloom_tweak.run_if(resource_exists::<tweak::GameTweaks>),
                update_pieces.run_if(resource_exists::<tweak::GameTweaks>),
            ),
        )
        .add_systems(
            OnEnter(GameState::Play),
            (
                update_tweaks.run_if(resource_exists::<tweak::GameTweaks>),
                opening_animation
                    .run_if(run_once())
                    .run_if(in_state(DisplayState::Display3d)),
            ),
        )
        .add_systems(
            OnEnter(GameState::Title),
            (fixup_shadows, intro_title_dissolve),
        )
        .add_systems(OnExit(GameState::Title), outro_title_dissolve)
        .add_systems(
            Update,
            continue_title
                .run_if(in_state(GameState::Title))
                .run_if(not(any_with_component::<Dissolving>))
                .run_if(just_pressed(KeyCode::Enter).or_else(just_pressed(MouseButton::Left))),
        );
    }
}

#[derive(Debug, Component, PartialEq)]
pub(crate) struct Display3d;

#[derive(Debug, Component)]
pub(crate) struct TitleText;

#[derive(Debug, Component)]
pub(crate) struct Dissolvable {
    start: f32,
    duration: f32,
}

#[derive(Debug, Component)]
pub(crate) enum Dissolving {
    In(f32),
    Out(f32),
}

#[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone, Component)]
enum DissolvingAnimation {
    #[default]
    None,
    In,
    Out
}

#[derive(Debug, Resource, Clone)]
struct AssetsMap {
    hitbox_shape: Handle<Mesh>,
    hitbox_material: Handle<StandardMaterial>,
}

/// Load 3d models
/// This is kind of pulling double duty.
/// Both loads the GLTF file _and_ populates the ModelMap once that is loaded.
fn load_assets(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let hitbox_shape = meshes.add(Cuboid::new(1.0, 0.1, 1.0));
    let hitbox_material = materials.add(StandardMaterial {
        base_color: Color::NONE,
        perceptual_roughness: 0.0,
        reflectance: 0.0,
        alpha_mode: AlphaMode::Blend,
        ..default()
    });
    commands.insert_resource(AssetsMap {
        hitbox_shape,
        hitbox_material,
    });
}

/// Initialize the board and pieces
fn initialize(mut commands: Commands, board: Res<game::Board>, assets: Res<AssetsMap>) {
    info!("Initializing root");
    commands
        .spawn((
            SpatialBundle {
                visibility: Visibility::Hidden,
                ..default()
            },
            Display3d,
            DisplayState::Display3d,
        ))
        .with_children(|parent| {
            info!("Intializing 3D Board!");
            parent
                .spawn((
                    Display3d,
                    DisplayState::Display3d,
                    game::BoardComponent,
                    SceneBundle { ..default() },
                ))
                .with_children(|parent| {
                    // Hitboxes
                    game::tiles().for_each(|(index, tile)| {
                        let side = Board::side(index).expect("Spawn valid side");

                        parent.spawn((
                            DisplayState::Display3d,
                            Display3d,
                            index,
                            tile,
                            PbrBundle {
                                mesh: assets.hitbox_shape.clone(),
                                material: assets.hitbox_material.clone(),
                                visibility: Visibility::Hidden,
                                ..default()
                            },
                            side,
                            game::Selectable,
                        ));
                    });

                    // Valid move indicators
                    game::tiles().for_each(|(index, _)| {
                        let side = Board::side(index).expect("Spawn valid side");

                        parent.spawn((
                            DisplayState::Display3d,
                            Display3d,
                            index,
                            SceneBundle {
                                visibility: Visibility::Hidden,
                                transform: Transform::from_translation(board_translation(&index)),
                                ..default()
                            },
                            side,
                            game::ValidMove,
                        ));
                    });

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

                        parent.spawn((
                            side,
                            DisplayState::Display3d,
                            Display3d,
                            *piece,
                            *index,
                            SceneBundle { ..default() },
                            game::Selectable,
                            Dissolvable {
                                start: 1.0,
                                duration: 3.0,
                            }, // Marks pieces as dissolving
                        ));
                    });

                    // Title
                    parent.spawn((
                        DisplayState::Display3d,
                        Display3d,
                        // GameState::Title,
                        SceneBundle { ..default() },
                        TitleText,
                        Dissolvable {
                            start: 0.0,
                            duration: 6.0,
                        }, // Marks title text as dissolving
                    ));
                });
        });
}

fn hydrate_camera(
    events: Query<(&Name, Entity), Added<Camera>>,
    gltfs: Res<Assets<Gltf>>,
    state: Res<State<game::TurnState>>,
    mut players: Query<&mut AnimationPlayer>,
    mut commands: Commands,
    tweaks_file: Res<tweak::GameTweaks>,
    tweaks: Res<Assets<Tweaks>>,
) {
    let tweak = tweaks
        .get(tweaks_file.handle.clone())
        .expect("Load tweakfile");
    events
        .iter()
        .filter(|(name, _)| name.as_str() == "GameCam")
        .for_each(|(_, entity)| {
            info!("Initialize 3d camera");
            let skybox_handle = tweak
                .get_handle::<Image>("display3d_models_skybox_file")
                .unwrap();
            let skybox_brightness = tweak.get::<f32>("display3d_skybox_brightness").unwrap();
            let environment_map_intensity = tweak
                .get::<f32>("display3d_environment_map_light_intensity")
                .unwrap();

            info!("Hydrating camera {:?}", entity);
            // Populate the components for the camera
            commands.entity(entity).insert((
                DisplayState::Display3d,
                Display3d,
                Camera3dBundle {
                    camera: Camera {
                        is_active: true,
                        hdr: true,
                        ..default()
                    },
                    dither: DebandDither::Enabled,
                    color_grading: ColorGrading {
                        exposure: 1.0,
                        gamma: 1.0,
                        ..default()
                    },
                    tonemapping: Tonemapping::BlenderFilmic,
                    ..default()
                },
                Skybox {
                    image: skybox_handle.clone(),
                    brightness: skybox_brightness,
                },
                EnvironmentMapLight {
                    diffuse_map: skybox_handle.clone(),
                    specular_map: skybox_handle.clone(),
                    intensity: environment_map_intensity,
                },
                Fxaa {
                    enabled: true,
                    ..default()
                },
                ContrastAdaptiveSharpeningSettings {
                    enabled: false,
                    ..default()
                },
                BloomSettings {
                    intensity: 0.0,
                    ..default()
                },
                FogSettings {
                    color: Color::rgba(0.25, 0.25, 0.25, 1.0),
                    falloff: FogFalloff::Exponential { density: 0.0 },
                    ..default()
                },
            ));

            let assets_handle = tweak
                .get_handle::<Gltf>("display3d_models_assets_file")
                .unwrap();
            let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
            // Set it to the default position by starting the initial animation
            if let Ok(mut player) = players.get_mut(entity) {
                debug!("Animations: {:?}", gltf.named_animations.keys());
                // GameCamIntro1, GameCamIntro2, GameCamSide1>2, GameCamSide2>1
                let animation = match state.get() {
                    game::TurnState(game::Side::A) => gltf.named_animations.get(
                        tweak
                            .get::<String>("display3d_models_animations_intro_a")
                            .unwrap()
                            .as_str(),
                    ),
                    game::TurnState(game::Side::B) => gltf.named_animations.get(
                        tweak
                            .get::<String>("display3d_models_animations_intro_b")
                            .unwrap()
                            .as_str(),
                    ),
                }
                .expect("Camera Startup");
                player.play(animation.clone()).pause();
            }
        });
}

/// Update display3d tweaks in the game
/// Triggered on entering 3d state and when the tweakfile is updated.
fn update_tweaks(
    mut camera_settings: Query<(&mut Skybox, &mut EnvironmentMapLight), With<Display3d>>,
    tweaks_file: Res<tweak::GameTweaks>,
    tweaks: Res<Assets<Tweaks>>,
) {
    if let Some(tweak) = tweaks.get(tweaks_file.handle.clone()) {
        warn!("Updating tweaks!");
        camera_settings
            .iter_mut()
            .for_each(|(mut skybox, mut environment_map_light)| {
                skybox.brightness = tweak.get::<f32>("display3d_skybox_brightness").unwrap();
                environment_map_light.intensity = tweak
                    .get::<f32>("display3d_environment_map_light_intensity")
                    .unwrap();
            });
    }
}

fn fix_skybox(
    mut images: ResMut<Assets<Image>>,
    tweaks_file: Res<tweak::GameTweaks>,
    tweaks: Res<Assets<Tweaks>>,
) {
    let tweak = tweaks.get(tweaks_file.handle.clone()).unwrap();
    let handle = tweak
        .get_handle_unchecked::<Image>("display3d_models_skybox_file")
        .unwrap();
    let image = images.get_mut(handle).unwrap();

    info!("Loaded skybox image");
    // NOTE: PNGs do not have any metadata that could indicate they contain a cubemap texture,
    // so they appear as one texture. The following code reconfigures the texture as necessary.
    if image.texture_descriptor.array_layer_count() == 1 {
        image.reinterpret_stacked_2d_as_array(
            image.texture_descriptor.size.height / image.texture_descriptor.size.width,
        );
        image.texture_view_descriptor = Some(TextureViewDescriptor {
            dimension: Some(TextureViewDimension::Cube),
            ..default()
        });
    }
}

fn set_board_model(
    mut boards: Query<&mut Handle<Scene>, (Added<game::BoardComponent>, With<Display3d>)>,
    gltfs: Res<Assets<Gltf>>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    let tweak = tweaks
        .get(tweaks_file.handle.clone())
        .expect("Load tweakfile");
    boards.iter_mut().for_each(|mut handle| {
        info!("Setting board model");
        let assets_handle = tweak
            .get_handle::<Gltf>("display3d_models_assets_file")
            .unwrap();
        let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
        *handle = gltf
            .named_scenes
            .get(
                tweak
                    .get::<String>("display3d_models_scenes_board")
                    .unwrap()
                    .as_str(),
            )
            .expect("Game board model")
            .clone();
    });
}

fn set_title_model(
    mut titles: Query<(&mut Handle<Scene>, &mut Transform, &mut Visibility), (Added<TitleText>, With<Display3d>)>,
    gltfs: Res<Assets<Gltf>>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    let tweak = tweaks
        .get(tweaks_file.handle.clone())
        .expect("Load tweakfile");
    titles.iter_mut().for_each(|(mut handle, mut transform, mut visibility)| {
        info!("Setting title model");
        let assets_handle = tweak
            .get_handle::<Gltf>("display3d_models_assets_file")
            .unwrap();
        let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
        *handle = gltf
            .named_scenes
            .get(
                tweak
                    .get::<String>("display3d_models_scenes_title")
                    .unwrap()
                    .as_str(),
            )
            .expect("Game title model")
            .clone();

        transform.translation -= Vec3::Y * 0.5;
        transform.rotate_local_z(std::f32::consts::PI);
        transform.rotate_local_y(std::f32::consts::PI / 2.0);

        *visibility = Visibility::Hidden;
    });
}

/// Given a board index returns the Vec3 location in space
fn board_translation(&BoardIndex { x, y }: &BoardIndex) -> Vec3 {
    // Scale x down by 4 to account for -4..4 scaling
    let x = x as i8 - 4;
    // Mirror y axis because our board index is inverted...
    let y = -(y as i8) + 1;

    let x = if x < 0 {
        x as f32 * 1.3 + 0.275 // 0.325
    } else {
        x as f32 * 1.3 + 1.05 // 1.0
    };

    let y = y as f32 * 1.3 + 0.65;

    Vec3::new(x, 0.0, y)
}

fn capture_translation(side: &Side, num: usize) -> Vec3 {
    info!("Side: {:?} Num: {:?}", side, num);
    let x = 5.0 - ((num % 4) as f32 * 1.3);
    let y = -1.3;
    let z = 4.0 + ((num / 4) as f32 * 1.3);
    match side {
        Side::B => Vec3::new(-x, y, z),
        Side::A => Vec3::new(x, y, -z),
    }
}

fn gizmo_system(mut gizmos: Gizmos) {
    for y in 0..4 {
        for x in 0..8 {
            gizmos.cuboid(
                Transform::from_translation(board_translation(&BoardIndex { x, y }))
                    .with_scale(Vec3::new(1.0, 0.1, 1.0)),
                Color::PURPLE,
            )
        }
    }
}

/// TODO: This has bad feel, needs to be tuned
fn move_camera(
    buttons: Res<ButtonInput<MouseButton>>,
    mut events: EventReader<MouseMotion>,
    mut camera: Query<&mut Transform, (With<Display3d>, With<Camera>)>,
) {
    events.read().for_each(|MouseMotion { delta }| {
        if buttons.pressed(MouseButton::Left) {
            camera.iter_mut().for_each(|mut t| {
                t.rotate_around(Vec3::ZERO, Quat::from_rotation_y(delta.x / 256.0));
                t.rotate_around(Vec3::ZERO, Quat::from_rotation_x(delta.y / 256.0));
                t.look_at(Vec3::ZERO, Vec3::Y);
            });
        }
    });
}

fn mouse_zoom(
    mut events: EventReader<MouseWheel>,
    mut camera: Query<&mut Transform, (With<Display3d>, With<Camera>)>,
) {
    events.read().for_each(|MouseWheel { unit, y, .. }| {
        camera.iter_mut().for_each(|mut t| {
            match unit {
                MouseScrollUnit::Line => {
                    t.translation *= 1.0 - (*y / 4.0);
                }
                MouseScrollUnit::Pixel => {
                    t.translation *= 1.0 - (*y / 64.0);
                }
            }
            t.look_at(Vec3::ZERO, Vec3::Y);
        });
    });
}

// The dumbest way to achieve this goal
// Essentially we iterate over every piece and set the appropriate model and texture
fn update_pieces(
    mut query: Query<(
        Entity,
        &Piece,
        &Side,
        &BoardIndex,
        &mut Transform,
        &mut Handle<Scene>,
    )>,
    children: Query<&Children>,
    active_animation_players: Query<&AnimationPlayer, With<Animating>>,
    gltfs: Res<Assets<Gltf>>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    let tweak = tweaks
        .get(tweaks_file.handle.clone())
        .expect("Load tweakfile");
    let assets_handle = tweak
        .get_handle::<Gltf>("display3d_models_assets_file")
        .unwrap();
    let models = gltfs.get(assets_handle).unwrap();

    query.iter_mut().for_each(
        |(entity, piece, side, board_index, mut transform, mut scene)| {
            // Set position of piece
            let new_translation = board_translation(board_index);
            if transform.translation != new_translation {
                info!("Updating piece transform");
                transform.translation = new_translation;
            }

            // Check if any children are animating
            let animating = active_animation_players
                .iter_many(children.iter_descendants(entity))
                .count();

            if animating > 0 {
                info!("Piece {:?} is animating. Skipping...", entity);
            } else {
                info!("Checking piece object scene for {:?}", entity);

                // Find name of this piece's model scene
                let scene_tweak_name: Option<String> = match (piece, side) {
                    (Piece::Pawn, Side::A) => tweak.get("display3d_models_scenes_pawn_red"),
                    (Piece::Pawn, Side::B) => tweak.get("display3d_models_scenes_pawn_blue"),
                    (Piece::Drone, Side::A) => tweak.get("display3d_models_scenes_drone_red"),
                    (Piece::Drone, Side::B) => tweak.get("display3d_models_scenes_drone_blue"),
                    (Piece::Queen, Side::A) => tweak.get("display3d_models_scenes_queen_red"),
                    (Piece::Queen, Side::B) => tweak.get("display3d_models_scenes_queen_blue"),
                };

                // Get model scene for this piece
                let scene_handle = models.named_scenes.get(&scene_tweak_name.unwrap()).unwrap();

                // Set scene model for this piece
                // if *scene != *scene_handle {
                warn!("Updating scene for piece {:?}", entity);
                *scene = scene_handle.clone();
                // }
            }
        },
    );
}

/// Select tiles and pieces in 3d
/// There is a bug where we are selecting multiple entities...
/// TODO: Selectable generalize picking pieces **and** hitboxes
fn select(
    mut events: EventReader<MouseButtonInput>,
    query: Query<(Entity, &Handle<Mesh>, &GlobalTransform)>,
    meshes: Res<Assets<Mesh>>,
    cameras: Query<(&Camera, &GlobalTransform)>,
    windows: Query<&Window, With<PrimaryWindow>>,
    selectable: Query<(Entity, &BoardIndex, &Side), (With<game::Selectable>, With<Display3d>)>,
    selected: Query<Entity, With<game::Selected>>,
    children: Query<&Children>,
    mut selections: EventWriter<game::Selection>,
    state: Res<State<game::TurnState>>,
) {
    // For every mouse click event
    events
        .read()
        // Only read button presses
        .filter(|ev| ev.state == ButtonState::Pressed && ev.button == MouseButton::Left)
        .for_each(|_| {
            // For each window (there should be only one)
            windows.iter().for_each(|window| {
                // Get the cursor position
                if let Some(pos) = window.cursor_position() {
                    // iterate over every camera
                    cameras.iter().for_each(|(camera, gt)| {
                        // Get a ray from the camera through the cursor into the world
                        if let Some(ray) = camera.viewport_to_world(gt, pos) {
                            // Iterate over every entity with a 3d scene
                            query
                                .iter()
                                // Transform the scene handle into mesh data
                                .filter_map(|(entity, handle, gt)| {
                                    meshes.get(handle).map(|mesh| (entity, mesh, gt))
                                })
                                // Iterate over every mesh + global transform
                                .filter_map(|(entity, mesh, gt)| {
                                    // If the camera -> cursor -> * ray intersects with the mesh
                                    hit3d::intersects3d(&ray, mesh, gt).map(|hit| (entity, hit))
                                })
                                .filter_map(|(entity, hit)| {
                                    // Find this entity in the set of selectable entities
                                    selectable
                                        .iter()
                                        .find_map(|(e, &board_index, &side)| {
                                            // Check the side of the selection if no piece is selected
                                            // Otherwise this is fine, select away
                                            let side_check =
                                                !selected.is_empty() || state.get().0 == side;

                                            let hit_check = {
                                                // This entity was hit (tile hitboxes)
                                                let primary = entity == e;

                                                // A child was hit (pieces)
                                                let secondary = children
                                                    .iter_descendants(e)
                                                    .any(|child| child == entity);

                                                primary || secondary
                                            };

                                            // Return the board index of this piece
                                            (side_check && hit_check).then_some(board_index)
                                        })
                                        .map(|board_index| {
                                            // Include the hit from the camera
                                            (hit, board_index)
                                        })
                                })
                                // Compare the distance of all hits, choosing the closest one
                                .min_by(|(hit_a, _), (hit_b, _)| {
                                    hit_a.distance.partial_cmp(&hit_b.distance).unwrap()
                                })
                                // Iterate over the 0 or 1 outcomes of the above
                                .iter()
                                // Send an event that this board index was selected
                                .for_each(|(_, board_index)| {
                                    info!("Board index selected: {:?}", board_index);
                                    selections.send(game::Selection(*board_index));
                                });
                        }
                    });
                }
            });
        });
}

fn selected_gizmo(
    selected: Query<&Transform, (With<game::Selected>, With<Display3d>)>,
    mut gizmos: Gizmos,
) {
    selected.iter().for_each(|transform| {
        gizmos.cuboid(*transform, Color::GREEN);
    })
}

fn moves_gizmo(
    selected: Query<&BoardIndex, (With<game::Piece>, With<game::Selected>, With<Display3d>)>,
    board: Res<Board>,
    mut gizmos: Gizmos,
) {
    selected.iter().for_each(|idx| {
        board
            .valid_moves(*idx)
            .iter()
            .map(|i| Transform::from_translation(board_translation(i)))
            .for_each(|t| gizmos.cuboid(t, Color::WHITE))
    });
}

fn set_valid_move_model(
    mut events: Query<
        (&mut Handle<Scene>, &mut Visibility),
        (With<Display3d>, Added<game::ValidMove>),
    >,
    gltfs: Res<Assets<Gltf>>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    let tweak = tweaks
        .get(tweaks_file.handle.clone())
        .expect("Load tweakfile");
    let assets_handle = tweak
        .get_handle::<Gltf>("display3d_models_assets_file")
        .unwrap();
    if let Some(gltf) = gltfs.get(assets_handle) {
        info!("Setting valid move model");

        events.iter_mut().for_each(|(mut handle, mut visibility)| {
            *handle = gltf
                .named_scenes
                .get(
                    tweak
                        .get::<String>("display3d_models_scenes_valid_move")
                        .unwrap()
                        .as_str(),
                )
                .unwrap()
                .clone();

            *visibility = Visibility::Hidden;
        })
    }
}

fn pick_up(
    mut events: Query<
        (Entity, &game::Piece, &game::Side),
        (With<game::Piece>, With<Display3d>, Added<game::Selected>),
    >,
    gltfs: Res<Assets<Gltf>>,
    children: Query<&Children>,
    mut players: Query<(&Name, &mut AnimationPlayer)>,
    clips: Res<Assets<AnimationClip>>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    let tweak = tweaks
        .get(tweaks_file.handle.clone())
        .expect("Load tweakfile");
    events.iter_mut().for_each(|(entity, piece, side)| {
        info!("Picking up piece");

        let assets_handle = tweak
            .get_handle::<Gltf>("display3d_models_assets_file")
            .unwrap();
        let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
        info!("Pickup animation for {:?}", entity);
        children.iter_descendants(entity).for_each(|child| {
            info!("    Child: {:?}", child);
            if let Ok((name, mut player)) = players.get_mut(child) {
                info!("Picking up {:?} ({:?}) {:?}", name, entity, piece);
                let pickup_animation = format!(
                    "display3d_models_animations_pick_up_{:?}_{}",
                    piece,
                    side.color_str()
                )
                .to_ascii_lowercase();
                let pickup_handle = gltf
                    .named_animations
                    .get(
                        tweak
                            .get::<String>(pickup_animation.as_str())
                            .unwrap()
                            .as_str(),
                    )
                    .expect("Pickup Animation");
                let idle_animation = format!(
                    "display3d_models_animations_idle_{:?}_{}",
                    piece,
                    side.color_str()
                )
                .to_ascii_lowercase();
                let idle_handle = gltf
                    .named_animations
                    .get(
                        tweak
                            .get::<String>(idle_animation.as_str())
                            .unwrap()
                            .as_str(),
                    )
                    .expect("Idle animation");
                if let Some(pickup_clip) = clips.get(pickup_handle) {
                    if let Some(idle_clip) = clips.get(idle_handle) {
                        if pickup_clip.compatible_with(name) && idle_clip.compatible_with(name) {
                            player
                                .start_with_transition(
                                    pickup_handle.clone(),
                                    Duration::from_secs_f32(0.75),
                                )
                                .start_with_transition(
                                    idle_handle.clone(),
                                    Duration::from_secs_f32(1.5),
                                )
                                .set_repeat(RepeatAnimation::Forever);
                        }
                    };
                }
            }
        })
    });
}

fn put_down(
    mut events: RemovedComponents<game::Selected>,
    mut query: Query<
        (&game::Piece, &game::Side),
        (With<game::Piece>, With<game::Selectable>, With<Display3d>),
    >,
    gltfs: Res<Assets<Gltf>>,
    children: Query<&Children>,
    mut players: Query<(&Name, &mut AnimationPlayer)>,
    clips: Res<Assets<AnimationClip>>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    let tweak = tweaks
        .get(&tweaks_file.handle.clone())
        .expect("Load tweakfile");
    events.read().for_each(|entity| {
        if let Ok((piece, side)) = query.get_mut(entity) {
            info!("Putting down piece");

            let assets_handle = tweak
                .get_handle::<Gltf>("display3d_models_assets_file")
                .unwrap();
            let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
            children.iter_descendants(entity).for_each(|child| {
                if let Ok((name, mut player)) = players.get_mut(child) {
                    info!("Putting down {:?}", entity);
                    let putdown_animation = format!(
                        "display3d_models_animations_put_down_{:?}_{}",
                        piece,
                        side.color_str()
                    )
                    .to_ascii_lowercase();
                    let putdown_handle = gltf
                        .named_animations
                        .get(
                            tweak
                                .get::<String>(putdown_animation.as_str())
                                .unwrap()
                                .as_str(),
                        )
                        .expect("PutDown Animation");
                    if let Some(putdown_clip) = clips.get(putdown_handle) {
                        if putdown_clip.compatible_with(name) {
                            info!("Compatible with put-down clip!");
                            player
                                .start_with_transition(
                                    putdown_handle.clone(),
                                    Duration::from_secs_f32(0.75),
                                )
                                .set_repeat(RepeatAnimation::Never);
                        }
                    }
                }
            })
        }
    })
}

fn set_tile_hitbox(
    mut events: Query<(&mut Transform, &BoardIndex), (With<Display3d>, Added<game::Tile>)>,
) {
    events.iter_mut().for_each(|(mut transform, index)| {
        info!("Setting tile hitbox");

        *transform = Transform::from_translation(board_translation(index));
    });
}

fn opening_animation(mut players: Query<&mut AnimationPlayer, (With<Camera>, With<Display3d>)>) {
    players.iter_mut().for_each(|mut player| {
        info!("Playing intro camera animation");
        player.resume()
    });
}

// When called skips any running animations
fn skip_animation(
    mut players: Query<&mut AnimationPlayer, With<Animating>>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    let tweak = tweaks
        .get(tweaks_file.handle.clone())
        .expect("Load tweakfile");
    let speed = tweak.get::<f32>("animation_fast_speed").unwrap();
    players.iter_mut().for_each(|mut p| {
        debug!("Skipping animation");
        // HACK: Set the speed of the animation to turbo
        p.set_speed(speed);
    })
}

fn un_skip_animation(mut players: Query<&mut AnimationPlayer, With<Animating>>) {
    players.iter_mut().for_each(|mut p| {
        debug!("Un-Skipping animation");
        p.set_speed(1.0);
    })
}

fn switch_sides(
    mut players: Query<&mut AnimationPlayer, (With<Camera>, With<Display3d>)>,
    gltfs: Res<Assets<Gltf>>,
    state: Res<State<game::TurnState>>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    let tweak = tweaks
        .get(tweaks_file.handle.clone())
        .expect("Load tweakfile");
    let assets_handle = tweak
        .get_handle::<Gltf>("display3d_models_assets_file")
        .unwrap();
    let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
    players.iter_mut().for_each(|mut player| {
        info!("Switching sides");

        let animation = match state.get() {
            game::TurnState(game::Side::A) => gltf.named_animations.get(
                tweak
                    .get::<String>("display3d_models_animations_turn_a")
                    .unwrap()
                    .as_str(),
            ),
            game::TurnState(game::Side::B) => gltf.named_animations.get(
                tweak
                    .get::<String>("display3d_models_animations_turn_b")
                    .unwrap()
                    .as_str(),
            ),
        };
        player.start_with_transition(
            animation.expect("Camera Transition Animation").clone(),
            Duration::from_secs_f32(1.00),
        );
    });
}

/// Type expressing the extended material of standardMaterial + dissolveMaterial
type DissolveMaterial = ExtendedMaterial<StandardMaterial, DissolveExtension>;

/// Material extension for dissolving effect
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
struct DissolveExtension {
    #[uniform(100)]
    percentage: f32,
}

impl MaterialExtension for DissolveExtension {
    fn fragment_shader() -> ShaderRef {
        "shaders/dissolve.wgsl".into()
    }
}

/// Sets up all pieces to have an associated "dissolve" material ready for capture
fn setup_dissolve_materials(
    // All entities with materials are candidates for this procedure
    events: Query<
        (Entity, &Handle<StandardMaterial>, Option<&Name>),
        Added<Handle<StandardMaterial>>,
    >,
    // Only process newly created pieces (we do not delete pieces at runtime)
    query: Query<&Dissolvable, Added<Children>>,
    // Children of pieces are the actual meshes that need materials
    parents: Query<&Parent>,
    // Used to create DissolveMaterial
    standard_materials: Res<Assets<StandardMaterial>>,
    // Used to create Handle<DissolveMaterial>
    mut dissolve_materials: ResMut<Assets<DissolveMaterial>>,
    // Used to insert Handle<DissolveMaterial>;
    mut commands: Commands,
) {
    events
        .iter()
        // Only process if this is a child of a piece
        .filter(|(child, _, _)| query.iter_many(parents.iter_ancestors(*child)).count() > 0)
        // Handle this entity (mesh)
        .for_each(|(child, std_handle, name)| {
            info!("Setting up dissolve material for {:?} {:?}", name, child);

            // Get dissolvable data for percentage start
            let dissolvable = query
                .iter_many(parents.iter_ancestors(child))
                .next()
                .expect("Dissolving data");

            // Extension we will add to existing gltf-sourced materials
            let extension = DissolveExtension {
                percentage: dissolvable.start,
            };
            // Base material we will extend for the duration of the dissolve effect
            let mut base: StandardMaterial = standard_materials
                .get(std_handle)
                .expect("Resolve material data")
                .clone();

            base.alpha_mode = AlphaMode::Mask(0.5);
            base.base_color = base.base_color.clone().with_a(0.0);
            debug!("Base material {:#?}", base);

            let dis_handle = dissolve_materials.add(ExtendedMaterial { base, extension });

            // Add the dissolve handle as a Backup(T)
            commands
                .entity(child)
                .insert(dis_handle.clone())
                .remove::<Handle<StandardMaterial>>();
        });
}

/// When a piece is captured...
/// 1. Play a cool "captured" animation and a neat sound
/// 2. Move the piece to the side of the board
/// 3. Play the same "captured" animation in reverse
/// The animation is like a 'beam me up scotty' sorta thing.
fn capture_piece(
    events: Query<(Entity, &Dissolvable), (With<Display3d>, Added<game::Captured>)>,
    mut query: Query<
        (&mut Transform, &Side, &Dissolvable),
        (With<Display3d>, With<game::Captured>),
    >,
    dissolving: Query<Entity, With<Dissolving>>,
    mut state: Local<Option<game::CaptureFlow>>,
    mut commands: Commands,
    score: Res<game::Score>,
) {
    match *state {
        // State is None, so we need to initiate the animation
        None => {
            *state = events.iter().next().map(|(entity, dissolvable)| {
                // Insert the "Dissolving::Out" tag on the entity we want to fade out
                commands
                    .entity(entity)
                    .insert(Dissolving::Out(dissolvable.duration));

                // Set the next state to start fading out
                game::CaptureFlow::FadeOut(entity)
            });
        }
        Some(s) => {
            match s {
                game::CaptureFlow::FadeOut(_entity) => {
                    // Wait for fade-out animation
                    // If all pieces are done dissolving
                    if dissolving.is_empty() {
                        info!("Nothing dissolving, moving on to next step!");
                        // Move to next state now that animation is done
                        *state = s.next();
                        // This takes effect after updating all children
                    }
                }
                game::CaptureFlow::Store(entity) => {
                    let (mut t, side, dissolvable) = query
                        .get_mut(entity)
                        .expect("Visibility and Transform of captured piece");

                    // HACK: This is dirty. Why side, but score.captures(!side)?
                    info!(
                        "Capture translation: ({:?}, {:?}) {:?}",
                        side,
                        score.captures(!*side).saturating_sub(1),
                        t.translation
                    );
                    t.translation =
                        capture_translation(side, score.captures(!*side).saturating_sub(1));

                    commands
                        .entity(entity)
                        .insert(Dissolving::In(dissolvable.duration));

                    *state = s.next();
                }
                game::CaptureFlow::FadeIn(_entity) => {
                    // If we have completed the dissolve-in animation
                    if dissolving.is_empty() {
                        info!("Nothing dissolving, moving on to next step!");

                        // Move to next state now that animation is done
                        *state = s.next();
                    }
                }
            }
        }
    }
}

fn debug_selected(
    query: Query<(Entity, &BoardIndex, &Piece, &Side), With<game::Selected>>,
    mut debug_info: ResMut<debug::DebugInfo>,
) {
    query.iter().for_each(|(e, bi, p, s)| {
        debug_info.set(
            "Active".into(),
            format!(
                "\n>>ID: {:?}\n>>Piece: {:?}\n>>Side: {:?}\n>>Index: {:?}",
                e, p, s, bi
            ),
        );
    });
}

fn debug_textures(mut events: EventReader<AssetEvent<StandardMaterial>>) {
    events.read().for_each(|event| {
        debug!("Material Event: {:?}", event);
    });
}

// Marker for entities which are currently animating
#[derive(Component, Debug)]
struct Animating;

fn monitor_animations(
    active: Query<(Entity, &AnimationPlayer), (Changed<AnimationPlayer>, With<Animating>)>,
    mut inactive: Query<
        (Entity, &mut AnimationPlayer),
        (Changed<AnimationPlayer>, Without<Animating>),
    >,
    mut commands: Commands,
) {
    // Remove Animating component from players that are done
    active.iter().for_each(|(entity, player)| {
        if player.is_finished() {
            info!("Entity {:?} is done, removing animating marker", entity);
            commands.entity(entity).remove::<Animating>();
        }
    });
    // Set inactive entities to active
    inactive.iter_mut().for_each(|(entity, mut player)| {
        if !player.is_finished() && *player.animation_clip() != Handle::<AnimationClip>::default() {
            info!(
                "Entity {:?} is playing {:?}, adding animating marker",
                entity,
                player.animation_clip()
            );
            commands.entity(entity).insert(Animating);
            player.set_speed(1.0);
        }
    });
}

fn intro_title_dissolve(
    mut query: Query<(Entity, &Dissolvable), With<TitleText>>,
    mut commands: Commands,
    mut next: ResMut<NextState<DissolvingAnimation>>,
) {
    query.iter_mut().for_each(|(entity, dissolving)| {
        commands
            .entity(entity)
            .insert(Dissolving::In(dissolving.duration));
        next.set(DissolvingAnimation::In);
    });
}

fn outro_title_dissolve(
    mut query: Query<(Entity, &Dissolvable), With<TitleText>>,
    mut commands: Commands,
    mut next: ResMut<NextState<DissolvingAnimation>>,
) {
    query.iter_mut().for_each(|(entity, dissolving)| {
        commands
            .entity(entity)
            .insert(Dissolving::Out(dissolving.duration));
        next.set(DissolvingAnimation::Out);
    });
}

fn continue_title(mut next_state: ResMut<NextState<GameState>>) {
    next_state.set(GameState::Play)
}

/// When a piece is tagged with `Dissolving` play the dissolve animation
/// This is done by updating the Material and referencing the `Dissolvable` tag
/// Calculating how far along the animation it should be update the material's percentage
/// Materials are on the children of the tagged entity
fn dissolve_animation(
    mut query: Query<(Entity, &Dissolvable, &mut Dissolving)>,
    children: Query<&Children>,
    // Used to create Handle<DissolveMaterial>
    mut dissolve_materials: ResMut<Assets<DissolveMaterial>>,
    object_materials: Query<(Entity, &Handle<DissolveMaterial>)>,
    mut commands: Commands,
    keys: Res<ButtonInput<KeyCode>>,
    mouse: Res<ButtonInput<MouseButton>>,
    time: Res<Time>,
    mut next: ResMut<NextState<DissolvingAnimation>>,
) {
    query
        .iter_mut()
        .for_each(|(entity, dissolvable, mut dissolving)| {
            debug!("Entity {:?} has Dissolving {:?}", entity, dissolving);

            let skip = keys.just_pressed(KeyCode::Enter) || mouse.just_pressed(MouseButton::Left);

            let percentage = match *dissolving {
                Dissolving::In(mut sec) => {
                    if skip {
                        1.0
                    } else {
                        // Check if seconds is below 0.0
                        sec = (sec - time.delta_seconds()).max(0.0);

                        *dissolving = Dissolving::In(sec);

                        // Calculate the target percentage value
                        1.0 - (sec / dissolvable.duration)
                    }
                }
                Dissolving::Out(mut sec) => {
                    if skip {
                        0.0
                    } else {
                        // Check if seconds is below 0.0
                        sec = (sec - time.delta_seconds()).max(0.0);

                        *dissolving = Dissolving::Out(sec);

                        // Calculate the target percentage value
                        sec / dissolvable.duration
                    }
                }
            };

            object_materials
                .iter_many(children.iter_descendants(entity))
                .for_each(|(_child, handle)| {
                    let dissolve_material = dissolve_materials
                        .get_mut(handle)
                        .expect("Get the dissolve material");

                    // Change the material's value to create animation
                    dissolve_material.extension.percentage = percentage;
                });

            // If animation is done, remove dissolving component
            if percentage <= 0.0 || percentage >= 1.0 {
                info!(
                    "Removing dissolving from {:?} with percentage {:?}",
                    entity, percentage
                );
                commands.entity(entity).remove::<Dissolving>();
                next.set(DissolvingAnimation::None);
            }
        });
}

fn fixup_shadows(
    mut spot_lights: Query<&mut SpotLight>,
    mut point_lights: Query<&mut PointLight>,
    mut directional_lights: Query<&mut DirectionalLight>,
) {
    info!("Fixing up shadows");
    spot_lights.iter_mut().for_each(|mut l| {
        l.shadows_enabled = true;
    });
    point_lights.iter_mut().for_each(|mut l| {
        l.shadows_enabled = true;
    });
    directional_lights.iter_mut().for_each(|mut l| {
        l.shadows_enabled = true;
    });
}

fn color_grading_tweak(
    mut query: Query<&mut ColorGrading>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    let tweak = tweaks
        .get(tweaks_file.handle.clone())
        .expect("Load tweakfile");
    let exposure = tweak.get::<f32>("color_grading_exposure").unwrap();
    let gamma = tweak.get::<f32>("color_grading_gamma").unwrap();
    let pre_saturation = tweak.get::<f32>("color_grading_pre_saturation").unwrap();
    let post_saturation = tweak.get::<f32>("color_grading_post_saturation").unwrap();

    query.iter_mut().for_each(|mut cg| {
        *cg = ColorGrading {
            exposure,
            gamma,
            pre_saturation,
            post_saturation,
        }
    })
}

fn fog_tweak(
    mut query: Query<&mut FogSettings>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    query.iter_mut().for_each(|mut fog| {
        let tweak = tweaks
            .get(tweaks_file.handle.clone())
            .expect("Load tweakfile");

        fog.falloff = FogFalloff::Exponential {
            density: tweak.get::<f32>("color_fog_density").unwrap(),
        };
    })
}

fn bloom_tweak(
    mut query: Query<&mut BloomSettings>,
    tweaks: Res<Assets<Tweaks>>,
    tweaks_file: Res<tweak::GameTweaks>,
) {
    query.iter_mut().for_each(|mut bloom| {
        let tweak = tweaks
            .get(tweaks_file.handle.clone())
            .expect("Load tweakfile");

        bloom.intensity = tweak.get::<f32>("color_bloom_intensity").unwrap();
    })
}

fn animate_title_light_in(
    mut query: Query<(Entity, &mut Visibility), With<TitleText>>,
    children: Query<&Children>,
    mut lights: Query<&mut SpotLight>,
    mut t: Local<Timer>,
    mut started: Local<bool>,
    time: Res<Time>,
) {
    // Over 6 seconds, fade in to 600 intensity
    if !(*started) {
        *t = Timer::from_seconds(6.0, TimerMode::Once);
        *started = true
    } else {
        t.tick(time.delta());
    }

    info!("Spotlight in");

    let intensity = t.fraction() * 409800.0;

    query.iter_mut().for_each(|(e, mut v)| {
        if *v == Visibility::Hidden {
            *v = Visibility::Inherited;
        }

        children.iter_descendants(e).for_each(|c| {
            if let Ok(mut spot_light) = lights.get_mut(c) {
                info!("SPOTLIGHT UPDATE {:?}", spot_light);
                spot_light.intensity = intensity;
            }
        })
    });
}

fn animate_title_light_out(
    mut query: Query<(Entity, &mut Visibility), With<TitleText>>,
    children: Query<&Children>,
    mut lights: Query<&mut SpotLight>,
    mut t: Local<Timer>,
    mut started: Local<bool>,
    time: Res<Time>,
) {
    // Over 6 seconds, fade in to 600 intensity
    if !(*started) {
        *t = Timer::from_seconds(3.0, TimerMode::Once);
        *started = true
    } else {
        t.tick(time.delta());
    }

    info!("Spotlight out");

    let intensity = (1.0 - t.fraction()) * 409800.0;

    query.iter_mut().for_each(|(e, mut v)| {
        if *v == Visibility::Hidden {
            *v = Visibility::Inherited;
        }

        children.iter_descendants(e).for_each(|c| {
            if let Ok(mut spot_light) = lights.get_mut(c) {
                info!("SPOTLIGHT UPDATE {:?}", spot_light);
                spot_light.intensity = intensity;
            }
        })
    });
}