// Bevy basically forces "complex types" with Querys
#![allow(clippy::type_complexity)]

use bevy::image::{ImageLoaderSettings, ImageSampler};
use bevy::platform::time::Instant;
use bevy::render::view::ColorGrading;
use games::physics2d::*;
use games::*;
use std::hash::BuildHasher;

fn main() {
    App::new()
        .add_plugins((
            BaseGamePlugin {
                title: "flappy bird (with rewind)".into(),
                name: "flappy".into(),
                game_type: GameType::Two,
                target_resolution: (360, 640).into(),
            },
            Physics2dPlugin,
        ))
        .insert_resource(Gravity(Vec2::NEG_Y * 9.8 * 100.0))
        .init_resource::<BirdAssets>()
        .init_resource::<PipeAssets>()
        .init_resource::<GroundAssets>()
        .init_resource::<CeilingAssets>()
        .init_resource::<Score>()
        .init_resource::<RewindFrames>()
        .init_resource::<Flaps>()
        .init_resource::<Deaths>()
        .init_resource::<LongestRun>()
        .init_state::<PlayerState>()
        .add_systems(
            Startup,
            (
                // init_obstacles,
                init_assets,
                init_bird.after(init_assets),
                init_first_batches.after(init_assets),
                init_ui,
                init_background,
                tweak_camera.after(create_camera_2d),
            ),
        )
        .add_systems(
            OnEnter(PlayerState::Alive),
            (alive_bird, start_run, reset_button::<FlapButton>),
        )
        .add_systems(OnExit(PlayerState::Alive), end_run)
        .add_systems(OnEnter(PlayerState::Rewind), alive_bird)
        .add_systems(OnEnter(PlayerState::Pause), pause_bird)
        .add_systems(OnEnter(PlayerState::Stasis), pause_bird)
        .add_systems(OnExit(PlayerState::Stasis), reset_button::<RewindButton>)
        .add_systems(
            Update,
            (
                // Systems to run when player state changes
                (
                    // Print out when this state changes for debugging purposes
                    debug_state_changes::<PlayerState>,
                    // Toggle (UI) elements when the player dies/alives
                    toggle_state_visibility::<PlayerState>,
                )
                    .run_if(state_changed::<PlayerState>),
                // Detect if the bird is "dead" by checking if it is visible
                // from the point of view of the camera
                detect_dead.run_if(in_state(PlayerState::Alive)),
                // Toggle rewinding state when "R" is pressed/released
                toggle_rewind.run_if(
                    input_just_pressed(KeyCode::KeyR).or(input_just_released(KeyCode::KeyR)),
                ),
                // Systems to run in the "play" state
                (
                    // Only flap when we press the space key
                    flap_kb.run_if(input_just_pressed(KeyCode::Space)),
                    // Rewinding systems
                    record.run_if(any_component_changed::<Transform>),
                )
                    .run_if(in_state(PlayerState::Alive)),
                // Rewinding systems
                rewind.run_if(in_state(PlayerState::Rewind)),
                // Camera follows when bird moves regardless of player state
                camera_follow_bird.run_if(any_component_changed::<Transform>),
                // Pause when the player presses Escape
                pause_game.run_if(input_just_pressed(KeyCode::Escape)),
                // Transition out of the pause screen if the player presses space
                un_pause_game
                    .run_if(input_just_pressed(KeyCode::Space))
                    .run_if(in_state(PlayerState::Pause)),
                // Stats that get synced to the UI
                (
                    sync_resource_to_ui::<Score>.run_if(resource_changed::<Score>),
                    sync_resource_to_ui::<LongestRun>.run_if(resource_changed::<LongestRun>),
                    sync_resource_to_ui::<Deaths>.run_if(resource_changed::<Deaths>),
                    sync_resource_to_ui::<Flaps>.run_if(resource_changed::<Flaps>),
                    sync_resource_to_ui::<RewindFrames>.run_if(resource_changed::<RewindFrames>),
                ),
                (update_tooltip, debug_trail).run_if(in_state(DebuggingState::On)),
                // TODO: Add run_if to this system
                update_batch_position.run_if(any_component_changed::<Batch>),
                move_batches.run_if(on_message::<CollisionStart>.or(on_message::<CollisionEnd>)),
                manage_score.run_if(on_message::<CollisionStart>.or(on_message::<CollisionEnd>)),
                shimmer_button::<RewindButton>.run_if(in_state(PlayerState::Stasis)),
                shimmer_button::<FlapButton>.run_if(in_state(PlayerState::Pause)),
            ),
        )
        .add_observer(flap)
        .add_observer(populate_batch)
        .add_observer(populate_pipe)
        .add_observer(move_pipe)
        .add_observer(populate_ground)
        .add_observer(populate_ceiling)
        .add_observer(populate_hitbox)
        .run();
}

fn tweak_camera(camera: Single<(Entity, &mut Camera)>, mut commands: Commands) {
    debug!("Tweaking camera");
    let (e, mut c) = camera.into_inner();
    c.clear_color = ClearColorConfig::Custom(SKY_BLUE.into());
    commands.entity(e).insert(ColorGrading { ..default() });
}

