/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2020 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var Bounds = require('./lib/geometry/Bounds');
var Class = require('../../utils/Class');
var Composite = require('./lib/body/Composite');
var Constraint = require('./lib/constraint/Constraint');
var Detector = require('./lib/collision/Detector');
var Events = require('./events');
var InputEvents = require('../../input/events');
var Merge = require('../../utils/object/Merge');
var Sleeping = require('./lib/core/Sleeping');
var Vector2 = require('../../math/Vector2');
var Vertices = require('./lib/geometry/Vertices');

/**
 * @classdesc
 * A Pointer Constraint is a special type of constraint that allows you to click
 * and drag bodies in a Matter World. It monitors the active Pointers in a Scene,
 * and when one is pressed down it checks to see if that hit any part of any active
 * body in the world. If it did, and the body has input enabled, it will begin to
 * drag it until either released, or you stop it via the `stopDrag` method.
 * 
 * You can adjust the stiffness, length and other properties of the constraint via
 * the `options` object on creation.
 *
 * @class PointerConstraint
 * @memberof Phaser.Physics.Matter
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Scene} scene - A reference to the Scene to which this Pointer Constraint belongs.
 * @param {Phaser.Physics.Matter.World} world - A reference to the Matter World instance to which this Constraint belongs.
 * @param {object} [options] - A Constraint configuration object.
 */
