use crate::{
    global_observer,
    heads_database::{HeadControls, HeadsDatabase},
    loading_assets::AudioAssets,
};
use bevy::prelude::*;
use lightyear::prelude::MessageReceiver;
use shared::{
    player::PlayerBodyMesh,
    protocol::{PlaySound, PlayerId, events::ClientHeadChanged, messages::AssignClientPlayer},
};

pub fn plugin(app: &mut App) {
    app.register_type::<ClientPlayerId>();
    app.register_type::<LocalPlayer>();

    app.init_state::<PlayerAssignmentState>();

    app.add_systems(
        Update,
        receive_player_id.run_if(in_state(PlayerAssignmentState::Waiting)),
    );
    app.add_systems(
        Update,
        match_player_id.run_if(in_state(PlayerAssignmentState::IdReceived)),
    );

    global_observer!(app, on_update_head_mesh);
}

#[derive(Resource, Reflect)]
#[reflect(Resource)]
pub struct ClientPlayerId {
    id: u8,
}

fn receive_player_id(
    mut commands: Commands,
    mut recv: Single<&mut MessageReceiver<AssignClientPlayer>>,
    mut next: ResMut<NextState<PlayerAssignmentState>>,
) {
    for AssignClientPlayer(id) in recv.receive() {
        commands.insert_resource(ClientPlayerId { id });
        next.set(PlayerAssignmentState::IdReceived);
        info!("player id `{id}` received");
    }
}

fn match_player_id(
    mut commands: Commands,
    players: Query<(Entity, &PlayerId), Changed<PlayerId>>,
    client: Res<ClientPlayerId>,
    mut next: ResMut<NextState<PlayerAssignmentState>>,
) {
    for (entity, player) in players.iter() {
        if player.id == client.id {
            commands.entity(entity).insert(LocalPlayer);
            next.set(PlayerAssignmentState::Confirmed);
            info!("player entity {entity:?} confirmed with id `{}`", player.id);
            break;
        }
    }
}

/// Various states while trying to assign and match an ID to the player character.
/// Every client is given an ID (its player index in the match) and every character controller
/// is given an ID matching the client controlling it. This way the client can easily see which
/// controller it owns.
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, States)]
pub enum PlayerAssignmentState {
    /// Waiting for the server to send an [`AssignClientPlayer`] message
    #[default]
    Waiting,
    /// Received an [`AssignClientPlayer`], querying for a matching controller
    IdReceived,
    /// Matching controller confirmed; a [`LocalPlayer`] exists
    Confirmed,
}

#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
pub struct LocalPlayer;

fn on_update_head_mesh(
    trigger: Trigger<ClientHeadChanged>,
    mut commands: Commands,
    body_mesh: Single<(Entity, &Children), With<PlayerBodyMesh>>,
    head_db: Res<HeadsDatabase>,
    audio_assets: Res<AudioAssets>,
    sfx: Query<&AudioPlayer>,
) -> Result {
    let head = trigger.0 as usize;
    let (body_mesh, mesh_children) = *body_mesh;

    let head_str = head_db.head_key(head);

    commands.trigger(PlaySound::Head(head_str.to_string()));

    //TODO: make part of full character mesh later
    for child in mesh_children.iter().filter(|child| sfx.contains(*child)) {
        commands.entity(child).despawn();
    }
    if head_db.head_stats(head).controls == HeadControls::Plane {
        commands.entity(body_mesh).with_child((
            Name::new("sfx"),
            AudioPlayer::new(audio_assets.jet.clone()),
            PlaybackSettings {
                mode: bevy::audio::PlaybackMode::Loop,
                ..Default::default()
            },
        ));
    }

    Ok(())
}