#[derive(Component)]
struct Bird;

#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
enum PlayerState {
    Alive,
    Rewind,
    Stasis,
    // Kind of a hack, but we need a state for this
    Credits,
    #[default]
    Pause,
}

// A tape tracking the bird's state every frame
#[derive(Component)]
struct Tape {
    capacity: usize,
    linear_velocities: VecDeque<LinearVelocity>,
    angular_velocities: VecDeque<AngularVelocity>,
    constant_forces: VecDeque<ConstantForce>,
    positions: VecDeque<Position>,
    rotations: VecDeque<Rotation>,
}

impl Tape {
    fn new_with_capacity(capacity: usize) -> Self {
        Tape {
            capacity,
            linear_velocities: VecDeque::with_capacity(capacity),
            angular_velocities: VecDeque::with_capacity(capacity),
            constant_forces: VecDeque::with_capacity(capacity),
            positions: VecDeque::with_capacity(capacity),
            rotations: VecDeque::with_capacity(capacity),
        }
    }

    fn push(
        &mut self,
        lv: LinearVelocity,
        av: AngularVelocity,
        ei: ConstantForce,
        p: Position,
        r: Rotation,
    ) {
        // If we are at capacity, make room
        if self.linear_velocities.len() == self.capacity {
            self.linear_velocities.pop_front().unwrap();
            self.angular_velocities.pop_front().unwrap();
            self.constant_forces.pop_front().unwrap();
            self.positions.pop_front().unwrap();
            self.rotations.pop_front().unwrap();
        }

        self.linear_velocities.push_back(lv);
        self.angular_velocities.push_back(av);
        self.constant_forces.push_back(ei);
        self.positions.push_back(p);
        self.rotations.push_back(r);
    }

    fn pop(
        &mut self,
    ) -> Option<(
        LinearVelocity,
        AngularVelocity,
        ConstantForce,
        Position,
        Rotation,
    )> {
        if self.linear_velocities.is_empty() {
            None
        } else {
            let lv = self.linear_velocities.pop_back().unwrap();
            let av = self.angular_velocities.pop_back().unwrap();
            let cf = self.constant_forces.pop_back().unwrap();
            let p = self.positions.pop_back().unwrap();
            let r = self.rotations.pop_back().unwrap();
            Some((lv, av, cf, p, r))
        }
    }
}

fn init_bird(mut commands: Commands, bird_assets: Res<BirdAssets>) {
    let name = Name::new("bird");

    let t = Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::new(62.5, 50.0, 50.0));

    let physics = (
        RigidBody::Static,
        Collider::rectangle(1.0, 1.0),
        Mass(10.0),
        ConstantForce::default(),
        MaxLinearSpeed(500.0),
    );

    // 60fps * 5 seconds
    const REWIND_SECONDS: usize = 5;
    let tape = Tape::new_with_capacity(60 * REWIND_SECONDS);

    commands.spawn((
        name,
        bird_assets.mesh.clone(),
        bird_assets.material.clone(),
        physics,
        t,
        Bird,
        tape,
        CollisionEventsEnabled,
    ));
}

#[derive(Component, Clone)]
struct Ground(isize);

#[derive(Component, Clone)]
struct Ceiling(isize);

#[derive(Component, Clone)]
enum Pipe {
    Top,
    Bottom,
}

#[derive(Component, Clone)]
struct Hitbox;

#[derive(Component, Clone)]
struct Batch(usize);

fn init_first_batches(mut commands: Commands) {
    commands.spawn(Batch(0));
    commands.spawn(Batch(1));
    commands.spawn(Batch(2));
    commands.spawn(Batch(3));
    commands.spawn(Batch(4));
}

/// We have the concept of an environment "batch" which contains:
/// * The ground obstacle
/// * The pipe obstacle
/// * The pipe scoring sensor
///
/// These are all populated via `On<Add, >` observers for the respected components.
/// This makes it much more concise to spawn each part of the environment.
fn populate_batch(
    event: On<Add, Batch>,
    batches: Query<&Batch>,
    children: Query<&ChildOf>,
    mut commands: Commands,
) {
    // Only run this for top level batch entities,
    // not children containing a reference to their batch, like hitboxes
    if !children.contains(event.entity) {
        let Batch(batch_id) = batches.get(event.entity).unwrap();
        commands
            .entity(event.entity)
            .insert((
                Transform::from_xyz(500.0 * (*batch_id) as f32, 0.0, 0.0),
                Visibility::Inherited,
            ))
            .with_children(|parent| {
                parent.spawn(Ground(-2));
                parent.spawn(Ground(-1));
                parent.spawn(Ground(0));
                parent.spawn(Ground(1));
                parent.spawn(Ground(2));
                parent.spawn(Ceiling(-2));
                parent.spawn(Ceiling(-1));
                parent.spawn(Ceiling(0));
                parent.spawn(Ceiling(1));
                parent.spawn(Ceiling(2));
                if *batch_id > 0 {
                    parent.spawn((Pipe::Top, Batch(*batch_id)));
                    parent.spawn((Pipe::Bottom, Batch(*batch_id)));
                    parent.spawn((Hitbox, Batch(*batch_id)));
                }
            });
    }
}

