package me.earthme.luminol.collision;

import me.earthme.luminol.config.modules.optimizations.AcceleratedCollisionConfig;
import net.minecraft.world.entity.Entity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
 * Builds the per-region collision graph for a Folia region tick, ported from
 * AcceleratedRecoiling's architecture (GPL-3.0).
 *
 * <p>Called once after the region's entity tick loop. It runs the accelerated broadphase
 * (FFM/GPU/JAVA) over every pushable entity in the region and records overlapping pairs into
 * {@link RegionCollisionMap}. It does <b>not</b> apply pushes itself — instead
 * {@code LivingEntity.pushEntities()} keeps running its normal per-entity push loop, but reads
 * its candidate list from the graph (O(1)) rather than issuing a fresh spatial
 * {@code getPushableEntities} query. Replacing that spatial query is where the speedup comes
 * from; the vanilla push/cramming/collision-cap logic is left untouched.
 *
 * <p>Folia-safe: a region ticks on one thread, so the thread-local graph and scratch are
 * naturally isolated and lock-free.
 */
public final class RegionCollisionHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger("NekoCore/Collision");

    // Per-region-thread scratch to avoid per-tick allocation churn.
    private static final class Scratch {
        Entity[] pushable = new Entity[256];
        double[] aabb = new double[256 * 6];
        int[] hits = new int[256]; // per-entity applied-collision counter
        int[] outA = new int[256 * 8]; // pair index output (sized to count * maxCollision)
        int[] outB = new int[256 * 8];
        void ensure(int n) {
            if (n > pushable.length) {
                int cap = Integer.highestOneBit(n - 1) << 1;
                pushable = new Entity[cap];
                aabb = new double[cap * 6];
                hits = new int[cap];
            }
        }
        void ensurePairs(int maxPairs) {
            if (maxPairs > outA.length) {
                int cap = Integer.highestOneBit(maxPairs - 1) << 1;
                outA = new int[cap];
                outB = new int[cap];
            }
        }
    }
    private static final ThreadLocal<Scratch> SCRATCH = ThreadLocal.withInitial(Scratch::new);

    private RegionCollisionHandler() {}

    public static void handleEntityPush(Entity[] entities, double inflate) {
        if (!AcceleratedCollisionConfig.enabled) return;

        INativeCollisionBackend backend = CollisionBackendSelector.getActive();
        if (backend == null || CollisionBackendSelector.isVanilla()) return;

        final RegionCollisionMap map = RegionCollisionMap.get();
        map.beginFill(); // fresh graph for this region tick (marks it fresh even if empty)

        final int total = entities.length;
        if (total < 2) return;

        final Scratch s = SCRATCH.get();
        s.ensure(total);
        final Entity[] pushable = s.pushable;

        int count = 0;
        for (int i = 0; i < total; i++) {
            Entity e = entities[i];
            if (e != null && e.isPushable() && !e.isRemoved()) {
                pushable[count++] = e;
                map.registerEntity(e); // so the graph view can resolve ids during pushEntities()
            }
        }
        if (count < 2) return;

        final double[] aabb = s.aabb;
        for (int i = 0; i < count; i++) {
            net.minecraft.world.phys.AABB box = pushable[i].getBoundingBox().inflate(inflate);
            int o = i * 6;
            aabb[o]   = box.minX;
            aabb[o+1] = box.minY;
            aabb[o+2] = box.minZ;
            aabb[o+3] = box.maxX;
            aabb[o+4] = box.maxY;
            aabb[o+5] = box.maxZ;
        }

        // Per-entity collision cap (>=1). Mirrors vanilla maxEntityCollisions to bound work
        // on dense pileups. 0/negative => treat as effectively unlimited.
        final int maxCollision = AcceleratedCollisionConfig.maxCollision > 0
                ? AcceleratedCollisionConfig.maxCollision : Integer.MAX_VALUE;
        // Pair-buffer capacity. Guard against int overflow for huge regions.
        final int maxPairs = (int) Math.min((long) count * Math.min(maxCollision, 64), Integer.MAX_VALUE - 8);
        s.ensurePairs(maxPairs);
        final int[] outA = s.outA, outB = s.outB;

        int pairCount;
        try {
            // Allocation-free broadphase: indices written straight into reused outA/outB.
            pairCount = backend.pushInto(aabb, count, outA, outB);
        } catch (Throwable t) {
            LOGGER.error("Collision backend pushInto failed; skipping region push this tick", t);
            for (int i = 0; i < count; i++) pushable[i] = null;
            return;
        }
        if (pairCount <= 0) {
            for (int i = 0; i < count; i++) pushable[i] = null;
            return;
        }

        // Record overlapping pairs into the per-region graph keyed by entity id. pushEntities()
        // will read each entity's candidate list from here instead of querying the world.
        for (int k = 0; k < pairCount; k++) {
            int ia = outA[k];
            int ib = outB[k];
            if (ia < 0 || ia >= count || ib < 0 || ib >= count || ia == ib) continue;
            map.addPair(pushable[ia].getId(), pushable[ib].getId());
        }

        // Help GC: don't retain entity references in the scratch between ticks.
        for (int i = 0; i < count; i++) pushable[i] = null;
    }
}
