;(function(undefined) {
  'use strict';

  if (typeof sigma === 'undefined')
    throw 'sigma is not declared';

  sigma.utils.pkg('sigma.classes');

  /**
   * The camera constructor. It just initializes its attributes and methods.
   *
   * @param  {string}       id       The id.
   * @param  {sigma.classes.graph}  graph    The graph.
   * @param  {configurable} settings The settings function.
   * @param  {?object}      options  Eventually some overriding options.
   * @return {camera}                Returns the fresh new camera instance.
   */
  sigma.classes.camera = function(id, graph, settings, options) {
    sigma.classes.dispatcher.extend(this);

    Object.defineProperty(this, 'graph', {
      value: graph
    });
    Object.defineProperty(this, 'id', {
      value: id
    });
    Object.defineProperty(this, 'readPrefix', {
      value: 'read_cam' + id + ':'
    });
    Object.defineProperty(this, 'prefix', {
      value: 'cam' + id + ':'
    });

    this.x = 0;
    this.y = 0;
    this.ratio = 1;
    this.angle = 0;
    this.isAnimated = false;
    this.settings = (typeof options === 'object' && options) ?
      settings.embedObject(options) :
      settings;
  };

  /**
   * Updates the camera position.
   *
   * @param  {object} coordinates The new coordinates object.
   * @return {camera}             Returns the camera.
   */
  sigma.classes.camera.prototype.goTo = function(coordinates) {
    if (!this.settings('enableCamera'))
      return this;

    var i,
        l,
        c = coordinates || {},
        keys = ['x', 'y', 'ratio', 'angle'];

    for (i = 0, l = keys.length; i < l; i++)
      if (c[keys[i]] !== undefined) {
        if (typeof c[keys[i]] === 'number' && !isNaN(c[keys[i]]))
          this[keys[i]] = c[keys[i]];
        else
          throw 'Value for "' + keys[i] + '" is not a number.';
      }

    this.dispatchEvent('coordinatesUpdated');
    return this;
  };

  /**
   * This method takes a graph and computes for each node and edges its
   * coordinates relatively to the center of the camera. Basically, it will
   * compute the coordinates that will be used by the graphic renderers.
   *
   * Since it should be possible to use different cameras and different
   * renderers, it is possible to specify a prefix to put before the new
   * coordinates (to get something like "node.camera1_x")
   *
   * @param  {?string} read    The prefix of the coordinates to read.
   * @param  {?string} write   The prefix of the coordinates to write.
   * @param  {?object} options Eventually an object of options. Those can be:
   *                           - A restricted nodes array.
   *                           - A restricted edges array.
   *                           - A width.
   *                           - A height.
   * @return {camera}        Returns the camera.
   */
  sigma.classes.camera.prototype.applyView = function(read, write, options) {
    options = options || {};
    write = write !== undefined ? write : this.prefix;
    read = read !== undefined ? read : this.readPrefix;

    var nodes = options.nodes || this.graph.nodes(),
        edges = options.edges || this.graph.edges();

    var i,
        l,
        node,
        relCos = Math.cos(this.angle) / this.ratio,
        relSin = Math.sin(this.angle) / this.ratio,
        nodeRatio = Math.pow(this.ratio, this.settings('nodesPowRatio')),
        edgeRatio = Math.pow(this.ratio, this.settings('edgesPowRatio')),
        xOffset = (options.width || 0) / 2 - this.x * relCos - this.y * relSin,
        yOffset = (options.height || 0) / 2 - this.y * relCos + this.x * relSin;

    for (i = 0, l = nodes.length; i < l; i++) {
      node = nodes[i];
      node[write + 'x'] =
        (node[read + 'x'] || 0) * relCos +
        (node[read + 'y'] || 0) * relSin +
        xOffset;
      node[write + 'y'] =
        (node[read + 'y'] || 0) * relCos -
        (node[read + 'x'] || 0) * relSin +
        yOffset;
      node[write + 'size'] =
        (node[read + 'size'] || 0) /
        nodeRatio;
    }

    for (i = 0, l = edges.length; i < l; i++) {
      edges[i][write + 'size'] =
        (edges[i][read + 'size'] || 0) /
        edgeRatio;
    }

    return this;
  };

  /**
   * This function converts the coordinates of a point from the frame of the
   * camera to the frame of the graph.
   *
   * @param  {number} x The X coordinate of the point in the frame of the
   *                    camera.
   * @param  {number} y The Y coordinate of the point in the frame of the
   *                    camera.
   * @return {object}   The point coordinates in the frame of the graph.
   */
  sigma.classes.camera.prototype.graphPosition = function(x, y, vector) {
    var X = 0,
        Y = 0,
        cos = Math.cos(this.angle),
        sin = Math.sin(this.angle);

    // Revert the origin differential vector:
    if (!vector) {
      X = - (this.x * cos + this.y * sin) / this.ratio;
      Y = - (this.y * cos - this.x * sin) / this.ratio;
    }

    return {
      x: (x * cos + y * sin) / this.ratio + X,
      y: (y * cos - x * sin) / this.ratio + Y
    };
  };

  /**
   * This function converts the coordinates of a point from the frame of the
   * graph to the frame of the camera.
   *
   * @param  {number} x The X coordinate of the point in the frame of the
   *                    graph.
   * @param  {number} y The Y coordinate of the point in the frame of the
   *                    graph.
   * @return {object}   The point coordinates in the frame of the camera.
   */
  sigma.classes.camera.prototype.cameraPosition = function(x, y, vector) {
    var X = 0,
        Y = 0,
        cos = Math.cos(this.angle),
        sin = Math.sin(this.angle);

    // Revert the origin differential vector:
    if (!vector) {
      X = - (this.x * cos + this.y * sin) / this.ratio;
      Y = - (this.y * cos - this.x * sin) / this.ratio;
    }

    return {
      x: ((x - X) * cos - (y - Y) * sin) * this.ratio,
      y: ((y - Y) * cos + (x - X) * sin) * this.ratio
    };
  };

  /**
   * This method returns the transformation matrix of the camera. This is
   * especially useful to apply the camera view directly in shaders, in case of
   * WebGL rendering.
   *
   * @return {array} The transformation matrix.
   */
  sigma.classes.camera.prototype.getMatrix = function() {
    var scale = sigma.utils.matrices.scale(1 / this.ratio),
        rotation = sigma.utils.matrices.rotation(this.angle),
        translation = sigma.utils.matrices.translation(-this.x, -this.y),
        matrix = sigma.utils.matrices.multiply(
          translation,
          sigma.utils.matrices.multiply(
            rotation,
            scale
          )
        );

    return matrix;
  };

  /**
   * Taking a width and a height as parameters, this method returns the
   * coordinates of the rectangle representing the camera on screen, in the
   * graph's referentiel.
   *
   * To keep displaying labels of nodes going out of the screen, the method
   * keeps a margin around the screen in the returned rectangle.
   *
   * @param  {number} width  The width of the screen.
   * @param  {number} height The height of the screen.
   * @return {object}        The rectangle as x1, y1, x2 and y2, representing
   *                         two opposite points.
   */
  sigma.classes.camera.prototype.getRectangle = function(width, height) {
    var widthVect = this.cameraPosition(width, 0, true),
        heightVect = this.cameraPosition(0, height, true),
        centerVect = this.cameraPosition(width / 2, height / 2, true),
        marginX = this.cameraPosition(width / 4, 0, true).x,
        marginY = this.cameraPosition(0, height / 4, true).y;

    return {
      x1: this.x - centerVect.x - marginX,
      y1: this.y - centerVect.y - marginY,
      x2: this.x - centerVect.x + marginX + widthVect.x,
      y2: this.y - centerVect.y - marginY + widthVect.y,
      height: Math.sqrt(
        Math.pow(heightVect.x, 2) +
        Math.pow(heightVect.y + 2 * marginY, 2)
      )
    };
  };
}).call(this);