fn update_batch_position(
    mut root_changes: Query<(&mut Transform, &Batch), (Changed<Batch>, Without<ChildOf>)>,
) {
    root_changes.iter_mut().for_each(|(mut t, Batch(idx))| {
        debug!("Updating batch {:?}", idx);
        t.translation.x = 500.0 * (*idx) as f32;
    });
}

fn populate_ground(
    event: On<Add, Ground>,
    grounds: Query<&Ground>,
    ground_assets: Res<GroundAssets>,
    mut commands: Commands,
) {
    let Ground(idx) = grounds.get(event.entity).unwrap();
    debug!("populating ground {:?}", idx);
    commands.entity(event.entity).insert((
        ground_assets.material.clone(),
        ground_assets.mesh.clone(),
        Name::new("ground"),
        RigidBody::Static,
        Collider::rectangle(1.0, 0.75),
        Transform::from_xyz(100.0 * (*idx) as f32, -275.0, -1.0).with_scale(Vec3::splat(106.0)),
    ));
}

fn populate_ceiling(
    event: On<Add, Ceiling>,
    ceiling: Query<&Ceiling>,
    ceiling_assets: Res<CeilingAssets>,
    mut commands: Commands,
) {
    let Ceiling(idx) = ceiling.get(event.entity).unwrap();
    debug!("populating ceiling{:?}", idx);
    commands.entity(event.entity).insert((
        ceiling_assets.material.clone(),
        ceiling_assets.mesh.clone(),
        Name::new("ceiling"),
        RigidBody::Static,
        Collider::rectangle(1.0, 0.75),
        Transform::from_xyz(100.0 * (*idx) as f32, 300.0, -3.0).with_scale(Vec3::splat(100.0)),
    ));
}

fn move_pipe(
    event: On<Insert, Batch>,
    mut pipes: Query<(&Batch, &Pipe, &mut Transform)>,
    rand: Res<Rand>,
) {
    if let Ok((Batch(id), pipe, mut pipe_t)) = pipes.get_mut(event.entity) {
        *pipe_t =
            {
                let offset = {
                    let val = rand.0.hash_one(id);

                    let option = val % 3;

                    match option {
                        0 => 100.0,
                        1 => 0.0,
                        2 => -100.0,
                        _ => panic!("Can only pick 1 of 3 pipe offsets"),
                    }
                };

                match pipe {
                    Pipe::Top => Transform::from_xyz(0.0, 300.0 + offset, -2.0)
                        .with_scale(Vec3::splat(100.0)),
                    Pipe::Bottom => Transform::from_xyz(0.0, -300.0 + offset, -2.0)
                        .with_scale(Vec3::splat(100.0)),
                }
            };
    }
}

/// Based on if this is a Top or Bottom pipe the placement changes
/// Otherwise this just spawns in the center of the batch.
fn populate_pipe(
    event: On<Add, Pipe>,
    pipes: Query<(&Batch, &Pipe)>,
    pipe_assets: Res<PipeAssets>,
    mut commands: Commands,
    rand: Res<Rand>,
) {
    let pipe_t = {
        let (Batch(id), pipe) = pipes.get(event.entity).unwrap();
        let offset = {
            let val = rand.0.hash_one(id);

            let option = val % 3;

            match option {
                0 => 100.0,
                1 => 0.0,
                2 => -100.0,
                _ => panic!("Can only pick 1 of 3 pipe offsets"),
            }
        };

        match pipe {
            Pipe::Top => {
                Transform::from_xyz(0.0, 300.0 + offset, -2.0).with_scale(Vec3::splat(100.0))
            }
            Pipe::Bottom => {
                Transform::from_xyz(0.0, -300.0 + offset, -2.0).with_scale(Vec3::splat(100.0))
            }
        }
    };

    commands.entity(event.entity).insert((
        pipe_t,
        pipe_assets.material.clone(),
        pipe_assets.mesh.clone(),
        RigidBody::Static,
        Collider::rectangle(1.0, 4.0),
        Name::new("pipe"),
    ));
}

/// The hitbox should cover the entire height of the screen so if the player
/// passes between the pipes it registers a point
fn populate_hitbox(event: On<Add, Hitbox>, mut commands: Commands) {
    commands.entity(event.entity).insert((
        RigidBody::Static,
        Collider::rectangle(1.0, 10.0),
        Sensor,
        CollisionEventsEnabled,
        Name::new("hitbox"),
        Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::splat(100.0)),
    ));
}

#[derive(Resource, Default)]
struct BirdAssets {
    material: MeshMaterial2d<ColorMaterial>,
    mesh: Mesh2d,
}

#[derive(Resource, Default)]
struct GroundAssets {
    material: MeshMaterial2d<ColorMaterial>,
    mesh: Mesh2d,
}

#[derive(Resource, Default)]
struct CeilingAssets {
    material: MeshMaterial2d<ColorMaterial>,
    mesh: Mesh2d,
}

#[derive(Resource, Default)]
struct PipeAssets {
    material: MeshMaterial2d<ColorMaterial>,
    mesh: Mesh2d,
}