var PointerConstraint = new Class({

    initialize:

    function PointerConstraint (scene, world, options)
    {
        if (options === undefined) { options = {}; }

        //  Defaults
        var defaults = {
            label: 'Pointer Constraint',
            pointA: { x: 0, y: 0 },
            pointB: { x: 0, y: 0 },
            length: 0.01,
            stiffness: 0.1,
            angularStiffness: 1,
            collisionFilter: {
                category: 0x0001,
                mask: 0xFFFFFFFF,
                group: 0
            }
        };

        /**
         * A reference to the Scene to which this Pointer Constraint belongs.
         * This is the same Scene as the Matter World instance.
         *
         * @name Phaser.Physics.Matter.PointerConstraint#scene
         * @type {Phaser.Scene}
         * @since 3.0.0
         */
        this.scene = scene;

        /**
         * A reference to the Matter World instance to which this Constraint belongs.
         *
         * @name Phaser.Physics.Matter.PointerConstraint#world
         * @type {Phaser.Physics.Matter.World}
         * @since 3.0.0
         */
        this.world = world;

        /**
         * The Camera the Pointer was interacting with when the input
         * down event was processed.
         *
         * @name Phaser.Physics.Matter.PointerConstraint#camera
         * @type {Phaser.Cameras.Scene2D.Camera}
         * @since 3.0.0
         */
        this.camera = null;

        /**
         * A reference to the Input Pointer that activated this Constraint.
         * This is set in the `onDown` handler.
         *
         * @name Phaser.Physics.Matter.PointerConstraint#pointer
         * @type {Phaser.Input.Pointer}
         * @default null
         * @since 3.0.0
         */
        this.pointer = null;

        /**
         * Is this Constraint active or not?
         * 
         * An active constraint will be processed each update. An inactive one will be skipped.
         * Use this to toggle a Pointer Constraint on and off.
         *
         * @name Phaser.Physics.Matter.PointerConstraint#active
         * @type {boolean}
         * @default true
         * @since 3.0.0
         */
        this.active = true;

        /**
         * The internal transformed position.
         *
         * @name Phaser.Physics.Matter.PointerConstraint#position
         * @type {Phaser.Math.Vector2}
         * @since 3.0.0
         */
        this.position = new Vector2();

        /**
         * The body that is currently being dragged, if any.
         *
         * @name Phaser.Physics.Matter.PointerConstraint#body
         * @type {?MatterJS.BodyType}
         * @since 3.16.2
         */
        this.body = null;

        /**
         * The part of the body that was clicked on to start the drag.
         *
         * @name Phaser.Physics.Matter.PointerConstraint#part
         * @type {?MatterJS.BodyType}
         * @since 3.16.2
         */
        this.part = null;

        /**
         * The native Matter Constraint that is used to attach to bodies.
         *
         * @name Phaser.Physics.Matter.PointerConstraint#constraint
         * @type {MatterJS.ConstraintType}
         * @since 3.0.0
         */
        this.constraint = Constraint.create(Merge(options, defaults));

        this.world.on(Events.BEFORE_UPDATE, this.update, this);

        scene.sys.input.on(InputEvents.POINTER_DOWN, this.onDown, this);
        scene.sys.input.on(InputEvents.POINTER_UP, this.onUp, this);
    },

    /**
     * A Pointer has been pressed down onto the Scene.
     * 
     * If this Constraint doesn't have an active Pointer then a hit test is set to
     * run against all active bodies in the world during the _next_ call to `update`.
     * If a body is found, it is bound to this constraint and the drag begins.
     *
     * @method Phaser.Physics.Matter.PointerConstraint#onDown
     * @since 3.0.0
     *
     * @param {Phaser.Input.Pointer} pointer - A reference to the Pointer that was pressed.
     */
    onDown: function (pointer)
    {
        if (!this.pointer)
        {
            this.pointer = pointer;
            this.camera = pointer.camera;
        }
    },

    /**
     * A Pointer has been released from the Scene. If it was the one this constraint was using, it's cleared.
     *
     * @method Phaser.Physics.Matter.PointerConstraint#onUp
     * @since 3.22.0
     *
     * @param {Phaser.Input.Pointer} pointer - A reference to the Pointer that was pressed.
     */
    onUp: function (pointer)
    {
        if (pointer === this.pointer)
        {
            this.pointer = null;
        }
    },

    /**
     * Scans all active bodies in the current Matter World to see if any of them
     * are hit by the Pointer. The _first one_ found to hit is set as the active contraint
     * body.
     *
     * @method Phaser.Physics.Matter.PointerConstraint#getBody
     * @fires Phaser.Physics.Matter.Events#DRAG_START
     * @since 3.16.2
     * 
     * @return {boolean} `true` if a body was found and set, otherwise `false`.
     */
    getBody: function (pointer)
    {
        var pos = this.position;
        var constraint = this.constraint;

        this.camera.getWorldPoint(pointer.x, pointer.y, pos);

        var bodies = Composite.allBodies(this.world.localWorld);

        for (var i = 0; i < bodies.length; i++)
        {
            var body = bodies[i];

            if (!body.ignorePointer &&
                Bounds.contains(body.bounds, pos) &&
                Detector.canCollide(body.collisionFilter, constraint.collisionFilter))
            {
                if (this.hitTestBody(body, pos))
                {
                    this.world.emit(Events.DRAG_START, body, this.part, this);

                    return true;
                }
            }
        }

        return false;
    },

    /**
     * Scans the current body to determine if a part of it was clicked on.
     * If a part is found the body is set as the `constraint.bodyB` property,
     * as well as the `body` property of this class. The part is also set.
     *
     * @method Phaser.Physics.Matter.PointerConstraint#hitTestBody
     * @since 3.16.2
     *
     * @param {MatterJS.BodyType} body - The Matter Body to check.
     * @param {Phaser.Math.Vector2} position - A translated hit test position.
     *
     * @return {boolean} `true` if a part of the body was hit, otherwise `false`.
     */
    hitTestBody: function (body, position)
    {
        var constraint = this.constraint;
        var partsLength = body.parts.length;

        var start = (partsLength > 1) ? 1 : 0;

        for (var i = start; i < partsLength; i++)
        {
            var part = body.parts[i];

            if (Vertices.contains(part.vertices, position))
            {
                constraint.pointA = position;
                constraint.pointB = { x: position.x - body.position.x, y: position.y - body.position.y };

                constraint.bodyB = body;
                constraint.angleB = body.angle;

                Sleeping.set(body, false);

                this.part = part;
                this.body = body;

                return true;
            }
        }

        return false;
    },

    /**
     * Internal update handler. Called in the Matter BEFORE_UPDATE step.
     *
     * @method Phaser.Physics.Matter.PointerConstraint#update
     * @fires Phaser.Physics.Matter.Events#DRAG
     * @since 3.0.0
     */
    update: function ()
    {
        var pointer = this.pointer;
        var body = this.body;

        if (!this.active || !pointer)
        {
            if (body)
            {
                this.stopDrag();
            }

            return;
        }

        if (!pointer.isDown && body)
        {
            this.stopDrag();

            return;
        }
        else if (pointer.isDown)
        {
            if (!body && !this.getBody(pointer))
            {
                return;
            }

            body = this.body;

            var pos = this.position;
            var constraint = this.constraint;
    
            this.camera.getWorldPoint(pointer.x, pointer.y, pos);
   
            //  Drag update
            constraint.pointA.x = pos.x;
            constraint.pointA.y = pos.y;

            Sleeping.set(body, false);

            this.world.emit(Events.DRAG, body, this);
        }
    },

    /**
     * Stops the Pointer Constraint from dragging the body any further.
     * 
     * This is called automatically if the Pointer is released while actively
     * dragging a body. Or, you can call it manually to release a body from a
     * constraint without having to first release the pointer.
     *
     * @method Phaser.Physics.Matter.PointerConstraint#stopDrag
     * @fires Phaser.Physics.Matter.Events#DRAG_END
     * @since 3.16.2
     */
    stopDrag: function ()
    {
        var body = this.body;
        var constraint = this.constraint;

        constraint.bodyB = null;
        constraint.pointB = null;

        this.pointer = null;
        this.body = null;
        this.part = null;

        if (body)
        {
            this.world.emit(Events.DRAG_END, body, this);
        }
    },

    /**
     * Destroys this Pointer Constraint instance and all of its references.
     *
     * @method Phaser.Physics.Matter.PointerConstraint#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        this.world.removeConstraint(this.constraint);

        this.pointer = null;
        this.constraint = null;
        this.body = null;
        this.part = null;

        this.world.off(Events.BEFORE_UPDATE, this.update);

        this.scene.sys.input.off(InputEvents.POINTER_DOWN, this.onDown, this);
        this.scene.sys.input.off(InputEvents.POINTER_UP, this.onUp, this);
    }

});

module.exports = PointerConstraint;
