pub mod pick;

use std::collections::HashMap;

use azalea_block::BlockState;
use azalea_core::{
    delta::LpVec3,
    direction::Direction,
    game_type::GameMode,
    hit_result::{BlockHitResult, HitResult},
    position::{BlockPos, Vec3},
    tick::GameTick,
};
use azalea_entity::{
    Attributes, LocalEntity, LookDirection, PlayerAbilities, Position,
    attributes::{
        creative_block_interaction_range_modifier, creative_entity_interaction_range_modifier,
    },
    clamp_look_direction,
    indexing::EntityIdIndex,
    inventory::Inventory,
};
use azalea_inventory::{ItemStack, ItemStackData, components};
use azalea_physics::{
    PhysicsSystems, collision::entity_collisions::update_last_bounding_box,
    local_player::PhysicsState,
};
use azalea_protocol::packets::game::{
    ServerboundInteract, ServerboundUseItem, s_interact::InteractionHand,
    s_swing::ServerboundSwing, s_use_item_on::ServerboundUseItemOn,
};
use azalea_world::World;
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
use tracing::warn;

use super::mining::Mining;
use crate::{
    attack::handle_attack_event,
    interact::pick::{HitResultComponent, update_hit_result_component},
    inventory::InventorySystems,
    local_player::{LocalGameMode, PermissionLevel},
    movement::MoveEventsSystems,
    packet::game::SendGamePacketEvent,
    respawn::perform_respawn,
};

/// A plugin that allows clients to interact with blocks in the world.
pub struct InteractPlugin;
impl Plugin for InteractPlugin {
    fn build(&self, app: &mut App) {
        app.add_message::<StartUseItemEvent>()
            .add_systems(
                Update,
                (
                    update_attributes_for_gamemode,
                    handle_start_use_item_event,
                    update_hit_result_component
                        .after(clamp_look_direction)
                        .after(update_last_bounding_box),
                )
                    .after(InventorySystems)
                    .after(MoveEventsSystems)
                    .after(perform_respawn)
                    .after(handle_attack_event)
                    .chain(),
            )
            .add_systems(
                GameTick,
                handle_start_use_item_queued.before(PhysicsSystems),
            )
            .add_observer(handle_entity_interact)
            .add_observer(handle_swing_arm_trigger);
    }
}

/// A component that contains information about our local block state
/// predictions.
#[derive(Clone, Component, Debug, Default)]
pub struct BlockStatePredictionHandler {
    /// The total number of changes that this client has made to blocks.
    seq: u32,
    server_state: HashMap<BlockPos, ServerVerifiedState>,
}
#[derive(Clone, Debug)]
struct ServerVerifiedState {
    seq: u32,
    block_state: BlockState,
    /// Used for teleporting the player back if we're colliding with the block
    /// that got placed back.
    #[allow(unused)]
    player_pos: Vec3,
}

impl BlockStatePredictionHandler {
    /// Get the next sequence number that we're going to use and increment the
    /// value.
    pub fn start_predicting(&mut self) -> u32 {
        self.seq += 1;
        self.seq
    }

    /// Should be called right before the client updates a block with its
    /// prediction.
    ///
    /// This is used to make sure that we can rollback to this state if the
    /// server acknowledges the sequence number (with
    /// [`ClientboundBlockChangedAck`]) without having sent a block update.
    ///
    /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
    pub fn retain_known_server_state(
        &mut self,
        pos: BlockPos,
        old_state: BlockState,
        player_pos: Vec3,
    ) {
        self.server_state
            .entry(pos)
            .and_modify(|s| s.seq = self.seq)
            .or_insert(ServerVerifiedState {
                seq: self.seq,
                block_state: old_state,
                player_pos,
            });
    }

    /// Save this update as the correct server state so when the server sends a
    /// [`ClientboundBlockChangedAck`] we don't roll back this new update.
    ///
    /// This should be used when we receive a block update from the server.
    ///
    /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
    pub fn update_known_server_state(&mut self, pos: BlockPos, state: BlockState) -> bool {
        if let Some(s) = self.server_state.get_mut(&pos) {
            s.block_state = state;
            true
        } else {
            false
        }
    }

    pub fn end_prediction_up_to(&mut self, seq: u32, world: &World) {
        let mut to_remove = Vec::new();
        for (pos, state) in &self.server_state {
            if state.seq > seq {
                continue;
            }
            to_remove.push(*pos);

            // syncBlockState
            let client_block_state = world.get_block_state(*pos).unwrap_or_default();
            let server_block_state = state.block_state;
            if client_block_state == server_block_state {
                continue;
            }
            world.set_block_state(*pos, server_block_state);
            // TODO: implement these two functions
            // if is_colliding(player, *pos, server_block_state) {
            //     abs_snap_to(state.player_pos);
            // }
        }

        for pos in to_remove {
            self.server_state.remove(&pos);
        }
    }
}