/// Initialize some materials and meshes used by the environment obstacles
fn init_assets(
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    mut bird_assets: ResMut<BirdAssets>,
    mut pipe_assets: ResMut<PipeAssets>,
    mut ground_assets: ResMut<GroundAssets>,
    mut ceiling_assets: ResMut<CeilingAssets>,
    server: Res<AssetServer>,
) {
    pipe_assets.material = MeshMaterial2d(materials.add(ColorMaterial {
        texture: Some(server.load("flappy/pipe.png")),
        color: GREEN.into(),
        ..default()
    }));
    pipe_assets.mesh = Mesh2d(meshes.add(Rectangle::new(0.875, 4.0)));

    ground_assets.material = MeshMaterial2d(materials.add(ColorMaterial {
        texture: Some(server.load("flappy/ground.png")),
        color: GREEN.into(),
        ..default()
    }));
    ground_assets.mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.0)));

    ceiling_assets.material = MeshMaterial2d(materials.add(ColorMaterial {
        texture: Some(server.load("flappy/ceiling.png")),
        color: FIRE_BRICK.into(),
        ..default()
    }));
    ceiling_assets.mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 0.777)));

    bird_assets.material = MeshMaterial2d(materials.add(ColorMaterial {
        texture: Some(server.load("flappy/bird.png")),
        color: ORANGE.into(),
        alpha_mode: AlphaMode2d::Blend,
        ..default()
    }));
    bird_assets.mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.2)));
}

#[derive(Component)]
struct FlapButton;

#[derive(Component)]
struct RewindButton;

