use super::sentinel::{DeletedEntities, TrackedComps};
use crate::{
    client::Client,
    presence::{self, Presence, RegionSubscription},
};
use common::{
    comp::{Ori, Pos, Vel},
    region::{region_in_vd, regions_in_vd, Event as RegionEvent, RegionMap},
    terrain::TerrainChunkSize,
    uid::Uid,
    vol::RectVolSize,
};
use common_ecs::{Job, Origin, Phase, System};
use common_net::msg::ServerGeneral;
use specs::{
    Entities, Join, ReadExpect, ReadStorage, SystemData, World, WorldExt, Write, WriteStorage,
};
use tracing::{debug, error};
use vek::*;

/// This system will update region subscriptions based on client positions
#[derive(Default)]
pub struct Sys;
impl<'a> System<'a> for Sys {
    #[allow(clippy::type_complexity)]
    type SystemData = (
        Entities<'a>,
        ReadExpect<'a, RegionMap>,
        ReadStorage<'a, Uid>,
        ReadStorage<'a, Pos>,
        ReadStorage<'a, Vel>,
        ReadStorage<'a, Ori>,
        ReadStorage<'a, Presence>,
        ReadStorage<'a, Client>,
        WriteStorage<'a, RegionSubscription>,
        Write<'a, DeletedEntities>,
        TrackedComps<'a>,
    );

    const NAME: &'static str = "subscription";
    const ORIGIN: Origin = Origin::Server;
    const PHASE: Phase = Phase::Create;

    #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587
    fn run(
        _job: &mut Job<Self>,
        (
            entities,
            region_map,
            uids,
            positions,
            velocities,
            orientations,
            presences,
            clients,
            mut subscriptions,
            mut deleted_entities,
            tracked_comps,
        ): Self::SystemData,
    ) {
        // To update subscriptions
        // 1. Iterate through clients
        // 2. Calculate current chunk position
        // 3. If chunk is the same return, otherwise continue (use fuzziness)
        // 4. Iterate through subscribed regions
        // 5. Check if region is still in range (use fuzziness)
        // 6. If not in range
        //     - remove from hashset
        //     - inform client of which entities to remove
        // 7. Determine list of regions that are in range and iterate through it
        //    - check if in hashset (hash calc) if not add it
        let mut regions_to_remove = Vec::new();
        for (mut subscription, pos, presence, client_entity, client) in (
            &mut subscriptions,
            &positions,
            &presences,
            &entities,
            &clients,
        )
            .join()
        {
            let vd = presence.view_distance;
            // Calculate current chunk
            let chunk = (Vec2::<f32>::from(pos.0))
                .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32);
            // Only update regions when moving to a new chunk
            // uses a fuzzy border to prevent rapid triggering when moving along chunk
            // boundaries
            if chunk != subscription.fuzzy_chunk
                && (subscription
                    .fuzzy_chunk
                    .map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
                        (e as f32 + 0.5) * sz as f32
                    })
                    - Vec2::from(pos.0))
                .map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
                    e.abs() > (sz / 2 + presence::CHUNK_FUZZ) as f32
                })
                .reduce_or()
            {
                // Update current chunk
                subscription.fuzzy_chunk = Vec2::<f32>::from(pos.0)
                    .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32);
                // Use the largest side length as our chunk size
                let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32;
                // Iterate through currently subscribed regions
                for key in &subscription.regions {
                    // Check if the region is not within range anymore
                    if !region_in_vd(
                        *key,
                        pos.0,
                        (vd as f32 * chunk_size)
                            + (presence::CHUNK_FUZZ as f32
                                + presence::REGION_FUZZ as f32
                                + chunk_size)
                                * 2.0f32.sqrt(),
                    ) {
                        // Add to the list of regions to remove
                        regions_to_remove.push(*key);
                    }
                }

                // Iterate through regions to remove
                for key in regions_to_remove.drain(..) {
                    // Remove region from this client's set of subscribed regions
                    subscription.regions.remove(&key);
                    // Tell the client to delete the entities in that region if it exists in the
                    // RegionMap
                    if let Some(region) = region_map.get(key) {
                        // Process entity left events since they won't be processed during entity
                        // sync because this region is no longer subscribed to
                        // TODO: consider changing system ordering??
                        for event in region.events() {
                            match event {
                                RegionEvent::Entered(_, _) => {}, /* These don't need to be */
                                // processed because this
                                // region is being thrown out
                                // anyway
                                RegionEvent::Left(id, maybe_key) => {
                                    // Lookup UID for entity
                                    // Doesn't overlap with entity deletion in sync packages
                                    // because the uid would not be available if the entity was
                                    // deleted
                                    if let Some(&uid) = uids.get(entities.entity(*id)) {
                                        if !maybe_key
                                            .as_ref()
                                            // Don't need to check that this isn't also in the regions to remove since the entity will be removed when we get to that one
                                            .map(|key| subscription.regions.contains(key))
                                            .unwrap_or(false)
                                        {
                                            client.send_fallible(ServerGeneral::DeleteEntity(uid));
                                        }
                                    }
                                },
                            }
                        }
                        // Tell client to delete entities in the region
                        for (&uid, _) in (&uids, region.entities()).join() {
                            client.send_fallible(ServerGeneral::DeleteEntity(uid));
                        }
                    }
                    // Send deleted entities since they won't be processed for this client in entity
                    // sync
                    for uid in deleted_entities
                        .get_deleted_in_region(key)
                        .iter()
                        .flat_map(|v| v.iter())
                    {
                        client.send_fallible(ServerGeneral::DeleteEntity(Uid(*uid)));
                    }
                }

                for key in regions_in_vd(
                    pos.0,
                    (vd as f32 * chunk_size)
                        + (presence::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(),
                ) {
                    // Send client initial info about the entities in this region if it was not
                    // already within the set of subscribed regions
                    if subscription.regions.insert(key) {
                        if let Some(region) = region_map.get(key) {
                            (
                                &positions,
                                velocities.maybe(),
                                orientations.maybe(),
                                region.entities(),
                                &entities,
                            )
                                .join()
                                .filter(|(_, _, _, _, e)| *e != client_entity)
                                .filter_map(|(pos, vel, ori, _, entity)| {
                                    tracked_comps.create_entity_package(
                                        entity,
                                        Some(*pos),
                                        vel.copied(),
                                        ori.copied(),
                                    )
                                })
                                .for_each(|msg| {
                                    // Send message to create entity and tracked components and
                                    // physics components
                                    client.send_fallible(ServerGeneral::CreateEntity(msg));
                                })
                        }
                    }
                }
            }
        }
    }
}