/// An event that makes one of our clients simulate a right-click.
///
/// This event just inserts the [`StartUseItemQueued`] component on the given
/// entity.
#[doc(alias("right click"))]
#[derive(Message)]
pub struct StartUseItemEvent {
    pub entity: Entity,
    pub hand: InteractionHand,
    /// See [`StartUseItemQueued::force_block`].
    pub force_block: Option<BlockPos>,
}
pub fn handle_start_use_item_event(
    mut commands: Commands,
    mut events: MessageReader<StartUseItemEvent>,
) {
    for event in events.read() {
        commands.entity(event.entity).insert(StartUseItemQueued {
            hand: event.hand,
            force_block: event.force_block,
        });
    }
}

/// A component that makes our client simulate a right-click on the next
/// [`GameTick`]. It's removed after that tick.
///
/// You may find it more convenient to use [`StartUseItemEvent`] instead, which
/// just inserts this component for you.
///
/// [`GameTick`]: azalea_core::tick::GameTick
#[derive(Component, Debug)]
pub struct StartUseItemQueued {
    pub hand: InteractionHand,
    /// Optionally force us to send a [`ServerboundUseItemOn`] on the given
    /// block.
    ///
    /// This is useful if you want to interact with a block without looking at
    /// it, but should be avoided to stay compatible with anticheats.
    pub force_block: Option<BlockPos>,
}
#[allow(clippy::type_complexity)]
pub fn handle_start_use_item_queued(
    mut commands: Commands,
    query: Query<(
        Entity,
        &StartUseItemQueued,
        &mut BlockStatePredictionHandler,
        &HitResultComponent,
        &LookDirection,
        Option<&Mining>,
    )>,
) {
    for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
        query
    {
        commands.entity(entity).remove::<StartUseItemQueued>();

        if mining.is_some() {
            warn!("Got a StartUseItemEvent for a client that was mining");
        }

        // TODO: this also skips if LocalPlayer.handsBusy is true, which is used when
        // rowing a boat

        let mut hit_result = (**hit_result).clone();

        if let Some(force_block) = start_use_item.force_block {
            let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
                block_hit_result.block_pos == force_block
            } else {
                false
            };

            if !hit_result_matches {
                // we're not looking at the block, so make up some numbers
                hit_result = HitResult::Block(BlockHitResult {
                    location: force_block.center(),
                    direction: Direction::Up,
                    block_pos: force_block,
                    inside: false,
                    world_border: false,
                    miss: false,
                });
            }
        }

        match &hit_result {
            HitResult::Block(r) => {
                let seq = prediction_handler.start_predicting();
                if r.miss {
                    commands.trigger(SendGamePacketEvent::new(
                        entity,
                        ServerboundUseItem {
                            hand: start_use_item.hand,
                            seq,
                            x_rot: look_direction.x_rot(),
                            y_rot: look_direction.y_rot(),
                        },
                    ));
                } else {
                    commands.trigger(SendGamePacketEvent::new(
                        entity,
                        ServerboundUseItemOn {
                            hand: start_use_item.hand,
                            block_hit: r.into(),
                            seq,
                        },
                    ));
                    // TODO: depending on the result of useItemOn, this might
                    // also need to send a SwingArmEvent.
                    // basically, this TODO is for simulating block
                    // interactions/placements on the client-side.
                }
            }
            HitResult::Entity(r) => {
                commands.trigger(EntityInteractEvent {
                    client: entity,
                    target: r.entity,
                    location: Some(r.location),
                });
            }
        }
    }
}

/// An ECS `Event` that makes the client tell the server that we right-clicked
/// an entity.
#[derive(Clone, Debug, EntityEvent)]
pub struct EntityInteractEvent {
    #[event_target]
    pub client: Entity,
    pub target: Entity,
    /// The position on the entity that we'll tell the server that we clicked
    /// on.
    ///
    /// This doesn't matter for most entities. If it's set to `None` but we're
    /// looking at the target, it'll use the correct value. If it's `None` and
    /// we're not looking at the entity, then it'll arbitrary send the target's
    /// exact position.
    pub location: Option<Vec3>,
}