fn init_ui(mut commands: Commands, server: Res<AssetServer>) {
    commands
        .spawn((
            Node {
                align_self: AlignSelf::Center,
                justify_self: JustifySelf::Center,
                flex_direction: FlexDirection::Column,
                ..default()
            },
            BorderRadius::all(Val::Px(5.0)),
            BackgroundColor(Color::WHITE),
            BorderColor::all(BLACK),
            PlayerState::Stasis,
        ))
        .with_children(|parent| {
            parent.spawn((
                Text::new("Game Over...?"),
                TextColor(BLACK.into()),
                TextLayout::new_with_justify(Justify::Center),
            ));
            parent.spawn((
                SyncResource::<Score>::default(),
                Text::default(),
                TextColor(BLACK.into()),
                TextLayout::new_with_justify(Justify::Center),
            ));
            parent.spawn((
                SyncResource::<LongestRun>::default(),
                Text::default(),
                TextColor(BLACK.into()),
                TextLayout::new_with_justify(Justify::Center),
            ));
            parent.spawn((
                SyncResource::<Deaths>::default(),
                Text::default(),
                TextColor(BLACK.into()),
                TextLayout::new_with_justify(Justify::Center),
            ));
            parent.spawn((
                SyncResource::<Flaps>::default(),
                Text::default(),
                TextColor(BLACK.into()),
                TextLayout::new_with_justify(Justify::Center),
            ));
            parent.spawn((
                SyncResource::<RewindFrames>::default(),
                Text::default(),
                TextColor(BLACK.into()),
                TextLayout::new_with_justify(Justify::Center),
            ));
            parent.spawn((
                Text::new("Press R to Rewind"),
                TextColor(BLACK.into()),
                TextLayout::new_with_justify(Justify::Center),
            ));
            parent
                .spawn((Node {
                    flex_direction: FlexDirection::Row,
                    justify_content: JustifyContent::SpaceEvenly,
                    ..default()
                },))
                .with_children(|parent| {
                    fn show_credits(
                        _event: On<Pointer<Click>>,
                        mut state: ResMut<NextState<PlayerState>>,
                    ) {
                        state.set(PlayerState::Credits);
                    }

                    parent
                        .spawn((
                            Button,
                            BorderRadius::all(Val::Px(5.0)),
                            BorderColor::all(BLACK),
                            Node { ..default() },
                            children![(TextColor(BLACK.into()), Text::new("Credits")),],
                        ))
                        .observe(show_credits);

                    #[cfg(not(target_arch = "wasm32"))]
                    {
                        fn quit_game(_event: On<Pointer<Click>>, mut exit: MessageWriter<AppExit>) {
                            warn!("Quitting game");
                            exit.write(AppExit::Success);
                        }

                        parent
                            .spawn((
                                BorderRadius::all(Val::Px(5.0)),
                                BorderColor::all(BLACK),
                                Button,
                                Node { ..default() },
                                children![(Text::new("Quit"), TextColor(BLACK.into()))],
                            ))
                            .observe(quit_game);
                    }
                });
        });

    fn hide_credits(_event: On<Pointer<Click>>, mut state: ResMut<NextState<PlayerState>>) {
        state.set(PlayerState::Stasis)
    }

    let credits_str = include_str!("../../../assets/flappy/CREDITS");

    commands
        .spawn((
            Node {
                align_self: AlignSelf::Center,
                justify_self: JustifySelf::Center,
                flex_direction: FlexDirection::Column,
                ..default()
            },
            BorderRadius::all(Val::Px(5.0)),
            BackgroundColor(WHITE.into()),
            BorderColor::all(BLACK),
            PlayerState::Credits,
            children![(
                Text::new(credits_str),
                TextColor(BLACK.into()),
                TextLayout::new_with_justify(Justify::Center)
            )],
        ))
        .with_children(|parent| {
            parent.spawn((
                Node {
                    align_self: AlignSelf::Center,
                    ..default()
                },
                BorderRadius::all(Val::Px(5.0)),
                BorderColor::all(BLACK),
                Button,
                children![(Text::new("Close"), TextColor(BLACK.into()))],
            ));
        })
        .observe(hide_credits);

    let logo =
        server.load_with_settings("flappy/logo.png", |settings: &mut ImageLoaderSettings| {
            // Need to use nearest filtering to avoid bleeding between the slices with tiling
            settings.sampler = ImageSampler::nearest();
        });
    commands.spawn((
        Node {
            align_self: AlignSelf::Center,
            justify_self: JustifySelf::Center,
            flex_direction: FlexDirection::Column,
            align_items: AlignItems::Center,
            justify_items: JustifyItems::Center,
            ..default()
        },
        BackgroundColor(Color::NONE),
        PlayerState::Pause,
        children![
            (
                ImageNode {
                    image: logo,
                    ..default()
                },
                Node {
                    width: Val::Px(256.0),
                    ..default()
                }
            ),
            (
                Node {
                    margin: UiRect::all(Val::Px(5.0)),
                    ..default()
                },
                BorderRadius::all(Val::Px(5.0)),
                BorderColor::all(BLACK),
                Text::new("(paused)"),
                TextColor(BLACK.into()),
            )
        ],
    ));

    commands
        .spawn((Node {
            align_self: AlignSelf::End,
            justify_self: JustifySelf::Center,
            flex_direction: FlexDirection::Row,
            justify_content: JustifyContent::SpaceEvenly,
            width: Val::Percent(100.0),
            min_height: Val::Percent(10.0),
            ..default()
        },))
        .with_children(|parent| {
            let rewind_image = server.load_with_settings(
                "flappy/rewind.png",
                |settings: &mut ImageLoaderSettings| {
                    // Need to use nearest filtering to avoid bleeding between the slices with tiling
                    settings.sampler = ImageSampler::nearest();
                },
            );

            parent
                .spawn((
                    Node {
                        width: Val::Percent(50.0),
                        align_items: AlignItems::Center,
                        justify_content: JustifyContent::Center,
                        ..default()
                    },
                    BorderRadius::all(Val::Px(5.0)),
                    BorderColor::all(BLACK),
                    BackgroundColor(Color::WHITE.with_alpha(0.9)),
                    Button,
                    RewindButton,
                    children![
                        (
                            ImageNode {
                                color: BLACK.into(),
                                image: rewind_image,
                                ..default()
                            },
                            Node {
                                height: Val::Px(50.0),
                                ..default()
                            },
                        ),
                        (
                            Text::new("Rewind!\n(Hold R)"),
                            TextColor(BLACK.into()),
                            TextFont::from_font_size(20.0),
                            TextLayout::new_with_justify(Justify::Center)
                        ),
                    ],
                ))
                .observe(start_rewind)
                .observe(end_rewind);

            let play_image = server.load_with_settings(
                "flappy/play.png",
                |settings: &mut ImageLoaderSettings| {
                    // Need to use nearest filtering to avoid bleeding between the slices with tiling
                    settings.sampler = ImageSampler::nearest();
                },
            );
            parent
                .spawn((
                    Node {
                        width: Val::Percent(50.0),
                        align_items: AlignItems::Center,
                        justify_content: JustifyContent::Center,
                        ..default()
                    },
                    BorderRadius::all(Val::Px(5.0)),
                    BorderColor::all(BLACK),
                    BackgroundColor(Color::WHITE.with_alpha(0.9)),
                    Button,
                    FlapButton,
                    children![
                        (
                            Text::new("Flap!\n(Spacebar)"),
                            TextColor(BLACK.into()),
                            TextFont::from_font_size(20.0),
                            TextLayout::new_with_justify(Justify::Center)
                        ),
                        (
                            ImageNode {
                                color: BLACK.into(),
                                image: play_image,
                                ..default()
                            },
                            Node {
                                height: Val::Px(50.0),
                                ..default()
                            },
                        ),
                    ],
                ))
                .observe(flap_button);
        });

    commands.spawn((
        Node {
            align_self: AlignSelf::Start,
            justify_self: JustifySelf::Center,
            ..default()
        },
        BorderRadius::all(Val::Px(5.0)),
        BackgroundColor(Color::WHITE),
        BorderColor::all(BLACK),
        children![(
            SyncResource::<Score>::default(),
            Text::default(),
            TextColor(BLACK.into()),
            TextLayout::new_with_justify(Justify::Center),
        )],
    ));
}

