/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

var Class = require('../../utils/Class');
var COLLIDES = require('./COLLIDES');
var GetVelocity = require('./GetVelocity');
var TYPE = require('./TYPE');
var UpdateMotion = require('./UpdateMotion');

/**
 * @callback BodyUpdateCallback
 *
 * @param {Phaser.Physics.Impact.Body} body - [description]
 */

/**
 * @typedef {object} JSONImpactBody
 * @todo Replace object types
 *
 * @property {string} name - [description]
 * @property {object} size - [description]
 * @property {object} pos - [description]
 * @property {object} vel - [description]
 * @property {object} accel - [description]
 * @property {object} friction - [description]
 * @property {object} maxVel - [description]
 * @property {number} gravityFactor - [description]
 * @property {number} bounciness - [description]
 * @property {number} minBounceVelocity - [description]
 * @property {Phaser.Physics.Impact.TYPE} type - [description]
 * @property {Phaser.Physics.Impact.TYPE} checkAgainst - [description]
 * @property {Phaser.Physics.Impact.COLLIDES} collides - [description]
 */

/**
 * @classdesc
 * An Impact.js compatible physics body.
 * This re-creates the properties you'd get on an Entity and the math needed to update them.
 *
 * @class Body
 * @memberOf Phaser.Physics.Impact
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Physics.Impact.World} world - [description]
 * @param {number} x - [description]
 * @param {number} y - [description]
 * @param {number} [sx=16] - [description]
 * @param {number} [sy=16] - [description]
 */
