components/HandlerCollision.js

import {arrayCache, greenSplice, union} from '../utils/array.js';
import AABB from '../AABB.js';
import CollisionData from '../CollisionData.js';
import CollisionDataContainer from '../CollisionDataContainer.js';
import Data from '../Data.js';
import DataMap from '../DataMap.js';
import Vector from '../Vector.js';
import createComponentClass from '../factory.js';

const
    BIT_16 = 0xffff,
    combine = function (x, y) {
        return (x << 16) | (y & BIT_16);
    },
    getBucketId = function (x, y, bits) {
        return combine(x >> bits, y >> bits);
    },
    triggerMessage = {
        entity: null,
        target: null,
        type: null,
        x: 0,
        y: 0,
        hitType: null,
        myType: null
    },
    groupSortBySize = function (a, b) {
        return a.collisionGroup.getAllEntities() - b.collisionGroup.getAllEntities();
    };

export default createComponentClass(/** @lends platypus.components.HandlerCollision.prototype */{
    id: 'HandlerCollision',
    
    properties: {
        /**
         *
         */
        gridBits: 8
    },
    
    /**
     * This component checks for collisions between entities which typically have either a [CollisionTiles](platypus.components.CollisionTiles.html) component for tile maps or a [CollisionBasic](platypus.components.CollisionBasic.html) component for other entities. It uses `EntityContainer` component messages if triggered to add to its collision list and also listens for explicit add/remove messages (useful in the absence of an `EntityContainer` component).
     *
     * @memberof platypus.components
     * @uses platypus.Component
     * @constructs
     * @listens platypus.Entity#add-collision-entity
     * @listens platypus.Entity#check-collision-group
     * @listens platypus.Entity#child-entity-added
     * @listens platypus.Entity#child-entity-removed
     * @listens platypus.Entity#child-entity-updated
     * @listens platypus.Entity#remove-collision-entity
     * @fires platypus.Entity#hit-by-*
     * @fires platypus.Entity#relocate-entity
     */
    initialize: function () {
        this.againstGrid = Data.setUp();
        
        this.solidEntitiesLive = arrayCache.setUp();
        this.softEntitiesLive = arrayCache.setUp();
        this.allEntitiesLive = arrayCache.setUp();
        this.groupsLive = arrayCache.setUp();
        this.nonColliders = arrayCache.setUp();
        
        this.terrain = null;
        this.owner.previousX = this.owner.previousX || this.owner.x;
        this.owner.previousY = this.owner.previousY || this.owner.y;
        
        this.relocationMessage = Data.setUp(
            "position", Vector.setUp(),
            "relative", false
        );
    },
    
    events: {
        "child-entity-added": function (entity) {
            if (!entity.collideOff) {
                this.addCollisionEntity(entity);
            }
        },
        
        "add-collision-entity": function (entity) {
            this.addCollisionEntity(entity);
        },
        
        "child-entity-removed": function (entity) {
            this.removeCollisionEntity(entity);
        },
        
        "remove-collision-entity": function (entity) {
            this.removeCollisionEntity(entity);
        },
        
        "child-entity-updated": function (entity) {
            this.removeCollisionEntity(entity);
            this.addCollisionEntity(entity);
        },
        
        "check-collision-group": function (resp) {
            this.checkCamera(resp.camera, resp.entities);
            this.checkGroupCollisions();
            this.checkSolidCollisions();
            this.resolveNonCollisions();
            this.checkSoftCollisions(resp);
        }
    },
    
    methods: {
        mapDown: function (aabb2) {
            var aabb1 = AABB.setUp(),
                gb = this.gridBits;
            
            return aabb1.setBounds(aabb2.left >> gb, aabb2.top >> gb, aabb2.right >> gb, aabb2.bottom >> gb);
        },
        
        getAgainstGrid: function (entity, sweep, types) {
            var aabb = this.mapDown(sweep),
                data = Data.setUp(),
                list = null,
                thisAgainstGrid = this.againstGrid,
                x = 0,
                y = 0;
            
            if (entity && sweep.equals(entity.againstAABB)) {
                return this.getEntityAgainstGrid(entity, types);
            }

            for (x = aabb.left; x <= aabb.right; x++) {
                for (y = aabb.top; y <= aabb.bottom; y++) {
                    list = thisAgainstGrid[combine(x, y)];
                    if (list) {
                        this.mergeAGCell(list, data, types);
                    }
                }
            }
            
            aabb.recycle();
            return data;
        },
        
        getEntityAgainstGrid: function (entity, types) {
            const
                ag = entity.againstGrid,
                data = Data.setUp();
            let i = ag.length;

            while (i--) {
                this.mergeAGCell(ag[i], data, types);
            }
            
            return data;
        },

        mergeAGCell: function (list, data, types) {
            let i = types.length;

            while (i--) {
                const
                    type = types[i],
                    arr = list.get(type);

                if (arr && arr.length) {
                    const tList = data[type];
                    if (!tList) {
                        data[type] = union(arrayCache.setUp(), arr);
                    } else {
                        union(tList, arr);
                    }
                }
            }
        },
        
        removeAgainst: function (entity) {
            var ag = entity.againstGrid,
                types = entity.collisionTypes,
                arr = null,
                i = ag.length,
                j = 0,
                id = 0,
                len = types.length,
                list = null;
                
            while (i--) {
                list = ag[i];
                j = len;
                while (j--) {
                    arr = list.get(types[j]);
                    if (arr) {
                        id = arr.indexOf(entity);
                        if (id >= 0) {
                            greenSplice(arr, id);
                        }
                    }
                }
            }
            ag.length = 0;
        },
        
        updateAgainst: function (entity) {
            var arr = null,
                i = 0,
                type = '',
                types = entity.collisionTypes,
                aabb = this.mapDown(entity.getAABB()),
                ag = entity.againstGrid,
                id = 0,
                list = null,
                thisAgainstGrid = this.againstGrid,
                x = 0,
                y = 0;
            
            if (!aabb.equals(entity.againstAABB)) {
                entity.againstAABB.set(aabb);
                this.removeAgainst(entity);

                for (x = aabb.left; x <= aabb.right; x++) {
                    for (y = aabb.top; y <= aabb.bottom; y++) {
                        id = combine(x, y);
                        list = thisAgainstGrid[id];
                        if (!list) {
                            list = thisAgainstGrid[id] = DataMap.setUp();
                        }
                        i = types.length;
                        while (i--) {
                            type = types[i];
                            arr = list.get(type);
                            if (!arr) {
                                arr = list.set(type, arrayCache.setUp());
                            }
                            arr.push(entity);
                        }
                        ag.push(list);
                    }
                }
            }
            
            aabb.recycle();
        },
        
        addCollisionEntity: function (entity) {
            if (entity.getTileShapes) { // Has a CollisionTiles component
                this.terrain = entity;
            } else if (entity.collisionTypes && !entity.againstGrid) {
                entity.againstGrid = arrayCache.setUp();
                entity.againstAABB = AABB.setUp();
                this.updateAgainst(entity);
            }
        },

        removeCollisionEntity: function (entity) {
            if (entity.againstGrid) {
                this.removeAgainst(entity);
                arrayCache.recycle(entity.againstGrid);
                entity.againstGrid = null;
                entity.againstAABB.recycle();
                entity.againstAABB = null;
            }
        },
        
        checkCamera: function (camera, all) {
            var i        = all.length,
                j        = 0,
                allLive  = this.allEntitiesLive,
                softs    = this.softEntitiesLive,
                solids   = this.solidEntitiesLive,
                nons     = this.nonColliders,
                groups   = this.groupsLive,
                entity        = null,
                types = null,
                collides = false;
            
            allLive.length = 0;
            solids.length = 0;
            softs.length = 0;
            nons.length = 0;
            groups.length = 0;

            while (i--) {
                collides = false;
                entity = all[i];
                types = entity.collisionTypes;
                if (!entity.immobile && types && types.length) {
                    allLive.push(entity);

                    if (entity !== this.owner) {
                        j = types.length;
                        while (j--) {
                            if (entity.solidCollisionMap.get(types[j]).length) {
                                solids.push(entity);
                                collides = true;
                                break;
                            }
                        }
                    }
                    j = types.length;
                    while (j--) {
                        if (entity.softCollisionMap.get(types[j]).length) {
                            softs.push(entity);
                            break;
                        }
                    }

                    if (!collides) {
                        nons.push(entity);
                    }

                    if (entity.collisionGroup) {
                        groups.push(entity);
                    }
                }
            }
            
            groups.sort(groupSortBySize);
        },
        
        resolveNonCollisions: function () {
            var entity = null,
                msg    = this.relocationMessage,
                nons   = this.nonColliders,
                i      = nons.length;
            
            msg.relative = false;
            while (i--) {
                entity = nons[i];
                if ((entity.position.x !== entity.previousPosition.x) || (entity.position.y !== entity.previousPosition.y)) {
                    msg.position.setVector(entity.position);
                    entity.triggerEvent('relocate-entity', msg);
                    this.updateAgainst(entity);
                }
            }
        },
        
        checkGroupCollisions: (function () {
            /**
             * When an entity collides with an entity of a listed collision-type, this message is triggered on the entity. * is the other entity's collision-type.
             *
             * @event platypus.Entity#hit-by-*
             * @param collision {Object}
             * @param collision.entity {Entity} The entity with which the collision occurred.
             * @param collision.target {Entity} The entity that's receiving the collision event.
             * @param collision.type {String} The collision type of the other entity.
             * @param collision.shape {CollisionShape} This is the shape of the other entity that caused the collision.
             * @param collision.x {number} Returns -1, 0, or 1 indicating on which side of this entity the collision occurred: left, neither, or right respectively.
             * @param collision.y {number} Returns -1, 0, or 1 indicating on which side of this entity the collision occurred: top, neither, or bottom respectively.
             */
            var triggerCollisionMessages = function (entity, otherEntity, thisType, thatType, x, y, hitType, vector) {
                var msg = triggerMessage;
                
                msg.entity    = otherEntity;
                msg.target    = entity;
                msg.myType    = thisType;
                msg.type      = thatType;
                msg.x         = x;
                msg.y         = y;
                msg.direction = vector;
                msg.hitType   = hitType;
                entity.triggerEvent('hit-by-' + thatType, msg);
                
                if (otherEntity) {
                    msg.entity    = entity;
                    msg.target    = otherEntity;
                    msg.type      = thisType;
                    msg.myType    = thatType;
                    msg.x         = -x;
                    msg.y         = -y;
                    msg.direction = vector.getInverse();
                    msg.hitType   = hitType;
                    otherEntity.triggerEvent('hit-by-' + thisType, msg);
                    
                    msg.direction.recycle();
                }
            };

            return function () {
                var i           = 0,
                    entities    = this.groupsLive,
                    x           = entities.length,
                    entity      = null,
                    list        = null,
                    messageData = null,
                    entityCDC   = null;
                
                while (x--) {
                    entity = entities[x];
                    if (entity.collisionGroup.getSize() > 1) {
                        entityCDC = this.checkSolidEntityCollision(entity, entity.collisionGroup);
                        
                        list = entityCDC.xData;
                        i = list.length;
                        while (i--) {
                            messageData = list[i];
                            triggerCollisionMessages(messageData.thisShape.owner, messageData.thatShape.owner, messageData.thisShape.collisionType, messageData.thatShape.collisionType, messageData.direction, 0, 'solid', messageData.vector);
                        }
                        
                        list = entityCDC.yData;
                        i = list.length;
                        while (i--) {
                            messageData = list[i];
                            triggerCollisionMessages(messageData.thisShape.owner, messageData.thatShape.owner, messageData.thisShape.collisionType, messageData.thatShape.collisionType, 0, messageData.direction, 'solid', messageData.vector);
                        }
                        
                        entityCDC.recycle();
                    }
                }
            };
        }()),
        
        checkSolidCollisions: (function () {
            var triggerCollisionMessages = function (entity, otherEntity, thisType, thatType, x, y, hitType, vector) {
                var msg = triggerMessage;
                
                msg.entity    = otherEntity;
                msg.target    = entity;
                msg.myType    = thisType;
                msg.type      = thatType;
                msg.x         = x;
                msg.y         = y;
                msg.direction = vector;
                msg.hitType   = hitType;
                entity.triggerEvent('hit-by-' + thatType, msg);
                
                if (otherEntity) {
                    msg.entity    = entity;
                    msg.target    = otherEntity;
                    msg.type      = thisType;
                    msg.myType    = thatType;
                    msg.x         = -x;
                    msg.y         = -y;
                    msg.direction = vector.getInverse();
                    msg.hitType   = hitType;
                    otherEntity.triggerEvent('hit-by-' + thisType, msg);
                    
                    msg.direction.recycle();
                }
            };

            return function () {
                var i           = 0,
                    entities    = this.solidEntitiesLive,
                    x           = entities.length,
                    entity      = null,
                    list        = null,
                    messageData = null,
                    entityCDC   = null,
                    trigger = triggerCollisionMessages;
                
                while (x--) {
                    entity = entities[x];
                    entityCDC = this.checkSolidEntityCollision(entity, entity);
                    
                    list = entityCDC.xData;
                    i = list.length;
                    while (i--) {
                        messageData = list[i];
                        trigger(messageData.thisShape.owner, messageData.thatShape.owner, messageData.thisShape.collisionType, messageData.thatShape.collisionType, messageData.direction, 0, 'solid', messageData.vector);
                    }
                    
                    list = entityCDC.yData;
                    i = list.length;
                    while (i--) {
                        messageData = list[i];
                        trigger(messageData.thisShape.owner, messageData.thatShape.owner, messageData.thisShape.collisionType, messageData.thatShape.collisionType, 0, messageData.direction, 'solid', messageData.vector);
                    }
                    
                    entityCDC.recycle();
                }
            };
        }()),
        
        checkSolidEntityCollision: function (ent, entityOrGroup) {
            var collisionDataCollection = CollisionDataContainer.setUp(),
                step              = 0,
                finalMovementInfo = null,
                aabb              = null,
                pX                = ent.previousX,
                pY                = ent.previousY,
                dX                = ent.x - pX,
                dY                = ent.y - pY,
                sW                = Infinity,
                sH                = Infinity,
                collisionTypes    = entityOrGroup.getCollisionTypes(),
                i                 = 0,
                ignoredEntities   = false,
                min               = null;
            
            if (entityOrGroup.getSolidEntities) {
                ignoredEntities = entityOrGroup.getSolidEntities();
            }
            
            finalMovementInfo = Vector.setUp(ent.position);

            if (dX || dY || ent.collisionDirty) {
                
                if (ent.bullet) {
                    min = Math.min;
                    
                    i = collisionTypes.length;
                    while (i--) {
                        aabb = entityOrGroup.getAABB(collisionTypes[i]);
                        sW = min(sW, aabb.width);
                        sH = min(sH, aabb.height);
                    }

                    //Stepping to catch really fast entities - this is not perfect, but should prevent the majority of fallthrough cases.
                    step = Math.ceil(Math.max(Math.abs(dX) / sW, Math.abs(dY) / sH));
                    step = min(step, 100); //Prevent memory overflow if things move exponentially far.
                    dX   = dX / step;
                    dY   = dY / step;

                    while (step--) {
                        entityOrGroup.prepareCollision(ent.previousX + dX, ent.previousY + dY);

                        finalMovementInfo = this.processCollisionStep(ent, entityOrGroup, ignoredEntities, collisionDataCollection, finalMovementInfo.setVector(ent.position), dX, dY, collisionTypes);
                        
                        if ((finalMovementInfo.x === ent.previousX) && (finalMovementInfo.y === ent.previousY)) {
                            entityOrGroup.relocateEntity(finalMovementInfo, collisionDataCollection);
                            //No more movement so we bail!
                            break;
                        } else {
                            entityOrGroup.relocateEntity(finalMovementInfo, collisionDataCollection);
                        }
                    }
                } else {
                    entityOrGroup.prepareCollision(ent.previousX + dX, ent.previousY + dY);
                    finalMovementInfo = this.processCollisionStep(ent, entityOrGroup, ignoredEntities, collisionDataCollection, finalMovementInfo, dX, dY, collisionTypes);
                    entityOrGroup.relocateEntity(finalMovementInfo, collisionDataCollection);
                }

                if ((finalMovementInfo.x !== pX) || (finalMovementInfo.y !== pY)) {
                    this.updateAgainst(ent);
                }
            }
            
            finalMovementInfo.recycle();
            
            return collisionDataCollection;
        },
        
        processCollisionStep: (function () {
            var sweeper       = AABB.setUp(),
                includeEntity = function (thisEntity, aabb, otherEntity, otherAABB, ignoredEntities, sweepAABB) {
                    var i = 0;
                    
                    //Chop out all the special case entities we don't want to check against.
                    if (otherEntity === thisEntity) {
                        return false;
                    } else if (otherEntity.jumpThrough && (aabb.bottom > otherAABB.top)) {
                        return false;
                    } else if (thisEntity.jumpThrough  && (otherAABB.bottom > aabb.top)) { // This will allow platforms to hit something solid sideways if it runs into them from the side even though originally they were above the top. - DDD
                        return false;
                    } else if (ignoredEntities) {
                        i = ignoredEntities.length;
                        while (i--) {
                            if (otherEntity === ignoredEntities[i]) {
                                return false;
                            }
                        }
                    }
                    
                    return sweepAABB.collides(otherAABB);
                };

            return function (ent, entityOrGroup, ignoredEntities, collisionDataCollection, finalMovementInfo, entityDeltaX, entityDeltaY, collisionTypes) {
                var i = collisionTypes.length,
                    j = 0,
                    k = 0,
                    l = 0,
                    isIncluded = includeEntity,
                    potentialCollision       = false,
                    potentialCollidingShapes = arrayCache.setUp(),
                    pcsGroup                 = null,
                    previousAABB             = null,
                    currentAABB              = null,
                    collisionType            = null,
                    otherEntity              = null,
                    otherCollisionType       = '',
                    otherAABB                = null,
                    otherShapes              = null,
                    otherEntities            = null,
                    terrain                  = this.terrain,
                    againstGrid          = null,
                    solidCollisionMap        = entityOrGroup.getSolidCollisions(),
                    collisionSubTypes        = null,
                    sweepAABB                = sweeper;
                
//                    if (!entityOrGroup.jumpThrough || (entityDeltaY >= 0)) { //TODO: Need to extend jumpthrough to handle different directions and forward motion - DDD

                while (i--) {
                    //Sweep the full movement of each collision type
                    potentialCollidingShapes[i] = pcsGroup = arrayCache.setUp();
                    collisionType = collisionTypes[i];
                    previousAABB = entityOrGroup.getPreviousAABB(collisionType);
                    currentAABB = entityOrGroup.getAABB(collisionType);

                    sweepAABB.set(currentAABB);
                    sweepAABB.include(previousAABB);
                    
                    collisionSubTypes = solidCollisionMap.get(collisionType);
                    againstGrid = this.getAgainstGrid(ent, sweepAABB, collisionSubTypes);
                    j = collisionSubTypes.length;
                    while (j--) {
                        otherCollisionType = collisionSubTypes[j];
                        otherEntities = againstGrid[otherCollisionType];

                        if (otherEntities) {
                            k = otherEntities.length;
                            while (k--) {
                                otherEntity = otherEntities[k];
                                otherAABB = otherEntity.getAABB(otherCollisionType);

                                //Do our sweep check against the AABB of the other object and add potentially colliding shapes to our list.
                                if (isIncluded(ent, previousAABB, otherEntity, otherAABB, ignoredEntities, sweepAABB)) {
                                    otherShapes = otherEntity.getShapes(otherCollisionType);
                                    
                                    l = otherShapes.length;
                                    while (l--) {
                                        //Push the shapes on the end!
                                        pcsGroup.push(otherShapes[l]);
                                    }
                                    potentialCollision = true;
                                }
                            }
                            arrayCache.recycle(otherEntities);
                        } else if (terrain) {
                            //Do our sweep check against the tiles and add potentially colliding shapes to our list.
                            otherShapes = terrain.getTileShapes(sweepAABB, previousAABB, otherCollisionType);
                            k = otherShapes.length;
                            while (k--) {
                                //Push the shapes on the end!
                                pcsGroup.push(otherShapes[k]);
                                potentialCollision = true;
                            }
                        }
                    }
                    againstGrid.recycle();
                }

                if (potentialCollision) {
                    finalMovementInfo = this.resolveCollisionPosition(ent, entityOrGroup, finalMovementInfo, potentialCollidingShapes, collisionDataCollection, collisionTypes, entityDeltaX, entityDeltaY);
                }
                
                // Array recycling
                arrayCache.recycle(potentialCollidingShapes, 2);
                
                return finalMovementInfo;
            };
        }()),
        
        resolveCollisionPosition: function (ent, entityOrGroup, finalMovementInfo, potentialCollidingShapes, collisionDataCollection, collisionTypes, entityDeltaX, entityDeltaY) {
            var j = 0,
                cd = null;
            
            if (entityDeltaX !== 0) {
                j = collisionTypes.length;
                while (j--) {
                    //Move each collision type in X to find the min X movement
                    cd = this.findMinAxisMovement(ent, entityOrGroup, collisionTypes[j], 'x', potentialCollidingShapes[j]);
                    
                    if (!cd.occurred || !collisionDataCollection.tryToAddX(cd)) {
                        cd.recycle();
                    }
                }
            }
            
            cd = collisionDataCollection.xData[0];
            if (cd) {
                finalMovementInfo.x = ent.previousX + cd.deltaMovement * cd.direction;
            } else {
                finalMovementInfo.x = ent.x;
            }
            
            // This moves the previous position of everything so that the check in Y can begin.
            entityOrGroup.movePreviousX(finalMovementInfo.x);
            
            if (entityDeltaY !== 0) {
                j = collisionTypes.length;
                while (j--) {
                    //Move each collision type in Y to find the min Y movement
                    cd = this.findMinAxisMovement(ent, entityOrGroup, collisionTypes[j], 'y', potentialCollidingShapes[j]);
                    
                    if (!cd.occurred || !collisionDataCollection.tryToAddY(cd)) {
                        cd.recycle();
                    }
                }
            }
            
            cd = collisionDataCollection.yData[0];
            if (cd) {
                finalMovementInfo.y = ent.previousY + cd.deltaMovement * cd.direction;
            } else {
                finalMovementInfo.y = ent.y;
            }
            
            return finalMovementInfo;
        },
        
        findMinAxisMovement: function (ent, entityOrGroup, collisionType, axis, potentialCollidingShapes) {
            //Loop through my shapes of this type vs the colliding shapes and do precise collision returning the shortest movement in axis direction
            var bestCD     = CollisionData.setUp(),
                shapes     = entityOrGroup.getShapes(collisionType),
                prevShapes = entityOrGroup.getPrevShapes(collisionType),
                cd         = null,
                i          = shapes.length;
            
            while (i--) {
                cd = this.findMinShapeMovementCollision(prevShapes[i], shapes[i], axis, potentialCollidingShapes);
                
                if (cd.occurred && (!bestCD.occurred //if a collision occurred and we haven't already had a collision.
                    || (cd.deltaMovement < bestCD.deltaMovement))) { //if a collision occurred and the diff is smaller than our best diff.
                    bestCD.recycle();
                    bestCD = cd;
                } else {
                    cd.recycle();
                }
            }
            
            return bestCD;
        },
        
        /**
         * Find the earliest point at which this shape collides with one of the potential colliding shapes along this axis.
         * For example, cycles through shapes a, b, and c to find the earliest position:
         *
         *    O---->   [b]  [a]     [c]
         *
         *    Returns collision location for:
         *
         *            O[b]
         *
         */
        findMinShapeMovementCollision: (function () {
            var returnInfo = {
                    position: 0,
                    contactVector: Vector.setUp()
                },
                getMovementDistance = function (currentDistance, minimumDistance) {
                    var pow = Math.pow;
                    
                    return Math.sqrt(pow(minimumDistance, 2) - pow(currentDistance, 2));
                },
                getCorner = function (circlePos, rectanglePos, half) {
                    var diff = circlePos - rectanglePos;
                    
                    return diff - (diff / Math.abs(diff)) * half;
                },
                getOffsetForCircleVsAABBX = function (circle, rect, moving, direction, v) {
                    var newAxisPosition = 0,
                        aabb = rect.aABB,
                        hw = aabb.halfWidth,
                        x = circle.x,
                        y = circle.y;

                    if (y >= aabb.top && y <= aabb.bottom) {
                        return hw + circle.radius;
                    } else {
                        y = getCorner(y, rect.y, aabb.halfHeight); // reusing y.
                        newAxisPosition = hw + getMovementDistance(y, circle.radius);
                        if (moving === circle) {
                            v.x = -getCorner(x - direction * newAxisPosition, rect.x, hw) / 2;
                            y = -y;
                        } else {
                            v.x = getCorner(x, rect.x - direction * newAxisPosition, hw) / 2;
                        }
                        v.y = y;
                        v.normalize();
                        return newAxisPosition;
                    }
                },
                getOffsetForCircleVsAABBY = function (circle, rect, moving, direction, v) {
                    var newAxisPosition = 0,
                        aabb = rect.aABB,
                        hh = aabb.halfHeight,
                        x = circle.x,
                        y = circle.y;

                    if (x >= aabb.left && x <= aabb.right) {
                        return hh + circle.radius;
                    } else {
                        x = getCorner(x, rect.x, aabb.halfWidth); // reusing x.
                        newAxisPosition = hh + getMovementDistance(x, circle.radius);
                        if (moving === circle) {
                            x = -x;
                            v.y = -getCorner(y - direction * newAxisPosition, rect.y, hh) / 2;
                        } else {
                            v.y = getCorner(y, rect.y - direction * newAxisPosition, hh) / 2;
                        }
                        v.x = x;
                        v.normalize();
                        return newAxisPosition;
                    }
                },
                findAxisCollisionPosition = { // Decision tree for quicker access, optimized for mobile devices.
                    x: {
                        rectangle: {
                            rectangle: function (direction, thisShape, thatShape) {
                                var ri = returnInfo;

                                ri.position = thatShape.x - direction * (thatShape.aABB.halfWidth + thisShape.aABB.halfWidth);
                                ri.contactVector.setXYZ(direction, 0);

                                return ri;
                            },
                            circle: function (direction, thisShape, thatShape) {
                                var ri = returnInfo;

                                ri.position = thatShape.x - direction * getOffsetForCircleVsAABBX(thatShape, thisShape, thisShape, direction, ri.contactVector.setXYZ(direction, 0));

                                return ri;
                            }
                        },
                        circle: {
                            rectangle: function (direction, thisShape, thatShape) {
                                var ri = returnInfo;

                                ri.position = thatShape.x - direction * getOffsetForCircleVsAABBX(thisShape, thatShape, thisShape, direction, ri.contactVector.setXYZ(direction, 0));

                                return ri;
                            },
                            circle: function (direction, thisShape, thatShape) {
                                var y = thatShape.y - thisShape.y,
                                    position = thatShape.x - direction * getMovementDistance(y, thisShape.radius + thatShape.radius),
                                    ri = returnInfo;
                                    
                                ri.contactVector.setXYZ(thatShape.x - position, y).normalize();
                                ri.position = position;

                                return ri;
                            }
                        }
                    },
                    y: {
                        rectangle: {
                            rectangle: function (direction, thisShape, thatShape) {
                                var ri = returnInfo;

                                ri.position = thatShape.y - direction * (thatShape.aABB.halfHeight + thisShape.aABB.halfHeight);
                                ri.contactVector.setXYZ(0, direction);
                                
                                return ri;
                            },
                            circle: function (direction, thisShape, thatShape) {
                                var ri = returnInfo;

                                ri.position = thatShape.y - direction * getOffsetForCircleVsAABBY(thatShape, thisShape, thisShape, direction, ri.contactVector.setXYZ(0, direction));

                                return ri;
                            }
                        },
                        circle: {
                            rectangle: function (direction, thisShape, thatShape) {
                                var ri = returnInfo;

                                ri.position = thatShape.y - direction * getOffsetForCircleVsAABBY(thisShape, thatShape, thisShape, direction, ri.contactVector.setXYZ(0, direction));

                                return ri;
                            },
                            circle: function (direction, thisShape, thatShape) {
                                var x = thatShape.x - thisShape.x,
                                    position = thatShape.y - direction * getMovementDistance(x, thisShape.radius + thatShape.radius),
                                    ri = returnInfo;
                                    
                                ri.contactVector.setXYZ(x, thatShape.y - position).normalize();
                                ri.position = position;

                                return ri;
                            }
                        }
                    }
                };
            
            return function (prevShape, currentShape, axis, potentialCollidingShapes) {
                var i = 0,
                    initialPoint    = prevShape[axis],
                    goalPoint       = currentShape[axis],
                    translatedShape = prevShape,
                    direction       = ((initialPoint < goalPoint) ? 1 : -1),
                    position        = goalPoint,
                    pcShape         = null,
                    cd              = CollisionData.setUp(),
                    collisionInfo   = null,
                    finalPosition   = goalPoint,
                    findACP         = null;
                
                if (initialPoint !== goalPoint) {
                    findACP = findAxisCollisionPosition[axis][translatedShape.type];
                    
                    if (axis === 'x') {
                        translatedShape.moveX(goalPoint);
                    } else if (axis === 'y') {
                        translatedShape.moveY(goalPoint);
                    }
                    
                    i = potentialCollidingShapes.length;
                    while (i--) {
                        pcShape = potentialCollidingShapes[i];
                        position = goalPoint;
                        if (translatedShape.collides(pcShape)) {
                            collisionInfo = findACP[pcShape.type](direction, translatedShape, pcShape);
                            position = collisionInfo.position;
                            if (direction > 0) {
                                if (position < finalPosition) {
                                    if (position < initialPoint) { // Reality check: I think this is necessary due to floating point inaccuracies. - DDD
                                        position = initialPoint;
                                    }
                                    finalPosition = position;
                                    cd.set(true, direction, finalPosition, Math.abs(finalPosition - initialPoint), pcShape.aABB, currentShape, pcShape, collisionInfo.contactVector, 0);
                                }
                            } else if (position > finalPosition) {
                                if (position > initialPoint) { // Reality check: I think this is necessary due to floating point inaccuracies. - DDD
                                    position = initialPoint;
                                }
                                finalPosition = position;
                                cd.set(true, direction, finalPosition, Math.abs(finalPosition - initialPoint), pcShape.aABB, currentShape, pcShape, collisionInfo.contactVector, 0);
                            }
                        }
                    }
                }
                
                return cd;
            };
        }()),
        
        checkSoftCollisions: (function () {
            var
                trigger = function (collision) {
                    this.triggerEvent('hit-by-' + collision.type, collision);
                };
            
            return function () {
                var softs = this.softEntitiesLive,
                    entity = null,
                    i = softs.length,
                    t = trigger;
                    
                while (i--) {
                    entity = softs[i];
                    this.checkEntityForSoftCollisions(entity, t.bind(entity));
                }
            };
        }()),
        
        checkEntityForSoftCollisions: function (ent, callback) {
            var againstGrid = null,
                otherEntity = null,
                message = triggerMessage,
                i   = ent.collisionTypes.length,
                j   = 0,
                k   = 0,
                l   = 0,
                m   = 0,
                collisionType = null,
                softCollisionMap = null,
                otherEntities  = null,
                otherCollisionType = null,
                shapes = null,
                otherShapes = null,
                collisionFound = false;

            message.x = 0;
            message.y = 0;

            while (i--) {
                collisionType = ent.collisionTypes[i];
                softCollisionMap = ent.softCollisionMap.get(collisionType);
                againstGrid = this.getEntityAgainstGrid(ent, softCollisionMap);
                j = softCollisionMap.length;
                while (j--) {
                    otherCollisionType = softCollisionMap[j];
                    otherEntities = againstGrid[otherCollisionType];
                    if (otherEntities) {
                        k = otherEntities.length;
                        while (k--) {
                            otherEntity = otherEntities[k];
                            if ((otherEntity !== ent) && (ent.getAABB(collisionType).collides(otherEntity.getAABB(otherCollisionType)))) {
                                collisionFound = false;
                                shapes = ent.getShapes(collisionType);
                                otherShapes = otherEntity.getShapes(otherCollisionType);
                                l = shapes.length;
                                while (l--) {
                                    m = otherShapes.length;
                                    while (m--) {
                                        if (shapes[l].collides(otherShapes[m])) {
                                            //TML - We're only reporting the first shape we hit even though there may be multiple that we could be hitting.
                                            message.entity  = otherEntity;
                                            message.target  = ent;
                                            message.type    = otherCollisionType;
                                            message.myType  = collisionType;
                                            message.shape   = otherShapes[m];
                                            message.hitType = 'soft';
                                            
                                            callback(message);
                                            
                                            collisionFound = true;
                                            break;
                                        }
                                    }
                                    if (collisionFound) {
                                        break;
                                    }
                                }
                            }
                        }
                        arrayCache.recycle(otherEntities);
                    }
                }
                againstGrid.recycle();
            }
        },


        checkShapeForCollisions: function (shape, softCollisionMap, callback) {
            var againstGrid = null,
                otherEntity = null,
                message = triggerMessage,
                j   = 0,
                k   = 0,
                m   = 0,
                otherEntities  = null,
                otherCollisionType = null,
                otherShapes = null;

            message.x = 0;
            message.y = 0;

            againstGrid = this.getAgainstGrid(null, shape.getAABB(), softCollisionMap);
            j = softCollisionMap.length;
            while (j--) {
                otherCollisionType = softCollisionMap[j];
                otherEntities = againstGrid[otherCollisionType];
                if (otherEntities) {
                    k = otherEntities.length;
                    while (k--) {
                        otherEntity = otherEntities[k];
                        if ((shape.getAABB().collides(otherEntity.getAABB(otherCollisionType)))) {
                            otherShapes = otherEntity.getShapes(otherCollisionType);
                            m = otherShapes.length;
                            while (m--) {
                                if (shape.collides(otherShapes[m])) {
                                    //TML - We're only reporting the first shape we hit even though there may be multiple that we could be hitting.
                                    message.entity  = otherEntity;
                                    message.target  = null;
                                    message.type    = otherCollisionType;
                                    message.myType  = '';
                                    message.shape   = otherShapes[m];
                                    message.hitType = 'soft';
                                    
                                    callback(message);
                                    break;
                                }
                            }
                        }
                    }
                    arrayCache.recycle(otherEntities);
                }
            }
            againstGrid.recycle();
        },

        
        checkPointForCollisions: function (x, y, collisions, callback) {
            var gb = this.gridBits,
                againstGrid = this.againstGrid[getBucketId(x, y, gb)],
                otherEntity = null,
                message = triggerMessage,
                j   = 0,
                k   = 0,
                m   = 0,
                otherEntities  = null,
                otherCollisionType = null,
                otherShapes = null;

            message.x = 0;
            message.y = 0;

            if (!againstGrid) {
                return;
            }
            
            j = collisions.length;
            while (j--) {
                otherCollisionType = collisions[j];
                otherEntities = againstGrid.get(otherCollisionType);
                if (otherEntities) {
                    k = otherEntities.length;
                    while (k--) {
                        otherEntity = otherEntities[k];
                        if (otherEntity.getAABB(otherCollisionType).containsPoint(x, y)) {
                            otherShapes = otherEntity.getShapes(otherCollisionType);
                            m = otherShapes.length;
                            while (m--) {
                                if (otherShapes[m].containsPoint(x, y)) {
                                    //TML - We're only reporting the first shape we hit even though there may be multiple that we could be hitting.
                                    message.entity  = otherEntity;
                                    message.target  = null;
                                    message.type    = otherCollisionType;
                                    message.myType  = '';
                                    message.shape   = otherShapes[m];
                                    message.hitType = 'soft';
                                    
                                    callback(message);
                                    
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        },
        
        destroy: function () {
            var ag = this.againstGrid,
                data = null,
                key = '',
                keys = null,
                i = 0;
            
            arrayCache.recycle(this.groupsLive);
            arrayCache.recycle(this.nonColliders);
            arrayCache.recycle(this.allEntitiesLive);
            arrayCache.recycle(this.softEntitiesLive);
            arrayCache.recycle(this.solidEntitiesLive);
            this.relocationMessage.position.recycle();
            this.relocationMessage.recycle();
            
            for (key in ag) {
                if (ag.hasOwnProperty(key)) {
                    data = ag[key];
                    keys = data.keys;
                    i = keys.length;
                    while (i--) {
                        arrayCache.recycle(data.get(keys[i]));
                    }
                    data.recycle();
                }
            }
            ag.recycle();
            this.againstGrid = null;
        }
    },
    
    publicMethods: {
        /**
         * This method returns an object containing world entities.
         *
         * @method platypus.components.HandlerCollision#getWorldEntities
         * @return {Array} A list of all world collision entities.
         */
        getWorldEntities: function () {
            return this.allEntitiesLive;
        },
        
        /**
         * This method returns an entity representing the collision map of the world.
         *
         * @method platypus.components.HandlerCollision#getWorldTerrain
         * @return {Entity} - An entity describing the collision map of the world. This entity typically includes a `CollisionTiles` component.
         */
        getWorldTerrain: function () {
            return this.terrain;
        },
        
        /**
         * This method returns a list of collision objects describing soft collisions between an entity and a list of other entities.
         *
         * @method platypus.components.HandlerCollision#getEntityCollisions
         * @param entity {Entity} The entity to test against the world.
         * @return collisions {Array} This is a list of collision objects describing the soft collisions.
         */
        getEntityCollisions: function (entity) {
            var collisions = arrayCache.setUp();
            
            this.checkEntityForSoftCollisions(entity, function (collision) {
                collisions.push(Data.setUp(collision));
            });
            
            return collisions;
        },
        
        /**
         * This method returns a list of collision objects describing collisions between a shape and a list of other entities.
         *
         * @method platypus.components.HandlerCollision#getShapeCollisions
         * @param shape {CollisionShape} The shape to check for collisions.
         * @param collisionTypes {String[]} The collision types to check against.
         * @return collisions {Array} This is a list of collision objects describing the soft collisions.
         */
        getShapeCollisions: function (shape, collisionTypes) {
            var collisions = arrayCache.setUp();
            
            this.checkShapeForCollisions(shape, collisionTypes, function (collision) {
                collisions.push(Data.setUp(collision));
            });
            
            return collisions;
        },

        /**
         * This method returns a list of collision objects describing collisions between a point and a list of other entities.
         *
         * @method platypus.components.HandlerCollision#getPointCollisions
         * @param x {number} The x-axis value.
         * @param y {number} The y-axis value.
         * @param collisionTypes {String[]} The collision types to check against.
         * @return collisions {Array} This is a list of collision objects describing the soft collisions.
         */
        getPointCollisions: function (x, y, collisionTypes) {
            var collisions = arrayCache.setUp();
            
            this.checkPointForCollisions(x, y, collisionTypes, function (collision) {
                collisions.push(Data.setUp(collision));
            });
            
            return collisions;
        }
    }
});