fn init_background(
    mut commands: Commands,
    server: Res<AssetServer>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    {
        let material = MeshMaterial2d(materials.add(ColorMaterial {
            texture: Some(server.load("flappy/background-city.png")),
            color: LIGHT_GREY.with_alpha(0.8).into(),
            alpha_mode: AlphaMode2d::Blend,
            ..default()
        }));
        let mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.0)));
        {
            let t = Transform::from_xyz(-325.0, 0.0, -32.0).with_scale(Vec3::splat(650.0));
            commands.spawn((ParallaxDepth(24.0), mesh.clone(), material.clone(), t));
        }

        {
            let t = Transform::from_xyz(325.0, 0.0, -32.0).with_scale(Vec3::splat(650.0));
            commands.spawn((ParallaxDepth(24.0), mesh.clone(), material.clone(), t));
        }
    }
    {
        let material = MeshMaterial2d(materials.add(ColorMaterial {
            texture: Some(server.load("flappy/background-clouds.png")),
            color: WHITE_SMOKE.with_alpha(0.8).into(),
            alpha_mode: AlphaMode2d::Blend,
            ..default()
        }));
        let mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.0)));
        {
            let t = Transform::from_xyz(-325.0, 0.0, -64.0).with_scale(Vec3::splat(650.0));
            commands.spawn((ParallaxDepth(32.0), mesh.clone(), material.clone(), t));
        }
        {
            let t = Transform::from_xyz(325.0, 0.0, -64.0).with_scale(Vec3::splat(650.0));
            commands.spawn((ParallaxDepth(32.0), mesh.clone(), material.clone(), t));
        }
    }
}

fn start_rewind(_event: On<Pointer<Press>>, mut next: ResMut<NextState<PlayerState>>) {
    next.set(PlayerState::Rewind);
}

fn end_rewind(_event: On<Pointer<Release>>, mut next: ResMut<NextState<PlayerState>>) {
    next.set(PlayerState::Alive);
}

fn flap_button(
    _event: On<Pointer<Press>>,
    mut commands: Commands,
    bird: Single<Entity, With<Bird>>,
    curr: Res<State<PlayerState>>,
    mut next: ResMut<NextState<PlayerState>>,
) {
    if !matches!(curr.get(), PlayerState::Alive) {
        next.set(PlayerState::Alive);
    }
    let e = *bird;
    debug!("Flapping {:?}", e);
    commands.trigger(Flap { entity: e });
}

/// Pause the game when the player presses "Escape"
fn pause_game(mut next: ResMut<NextState<PlayerState>>) {
    next.set(PlayerState::Pause);
}

fn un_pause_game(mut next: ResMut<NextState<PlayerState>>) {
    next.set(PlayerState::Alive);
}

#[derive(Component, Clone, Message, EntityEvent)]
struct Flap {
    entity: Entity,
}

// Observer for flapping
fn flap(event: On<Flap>, mut bird: Query<Forces, With<Bird>>, mut flaps: ResMut<Flaps>) {
    debug!("real flap for {:?}", event.entity);
    // Increment flap stat
    flaps.0 += 1;

    // Flap birds wings
    if let Ok(mut f) = bird.get_mut(event.entity) {
        f.apply_local_linear_impulse(Vec2::Y * 5000.0 + Vec2::X * 1000.0);
    }
}

fn flap_kb(
    #[cfg(debug_assertions)] state: Res<State<PlayerState>>,
    #[cfg(debug_assertions)] keycode: Res<ButtonInput<KeyCode>>,
    birds: Query<Entity, With<Bird>>,
    mut commands: Commands,
) {
    #[cfg(debug_assertions)]
    {
        debug_assert!(
            matches!(state.get(), PlayerState::Alive),
            "Only flap when playing"
        );
        debug_assert!(
            keycode.just_pressed(KeyCode::Space),
            "Only flap when space is just pressed"
        );
    }

    birds.iter().for_each(|e| {
        debug!("Flapping {:?}", e);
        commands.trigger(Flap { entity: e });
    });
}

fn toggle_rewind(keycode: Res<ButtonInput<KeyCode>>, mut next: ResMut<NextState<PlayerState>>) {
    debug_assert!(
        keycode.just_pressed(KeyCode::KeyR) || keycode.just_released(KeyCode::KeyR),
        "Only toggle rewind when R is pressed"
    );

    if keycode.just_pressed(KeyCode::KeyR) {
        debug!("Toggling rewind ON");
        next.set(PlayerState::Rewind)
    } else {
        debug!("Toggling rewind OFF");
        next.set(PlayerState::Alive)
    }
}

fn record(
    #[cfg(debug_assertions)] state: Res<State<PlayerState>>,
    mut birds: Query<
        (
            &LinearVelocity,
            &AngularVelocity,
            &ConstantForce,
            &Position,
            &Rotation,
            &mut Tape,
        ),
        With<Bird>,
    >,
) {
    #[cfg(debug_assertions)]
    debug_assert!(
        matches!(state.get(), PlayerState::Alive),
        "Only record in the alive state"
    );

    birds.iter_mut().for_each(|(lv, av, ei, p, r, mut tape)| {
        tape.push(*lv, *av, ei.clone(), *p, *r);
    });
}