var Body = new Class({

    initialize:

    function Body (world, x, y, sx, sy)
    {
        if (sx === undefined) { sx = 16; }
        if (sy === undefined) { sy = sx; }

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#world
         * @type {Phaser.Physics.Impact.World}
         * @since 3.0.0
         */
        this.world = world;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#gameObject
         * @type {Phaser.GameObjects.GameObject}
         * @default null
         * @since 3.0.0
         */
        this.gameObject = null;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#enabled
         * @type {boolean}
         * @default true
         * @since 3.0.0
         */
        this.enabled = true;

        /**
         * The ImpactBody, ImpactSprite or ImpactImage object that owns this Body, if any.
         *
         * @name Phaser.Physics.Impact.Body#parent
         * @type {?(Phaser.Physics.Impact.ImpactBody|Phaser.Physics.Impact.ImpactImage|Phaser.Physics.Impact.ImpactSprite)}
         * @since 3.0.0
         */
        this.parent;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#id
         * @type {integer}
         * @since 3.0.0
         */
        this.id = world.getNextID();

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#name
         * @type {string}
         * @default ''
         * @since 3.0.0
         */
        this.name = '';

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#size
         * @type {{x: number, y: number}}
         * @since 3.0.0
         */
        this.size = { x: sx, y: sy };

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#offset
         * @type {{x: number, y: number}}
         * @since 3.0.0
         */
        this.offset = { x: 0, y: 0 };

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#pos
         * @type {{x: number, y: number}}
         * @since 3.0.0
         */
        this.pos = { x: x, y: y };

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#last
         * @type {{x: number, y: number}}
         * @since 3.0.0
         */
        this.last = { x: x, y: y };

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#vel
         * @type {{x: number, y: number}}
         * @since 3.0.0
         */
        this.vel = { x: 0, y: 0 };

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#accel
         * @type {{x: number, y: number}}
         * @since 3.0.0
         */
        this.accel = { x: 0, y: 0 };

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#friction
         * @type {{x: number, y: number}}
         * @since 3.0.0
         */
        this.friction = { x: 0, y: 0 };

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#maxVel
         * @type {{x: number, y: number}}
         * @since 3.0.0
         */
        this.maxVel = { x: world.defaults.maxVelocityX, y: world.defaults.maxVelocityY };

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#standing
         * @type {boolean}
         * @default false
         * @since 3.0.0
         */
        this.standing = false;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#gravityFactor
         * @type {number}
         * @since 3.0.0
         */
        this.gravityFactor = world.defaults.gravityFactor;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#bounciness
         * @type {number}
         * @since 3.0.0
         */
        this.bounciness = world.defaults.bounciness;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#minBounceVelocity
         * @type {number}
         * @since 3.0.0
         */
        this.minBounceVelocity = world.defaults.minBounceVelocity;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#accelGround
         * @type {number}
         * @default 0
         * @since 3.0.0
         */
        this.accelGround = 0;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#accelAir
         * @type {number}
         * @default 0
         * @since 3.0.0
         */
        this.accelAir = 0;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#jumpSpeed
         * @type {number}
         * @default 0
         * @since 3.0.0
         */
        this.jumpSpeed = 0;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#type
         * @type {Phaser.Physics.Impact.TYPE}
         * @since 3.0.0
         */
        this.type = TYPE.NONE;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#checkAgainst
         * @type {Phaser.Physics.Impact.TYPE}
         * @since 3.0.0
         */
        this.checkAgainst = TYPE.NONE;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#collides
         * @type {Phaser.Physics.Impact.COLLIDES}
         * @since 3.0.0
         */
        this.collides = COLLIDES.NEVER;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#debugShowBody
         * @type {boolean}
         * @since 3.0.0
         */
        this.debugShowBody = world.defaults.debugShowBody;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#debugShowVelocity
         * @type {boolean}
         * @since 3.0.0
         */
        this.debugShowVelocity = world.defaults.debugShowVelocity;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#debugBodyColor
         * @type {integer}
         * @since 3.0.0
         */
        this.debugBodyColor = world.defaults.bodyDebugColor;

        /**
         * [description]
         *
         * @name Phaser.Physics.Impact.Body#updateCallback
         * @type {?BodyUpdateCallback}
         * @since 3.0.0
         */
        this.updateCallback;

        /**
         * min 44 deg, max 136 deg
         *
         * @name Phaser.Physics.Impact.Body#slopeStanding
         * @type {{ min: number, max: number }}
         * @since 3.0.0
         */
        this.slopeStanding = { min: 0.767944870877505, max: 2.3736477827122884 };
    },

    /**
     * [description]
     *
     * @method Phaser.Physics.Impact.Body#reset
     * @since 3.0.0
     *
     * @param {number} x - [description]
     * @param {number} y - [description]
     */
    reset: function (x, y)
    {
        this.pos = { x: x, y: y };
        this.last = { x: x, y: y };
        this.vel = { x: 0, y: 0 };
        this.accel = { x: 0, y: 0 };
        this.friction = { x: 0, y: 0 };
        this.maxVel = { x: 100, y: 100 };

        this.standing = false;

        this.gravityFactor = 1;
        this.bounciness = 0;
        this.minBounceVelocity = 40;

        this.accelGround = 0;
        this.accelAir = 0;
        this.jumpSpeed = 0;

        this.type = TYPE.NONE;
        this.checkAgainst = TYPE.NONE;
        this.collides = COLLIDES.NEVER;
    },

    /**
     * [description]
     *
     * @method Phaser.Physics.Impact.Body#update
     * @since 3.0.0
     *
     * @param {number} delta - The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
     */
    update: function (delta)
    {
        var pos = this.pos;

        this.last.x = pos.x;
        this.last.y = pos.y;

        this.vel.y += this.world.gravity * delta * this.gravityFactor;

        this.vel.x = GetVelocity(delta, this.vel.x, this.accel.x, this.friction.x, this.maxVel.x);
        this.vel.y = GetVelocity(delta, this.vel.y, this.accel.y, this.friction.y, this.maxVel.y);

        var mx = this.vel.x * delta;
        var my = this.vel.y * delta;

        var res = this.world.collisionMap.trace(pos.x, pos.y, mx, my, this.size.x, this.size.y);

        if (this.handleMovementTrace(res))
        {
            UpdateMotion(this, res);
        }

        var go = this.gameObject;

        if (go)
        {
            go.x = (pos.x - this.offset.x) + go.displayOriginX * go.scaleX;
            go.y = (pos.y - this.offset.y) + go.displayOriginY * go.scaleY;
        }

        if (this.updateCallback)
        {
            this.updateCallback(this);
        }
    },

    /**
     * [description]
     *
     * @method Phaser.Physics.Impact.Body#drawDebug
     * @since 3.0.0
     *
     * @param {Phaser.GameObjects.Graphics} graphic - [description]
     */
    drawDebug: function (graphic)
    {
        var pos = this.pos;

        if (this.debugShowBody)
        {
            graphic.lineStyle(1, this.debugBodyColor, 1);
            graphic.strokeRect(pos.x, pos.y, this.size.x, this.size.y);
        }

        if (this.debugShowVelocity)
        {
            var x = pos.x + this.size.x / 2;
            var y = pos.y + this.size.y / 2;

            graphic.lineStyle(1, this.world.defaults.velocityDebugColor, 1);
            graphic.lineBetween(x, y, x + this.vel.x, y + this.vel.y);
        }
    },

    /**
     * [description]
     *
     * @method Phaser.Physics.Impact.Body#willDrawDebug
     * @since 3.0.0
     *
     * @return {boolean} [description]
     */
    willDrawDebug: function ()
    {
        return (this.debugShowBody || this.debugShowVelocity);
    },

    /**
     * [description]
     *
     * @method Phaser.Physics.Impact.Body#skipHash
     * @since 3.0.0
     *
     * @return {boolean} [description]
     */
    skipHash: function ()
    {
        return (!this.enabled || (this.type === 0 && this.checkAgainst === 0 && this.collides === 0));
    },

    /**
     * [description]
     *
     * @method Phaser.Physics.Impact.Body#touches
     * @since 3.0.0
     *
     * @param {Phaser.Physics.Impact.Body} other - [description]
     *
     * @return {boolean} [description]
     */
    touches: function (other)
    {
        return !(
            this.pos.x >= other.pos.x + other.size.x ||
            this.pos.x + this.size.x <= other.pos.x ||
            this.pos.y >= other.pos.y + other.size.y ||
            this.pos.y + this.size.y <= other.pos.y
        );
    },

    /**
     * Reset the size and position of the physics body.
     *
     * @method Phaser.Physics.Impact.Body#resetSize
     * @since 3.0.0
     *
     * @param {number} x - The x coordinate to position the body.
     * @param {number} y - The y coordinate to position the body.
     * @param {number} width - The width of the body.
     * @param {number} height - The height of the body.
     *
     * @return {Phaser.Physics.Impact.Body} This Body object.
     */
    resetSize: function (x, y, width, height)
    {
        this.pos.x = x;
        this.pos.y = y;
        this.size.x = width;
        this.size.y = height;

        return this;
    },

    /**
     * [description]
     *
     * @method Phaser.Physics.Impact.Body#toJSON
     * @since 3.0.0
     *
     * @return {JSONImpactBody} [description]
     */
    toJSON: function ()
    {
        var output = {
            name: this.name,
            size: { x: this.size.x, y: this.size.y },
            pos: { x: this.pos.x, y: this.pos.y },
            vel: { x: this.vel.x, y: this.vel.y },
            accel: { x: this.accel.x, y: this.accel.y },
            friction: { x: this.friction.x, y: this.friction.y },
            maxVel: { x: this.maxVel.x, y: this.maxVel.y },
            gravityFactor: this.gravityFactor,
            bounciness: this.bounciness,
            minBounceVelocity: this.minBounceVelocity,
            type: this.type,
            checkAgainst: this.checkAgainst,
            collides: this.collides
        };

        return output;
    },

    /**
     * [description]
     *
     * @method Phaser.Physics.Impact.Body#fromJSON
     * @todo Code it!
     * @since 3.0.0
     *
     * @param {object} config - [description]
     */
    fromJSON: function ()
    {
    },

    /**
     * Can be overridden by user code
     *
     * @method Phaser.Physics.Impact.Body#check
     * @since 3.0.0
     *
     * @param {Phaser.Physics.Impact.Body} other - [description]
     */
    check: function ()
    {
    },

    /**
     * Can be overridden by user code
     *
     * @method Phaser.Physics.Impact.Body#collideWith
     * @since 3.0.0
     *
     * @param {Phaser.Physics.Impact.Body} other - [description]
     * @param {string} axis - [description]
     */
    collideWith: function (other, axis)
    {
        if (this.parent && this.parent._collideCallback)
        {
            this.parent._collideCallback.call(this.parent._callbackScope, this, other, axis);
        }
    },

    /**
     * Can be overridden by user code but must return a boolean.
     *
     * @method Phaser.Physics.Impact.Body#handleMovementTrace
     * @since 3.0.0
     *
     * @param {number} res - [description]
     *
     * @return {boolean} [description]
     */
    handleMovementTrace: function ()
    {
        return true;
    },

    /**
     * [description]
     *
     * @method Phaser.Physics.Impact.Body#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        this.world.remove(this);

        this.enabled = false;

        this.world = null;

        this.gameObject = null;

        this.parent = null;
    }

});

module.exports = Body;
