/**
 * @name Application#cannonWorld
 * @type {CannonWorld}
 * @description The {@link CannonWorld} wrapper.
 */

/**
 * @name Entity#cannonBody
 * @type {CANNON#RigidBody}
 * @description The Cannon body object.
 */
 
/**
 * @class
 * @name CannonWorld
 * @description The CannonWorld is a script type instance that wraps the Cannon.js Physics Engine https://github.com/schteppe/cannon.js
 * CannonWorld is accessible via the the application object `this.app.cannonWorld` and contains basic helper functions such 
 * as teleport and raycasting. For more advanced functionality, CannonWorld#world should be used directly with their API.
 * @property {CANNON.World} The Cannon world object.
 */

/**
 * @function
 * @name CannonWorld#teleport
 * @description Teleports an object to world position and orientation
 * @param {pc.Entity} entity The entity to teleport.
 * @param {pc.Vec3} position The world space point to teleport to.
 * @param {pc.Vec3 | null} eulerAngles The world rotation in euler angles to teleport to.
 */


/**
 * @name RaycastResult
 * @property {pc.Entity} entity The entity that was hit.
 * @property {pc.Vec3} point The point at which the ray hit the entity in world space.
 * @property {pc.Vec3} normal The normal vector of the surface where the ray hit in world space.
 */

/**
 * @function
 * @name CannonWorld#raycastFirst
 * @description Raycast into the world and returns the first entity it hits.
 * @return {RaycastResult | null} Returns the raycast result.
 */

/**
 * @function
 * @name CannonWorld#raycastAll
 * @description Raycast into the world and returns all the entities it hits.
 * @return {RaycastResult[]} Returns the raycast results.
 */


/**
 * @event
 * @name Entity#collisionstart
 * @description Fired when the physics body collides with another body
 * @param {Object} event - Event from the collision where:
 *     other: pc.Entity, // The other entity that this collided with
 */

/**
 * @event
 * @name Entity#collisionend
 * @description Fired when the physics body stops touching another body
 * @param {Object} event - Event from the collision where:
 *     other: pc.Entity, // The other entity that this entity was touching
 */

/**
 * @event
 * @name Entity#triggerenter
 * @description Fired when the physics body enters a trigger
 * @param {Object} event - Event from when a trigger is entered where:
 *     other: pc.Entity, // The other entity that was involved in the event
 */

/**
 * @event
 * @name Entity#triggerenter
 * @description Fired when the physics body leaves a trigger
 * @param {Object} event - Event from when a body leaves a trigger:
 *     other: pc.Entity, // The other entity that was involved in the event
 */


var CannonWorld = pc.createScript('cannonWorld');

CannonWorld.attributes.add('fixedTimeStep', {type: 'number', default: 0.016666, title: 'Fixed Time Step'});
CannonWorld.attributes.add('iterations', {type: 'number', default: 3, title: 'Iterations'});
CannonWorld.attributes.add('gravity', {type: 'vec3', default: [0, -9.8, 0], title: 'Gravity'});
CannonWorld.attributes.add('debugMode', {type: 'boolean', title: 'Debug Mode'});

CannonWorld.BODYTYPE_STATIC = 0;
CannonWorld.BODYTYPE_DYNAMIC = 1;
CannonWorld.BODYTYPE_KINEMATIC = 2;
CannonWorld.BODYTYPE_TRIGGER = 3;

CannonWorld.EVENT_COLLISION_START = 'collisionstart';
CannonWorld.EVENT_COLLISION_END = 'collisionend';

CannonWorld.EVENT_TRIGGER_ENTER = 'triggerenter';
CannonWorld.EVENT_TRIGGER_LEAVE = 'triggerleave';


// initialize code called once per entity
CannonWorld.prototype.initialize = function() {
    if (this.app.cannonWorld) {
        console.warn('An Cannon world has already been created. Not creating a new one.');
        return;
    }
        
    this.world = new CANNON.World();
    this.world.gravity.set(this.gravity.x, this.gravity.y, this.gravity.z);
    
    this.world.addEventListener('beginContact', this._onBeginContact.bind(this));
    this.world.addEventListener('endContact', this._onEndContact.bind(this));

    this.app.cannonWorld = this;
    
    this._rigidbodies = [];
    
    // Hook into an update event that happens after script system update
    this.app.systems.on('animationUpdate', this._update, this);
    
    this.on('destroy', function () {
    this.app.systems.off('animationUpdate', this._update, this);
 
        this._rigidbodies = [];
        this.world = null;
        this.app.cannonWorld = null;
    }, this);
};