fn rewind(
    #[cfg(debug_assertions)] state: Res<State<PlayerState>>,
    mut next: ResMut<NextState<PlayerState>>,
    mut birds: Query<
        (
            &mut LinearVelocity,
            &mut AngularVelocity,
            // TODO: Need to re-add impulse here
            &mut Position,
            &mut Rotation,
            &mut Tape,
        ),
        With<Bird>,
    >,
    mut frames: ResMut<RewindFrames>,
) {
    #[cfg(debug_assertions)]
    debug_assert!(
        matches!(state.get(), PlayerState::Rewind),
        "Only rewind in the rewinding state"
    );

    birds
        .iter_mut()
        .for_each(|(mut lv, mut av, mut p, mut r, mut tape)| {
            if let Some((new_lv, new_av, _new_ei, new_p, new_r)) = tape.pop() {
                lv.0 = new_lv.0;
                av.0 = new_av.0;
                // TODO: Need to re-add impulse here
                p.0 = new_p.0;
                *r = new_r;
                frames.0 += 1;
            } else {
                next.set(PlayerState::Pause);
            }
        });
}

// PERF: May run more than necessary, should be event-driven on aabb intersection
fn detect_dead(
    #[cfg(debug_assertions)] state: Res<State<PlayerState>>,
    bird: Single<&ColliderAabb, With<Bird>>,
    obstacles: Query<&ColliderAabb, Or<(With<Ground>, With<Pipe>, With<Ceiling>)>>,
    mut next: ResMut<NextState<PlayerState>>,
) {
    #[cfg(debug_assertions)]
    debug_assert!(
        matches!(state.get(), PlayerState::Alive),
        "Only check if dead while alive"
    );

    if obstacles.iter().any(|obstacle| bird.intersects(obstacle)) {
        next.set(PlayerState::Stasis);
    }
}

fn alive_bird(bird: Single<Entity, With<Bird>>, mut commands: Commands) {
    debug!("Setting bird to Dynamic");
    commands.entity(*bird).remove::<RigidBodyDisabled>();
}

fn pause_bird(
    state: Res<State<PlayerState>>,
    bird: Single<Entity, With<Bird>>,
    mut deaths: ResMut<Deaths>,
    mut commands: Commands,
) {
    // Increment death count
    if state.get() == &PlayerState::Stasis {
        deaths.0 += 1
    }

    debug!("Setting bird to Static");
    commands.entity(*bird).insert(RigidBodyDisabled);
}

fn camera_follow_bird(
    bird: Single<&Transform, (With<Bird>, Changed<Transform>)>,
    mut camera: Single<&mut Transform, (With<Camera>, Without<Bird>)>,
) {
    camera.translation.x = bird.translation.x;
}

/// How many pipes the player has passed
#[derive(Resource, Default)]
struct Score(usize);

impl Display for Score {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        writeln!(f, "Score: {}", self.0)
    }
}

/// Number of frames that were rewound
#[derive(Resource, Default)]
struct RewindFrames(usize);

impl Display for RewindFrames {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        writeln!(f, "Frames Rewound: {}", self.0)
    }
}

// Track number of times player flapped their wings
#[derive(Resource, Default)]
struct Flaps(usize);

impl Display for Flaps {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        writeln!(f, "Flaps: {}", self.0)
    }
}

/// Track the longest run a player has done
/// Store the latest run and upsert latest run to the longest run if it is longer
#[derive(Resource, Default)]
struct LongestRun {
    /// Longest run start
    start: Option<Instant>,
    /// Longest run end
    end: Option<Instant>,
    /// Latest run start
    latest_start: Option<Instant>,
    /// Latest run start
    latest_end: Option<Instant>,
}

impl Display for LongestRun {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match (self.start, self.end) {
            (Some(a), Some(b)) => {
                if b > a {
                    writeln!(f, "Longest Run: {:.2?}", b - a)
                } else {
                    writeln!(f, "Longest Run: ???")
                }
            }
            _ => writeln!(f, "Longest Run: ???"),
        }
    }
}

fn start_run(mut longest_run: ResMut<LongestRun>) {
    longest_run.latest_start = Some(Instant::now());
    longest_run.latest_end = None;
}

fn end_run(mut longest_run: ResMut<LongestRun>) {
    longest_run.latest_end = Some(Instant::now());

    match (longest_run.start, longest_run.end) {
        // Longest run hasn't been set yet, so set it to the latest run
        (None, None) => {
            longest_run.start = longest_run.latest_start;
            longest_run.end = longest_run.latest_end;
        }
        // Longest run was previously set
        (Some(start), Some(end)) => {
            // Check if latest run is longer than current longest run
            if longest_run.latest_end.unwrap() - longest_run.latest_start.unwrap() > end - start {
                longest_run.start = longest_run.latest_start;
                longest_run.end = longest_run.latest_end;
            }
        }
        _ => panic!("What?"),
    }
}

// Track number of times player died
#[derive(Resource, Default)]
struct Deaths(usize);

impl Display for Deaths {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        writeln!(f, "Deaths: {}", self.0)
    }
}

