import { v2 } from '../core/math.js';
import { EventBus } from '../core/EventBus.js';
import { System } from '../ecs/World.js';

/* ────────────────────────────────────────────────────────────────
   PhysicsEngine2D — Built-in 2D physics
   
   AABB, Circle, Polygon collision detection & resolution.
   Broad-phase spatial hashing + narrow-phase SAT / overlap.
──────────────────────────────────────────────────────────────── */
/* ── Spatial Hash ─────────────────────────────────────────────── */
class SpatialHash {
    constructor(cellSize = 128) {
        this.cells = new Map();
        this.cellSize = cellSize;
    }
    clear() {
        this.cells.clear();
    }
    key(cx, cy) {
        return `${cx},${cy}`;
    }
    insert(id, x, y, hw, hh) {
        const minCX = Math.floor((x - hw) / this.cellSize);
        const maxCX = Math.floor((x + hw) / this.cellSize);
        const minCY = Math.floor((y - hh) / this.cellSize);
        const maxCY = Math.floor((y + hh) / this.cellSize);
        for (let cx = minCX; cx <= maxCX; cx++) {
            for (let cy = minCY; cy <= maxCY; cy++) {
                const k = this.key(cx, cy);
                let set = this.cells.get(k);
                if (!set) {
                    set = new Set();
                    this.cells.set(k, set);
                }
                set.add(id);
            }
        }
    }
    query(x, y, hw, hh) {
        const result = new Set();
        const minCX = Math.floor((x - hw) / this.cellSize);
        const maxCX = Math.floor((x + hw) / this.cellSize);
        const minCY = Math.floor((y - hh) / this.cellSize);
        const maxCY = Math.floor((y + hh) / this.cellSize);
        for (let cx = minCX; cx <= maxCX; cx++) {
            for (let cy = minCY; cy <= maxCY; cy++) {
                const set = this.cells.get(this.key(cx, cy));
                if (set)
                    set.forEach(id => result.add(id));
            }
        }
        return result;
    }
}
/* ── PhysicsEngine2D ──────────────────────────────────────────── */
class PhysicsEngine2D {
    constructor() {
        this.events = new EventBus();
        this.gravity = { x: 0, y: 980 };
        this.hash = new SpatialHash(128);
        // Track collision pairs for enter/exit events
        this.activeCollisions = new Set();
        this.activeTriggers = new Set();
    }
    /* ── Fixed update (call at fixedTimestep) ────────────────────── */
    step(world, dt) {
        this.integrateForces(world, dt);
        const collisions = this.detectCollisions(world);
        this.resolveCollisions(world, collisions);
        this.emitEvents(collisions);
    }
    /* ── Integration ────────────────────────────────────────────── */
    integrateForces(world, dt) {
        for (const id of world.query('Transform', 'RigidBody2D')) {
            const transform = world.getComponent(id, 'Transform');
            const rb = world.getComponent(id, 'RigidBody2D');
            if (rb.bodyType === 'static' || !rb.enabled)
                continue;
            // Store previous position for interpolation
            transform.prevPosition = { ...transform.position };
            transform.prevRotation = transform.rotation;
            if (rb.bodyType === 'dynamic') {
                // Gravity
                rb.velocity.x += (this.gravity.x * rb.gravityScale + rb.acceleration.x) * dt;
                rb.velocity.y += (this.gravity.y * rb.gravityScale + rb.acceleration.y) * dt;
                // Drag
                rb.velocity.x *= 1 / (1 + rb.drag * dt);
                rb.velocity.y *= 1 / (1 + rb.drag * dt);
                // Angular
                if (!rb.fixedRotation) {
                    transform.rotation += rb.angularVelocity * dt;
                    rb.angularVelocity *= 1 / (1 + rb.angularDrag * dt);
                }
            }
            // Move
            transform.position.x += rb.velocity.x * dt;
            transform.position.y += rb.velocity.y * dt;
        }
    }
    /* ── Collision Detection ────────────────────────────────────── */
    detectCollisions(world) {
        const collisions = [];
        const bodies = world.query('Transform', 'Collider2D');
        // Broad phase: spatial hash insert
        this.hash.clear();
        for (const id of bodies) {
            const t = world.getComponent(id, 'Transform');
            const c = world.getComponent(id, 'Collider2D');
            const hw = c.shape === 'circle' ? c.radius : c.width / 2;
            const hh = c.shape === 'circle' ? c.radius : c.height / 2;
            this.hash.insert(id, t.position.x + c.offset.x, t.position.y + c.offset.y, hw, hh);
        }
        // Narrow phase
        const checked = new Set();
        for (const idA of bodies) {
            const tA = world.getComponent(idA, 'Transform');
            const cA = world.getComponent(idA, 'Collider2D');
            const hwA = cA.shape === 'circle' ? cA.radius : cA.width / 2;
            const hhA = cA.shape === 'circle' ? cA.radius : cA.height / 2;
            const nearby = this.hash.query(tA.position.x + cA.offset.x, tA.position.y + cA.offset.y, hwA, hhA);
            for (const idB of nearby) {
                if (idA >= idB)
                    continue; // avoid duplicates and self
                const pairKey = `${idA}:${idB}`;
                if (checked.has(pairKey))
                    continue;
                checked.add(pairKey);
                // Layer/mask check
                if (!(cA.layer & world.getComponent(idB, 'Collider2D').mask))
                    continue;
                if (!(world.getComponent(idB, 'Collider2D').layer & cA.mask))
                    continue;
                const tB = world.getComponent(idB, 'Transform');
                const cB = world.getComponent(idB, 'Collider2D');
                const collision = this.narrowPhase(tA, cA, tB, cB);
                if (collision) {
                    collisions.push({
                        entityA: idA,
                        entityB: idB,
                        normal: collision.normal,
                        depth: collision.depth,
                        point: collision.point,
                    });
                }
            }
        }
        return collisions;
    }
    narrowPhase(tA, cA, tB, cB) {
        const posA = { x: tA.position.x + cA.offset.x, y: tA.position.y + cA.offset.y };
        const posB = { x: tB.position.x + cB.offset.x, y: tB.position.y + cB.offset.y };
        // Circle vs Circle
        if (cA.shape === 'circle' && cB.shape === 'circle') {
            return circleVsCircle(posA, cA.radius, posB, cB.radius);
        }
        // AABB vs AABB
        if (cA.shape === 'aabb' && cB.shape === 'aabb') {
            return aabbVsAabb(posA, cA.width, cA.height, posB, cB.width, cB.height);
        }
        // Circle vs AABB
        if (cA.shape === 'circle' && cB.shape === 'aabb') {
            return circleVsAabb(posA, cA.radius, posB, cB.width, cB.height);
        }
        if (cA.shape === 'aabb' && cB.shape === 'circle') {
            const result = circleVsAabb(posB, cB.radius, posA, cA.width, cA.height);
            if (result)
                result.normal = v2.negate(result.normal);
            return result;
        }
        return null;
    }
    /* ── Resolution ─────────────────────────────────────────────── */
    resolveCollisions(world, collisions) {
        for (const col of collisions) {
            const cA = world.getComponent(col.entityA, 'Collider2D');
            const cB = world.getComponent(col.entityB, 'Collider2D');
            if (!cA || !cB)
                continue;
            // Triggers don't resolve
            if (cA.isTrigger || cB.isTrigger)
                continue;
            const tA = world.getComponent(col.entityA, 'Transform');
            const tB = world.getComponent(col.entityB, 'Transform');
            const rbA = world.getComponent(col.entityA, 'RigidBody2D');
            const rbB = world.getComponent(col.entityB, 'RigidBody2D');
            if (!tA || !tB)
                continue;
            const aStatic = !rbA || rbA.bodyType === 'static';
            const bStatic = !rbB || rbB.bodyType === 'static';
            // Positional correction (push apart)
            if (aStatic && !bStatic) {
                tB.position.x += col.normal.x * col.depth;
                tB.position.y += col.normal.y * col.depth;
            }
            else if (!aStatic && bStatic) {
                tA.position.x -= col.normal.x * col.depth;
                tA.position.y -= col.normal.y * col.depth;
            }
            else if (!aStatic && !bStatic) {
                tA.position.x -= col.normal.x * col.depth * 0.5;
                tA.position.y -= col.normal.y * col.depth * 0.5;
                tB.position.x += col.normal.x * col.depth * 0.5;
                tB.position.y += col.normal.y * col.depth * 0.5;
            }
            // Velocity resolution
            if (rbA && rbB && !aStatic && !bStatic) {
                const restitution = Math.min(rbA.restitution, rbB.restitution);
                const relVel = v2.sub(rbB.velocity, rbA.velocity);
                const velAlongNormal = v2.dot(relVel, col.normal);
                if (velAlongNormal > 0)
                    continue; // separating
                const invMassA = 1 / rbA.mass;
                const invMassB = 1 / rbB.mass;
                const j = (-(1 + restitution) * velAlongNormal) / (invMassA + invMassB);
                rbA.velocity.x -= j * invMassA * col.normal.x;
                rbA.velocity.y -= j * invMassA * col.normal.y;
                rbB.velocity.x += j * invMassB * col.normal.x;
                rbB.velocity.y += j * invMassB * col.normal.y;
            }
            else if (rbA && !aStatic && bStatic) {
                const restitution = rbA.restitution;
                const velAlongNormal = v2.dot(rbA.velocity, col.normal);
                if (velAlongNormal > 0)
                    continue;
                rbA.velocity.x -= (1 + restitution) * velAlongNormal * col.normal.x;
                rbA.velocity.y -= (1 + restitution) * velAlongNormal * col.normal.y;
            }
            else if (rbB && !bStatic && aStatic) {
                const restitution = rbB.restitution;
                const velAlongNormal = -v2.dot(rbB.velocity, col.normal);
                if (velAlongNormal > 0)
                    continue;
                rbB.velocity.x += (1 + restitution) * velAlongNormal * col.normal.x;
                rbB.velocity.y += (1 + restitution) * velAlongNormal * col.normal.y;
            }
        }
    }
    /* ── Events ─────────────────────────────────────────────────── */
    emitEvents(collisions) {
        const currentPairs = new Set();
        for (const col of collisions) {
            const cA = col.entityA < col.entityB
                ? `${col.entityA}:${col.entityB}`
                : `${col.entityB}:${col.entityA}`;
            currentPairs.add(cA);
            if (!this.activeCollisions.has(cA)) {
                this.events.emit('collision:enter', col);
            }
            this.events.emit('collision:stay', col);
        }
        // Emit exit events for pairs that ended
        for (const pair of this.activeCollisions) {
            if (!currentPairs.has(pair)) {
                const [a, b] = pair.split(':').map(Number);
                this.events.emit('collision:exit', { entityA: a, entityB: b });
            }
        }
        this.activeCollisions = currentPairs;
    }
    /* ── Raycast ────────────────────────────────────────────────── */
    raycast(world, origin, direction, maxDistance = Infinity, layerMask = 0xFFFF) {
        const dir = v2.normalize(direction);
        let closest = null;
        for (const id of world.query('Transform', 'Collider2D')) {
            const t = world.getComponent(id, 'Transform');
            const c = world.getComponent(id, 'Collider2D');
            if (!(c.layer & layerMask))
                continue;
            const center = { x: t.position.x + c.offset.x, y: t.position.y + c.offset.y };
            let hit = null;
            if (c.shape === 'circle') {
                hit = rayVsCircle(origin, dir, center, c.radius, maxDistance);
            }
            else if (c.shape === 'aabb') {
                hit = rayVsAABB(origin, dir, center, c.width, c.height, maxDistance);
            }
            if (hit && (!closest || hit.distance < closest.distance)) {
                closest = { entityId: id, ...hit };
            }
        }
        return closest;
    }
    /* ── Static collision test helpers (for unit tests & utilities) ── */
    static testAABBvsAABB(a, b) {
        return aabbVsAabb(a, a.width, a.height, b, b.width, b.height) !== null;
    }
    static testCircleVsCircle(a, b) {
        return circleVsCircle(a, a.radius, b, b.radius) !== null;
    }
    static testAABBvsCircle(aabb, circle) {
        return circleVsAabb(circle, circle.radius, aabb, aabb.width, aabb.height) !== null;
    }
}
/* ── PhysicsSystem (ECS wrapper) ──────────────────────────────── */
class PhysicsSystem extends System {
    constructor(engine, priority = -100) {
        super(priority);
        this.engine = engine;
    }
    fixedUpdate(world, dt) {
        this.engine.step(world, dt);
    }
}
/* ── Collision helpers ────────────────────────────────────────── */
function circleVsCircle(posA, rA, posB, rB) {
    const dx = posB.x - posA.x;
    const dy = posB.y - posA.y;
    const distSq = dx * dx + dy * dy;
    const radSum = rA + rB;
    if (distSq >= radSum * radSum)
        return null;
    const dist = Math.sqrt(distSq);
    const normal = dist > 0 ? { x: dx / dist, y: dy / dist } : { x: 1, y: 0 };
    return {
        normal,
        depth: radSum - dist,
        point: { x: posA.x + normal.x * rA, y: posA.y + normal.y * rA },
    };
}
function aabbVsAabb(posA, wA, hA, posB, wB, hB) {
    const dx = posB.x - posA.x;
    const dy = posB.y - posA.y;
    const overlapX = (wA / 2 + wB / 2) - Math.abs(dx);
    const overlapY = (hA / 2 + hB / 2) - Math.abs(dy);
    if (overlapX <= 0 || overlapY <= 0)
        return null;
    if (overlapX < overlapY) {
        const nx = dx > 0 ? 1 : -1;
        return {
            normal: { x: nx, y: 0 },
            depth: overlapX,
            point: { x: posA.x + nx * wA / 2, y: posA.y },
        };
    }
    else {
        const ny = dy > 0 ? 1 : -1;
        return {
            normal: { x: 0, y: ny },
            depth: overlapY,
            point: { x: posA.x, y: posA.y + ny * hA / 2 },
        };
    }
}
function circleVsAabb(circlePos, radius, aabbPos, aabbW, aabbH) {
    const halfW = aabbW / 2;
    const halfH = aabbH / 2;
    const closestX = Math.max(aabbPos.x - halfW, Math.min(aabbPos.x + halfW, circlePos.x));
    const closestY = Math.max(aabbPos.y - halfH, Math.min(aabbPos.y + halfH, circlePos.y));
    const dx = circlePos.x - closestX;
    const dy = circlePos.y - closestY;
    const distSq = dx * dx + dy * dy;
    if (distSq >= radius * radius)
        return null;
    const dist = Math.sqrt(distSq);
    const normal = dist > 0 ? { x: dx / dist, y: dy / dist } : { x: 0, y: -1 };
    return {
        normal,
        depth: radius - dist,
        point: { x: closestX, y: closestY },
    };
}
/* ── Raycast helpers ──────────────────────────────────────────── */
function rayVsCircle(origin, dir, center, radius, maxDist) {
    const oc = v2.sub(origin, center);
    const b = v2.dot(oc, dir);
    const c = v2.dot(oc, oc) - radius * radius;
    const discriminant = b * b - c;
    if (discriminant < 0)
        return null;
    const sqrtD = Math.sqrt(discriminant);
    let t = -b - sqrtD;
    if (t < 0)
        t = -b + sqrtD;
    if (t < 0 || t > maxDist)
        return null;
    const point = { x: origin.x + dir.x * t, y: origin.y + dir.y * t };
    const normal = v2.normalize(v2.sub(point, center));
    return { distance: t, point, normal };
}
function rayVsAABB(origin, dir, center, w, h, maxDist) {
    const hw = w / 2;
    const hh = h / 2;
    let tMin = 0;
    let tMax = maxDist;
    let normalMin = { x: 0, y: 0 };
    // X axis
    if (Math.abs(dir.x) < 1e-8) {
        if (origin.x < center.x - hw || origin.x > center.x + hw)
            return null;
    }
    else {
        let t1 = (center.x - hw - origin.x) / dir.x;
        let t2 = (center.x + hw - origin.x) / dir.x;
        let n = { x: -1, y: 0 };
        if (t1 > t2) {
            const tmp = t1;
            t1 = t2;
            t2 = tmp;
            n = { x: 1, y: 0 };
        }
        if (t1 > tMin) {
            tMin = t1;
            normalMin = n;
        }
        if (t2 < tMax)
            tMax = t2;
        if (tMin > tMax)
            return null;
    }
    // Y axis
    if (Math.abs(dir.y) < 1e-8) {
        if (origin.y < center.y - hh || origin.y > center.y + hh)
            return null;
    }
    else {
        let t1 = (center.y - hh - origin.y) / dir.y;
        let t2 = (center.y + hh - origin.y) / dir.y;
        let n = { x: 0, y: -1 };
        if (t1 > t2) {
            const tmp = t1;
            t1 = t2;
            t2 = tmp;
            n = { x: 0, y: 1 };
        }
        if (t1 > tMin) {
            tMin = t1;
            normalMin = n;
        }
        if (t2 < tMax)
            tMax = t2;
        if (tMin > tMax)
            return null;
    }
    if (tMin < 0)
        return null;
    const point = { x: origin.x + dir.x * tMin, y: origin.y + dir.y * tMin };
    return { distance: tMin, point, normal: normalMin };
}

export { PhysicsEngine2D, PhysicsSystem };
//# sourceMappingURL=PhysicsEngine2D.js.map
