// Copyright (c) 2013 Evan Shortiss
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

(function() {
  // Default is to not use objects
  var useObjects = false;
  // Default is to use standard array
  var AxesArray = Array;


  /**
   * Parse the arguments passed into Vec2D and Vec3D.
   * @param {Mixed}   args  The argument array from caller function.
   * @return {Array}        The resulting axes array.
   */
  function parseArgs(args) {
    // User passed x and y
    if (args.length === 2) {
      return new Array(args[0], args[1]);
    }
    // User passed an array
    else if (Object.prototype.toString.call(args[0]) === '[object Array]') {
      return args[0];
    }
    // User passed x and y in an object
    else if (typeof(args[0]) === 'object') {
      return [args[0].x, args[0].y];
    } else {
      return [0, 0];
    }
  }


  /**
   * Check is the passed param a Vec2D instance.
   * @param   {Mixed}   v   The var to check.
   * @return  {Boolean}     The result of check.
   */
  function isVector(v) {
    return v instanceof Vector || v instanceof ObjectVector;
  };


  /****************************************************************************
   * Main array based vector class. Has instance methods attached to prototype.
   * @constructor
   * @param {Array}  axes
   ****************************************************************************/
  var Vector = function(axes) {
    // Constructor for each array type is different. (Float32Array vs Array)
    if (AxesArray.name === 'Float32Array') {
      this._axes = new AxesArray([axes[0], axes[1]]);
    } else {
      this._axes = new AxesArray(axes[0], axes[1]);
    }
  };

  Vector.prototype = {
    /**
     * Set both x and y
     * @param x   New x val
     * @param y   New y val
     */
    setAxes: function(x, y) {
      this._axes[0] = x;
      this._axes[1] = y;
    },


    /**
     * Getter for x axis.
     * @return {Number}
     */
    getX: function() {
      return this._axes[0];
    },


    /**
     * Setter for x axis.
     */
    setX: function(x) {
      this._axes[0] = x;
    },


    /**
     * Getter for y axis.
     * @return {Number}
     */
    getY: function() {
      return this._axes[1];
    },


    /**
     * Setter for y axis.
     */
    setY: function(y) {
      this._axes[1] = y;
    },


    /**
     * View vector as a string such as "Vec2D: (0, 4)"
     * @param   {Boolean}
     * @return  {String}
     */
    toString: function(round) {
      if (round) {
        return '(' + Math.round(this.getX()) + ', ' + Math.round(this.getY()) + ')'
      }
      return '(' + this.getX() + ', ' + this.getY() + ')'
    },


    /**
     * Return an array containing the vector axes.
     * @return {Array}
     */
    toArray: function() {
      return new Array(this.getX(), this.getY());
    },


    /**
     * Return an array containing the vector axes.
     * @return {Object}
     */
    toObject: function() {
      return {
        "x": this.getX(),
        "y": this.getY()
      };
    },


    /**
     * Add the provided Vector to this one.
     * @param {Vector} vec
     */
    add: function(vec) {
      this._axes[0] += vec._axes[0];
      this._axes[1] += vec._axes[1];
      return this;
    },


    /**
     * Subtract the provided vector from this one.
     * @param {Vector} vec
     */
    subtract: function(vec) {
      this._axes[0] -= vec._axes[0];
      this._axes[1] -= vec._axes[1];
      return this;
    },


    /**
     * Check is the vector provided equal to this one.
     * @param   {Vec2D}   vec
     * @return  {Boolean}
     */
    equals: function(vec) {
      return (vec._axes[0] == this._axes[0] && vec._axes[1] == this._axes[1]);
    },


    /**
     * Multiply this vector by the provided vector.
     * @param {Vector} vec
     */
    multiplyByVector: function(vec) {
      this._axes[0] *= vec._axes[0];
      this._axes[1] *= vec._axes[1];
      return this;
    },


    /**
     * Multiply this vector by the provided vector.
     * @param {Vector} vec
     */
    divideByVector: function(vec) {
      this._axes[0] /= vec._axes[0];
      this._axes[1] /= vec._axes[1];
      return this;
    },


    /**
     * Multiply this vector by the provided number
     * @param {Number} n
     */
    multiplyByScalar: function(n) {
      this._axes[0] *= n;
      this._axes[1] *= n;
      return this;
    },


    /**
     * Divive this vector by the provided number
     * @param {Number} n
     */
    divideByScalar: function(n) {
      this._axes[0] /= n;
      this._axes[1] /= n;
      return this;
    },


    /**
     * Normalise this vector. Directly affects this vector.
     * Use Vec2D.normalise(vector) to create a normalised clone of this.
     */
    normalise: function() {
      return this.multiplyByScalar(1 / this.magnitude());
    },


    /**
     * For American spelling.
     * Same as unit/normalise function.
     */
    normalize: function() {
      return this.normalise();
    },


    /**
     * The same as normalise.
     */
    unit: function() {
      return this.normalise();
    },


    /**
     * Return the magnitude (length) of this vector.
     * @return  {Number}
     */
    magnitude: function() {
      return Math.sqrt((this._axes[0] * this._axes[0]) + (this._axes[1] * this._axes[1]));
    },


    /**
     * Return the magnitude (length) of this vector.
     * @return  {Number}
     */
    length: function() {
      return this.magnitude();
    },


    /**
     * Get the dot product of this vector by another.
     * @param   {Vector} vec
     * @return  {Number}
     */
    dot: function(vec) {
      return (vec._axes[0] * this._axes[0]) + (vec._axes[1] * this._axes[1]);
    },


    /**
     * Get the cross product of this vector by another.
     * @param   {Vector} vec
     * @return  {Number}
     */
    cross: function(vec) {
      return ((this._axes[0] * vec._axes[1]) - (this._axes[1] * vec._axes[0]));
    },


    /**
     * Returns the reverse of the provided vector.
     * @param   {Vector} vec
     */
    reverse: function(vec) {
      this._axes[0] = -this._axes[0];
      this._axes[1] = -this._axes[1];
      return this;
    },


    /**
     * Create a copy of this vector.
     * @return {Vector}
     */
    clone: function() {
      return new Vector([this._axes[0], this._axes[1]]);
    }
  };


  /****************************************************************************
   * Main object based vector class. Has instance methods attached to prototype.
   * @constructor
   * @param {Array}  axes
   ****************************************************************************/
  var ObjectVector = function(axes) {
    // Constructor for each array type is different. (Float32Array vs Array)
    this._axes = {
      x: axes[0],
      y: axes[1]
    };
  };

  ObjectVector.prototype = {
    /**
     * Set both x and y
     * @param x   New x val
     * @param y   New y val
     */
    setAxes: function(x, y) {
      this._axes.x = x;
      this._axes.y = y;
    },


    /**
     * Getter for x axis.
     * @return {Number}
     */
    getX: function() {
      return this._axes.x;
    },


    /**
     * Setter for x axis.
     */
    setX: function(x) {
      this._axes.x = x;
    },


    /**
     * Getter for y axis.
     * @return {Number}
     */
    getY: function() {
      return this._axes.y;
    },


    /**
     * Setter for y axis.
     */
    setY: function(y) {
      this._axes.y = y;
    },


    /**
     * View vector as a string such as "Vec2D: (0, 4)"
     * @param   {Boolean}
     * @return  {String}
     */
    toString: function(round) {
      if (round) {
        return '(' + Math.round(this.getX()) + ', ' + Math.round(this.getY()) + ')'
      }
      return '(' + this.getX() + ', ' + this.getY() + ')'
    },


    /**
     * Return an array containing the vector axes.
     * @return {Array}
     */
    toArray: function() {
      return new Array(this.getX(), this.getY());
    },


    /**
     * Return an array containing the vector axes.
     * @return {Object}
     */
    toObject: function() {
      return {
        "x": this.getX(),
        "y": this.getY()
      };
    },


    /**
     * Add the provided Vector to this one.
     * @param {Vector} vec
     */
    add: function(vec) {
      this._axes.x += vec._axes.x;
      this._axes.y += vec._axes.y;
      return this;
    },


    /**
     * Subtract the provided vector from this one.
     * @param {Vector} vec
     */
    subtract: function(vec) {
      this._axes.x -= vec._axes.x;
      this._axes.y -= vec._axes.y;
      return this;
    },


    /**
     * Check is the vector provided equal to this one.
     * @param   {Vec2D}   vec
     * @return  {Boolean}
     */
    equals: function(vec) {
      return (vec._axes.x == this._axes.x && vec._axes.y == this._axes.y);
    },


    /**
     * Multiply this vector by the provided vector.
     * @param {Vector} vec
     */
    multiplyByVector: function(vec) {
      this._axes.x *= vec._axes.x;
      this._axes.y *= vec._axes.y;
      return this;
    },


    /**
     * Multiply this vector by the provided vector.
     * @param {Vector} vec
     */
    divideByVector: function(vec) {
      this._axes.x /= vec._axes.x;
      this._axes.y /= vec._axes.y;
      return this;
    },


    /**
     * Multiply this vector by the provided number
     * @param {Number} n
     */
    multiplyByScalar: function(n) {
      this._axes.x *= n;
      this._axes.y *= n;
      return this;
    },


    /**
     * Divive this vector by the provided number
     * @param {Number} n
     */
    divideByScalar: function(n) {
      this._axes.x /= n;
      this._axes.y /= n;
      return this;
    },


    /**
     * Normalise this vector. Directly affects this vector.
     * Use Vec2D.normalise(vector) to create a normalised clone of this.
     */
    normalise: function() {
      return this.multiplyByScalar(1 / this.magnitude());
    },


    /**
     * For American spelling.
     * Same as unit/normalise function.
     */
    normalize: function() {
      return this.normalise();
    },


    /**
     * The same as normalise.
     */
    unit: function() {
      return this.normalise();
    },


    /**
     * Return the magnitude (length) of this vector.
     * @return  {Number}
     */
    magnitude: function() {
      return Math.sqrt((this._axes.x * this._axes.x) + (this._axes.y * this._axes.y));
    },


    /**
     * Return the magnitude (length) of this vector.
     * @return  {Number}
     */
    length: function() {
      return this.magnitude();
    },


    /**
     * Get the dot product of this vector by another.
     * @param   {Vector} vec
     * @return  {Number}
     */
    dot: function(vec) {
      return (vec._axes.x * this._axes.x) + (vec._axes.y * this._axes.y);
    },


    /**
     * Get the cross product of this vector by another.
     * @param   {Vector} vec
     * @return  {Number}
     */
    cross: function(vec) {
      return ((this._axes.x * vec._axes.y) - (this._axes.y * vec._axes.x));
    },


    /**
     * Returns the reverse of the provided vector.
     * @param   {Vector} vec
     */
    reverse: function(vec) {
      this._axes.x = -this._axes.x;
      this._axes.y = -this._axes.y;
      return this;
    },


    /**
     * Create a copy of this vector.
     * @return {Vector}
     */
    clone: function() {
      return new ObjectVector([this._axes.x, this._axes.y]);
    }
  };


  /****************************************************************************
   Publically exposed Vector interface.
   ****************************************************************************/
  var Vec2D = function() {

  };


  Vec2D.prototype = {
    Vector: Vector,
    ObjectVector: ObjectVector,

    /**
     * Create a new Vector.
     * @param {Number}  [x]
     * @param {Number}  [y]
     * @param {Object}  [axes]
     * @param {Array}   [axes]
     */
    create: function() {
      if(useObjects) {
        return new this.ObjectVector(parseArgs(arguments));
      }
      return new this.Vector(parseArgs(arguments));
    },


    /**
     * Instruct the library to use standard JavaScript arrays.
     * Otherwise the library will try use Float32Arrays if available. (This is default)
     */
    useStandardArrays: function() {
      useObjects = false;
      AxesArray = Array;
    },


    /**
     * Instruct the library to use Float32 JavaScript arrays. (This is default)
     * Otherwise the library will use standard array.
     */
    useFloat32Arrays: function() {
      useObjects = false;
      AxesArray = Float32Array;
    },


    /**
     * Instruct library to use Objects to represent vectors.
     */
    useObjects: function() {
      useObjects = true;
    },


    /**
     * Add v0 to v1 to produce a new vector
     * @param   {Vector} v0
     * @param   {Vector} v1
     * @return  {Vector}
     */
    add: function(v0, v1) {
      return this.create(v0.getX() + v1.getX(), v0.getY() + v1.getY())
    },


    /**
     * Subtract v0 from v1 to produce a new Vector.
     * @param   {Vector} v0
     * @param   {Vector} v1
     * @return  {Vector}
     */
    subtract: function(v0, v1) {
      return this.create(v0.getX() - v1.getX(), v0.getY()- v1.getY())
    },


    /**
     * Check are the provided vectors equal.
     * @param   {Vector}   v0
     * @param   {Vector}   v1
     * @return  {Boolean}
     */
    equals: function(v0, v1) {
      return v0.equals(v1);
    },


    /**
     * Multiply a vector by a vector to produce a new Vector
     * @param   {Vector} v0
     * @param   {Vector} v1
     * @return  {Vector}
     */
    vectorTimesVector: function(v0, v1) {
      return this.create(v0.getX() * v1.getX(), v0.getY() * v1.getY());
    },


    /**
     * Multiply a vector by a number to produce a new vector.
     * @param   {Vector} vec    The var to store a result in.
     * @param   {Number} n      The Vector to subtract from this one.
     * @return  {Vector}
     */
    vectorTimesScalar: function(vec, n) {
      return this.create(vec.getX() * n, vec.getY() * n);
    },


    /**
     * Return a normalised version of provided vector.
     * @param   {Vector} vec
     * @return  {Vector}
     */
    nomalise: function(vec) {
      return this.timesScalar(vec, 1 / vec.magnitude());
    },


    /**
     * Same as normalise
     */
    normalize: function(vec) {
      return this.normalise(vec);
    },


    /**
     * Same as normalise.
     */
    unit: function(vec) {
      return this.normalise(vec);
    },


    /**
     * Return the dot product of two vectors or this vector dot another.
     * @param   {Vector} v0
     * @param   {Vector} v1
     * @return  {Number}
     */
    dot: function(v0, v1) {
      return (v0.getX() * v1.getX()) + (v0.getY() * v1.getY());
    },


    /**
     * Calculate the cross product of two vectors.
     * @param   {Vector} v0
     * @param   {Vector} v1
     * @param   {Vector}
     */
    cross: function(v0, v1) {
      return ((v0.getX() * v1.getY()) - (v0.getY() * v1.getX()));
    },


    /**
     * Return the magnitude (length) of a vector.
     * @param   {Vector} vec
     * @return  {Number}
     */
    magnitude: function(vec) {
      return vec.magnitude();
    },


    /**
     * Same as the magnitude.
     */
    length: function(vec) {
      return vec.magnitude();
    },


    /**
     * Find sqaure distance between two vectors.
     * @param   {Vector} v0
     * @param   {Vector} v1
     * @return  {Number}
     */
    distance: function(v0, v1) {
      return Math.sqrt((v0.getX() - v1.getX()) * (v0.getX() - v1.getX()) + (v0.getY() - v1.getY()) * (v0.getY() - v1.getY()))
    },


    /**
     * Returns the reverse of the provided vector.
     * @param   {Vector} vec
     * @return  {Vector}
     */
    reverse: function(vec) {
      return this.create(-vec.getX(), -vec.getY());
    }
  };

  // Expose publically
  if (typeof window !== 'undefined') {
    window.Vec2D = new Vec2D();
  } else {
    module.exports = new Vec2D();
  }

})();