/// Initialize region subscription
pub fn initialize_region_subscription(world: &World, entity: specs::Entity) {
    if let (Some(client_pos), Some(presence), Some(client)) = (
        world.read_storage::<Pos>().get(entity),
        world.read_storage::<Presence>().get(entity),
        world.write_storage::<Client>().get(entity),
    ) {
        let fuzzy_chunk = (Vec2::<f32>::from(client_pos.0))
            .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32);
        let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32;
        let regions = common::region::regions_in_vd(
            client_pos.0,
            (presence.view_distance as f32 * chunk_size) as f32
                + (presence::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(),
        );

        let region_map = world.read_resource::<RegionMap>();
        let tracked_comps = TrackedComps::fetch(world);
        for key in &regions {
            if let Some(region) = region_map.get(*key) {
                (
                    &world.read_storage::<Pos>(), // We assume all these entities have a position
                    world.read_storage::<Vel>().maybe(),
                    world.read_storage::<Ori>().maybe(),
                    region.entities(),
                    &world.entities(),
                )
                .join()
                // Don't send client its own components because we do that below
                .filter(|t| t.4 != entity)
                .filter_map(|(pos, vel, ori, _, entity)|
                    tracked_comps.create_entity_package(
                        entity,
                        Some(*pos),
                        vel.copied(),
                        ori.copied(),
                    )
                )
                .for_each(|msg| {
                    // Send message to create entity and tracked components and physics components
                    client.send_fallible(ServerGeneral::CreateEntity(msg));
                });
            }
        }
        // If client position was modified it might not be updated in the region system
        // so we send its components here
        if let Some(pkg) = tracked_comps.create_entity_package(
            entity,
            Some(*client_pos),
            world.read_storage().get(entity).copied(),
            world.read_storage().get(entity).copied(),
        ) {
            client.send_fallible(ServerGeneral::CreateEntity(pkg));
        }

        if let Err(e) = world.write_storage().insert(entity, RegionSubscription {
            fuzzy_chunk,
            regions,
        }) {
            error!(?e, "Failed to insert region subscription component");
        }
    } else {
        debug!(
            ?entity,
            "Failed to initialize region subscription. Couldn't retrieve all the neccesary \
             components on the provided entity"
        );
    }
}