CannonWorld.prototype._update = (function() {
    var tempQuat = new pc.Quat();
    
    return function(dt) {
        this.world.step(this.fixedTimeStep, dt, this.iterations);

        // Update entity positions based on rigidBody position
        var i, pos, rot;
        for (i = 0; i < this._rigidbodies.length; ++i) {
            var pair = this._rigidbodies[i];
            if (pair.body.sleepState !== CANNON.Body.SLEEPING) {
                if (pair.body.type === CANNON.Body.STATIC) {
                    pos = pair.entity.getPosition();
                    rot = pair.entity.getRotation();
                    pair.body.position.set(pos.x, pos.y, pos.z);
                    pair.body.quaternion.set(rot.x,  rot.y,  rot.z, rot.w);
                } else {
                    pos = pair.body.position;
                    rot = pair.body.quaternion;

                    pair.entity.setPosition(pos.x, pos.y, pos.z);

                    tempQuat.set(rot.x, rot.y, rot.z, rot.w || 1);
                    pair.entity.setRotation(tempQuat);
                }
            }  
        }
    };
})();


CannonWorld.prototype._onBeginContact = (function () { 
    var collisionStartEvent = {
        other: null
    };
    
    return function(contactEvent) {
        var bodyA = contactEvent.bodyA;
        var bodyB = contactEvent.bodyB;
        var eventType = ((bodyA && bodyA.isTrigger) || (bodyB && bodyB.isTrigger)) ? CannonWorld.EVENT_TRIGGER_ENTER : CannonWorld.EVENT_COLLISION_START;

        collisionStartEvent.other = bodyB.entity;
        bodyA.entity.fire(eventType, collisionStartEvent);
        
        collisionStartEvent.other = bodyA.entity;
        bodyB.entity.fire(eventType, collisionStartEvent);
    };
})();


CannonWorld.prototype._onEndContact = (function () { 
    var collisionEndEvent = {
        other: null
    };
    
    return function(contactEvent) {
        var bodyA = contactEvent.bodyA;
        var bodyB = contactEvent.bodyB;
        var eventType = ((bodyA && bodyA.isTrigger) || (bodyB && bodyB.isTrigger)) ? CannonWorld.EVENT_TRIGGER_LEAVE : CannonWorld.EVENT_COLLISION_END;

        collisionEndEvent.other = bodyB && bodyB.entity;
        if (bodyA) {
            bodyA.entity.fire(eventType, collisionEndEvent);
        }
        
        collisionEndEvent.other = bodyA && bodyA.entity;
        if (bodyB) {
            bodyB.entity.fire(eventType, collisionEndEvent);
        }
    };
})();


CannonWorld.prototype._addRigidbody = function(entity) {
    this.world.addBody(entity.cannonBody);
    this._rigidbodies.push({ body: entity.cannonBody, entity: entity });
};


CannonWorld.prototype._removeRigidbody = function(entity) {    
    var i;
    for (i = 0; i < this._rigidbodies.length; ++i) {
        var pair = this._rigidbodies[i];
        if (entity.cannonBody === pair.body) {
            this._rigidbodies.splice(i, 1);    
            break;
        }
    }
    
    this.world.removeBody(entity.cannonBody);
};


CannonWorld.prototype._addShape = function (scriptType, shape, mass, rigidbodyType) {
    var pos = scriptType.entity.getPosition();
    var rot = scriptType.entity.getRotation();
    
    if (rigidbodyType === CannonWorld.BODYTYPE_STATIC || rigidbodyType === CannonWorld.BODYTYPE_TRIGGER) {
        mass = 0;
    } 
    
    scriptType.body = new CANNON.Body({
        mass: mass,
        position: new CANNON.Vec3(pos.x, pos.y, pos.z),
        quaternion: new CANNON.Quaternion(rot.x, rot.y, rot.z, rot.w),
        shape: shape,
        isTrigger: rigidbodyType === CannonWorld.BODYTYPE_TRIGGER
    });
    
    if (rigidbodyType === CannonWorld.BODYTYPE_KINEMATIC) {
        scriptType.body.type = CANNON.Body.KINEMATIC;
    }
    
    // Two way reference between the Cannon body and the Entity
    scriptType.entity.cannonBody = scriptType.body;   
    scriptType.body.entity = scriptType.entity;
    
    this._addRigidbody(scriptType.entity);
    
    scriptType.on('destroy', function() {
        this.app.cannonWorld._removeRigidbody(this.entity);
        this.entity.cannonBody = null;
        this.body = null;
    }, scriptType);
    
    scriptType.on('state', function (enabled) {
        if (enabled) {
            this.app.cannonWorld._addRigidbody(this.entity);
        } else {
            this.app.cannonWorld._removeRigidbody(this.entity);
        }
    }, scriptType);
};


CannonWorld.prototype.teleport = (function () {    
    return function (entity, position, rotation) {
        entity.cannonBody.position.set(position.x, position.y, position.z);

        if (rotation) {
            entity.cannonBody.quaternion.set(rotation.x,  rotation.y,  rotation.z, rotation.w);
        }

        entity.cannonBody.velocity.set(0,0,0);
        entity.cannonBody.angularVelocity.set(0,0,0);
    };
}());


