use bevy::{
    prelude::*,
    sprite::{MaterialMesh2dBundle, Mesh2dHandle},
    window::WindowResolution,
};
use bevy_tnua::{
    builtins::{TnuaBuiltinJump, TnuaBuiltinWalk},
    control_helpers::TnuaSimpleAirActionsCounter,
    controller::{TnuaController, TnuaControllerBundle, TnuaControllerPlugin},
    TnuaAction,
};
use bevy_tnua_xpbd2d::{TnuaXpbd2dPlugin, TnuaXpbd2dSensorShape};
use bevy_xpbd_2d::prelude::*;
use leafwing_input_manager::prelude::*;

mod behavior_script;
mod camera;
mod simple_bt;
mod undertaker;

pub const LOGICAL_WIDTH: u32 = 1280;
pub const LOGICAL_HEIGHT: u32 = 720;

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins
                .set(WindowPlugin {
                    primary_window: Some(Window {
                        title: "Bulle".to_string(),
                        resolution: WindowResolution::new(
                            LOGICAL_WIDTH as f32,
                            LOGICAL_HEIGHT as f32,
                        ),
                        ..default()
                    }),
                    ..default()
                })
                .set(ImagePlugin::default_nearest()),
            bevy_egui::EguiPlugin,
            camera::CameraPlugin,
            PhysicsPlugins::default(),
            TnuaControllerPlugin::default(),
            TnuaXpbd2dPlugin::default(),
            PhysicsDebugPlugin::default(),
            // Maps inputs into input-type agnostic action-state
            InputManagerPlugin::<PlayerMovement>::default(),
        ))
        .add_plugins((undertaker::UndertakerPlugin, layered::LayeredPlugin))
        .insert_resource(Gravity(Vec2::NEG_Y * 1000.0))
        .add_systems(Startup, create_test_stuff)
        .add_systems(Update, update_tnua)
        .add_systems(
            Update,
            action_state_swap_layers.in_set(layered::LayeredPluginSet::PreUpdate),
        )
        .run()
}

mod layered {
    use bevy::prelude::*;
    use bevy_xpbd_2d::{components::CollisionLayers, prelude::*};

    pub struct LayeredPlugin;

    #[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
    pub enum LayeredPluginSet {
        PreUpdate,
        Update,
    }

    impl Plugin for LayeredPlugin {
        fn build(&self, app: &mut App) {
            app.add_systems(
                Update,
                update_collision_layers.in_set(LayeredPluginSet::Update),
            )
            .configure_sets(
                Update,
                (LayeredPluginSet::PreUpdate, LayeredPluginSet::Update).chain(),
            );
        }
    }

    #[derive(PhysicsLayer, Clone, Copy)]
    pub enum GameLayer {
        Ground,
        LayerA,
        LayerB,
        ProjectileA,
        ProjectileB,
    }

    impl GameLayer {
        /// Switch layers into the proper projectile layer
        ///
        /// `Ground` is a special-case value meaning both A and B
        pub fn into_projectile(self) -> Self {
            match self {
                Self::Ground => Self::Ground,
                Self::LayerA | Self::ProjectileA => Self::ProjectileA,
                Self::LayerB | Self::ProjectileB => Self::ProjectileB,
            }
        }
    }

    #[derive(Component, Clone, Copy, Debug)]
    pub enum Layered {
        Ground,
        A,
        B,
    }

    impl Layered {
        /// Get the layer-swapped version of this [`Layered`]
        pub fn swapped(self) -> Self {
            match self {
                Self::Ground => Self::Ground,
                Self::A => Self::B,
                Self::B => Self::A,
            }
        }

        /// Get the corresponding [`GameLayer`]
        pub fn layer(self) -> GameLayer {
            match self {
                Self::Ground => GameLayer::Ground,
                Self::A => GameLayer::LayerA,
                Self::B => GameLayer::LayerB,
            }
        }
    }

    fn layer_a_collision_layers() -> CollisionLayers {
        CollisionLayers::new(
            [GameLayer::LayerA],
            [GameLayer::LayerA, GameLayer::ProjectileA, GameLayer::Ground],
        )
    }

    // NOTE Projectiles (generally) do not interact with each other
    fn layer_a_projectile_collision_layers(self_interact: bool) -> CollisionLayers {
        CollisionLayers::new(
            GameLayer::ProjectileA,
            if !self_interact {
                Into::<LayerMask>::into([GameLayer::Ground, GameLayer::LayerA])
            } else {
                [GameLayer::Ground, GameLayer::LayerA, GameLayer::ProjectileA].into()
            },
        )
    }