fn manage_score(
    mut start: MessageReader<CollisionStart>,
    mut end: MessageReader<CollisionEnd>,
    state: Res<State<PlayerState>>,
    hitboxes: Query<&Batch, With<Hitbox>>,
    mut score: ResMut<Score>,
) {
    match state.get() {
        PlayerState::Rewind => {
            start.read().for_each(
                |CollisionStart {
                     collider1,
                     collider2,
                     ..
                 }| {
                    // Set score to collided hitbox
                    if let Ok(Batch(this)) = hitboxes.get(*collider1) {
                        debug!("[Rewind] Setting score to {this}");
                        score.0 = this.saturating_sub(1);
                    } else if let Ok(Batch(this)) = hitboxes.get(*collider2) {
                        debug!("[Rewind] Setting score to {this}");
                        score.0 = this.saturating_sub(1);
                    }
                },
            )
        }
        _ => {
            end.read().for_each(
                |CollisionEnd {
                     collider1,
                     collider2,
                     ..
                 }| {
                    // Set score to collided hitbox
                    if let Ok(Batch(this)) = hitboxes.get(*collider2) {
                        debug!("[Alive] Setting score to {this}");
                        score.0 = *this;
                    } else if let Ok(Batch(this)) = hitboxes.get(*collider1) {
                        debug!("[Alive] Setting score to {this}");
                        score.0 = *this;
                    }
                },
            )
        }
    }
}

/// WARNING: This method is somewhat janky
///
/// We first figure out which collided entity was a hitbox
///     and bial out early if neither is a hitbox
/// Then we find the batch ID for the hitbox that was hit
/// Next we figure out the old -> new batch IDs based on offsets from the current batch
///     skipping the 0th batch intentionally as that is a special case
/// Finally we iterate over all entities with the old batch ID and upsert the new batch ID
///     This includes root batch entities as well as pipes and hitboxes
fn move_batches(
    mut start: MessageReader<CollisionStart>,
    mut end: MessageReader<CollisionEnd>,
    hitboxes: Query<Entity, With<Hitbox>>,
    batches: Query<(Entity, &Batch)>,
    state: Res<State<PlayerState>>,
    mut commands: Commands,
) {
    let s = start.read().map(
        |CollisionStart {
             collider1,
             collider2,
             ..
         }| (collider1, collider2),
    );
    let e = end.read().map(
        |CollisionEnd {
             collider1,
             collider2,
             ..
         }| (collider1, collider2),
    );
    let c = s.chain(e);
    c.for_each(|(a, b)| {
        debug!("[batches] Collision {a} -> {b}");

        let target = {
            if hitboxes.contains(*b) {
                *b
            } else if hitboxes.contains(*a) {
                *a
            } else {
                return;
            }
        };

        let (_, Batch(curr)) = batches.get(target).unwrap();

        debug!("[batches] Current: {curr}");
        let (old_batch, new_batch) = match state.get() {
            PlayerState::Alive => (curr.saturating_sub(2), curr.saturating_add(2)),
            PlayerState::Rewind => (curr.saturating_add(2), curr.saturating_sub(2)),
            _ => (*curr, *curr),
        };

        if old_batch > 0 && new_batch != 0 {
            batches
                .iter()
                // Filter to just entities with this batch ID
                .filter_map(|(e, Batch(id))| (*id == old_batch).then_some(e))
                .for_each(|old| {
                    debug!("Moving batch {old_batch}({old}) -> {new_batch}");
                    commands.entity(old).insert(Batch(new_batch));
                })
        }
    });
}

fn update_tooltip(
    mut query: Query<(
        &mut ToolTip,
        Option<&LinearVelocity>,
        Option<&Batch>,
        Option<&RigidBody>,
    )>,
) {
    query.iter_mut().for_each(|(mut tt, lv, b, rb)| {
        // Add Linear Velocity if present on entity
        lv.iter().for_each(|it| {
            tt.insert("Velocity", format!("{}", it.0));
        });
        // Add Batch ID to if present
        b.iter().for_each(|it| {
            tt.insert("Batch", format!("{}", it.0));
        });
        rb.iter().for_each(|it| {
            tt.insert("RigidBody", format!("{it:?}"));
        })
    });
}

fn debug_trail(
    mut physics_objects: Query<(Entity, &Transform), With<Mass>>,
    mut gizmos: Gizmos,
    mut positions: Local<HashMap<Entity, VecDeque<Vec3>>>,
) {
    physics_objects.iter_mut().for_each(|(e, obj)| {
        if let Some(list) = positions.get_mut(&e) {
            list.push_front(obj.translation);
            if list.len() > 60 {
                list.pop_back();
            }
        } else {
            positions.insert(e, vec![obj.translation].into());
        }

        positions.iter().for_each(|(_e, list)| {
            list.iter().for_each(|v| {
                gizmos.cross_2d(Vec2::new(v.x, v.y), 12., FUCHSIA);
            });
        });
    })
}

fn shimmer_button<T: Component>(mut bg: Single<&mut BackgroundColor, With<T>>, time: Res<Time>) {
    let t = time.elapsed_secs();
    let lightness = 0.5;
    let chroma = 0.5;
    let hue = (t * 60.0) % 360.0;
    bg.0 = Oklcha {
        lightness,
        chroma,
        hue,
        alpha: bg.0.alpha(),
    }
    .into();
}

fn reset_button<T: Component>(mut bg: Single<&mut BackgroundColor, With<T>>) {
    bg.0 = WHITE.into();
}