CannonWorld.prototype.raycastFirst = (function () {
    if (window.CANNON) {
        var cannonResult = new CANNON.RaycastResult();
        var result = {
            entity: null,
            normal: new pc.Vec3(),
            point: new pc.Vec3()	
        };
        var cannonPosFrom = new CANNON.Vec3();
        var cannonPosTo = new CANNON.Vec3(); 
        var cannonOptions = {};
    }
    
    return function (from, to) {
        cannonPosFrom.set(from.x, from.y, from.z);
        cannonPosTo.set(to.x, to.y, to.z);

        this.world.raycastClosest(cannonPosFrom, cannonPosTo, cannonOptions, cannonResult);
        
        if (cannonResult.hasHit) {
            result.entity = cannonResult.body.entity;
            result.normal.set(cannonResult.hitNormalWorld.x, cannonResult.hitNormalWorld.y, cannonResult.hitNormalWorld.x);
            result.point.set(cannonResult.hitPointWorld.x, cannonResult.hitPointWorld.y, cannonResult.hitPointWorld.z);
            
            return result;
        }
        
        return null;
    };
}());


CannonWorld.prototype.raycastAll = (function () {
    if (window.CANNON) {
        var results = [];
        var cannonPosFrom = new CANNON.Vec3();
        var cannonPosTo = new CANNON.Vec3(); 
        var cannonOptions = {};
    }
    
    var cannonCallback = function(cannonResult) {
        var result = {};
        result.entity = cannonResult.body.entity;
        result.normal = new pc.Vec3(cannonResult.hitNormalWorld.x, cannonResult.hitNormalWorld.y, cannonResult.hitNormalWorld.x);
        result.point = new pc.Vec3(cannonResult.hitPointWorld.x, cannonResult.hitPointWorld.y, cannonResult.hitPointWorld.z);
        
        results.push(result);
    };
    
    return function (from, to) {
        results.length = 0;
        cannonPosFrom.set(from.x, from.y, from.z);
        cannonPosTo.set(to.x, to.y, to.z);

        this.world.raycastAll(cannonPosFrom, cannonPosTo, cannonOptions, cannonCallback);
        
        return results;
    };
}());



// Script type helper
///////
var CannonHelper = {
    addCannonBodyDefaults: function (scriptType) {
        scriptType.attributes.add('mass', {type: 'number', default: 1, title: 'Mass'});
        scriptType.attributes.add('type', 
                                  {
            type: 'number',
            default: CannonWorld.BODYTYPE_DYNAMIC,
            title: 'Type',
            enum: [
                { 'Static': CannonWorld.BODYTYPE_STATIC },
                { 'Dynamic': CannonWorld.BODYTYPE_DYNAMIC },
                { 'Kinematic': CannonWorld.BODYTYPE_KINEMATIC },
                { 'Trigger': CannonWorld.BODYTYPE_TRIGGER }
            ]
        });
    }
};

// Box
////////
var CannonBox = pc.createScript('cannonBox');
CannonHelper.addCannonBodyDefaults(CannonBox);
CannonBox.attributes.add('halfExtents', {type: 'vec3', default: [0.5, 0.5, 0.5], title: 'Half Extents'});

// initialize code called once per entity
CannonBox.prototype.postInitialize = function() {
    if (this.app.cannonWorld) {
        var shape = new CANNON.Box(new CANNON.Vec3(this.halfExtents.x, this.halfExtents.y, this.halfExtents.z));
        this.app.cannonWorld._addShape(this, shape, this.mass, this.type);
    } 
};


// Sphere
////////
var CannonSphere = pc.createScript('cannonSphere');
CannonHelper.addCannonBodyDefaults(CannonSphere);
CannonSphere.attributes.add('radius', {type: 'number', default: 0.5, title: 'Radius'});

// initialize code called once per entity
CannonSphere.prototype.postInitialize = function() {
    if (this.app.cannonWorld) {
        var shape = new CANNON.Sphere(this.radius);
        this.app.cannonWorld._addShape(this, shape, this.mass, this.type);
    } 
};


// Cyclinder
////////
var CannonCylinder = pc.createScript('cannonCylinder');
CannonHelper.addCannonBodyDefaults(CannonCylinder);
CannonCylinder.attributes.add('radius', {type: 'number', default: 0.5, title: 'Radius'});
CannonCylinder.attributes.add('height', {type: 'number', default: 1, title: 'Height'});
CannonCylinder.attributes.add('segmentCount', {type: 'number', default: 8, title: 'Segment Count'});

// initialize code called once per entity
CannonCylinder.prototype.postInitialize = function() {
    if (this.app.cannonWorld) {
        var shape = new CANNON.Cylinder(this.radius, this.radius, this.height, this.segmentCount);
        this.app.cannonWorld._addShape(this, shape, this.mass, this.type);
    } 
};