import { SystemPriority } from '../EntityComponentSystem';
import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent';
import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent';
import { System, SystemType } from '../EntityComponentSystem/System';
import { CollisionEndEvent, CollisionStartEvent, ContactEndEvent, ContactStartEvent } from '../Events';
import { SolverStrategy } from './SolverStrategy';
import { ArcadeSolver } from './Solver/ArcadeSolver';
import { RealisticSolver } from './Solver/RealisticSolver';
import { ColliderComponent } from './ColliderComponent';
import { CompositeCollider } from './Colliders/CompositeCollider';
import { Side } from '../Collision/Side';
export class CollisionSystem extends System {
    get _processor() {
        return this._physics.collisionProcessor;
    }
    constructor(world, _physics) {
        super();
        this._physics = _physics;
        this.systemType = SystemType.Update;
        this.priority = SystemPriority.Higher;
        this._configDirty = false;
        this._lastFrameContacts = new Map();
        this._currentFrameContacts = new Map();
        this._arcadeSolver = new ArcadeSolver(_physics.config.arcade);
        this._realisticSolver = new RealisticSolver(_physics.config.realistic);
        this._physics.$configUpdate.subscribe(() => (this._configDirty = true));
        this._trackCollider = (c) => this._processor.track(c);
        this._untrackCollider = (c) => this._processor.untrack(c);
        this.query = world.query([TransformComponent, MotionComponent, ColliderComponent]);
        this.query.entityAdded$.subscribe((e) => {
            const colliderComponent = e.get(ColliderComponent);
            colliderComponent.$colliderAdded.subscribe(this._trackCollider);
            colliderComponent.$colliderRemoved.subscribe(this._untrackCollider);
            const collider = colliderComponent.get();
            if (collider) {
                this._processor.track(collider);
            }
        });
        this.query.entityRemoved$.subscribe((e) => {
            const colliderComponent = e.get(ColliderComponent);
            const collider = colliderComponent.get();
            if (colliderComponent && collider) {
                this._processor.untrack(collider);
            }
        });
    }
    initialize(world, scene) {
        this._engine = scene.engine;
    }
    update(elapsedMs) {
        var _a, _b, _c, _d;
        if (!this._physics.config.enabled) {
            return;
        }
        // Collect up all the colliders and update them
        let colliders = [];
        for (const entity of this.query.entities) {
            const colliderComp = entity.get(ColliderComponent);
            const collider = colliderComp === null || colliderComp === void 0 ? void 0 : colliderComp.get();
            if (colliderComp && ((_a = colliderComp.owner) === null || _a === void 0 ? void 0 : _a.active) && collider) {
                colliderComp.update();
                if (collider instanceof CompositeCollider) {
                    const compositeColliders = collider.getColliders();
                    if (!collider.compositeStrategy) {
                        collider.compositeStrategy = this._physics.config.colliders.compositeStrategy;
                    }
                    colliders = colliders.concat(compositeColliders);
                }
                else {
                    colliders.push(collider);
                }
            }
        }
        // Update the spatial partitioning data structures
        // TODO if collider invalid it will break the processor
        // TODO rename "update" to something more specific
        this._processor.update(colliders);
        // Run broadphase on all colliders and locates potential collisions
        const pairs = this._processor.broadphase(colliders, elapsedMs);
        this._currentFrameContacts.clear();
        // Given possible pairs find actual contacts
        let contacts = this._processor.narrowphase(pairs, (_d = (_c = (_b = this._engine) === null || _b === void 0 ? void 0 : _b.debug) === null || _c === void 0 ? void 0 : _c.stats) === null || _d === void 0 ? void 0 : _d.currFrame);
        const solver = this.getSolver();
        // Solve, this resolves the position/velocity so entities aren't overlapping
        contacts = solver.solve(contacts);
        // Record contacts for start/end
        for (const contact of contacts) {
            if (contact.isCanceled()) {
                continue;
            }
            // Process composite ids, things with the same composite id are treated as the same collider for start/end
            const index = contact.id.indexOf('|');
            if (index > 0) {
                const compositeId = contact.id.substring(index + 1);
                this._currentFrameContacts.set(compositeId, contact);
            }
            else {
                this._currentFrameContacts.set(contact.id, contact);
            }
        }
        // Emit contact start/end events
        this.runContactStartEnd();
        // reset the last frame cache
        this._lastFrameContacts.clear();
        // Keep track of collisions contacts that have started or ended
        this._lastFrameContacts = new Map(this._currentFrameContacts);
        // Process deferred collider removals
        for (const entity of this.query.entities) {
            const collider = entity.get(ColliderComponent);
            if (collider) {
                collider.processColliderRemoval();
            }
        }
    }
    getSolver() {
        if (this._configDirty) {
            this._configDirty = false;
            this._arcadeSolver = new ArcadeSolver(this._physics.config.arcade);
            this._realisticSolver = new RealisticSolver(this._physics.config.realistic);
        }
        return this._physics.config.solver === SolverStrategy.Realistic ? this._realisticSolver : this._arcadeSolver;
    }
    debug(ex) {
        this._processor.debug(ex);
    }
    runContactStartEnd() {
        // If composite colliders are 'together' collisions may have a duplicate id because we want to treat those as a singular start/end
        for (const [id, c] of this._currentFrameContacts) {
            // find all new contacts
            if (!this._lastFrameContacts.has(id)) {
                const colliderA = c.colliderA;
                const colliderB = c.colliderB;
                const side = Side.fromDirection(c.mtv);
                const opposite = Side.getOpposite(side);
                colliderA.events.emit('collisionstart', new CollisionStartEvent(colliderA, colliderB, side, c));
                colliderA.events.emit('contactstart', new ContactStartEvent(colliderA, colliderB, side, c));
                colliderB.events.emit('collisionstart', new CollisionStartEvent(colliderB, colliderA, opposite, c));
                colliderB.events.emit('contactstart', new ContactStartEvent(colliderB, colliderA, opposite, c));
            }
        }
        // find all contacts that have ceased
        for (const [id, c] of this._lastFrameContacts) {
            if (!this._currentFrameContacts.has(id)) {
                const colliderA = c.colliderA;
                const colliderB = c.colliderB;
                const side = Side.fromDirection(c.mtv);
                const opposite = Side.getOpposite(side);
                colliderA.events.emit('collisionend', new CollisionEndEvent(colliderA, colliderB, side, c));
                colliderA.events.emit('contactend', new ContactEndEvent(colliderA, colliderB, side, c));
                colliderB.events.emit('collisionend', new CollisionEndEvent(colliderB, colliderA, opposite, c));
                colliderB.events.emit('contactend', new ContactEndEvent(colliderB, colliderA, opposite, c));
            }
        }
    }
}
//# sourceMappingURL=CollisionSystem.js.map