/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Owner: david@famo.us
 * @license MPL 2.0
 * @copyright Famous Industries, Inc. 2014
 */

define(function(require, exports, module) {
    var Constraint = require('./Constraint');
    var Wall = require('./Wall');
    var Vector = require('famous/math/Vector');

    /**
     *  Walls combines one or more Wall primitives and exposes a simple API to
     *  interact with several walls at once. A common use case would be to set up
     *  a bounding box for a physics body, that would collide with each side.
     *
     *  @class Walls
     *  @constructor
     *  @extends Constraint
     *  @uses Wall
     *  @param {Options} [options] An object of configurable options.
     *  @param {Array} [options.sides] An array of sides e.g., [Walls.LEFT, Walls.TOP]
     *  @param {Array} [options.size] The size of the bounding box of the walls.
     *  @param {Array} [options.origin] The center of the wall relative to the size.
     *  @param {Array} [options.drift] Baumgarte stabilization parameter. Makes constraints "loosely" (0) or "tightly" (1) enforced. Range : [0, 1]
     *  @param {Array} [options.slop] Amount of penetration in pixels to ignore before collision event triggers.
     *  @param {Array} [options.restitution] The energy ratio lost in a collision (0 = stick, 1 = elastic) The energy ratio lost in a collision (0 = stick, 1 = elastic)
     *  @param {Array} [options.onContact] How to handle collision against the wall.
     */
    function Walls(options) {
        this.options = Object.create(Walls.DEFAULT_OPTIONS);
        if (options) this.setOptions(options);
        _createComponents.call(this, options.sides || this.options.sides);

        Constraint.call(this);
    }

    Walls.prototype = Object.create(Constraint.prototype);
    Walls.prototype.constructor = Walls;
    /**
     * @property Walls.ON_CONTACT
     * @type Object
     * @extends Wall.ON_CONTACT
     * @static
     */
    Walls.ON_CONTACT = Wall.ON_CONTACT;

    /**
     * An enumeration of common types of walls
     *    LEFT, RIGHT, TOP, BOTTOM, FRONT, BACK
     *    TWO_DIMENSIONAL, THREE_DIMENSIONAL
     *
     * @property Walls.SIDES
     * @type Object
     * @final
     * @static
     */
    Walls.SIDES = {
        LEFT   : 0,
        RIGHT  : 1,
        TOP    : 2,
        BOTTOM : 3,
        FRONT  : 4,
        BACK   : 5,
        TWO_DIMENSIONAL : [0, 1, 2, 3],
        THREE_DIMENSIONAL : [0, 1, 2, 3, 4, 5]
    };

    Walls.DEFAULT_OPTIONS = {
        sides : Walls.SIDES.TWO_DIMENSIONAL,
        size : [window.innerWidth, window.innerHeight, 0],
        origin : [.5, .5, .5],
        drift : 0.5,
        slop : 0,
        restitution : 0.5,
        onContact : Walls.ON_CONTACT.REFLECT
    };

    var _SIDE_NORMALS = {
        0 : new Vector(1, 0, 0),
        1 : new Vector(-1, 0, 0),
        2 : new Vector(0, 1, 0),
        3 : new Vector(0,-1, 0),
        4 : new Vector(0, 0, 1),
        5 : new Vector(0, 0,-1)
    };

    function _getDistance(side, size, origin) {
        var distance;
        var SIDES = Walls.SIDES;
        switch (parseInt(side)) {
            case SIDES.LEFT:
                distance = size[0] * origin[0];
                break;
            case SIDES.TOP:
                distance = size[1] * origin[1];
                break;
            case SIDES.FRONT:
                distance = size[2] * origin[2];
                break;
            case SIDES.RIGHT:
                distance = size[0] * (1 - origin[0]);
                break;
            case SIDES.BOTTOM:
                distance = size[1] * (1 - origin[1]);
                break;
            case SIDES.BACK:
                distance = size[2] * (1 - origin[2]);
                break;
        }
        return distance;
    }

    /*
     * Setter for options.
     *
     * @method setOptions
     * @param options {Objects}
     */
    Walls.prototype.setOptions = function setOptions(options) {
        var resizeFlag = false;
        if (options.restitution !== undefined) _setOptionsForEach.call(this, {restitution : options.restitution});
        if (options.drift !== undefined) _setOptionsForEach.call(this, {drift : options.drift});
        if (options.slop !== undefined) _setOptionsForEach.call(this, {slop : options.slop});
        if (options.onContact !== undefined) _setOptionsForEach.call(this, {onContact : options.onContact});
        if (options.size !== undefined) resizeFlag = true;
        if (options.sides !== undefined) this.options.sides = options.sides;
        if (options.origin !== undefined) resizeFlag = true;
        if (resizeFlag) this.setSize(options.size, options.origin);
    };

    function _createComponents(sides) {
        this.components = {};
        var components = this.components;

        for (var i = 0; i < sides.length; i++) {
            var side = sides[i];
            components[i] = new Wall({
                normal   : _SIDE_NORMALS[side].clone(),
                distance : _getDistance(side, this.options.size, this.options.origin)
            });
        }
    }

    /*
     * Setter for size.
     *
     * @method setOptions
     * @param options {Objects}
     */
    Walls.prototype.setSize = function setSize(size, origin) {
        origin = origin || this.options.origin;
        if (origin.length < 3) origin[2] = 0.5;

        this.forEach(function(wall, side) {
            var d = _getDistance(side, size, origin);
            wall.setOptions({distance : d});
        });

        this.options.size   = size;
        this.options.origin = origin;
    };

    function _setOptionsForEach(options) {
        this.forEach(function(wall) {
            wall.setOptions(options);
        });
        for (var key in options) this.options[key] = options[key];
    }

    /**
     * Adds an impulse to a physics body's velocity due to the walls constraint
     *
     * @method applyConstraint
     * @param targets {Array.Body}  Array of bodies to apply the constraint to
     * @param source {Body}         The source of the constraint
     * @param dt {Number}           Delta time
     */
    Walls.prototype.applyConstraint = function applyConstraint(targets, source, dt) {
        this.forEach(function(wall) {
            wall.applyConstraint(targets, source, dt);
        });
    };

    /**
     * Apply a method to each wall making up the walls
     *
     * @method applyConstraint
     * @param fn {Function}  Function that takes in a wall as its first parameter
     */
    Walls.prototype.forEach = function forEach(fn) {
        var sides = this.options.sides;
        for (var key in this.sides) fn(sides[key], key);
    };

    /**
     * Rotates the walls by an angle in the XY-plane
     *
     * @method applyConstraint
     * @param angle {Function}
     */
    Walls.prototype.rotateZ = function rotateZ(angle) {
        this.forEach(function(wall) {
            var n = wall.options.normal;
            n.rotateZ(angle).put(n);
        });
    };

    /**
     * Rotates the walls by an angle in the YZ-plane
     *
     * @method applyConstraint
     * @param angle {Function}
     */
    Walls.prototype.rotateX = function rotateX(angle) {
        this.forEach(function(wall) {
            var n = wall.options.normal;
            n.rotateX(angle).put(n);
        });
    };

    /**
     * Rotates the walls by an angle in the XZ-plane
     *
     * @method applyConstraint
     * @param angle {Function}
     */
    Walls.prototype.rotateY = function rotateY(angle) {
        this.forEach(function(wall) {
            var n = wall.options.normal;
            n.rotateY(angle).put(n);
        });
    };

    module.exports = Walls;
});
