package me.earthme.luminol.collision;

import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;

import java.util.AbstractList;
import java.util.Collections;
import java.util.List;

/**
 * Per-region collision graph (ported from AcceleratedRecoiling's CollisionMapData, GPL-3.0).
 *
 * <p>The accelerated broadphase runs once per region tick and records, for every entity, the
 * list of entities it overlaps plus a local density estimate. {@code LivingEntity.pushEntities}
 * then reads its push candidates straight from this graph (O(1)) instead of doing a fresh
 * spatial {@code getPushableEntities} query — that spatial query, not the overlap test, is the
 * real cost the native backend eliminates.
 *
 * <p>Folia-safe by construction: each region ticks on a single thread, and both the fill
 * (handler) and the read ({@code pushEntities}) happen within that same region tick. A
 * {@link ThreadLocal} instance therefore isolates regions with zero locking. The graph is keyed
 * by {@code entity.getId()} and is rebuilt (cleared) at the start of every fill.
 */
public final class RegionCollisionMap {

    private static final ThreadLocal<RegionCollisionMap> LOCAL = ThreadLocal.withInitial(RegionCollisionMap::new);

    public static RegionCollisionMap get() {
        return LOCAL.get();
    }

    // entityId -> list of overlapping entityIds (symmetric)
    private final Int2ObjectOpenHashMap<IntArrayList> collisions = new Int2ObjectOpenHashMap<>(4096);
    // entityId -> live Entity (so the view can resolve ids without a level lookup)
    private final Int2ObjectOpenHashMap<Entity> entitiesById = new Int2ObjectOpenHashMap<>(4096);
    // monotonically increasing fill generation; reads compare against it to detect a stale graph
    private long generation = 0L;
    private long lastFillGeneration = -1L;

    private RegionCollisionMap() {
    }

    /** Begin a fresh fill for this region tick. Clears the previous graph. */
    public void beginFill() {
        this.collisions.clear();
        this.entitiesById.clear();
        this.lastFillGeneration = ++this.generation;
    }

    /** Register an entity so the graph view can resolve its id later this tick. */
    public void registerEntity(Entity e) {
        this.entitiesById.put(e.getId(), e);
    }

    /** Record a symmetric overlapping pair. */
    public void addPair(int idA, int idB) {
        addSingle(idA, idB);
        addSingle(idB, idA);
    }

    private void addSingle(int source, int target) {
        IntArrayList list = this.collisions.get(source);
        if (list == null) {
            list = new IntArrayList(8);
            this.collisions.put(source, list);
        }
        list.add(target);
    }

    /**
     * Local density proxy for an entity: the number of entities overlapping it this tick.
     * Used to gate the accelerated path (only dense entities skip the vanilla spatial query).
     */
    public int getOverlapCount(int id) {
        IntArrayList list = this.collisions.get(id);
        return list == null ? 0 : list.size();
    }

    /** True if a fill happened during the current region tick (cheap freshness guard). */
    public boolean isFresh() {
        return this.lastFillGeneration == this.generation;
    }

    /**
     * Returns the overlap list for {@code source} as a lazy view backed by the graph.
     * Empty if the entity has no recorded collisions.
     */
    public List<Entity> getCollisionList(Entity source) {
        IntArrayList ids = this.collisions.get(source.getId());
        if (ids == null || ids.isEmpty()) return Collections.emptyList();
        return new EntityListView(ids, this.entitiesById, source);
    }

    private static final class EntityListView extends AbstractList<Entity> {
        private final IntArrayList ids;
        private final Int2ObjectOpenHashMap<Entity> byId;
        private final Entity source;

        EntityListView(IntArrayList ids, Int2ObjectOpenHashMap<Entity> byId, Entity source) {
            this.ids = ids;
            this.byId = byId;
            this.source = source;
        }

        @Override
        public Entity get(int index) {
            Entity target = this.byId.get(this.ids.getInt(index));
            // Fall back to source (a harmless self-reference that downstream filters drop) if the
            // id is no longer resolvable (entity removed mid-tick).
            return target == null ? this.source : target;
        }

        @Override
        public int size() {
            return this.ids.size();
        }

        @Override
        public boolean isEmpty() {
            return this.ids.isEmpty();
        }
    }
}