pub fn handle_entity_interact(
    trigger: On<EntityInteractEvent>,
    mut commands: Commands,
    client_query: Query<(&PhysicsState, &EntityIdIndex, &HitResultComponent)>,
    target_query: Query<&Position>,
) {
    let Some((physics_state, entity_id_index, hit_result)) = client_query.get(trigger.client).ok()
    else {
        warn!(
            "tried to interact with an entity but the client didn't have the required components"
        );
        return;
    };

    // TODO: worldborder check

    let Some(entity_id) = entity_id_index.get_by_ecs_entity(trigger.target) else {
        warn!("tried to interact with an entity that isn't known by the client");
        return;
    };

    let location = if let Some(l) = trigger.location {
        l
    } else {
        // if we're looking at the entity, use that
        if let Some(entity_hit_result) = hit_result.as_entity_hit_result()
            && entity_hit_result.entity == trigger.target
        {
            entity_hit_result.location
        } else {
            // if we're not looking at the entity, make up a value that's good enough by
            // using the entity's position
            let Ok(target_position) = target_query.get(trigger.target) else {
                warn!("tried to look at an entity without the entity having a position");
                return;
            };
            **target_position
        }
    };

    let interact = ServerboundInteract {
        entity_id,
        hand: InteractionHand::MainHand,
        location: LpVec3::from(location),
        using_secondary_action: physics_state.trying_to_crouch,
    };
    commands.trigger(SendGamePacketEvent::new(trigger.client, interact.clone()));

    // TODO: this is true if the interaction failed, which i think can only happen
    // in certain cases when interacting with armor stands
    let consumes_action = false;
    if !consumes_action {
        // but yes, most of the time vanilla really does send two identical interact
        // packets like this
        commands.trigger(SendGamePacketEvent::new(trigger.client, interact));
    }
}

/// Whether we can't interact with the block, based on your gamemode.
///
/// If this is false, then we can interact with the block.
///
/// The world, block position, and inventory are used for the adventure mode
/// check.
pub fn check_is_interaction_restricted(
    world: &World,
    block_pos: BlockPos,
    game_mode: &GameMode,
    inventory: &Inventory,
) -> bool {
    match game_mode {
        GameMode::Adventure => {
            // vanilla checks for abilities.mayBuild here but servers have no
            // way of modifying that

            let held_item = inventory.held_item();
            match &held_item {
                ItemStack::Present(item) => {
                    let block = world.chunks.get_block_state(block_pos);
                    let Some(block) = block else {
                        // block isn't loaded so just say that it is restricted
                        return true;
                    };
                    check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
                }
                _ => true,
            }
        }
        GameMode::Spectator => true,
        _ => false,
    }
}

/// Check if the item has the `CanDestroy` tag for the block.
pub fn check_block_can_be_broken_by_item_in_adventure_mode(
    item: &ItemStackData,
    _block: &BlockState,
) -> bool {
    // minecraft caches the last checked block but that's kind of an unnecessary
    // optimization and makes the code too complicated

    if item.get_component::<components::CanBreak>().is_none() {
        // no CanDestroy tag
        return false;
    };

    false

    // for block_predicate in can_destroy {
    //     // TODO
    //     // defined in BlockPredicateArgument.java
    // }

    // true
}

pub fn can_use_game_master_blocks(
    abilities: &PlayerAbilities,
    permission_level: &PermissionLevel,
) -> bool {
    abilities.instant_break && **permission_level >= 2
}

/// Swing your arm.
///
/// This is purely a visual effect and won't interact with anything in the
/// world.
#[derive(Clone, Debug, EntityEvent)]
pub struct SwingArmEvent {
    pub entity: Entity,
}
pub fn handle_swing_arm_trigger(swing_arm: On<SwingArmEvent>, mut commands: Commands) {
    commands.trigger(SendGamePacketEvent::new(
        swing_arm.entity,
        ServerboundSwing {
            hand: InteractionHand::MainHand,
        },
    ));
}

#[allow(clippy::type_complexity)]
fn update_attributes_for_gamemode(
    query: Query<(&mut Attributes, &LocalGameMode), (With<LocalEntity>, Changed<LocalGameMode>)>,
) {
    for (mut attributes, game_mode) in query {
        if game_mode.current == GameMode::Creative {
            attributes
                .block_interaction_range
                .insert(creative_block_interaction_range_modifier());
            attributes
                .entity_interaction_range
                .insert(creative_entity_interaction_range_modifier());
        } else {
            attributes
                .block_interaction_range
                .remove(&creative_block_interaction_range_modifier().id);
            attributes
                .entity_interaction_range
                .remove(&creative_entity_interaction_range_modifier().id);
        }
    }
}
