pub mod channels;
pub mod components;
pub mod events;
pub mod messages;

use crate::{
    GameState,
    abilities::{BuildExplosionSprite, healing::Healing},
    animation::AnimationFlags,
    backpack::{Backpack, backpack_ui::BackpackUiState},
    camera::{CameraArmRotation, CameraTarget},
    cash::CashResource,
    character::{self, AnimatedCharacter},
    control::{
        ControlState,
        controller_common::{MovementSpeedFactor, PlayerCharacterController},
        controls::ControllerSettings,
    },
    cutscene::StartCutscene,
    global_observer,
    head::ActiveHead,
    heads::{ActiveHeads, heads_ui::UiActiveHeads},
    hitpoints::Hitpoints,
    platforms::ActivePlatform,
    player::{Player, PlayerBodyMesh},
    protocol::channels::UnorderedReliableChannel,
    utils::triggers::TriggerAppExt,
};
use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity};
use bevy::prelude::*;
pub use components::*;
pub use events::*;
use happy_feet::{
    grounding::GroundingState,
    prelude::{
        CharacterDrag, CharacterGravity, CharacterMovement, GroundFriction, Grounding,
        GroundingConfig, KinematicVelocity, MoveInput, SteppingConfig,
    },
};
use lightyear::prelude::{
    AppChannelExt, AppComponentExt, AppMessageExt, AppTriggerExt, ChannelMode, ChannelSettings,
    NetworkDirection, PredictionMode, PredictionRegistrationExt, ReliableSettings,
    input::native::InputPlugin,
};
use lightyear_serde::{
    SerializationError, reader::ReadInteger, registry::SerializeFns, writer::WriteInteger,
};
use std::{collections::HashMap, time::Duration};

pub fn plugin(app: &mut App) {
    app.add_plugins(InputPlugin::<ControlState>::default());

    app.register_type::<PlayerId>();
    app.register_type::<TbMapEntityId>();
    app.register_type::<TbMapIdCounter>();
    app.register_type::<TbMapEntityMapping>();

    app.init_resource::<TbMapIdCounter>();
    app.init_resource::<TbMapEntityMapping>();

    app.add_channel::<UnorderedReliableChannel>(ChannelSettings {
        mode: ChannelMode::UnorderedReliable(ReliableSettings::default()),
        send_frequency: Duration::from_millis(100),
        priority: 1.0,
    })
    .add_direction(NetworkDirection::Bidirectional);

    app.add_message::<messages::DespawnTbMapEntity>()
        .add_direction(NetworkDirection::ServerToClient);
    app.add_message::<messages::AssignClientPlayer>()
        .add_direction(NetworkDirection::ServerToClient);

    app.register_component::<components::GltfSceneRoot>();
    app.register_component::<components::PlayerId>();
    app.register_component::<components::TbMapEntityId>();
    app.register_component::<ActiveHead>();
    app.register_component::<ActiveHeads>();
    app.register_component::<ActivePlatform>();
    app.register_component::<AngularVelocity>();
    app.register_component::<AnimatedCharacter>();
    app.register_component::<AnimationFlags>();
    app.register_component::<Backpack>();
    app.register_component::<BackpackUiState>();
    app.register_component::<CameraArmRotation>();
    app.register_component::<CameraTarget>();
    app.register_component::<CashResource>();
    app.register_component::<happy_feet::prelude::Character>();
    app.register_component::<character::HedzCharacter>();
    app.register_component::<CharacterDrag>();
    app.register_component::<CharacterGravity>();
    app.register_component::<CharacterMovement>();
    app.register_component::<CollisionLayers>();
    app.register_component::<ControllerSettings>();
    app.register_component::<GroundFriction>();
    app.register_component::<Grounding>();
    app.register_component::<GroundingConfig>();
    app.register_component::<GroundingState>();
    app.register_component::<Healing>();
    app.register_component::<Hitpoints>();
    app.register_component::<KinematicVelocity>();
    app.register_component::<LinearVelocity>();
    app.register_component::<MoveInput>();
    app.register_component::<MovementSpeedFactor>();
    app.register_component::<Name>();
    app.register_component::<Player>();
    app.register_component::<PlayerBodyMesh>();
    app.register_component::<PlayerCharacterController>();
    app.register_component::<SteppingConfig>();
    app.register_component::<Transform>()
        .add_prediction(PredictionMode::Full)
        .add_should_rollback(transform_should_rollback);
    app.register_component::<UiActiveHeads>();
    // `Visibility` isn't `(De)Serialize`, so we have to provide custom serde for it.
    app.register_component_custom_serde::<Visibility>(SerializeFns {
        serialize: |comp, writer| writer.write_u8(*comp as u8).map_err(SerializationError::Io),
        deserialize: |reader| {
            let byte = reader.read_u8().map_err(SerializationError::Io)?;
            Ok(match byte {
                0 => Visibility::Inherited,
                1 => Visibility::Hidden,
                2 => Visibility::Visible,
                _ => return Err(SerializationError::InvalidValue),
            })
        },
    });

    app.replicate_trigger::<BuildExplosionSprite, UnorderedReliableChannel>();
    app.replicate_trigger::<StartCutscene, UnorderedReliableChannel>();

    app.replicate_trigger::<events::ClientHeadChanged, UnorderedReliableChannel>();
    app.replicate_trigger::<events::PlaySound, UnorderedReliableChannel>();

    app.add_trigger::<events::ClientEnteredPlaying>()
        .add_direction(NetworkDirection::ClientToServer);

    app.add_systems(
        OnEnter(GameState::MapLoading),
        |mut counter: ResMut<TbMapIdCounter>| counter.reset(),
    );

    global_observer!(app, components::spawn_gltf_scene_roots);
}

fn transform_should_rollback(this: &Transform, that: &Transform) -> bool {
    this.translation.distance_squared(that.translation) >= 0.01f32.powf(2.)
}

/// A global allocator for `TbMapEntityId` values. Should be reset when a map begins loading.
#[derive(Resource, Reflect, Default)]
#[reflect(Resource)]
pub struct TbMapIdCounter(u64);

impl TbMapIdCounter {
    pub fn reset(&mut self) {
        self.0 = 0;
    }

    pub fn alloc(&mut self) -> TbMapEntityId {
        let id = self.0;
        self.0 += 1;
        TbMapEntityId { id }
    }
}

/// A mapping from TbMapEntityId to clientside map entity. When the serverside is spawned and the client's
/// components migrated to it, or the clientside is despawned because the serverside is already despawned,
/// the Id entry is removed from this mapping.
#[derive(Resource, Reflect, Default, Deref, DerefMut)]
#[reflect(Resource)]
pub struct TbMapEntityMapping(pub HashMap<u64, Entity>);