    fn layer_b_collision_layers() -> CollisionLayers {
        CollisionLayers::new(
            [GameLayer::LayerB],
            [GameLayer::LayerB, GameLayer::ProjectileB, GameLayer::Ground],
        )
    }

    fn layer_b_projectile_collision_layers(self_interact: bool) -> CollisionLayers {
        CollisionLayers::new(
            GameLayer::ProjectileB,
            if !self_interact {
                Into::<LayerMask>::into([GameLayer::Ground, GameLayer::LayerB])
            } else {
                [GameLayer::Ground, GameLayer::LayerB, GameLayer::ProjectileB].into()
            },
        )
    }

    fn ground_collision_layers() -> CollisionLayers {
        CollisionLayers::new(GameLayer::Ground, LayerMask::ALL)
    }

    fn wide_projectile_collision_layers(self_interact: bool) -> CollisionLayers {
        CollisionLayers::new(
            [GameLayer::ProjectileA, GameLayer::ProjectileB],
            if !self_interact {
                Into::<LayerMask>::into([GameLayer::Ground, GameLayer::LayerA, GameLayer::LayerB])
            } else {
                [
                    GameLayer::Ground,
                    GameLayer::LayerA,
                    GameLayer::LayerB,
                    GameLayer::ProjectileA,
                    GameLayer::ProjectileB,
                ]
                .into()
            },
        )
    }

    fn update_collision_layers(query: Query<(Entity, Ref<Layered>)>, mut commands: Commands) {
        for (entity, layered) in query.iter() {
            match &*layered {
                Layered::A => {
                    commands
                        .entity(entity)
                        .try_insert(layer_a_collision_layers());
                }
                Layered::B => {
                    commands
                        .entity(entity)
                        .try_insert(layer_b_collision_layers());
                }
                Layered::Ground => {
                    commands
                        .entity(entity)
                        .try_insert(ground_collision_layers());
                }
            }
        }
    }
}

#[derive(Actionlike, PartialEq, Eq, Hash, Clone, Copy, Debug, Reflect)]
enum PlayerMovement {
    Walk,
    Jump,
    // Swap layers
    Swap,

    // Moves (named after the keybord button you press to use)
    // High moves (tend to hit high when executing)
    Q,
    W,
    E,
    // Low moves (tend to crouch when executing)
    A,
    S,
    D,
}

#[derive(Component)]
struct Player;

fn default_player_map() -> InputMap<PlayerMovement> {
    InputMap::new([
        (
            PlayerMovement::Jump,
            Into::<UserInput>::into(KeyCode::ArrowUp),
        ),
        (
            PlayerMovement::Walk,
            VirtualAxis::horizontal_arrow_keys().into(),
        ),
        (PlayerMovement::Swap, KeyCode::Space.into()),
    ])
}

#[derive(Component)]
struct TnuaConfig {
    max_speed: f32,
    air_actions: usize,
    walk: TnuaBuiltinWalk,
    jump: TnuaBuiltinJump,
}

fn create_test_stuff(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.insert_resource(undertaker::UndertakerConfig {
        under_y: -1000.,
        ..default()
    });

    commands.spawn((
        Player,
        camera::CameraTarget(0),
        RigidBody::Dynamic,
        Collider::capsule(32.0, 16.0),
        // Restitution::new(0.7),
        layered::Layered::A,
        InputManagerBundle::with_map(default_player_map()),
        TnuaConfig {
            max_speed: 5000.,
            air_actions: 2,
            walk: TnuaBuiltinWalk {
                acceleration: 2000.,
                air_acceleration: 1500.,
                float_height: 64.0,
                ..default()
            },
            jump: TnuaBuiltinJump {
                height: 192.,
                reschedule_cooldown: Some(0.1),
                // Defaults x 10
                upslope_extra_gravity: 300.0,
                takeoff_extra_gravity: 300.0,
                takeoff_above_velocity: 64.0,
                fall_extra_gravity: 200.0,
                shorten_extra_gravity: 2400.0,
                peak_prevention_at_upward_velocity: 32.0,
                peak_prevention_extra_gravity: 2000.0,
                ..default()
            },
        },
        TnuaControllerBundle::default(),
        TnuaXpbd2dSensorShape(Collider::rectangle(30.0, 2.0)),
        TnuaSimpleAirActionsCounter::default(),
        undertaker::Mortal::respawnable(Vec3::new(320.0, 320.0, 0.0)),
        LayerTextureSwap(materials.add(Color::hsl(270., 0.85, 0.4))),
        MaterialMesh2dBundle {
            mesh: Mesh2dHandle(meshes.add(Capsule2d::new(32.0, 32.0))),
            material: materials.add(Color::hsl(0., 0.95, 0.7)),
            transform: Transform::from_xyz(320.0, 320.0, 0.0),
            ..default()
        },
    ));

    commands.spawn((
        camera::CameraTarget(1),
        RigidBody::Dynamic,
        Collider::capsule(64.0, 48.0),
        layered::Layered::A,
        undertaker::Mortal::respawnable(Vec3::new(320.0, 640.0, 0.0)),
        MaterialMesh2dBundle {
            mesh: Mesh2dHandle(meshes.add(Capsule2d::new(48.0, 64.0))),
            material: materials.add(Color::hsl(10., 0.95, 0.5)),
            transform: Transform::from_xyz(320.0, 640.0, 0.0),
            ..default()
        },
    ));

    commands.spawn((
        camera::CameraTarget(2),
        layered::Layered::Ground,
        RigidBody::Static,
        Collider::rectangle(1000.0, 100.0),
        TransformBundle::from(Transform::from_xyz(0.0, -100.0, 0.0)),
    ));

    commands.spawn((
        camera::CameraTarget(3),
        MaterialMesh2dBundle {
            mesh: Mesh2dHandle(meshes.add(Circle { radius: 64.0 })),
            material: materials.add(Color::hsl(180., 0.95, 0.7)),
            transform: Transform::from_xyz(-320.0, -640.0, 0.0),
            ..default()
        },
    ));

    commands.spawn((
        camera::CameraTarget(1),
        layered::Layered::B,
        RigidBody::Dynamic,
        Collider::capsule(32.0, 32.0),
        undertaker::Mortal::default(),
        MaterialMesh2dBundle {
            mesh: Mesh2dHandle(meshes.add(Capsule2d::new(32.0, 32.0))),
            material: materials.add(Color::hsl(5., 0.6, 0.75)),
            transform: Transform::from_xyz(320.0, 960.0, 0.0),
            ..default()
        },
    ));
}

#[derive(Component)]
struct LayerTextureSwap(Handle<ColorMaterial>);

#[allow(clippy::type_complexity)]
fn action_state_swap_layers(
    mut query: Query<(
        Entity,
        &ActionState<PlayerMovement>,
        &GlobalTransform,
        &mut layered::Layered,
        Option<&mut LayerTextureSwap>,
        Option<&mut Handle<ColorMaterial>>,
    )>,
    spatial_query: SpatialQuery,
) {
    for (ent, action_state, gtransform, mut layer, mut maybe_lts, mut maybe_mat) in query.iter_mut()
    {
        if action_state.just_pressed(&PlayerMovement::Swap) {
            // Attempt to swap layers
            let blocker_collider = Collider::circle(32. * 2.);
            let blockers = spatial_query.shape_intersections(
                &blocker_collider,
                gtransform.translation().truncate(),
                0.,
                SpatialQueryFilter::from_mask(layer.swapped().layer()),
            );

            if blockers.is_empty() {
                *layer = layer.swapped();
                if let Some(mut lts) = maybe_lts {
                    let new_mat = lts.0.clone();
                    if let Some(mat) = maybe_mat.as_mut() {
                        lts.0 = mat.clone();
                        *mat.as_mut() = new_mat;
                    }
                }
            }
        }
    }
}

fn update_tnua(
    mut query: Query<(
        &ActionState<PlayerMovement>,
        &mut TnuaController,
        &TnuaConfig,
        &mut TnuaSimpleAirActionsCounter,
    )>,
) {
    for (action_state, mut tnua_controller, config, mut air_counter) in query.iter_mut() {
        let mov_dir = action_state.clamped_value(&PlayerMovement::Walk);
        let direction = mov_dir * Vec3::X;

        air_counter.update(tnua_controller.as_mut());

        let speed_factor = 1.0;

        tnua_controller.basis(TnuaBuiltinWalk {
            desired_velocity: direction * speed_factor * config.max_speed,
            // TODO Handle shooting maybe?
            desired_forward: direction.normalize_or_zero(),
            ..config.walk
        });

        if action_state.pressed(&PlayerMovement::Jump) {
            tnua_controller.action(TnuaBuiltinJump {
                allow_in_air: air_counter.air_count_for(TnuaBuiltinJump::NAME)
                    <= config.air_actions,
                ..config.jump
            });
        }
    }
}