var util = require('../../util');

class View {
  constructor(body, canvas) {
    this.body = body;
    this.canvas = canvas;

    this.animationSpeed = 1/this.renderRefreshRate;
    this.animationEasingFunction = "easeInOutQuint";
    this.easingTime = 0;
    this.sourceScale = 0;
    this.targetScale = 0;
    this.sourceTranslation = 0;
    this.targetTranslation = 0;
    this.lockedOnNodeId = undefined;
    this.lockedOnNodeOffset = undefined;
    this.touchTime = 0;

    this.viewFunction = undefined;

    this.body.emitter.on("fit",                 this.fit.bind(this));
    this.body.emitter.on("animationFinished",   () => {this.body.emitter.emit("_stopRendering");});
    this.body.emitter.on("unlockNode",          this.releaseNode.bind(this));
  }


  setOptions(options = {}) {
    this.options = options;
  }


  /**
   * Find the center position of the network
   * @private
   */
  _getRange(specificNodes = []) {
    var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
    if (specificNodes.length > 0) {
      for (var i = 0; i < specificNodes.length; i++) {
        node = this.body.nodes[specificNodes[i]];
        if (minX > (node.shape.boundingBox.left)) {
          minX = node.shape.boundingBox.left;
        }
        if (maxX < (node.shape.boundingBox.right)) {
          maxX = node.shape.boundingBox.right;
        }
        if (minY > (node.shape.boundingBox.top)) {
          minY = node.shape.boundingBox.top;
        } // top is negative, bottom is positive
        if (maxY < (node.shape.boundingBox.bottom)) {
          maxY = node.shape.boundingBox.bottom;
        } // top is negative, bottom is positive
      }
    }
    else {
      for (var i = 0; i < this.body.nodeIndices.length; i++) {
        node = this.body.nodes[this.body.nodeIndices[i]];
        if (minX > (node.shape.boundingBox.left)) {
          minX = node.shape.boundingBox.left;
        }
        if (maxX < (node.shape.boundingBox.right)) {
          maxX = node.shape.boundingBox.right;
        }
        if (minY > (node.shape.boundingBox.top)) {
          minY = node.shape.boundingBox.top;
        } // top is negative, bottom is positive
        if (maxY < (node.shape.boundingBox.bottom)) {
          maxY = node.shape.boundingBox.bottom;
        } // top is negative, bottom is positive
      }
    }

    if (minX === 1e9 && maxX === -1e9 && minY === 1e9 && maxY === -1e9) {
      minY = 0, maxY = 0, minX = 0, maxX = 0;
    }
    return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  }


  /**
   * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
   * @returns {{x: number, y: number}}
   * @private
   */
  _findCenter(range) {
    return {x: (0.5 * (range.maxX + range.minX)),
      y: (0.5 * (range.maxY + range.minY))};
  }


  /**
   * This function zooms out to fit all data on screen based on amount of nodes
   * @param {Object} Options
   * @param {Boolean} [initialZoom]  | zoom based on fitted formula or range, true = fitted, default = false;
   */
  fit(options = {nodes:[]}, initialZoom = false) {
    var range;
    var zoomLevel;

    if (initialZoom === true) {
      // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation.
      var positionDefined = 0;
      for (var nodeId in this.body.nodes) {
        if (this.body.nodes.hasOwnProperty(nodeId)) {
          var node = this.body.nodes[nodeId];
          if (node.predefinedPosition === true) {
            positionDefined += 1;
          }
        }
      }
      if (positionDefined > 0.5 * this.body.nodeIndices.length) {
        this.fit(options,false);
        return;
      }

      range = this._getRange(options.nodes);

      var numberOfNodes = this.body.nodeIndices.length;
      zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.

      // correct for larger canvasses.
      var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600);
      zoomLevel *= factor;
    }
    else {
      this.body.emitter.emit("_resizeNodes");
      range = this._getRange(options.nodes);

      var xDistance = Math.abs(range.maxX - range.minX) * 1.1;
      var yDistance = Math.abs(range.maxY - range.minY) * 1.1;

      var xZoomLevel = this.canvas.frame.canvas.clientWidth  / xDistance;
      var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance;

      zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
    }

    if (zoomLevel > 1.0) {
      zoomLevel = 1.0;
    }
    else if (zoomLevel === 0) {
      zoomLevel = 1.0;
    }

    var center = this._findCenter(range);
    var animationOptions = {position: center, scale: zoomLevel, animation: options.animation};
    this.moveTo(animationOptions);
  }
  
  // animation

  /**
   * Center a node in view.
   *
   * @param {Number} nodeId
   * @param {Number} [options]
   */
  focus(nodeId, options = {}) {
    if (this.body.nodes[nodeId] !== undefined) {
      var nodePosition = {x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y};
      options.position = nodePosition;
      options.lockedOnNode = nodeId;

      this.moveTo(options)
    }
    else {
      console.log("Node: " + nodeId + " cannot be found.");
    }
  }

  /**
   *
   * @param {Object} options  |  options.offset   = {x:Number, y:Number}   // offset from the center in DOM pixels
   *                          |  options.scale    = Number                 // scale to move to
   *                          |  options.position = {x:Number, y:Number}   // position to move to
   *                          |  options.animation = {duration:Number, easingFunction:String} || Boolean   // position to move to
   */
  moveTo(options) {
    if (options === undefined) {
      options = {};
      return;
    }
    if (options.offset    === undefined)           {options.offset    = {x: 0, y: 0};    }
    if (options.offset.x  === undefined)           {options.offset.x  = 0;               }
    if (options.offset.y  === undefined)           {options.offset.y  = 0;               }
    if (options.scale     === undefined)           {options.scale     = this.body.view.scale;  }
    if (options.position  === undefined)           {options.position  = this.getViewPosition();}
    if (options.animation === undefined)           {options.animation = {duration:0};    }
    if (options.animation === false    )           {options.animation = {duration:0};    }
    if (options.animation === true     )           {options.animation = {};              }
    if (options.animation.duration === undefined)  {options.animation.duration = 1000;   }  // default duration
    if (options.animation.easingFunction === undefined)  {options.animation.easingFunction = "easeInOutQuad";  } // default easing function

    this.animateView(options);
  }

  /**
   *
   * @param {Object} options  |  options.offset   = {x:Number, y:Number}   // offset from the center in DOM pixels
   *                          |  options.time     = Number                 // animation time in milliseconds
   *                          |  options.scale    = Number                 // scale to animate to
   *                          |  options.position = {x:Number, y:Number}   // position to animate to
   *                          |  options.easingFunction = String           // linear, easeInQuad, easeOutQuad, easeInOutQuad,
   *                                                                       // easeInCubic, easeOutCubic, easeInOutCubic,
   *                                                                       // easeInQuart, easeOutQuart, easeInOutQuart,
   *                                                                       // easeInQuint, easeOutQuint, easeInOutQuint
   */
  animateView(options) {
    if (options === undefined) {
      return;
    }
    this.animationEasingFunction = options.animation.easingFunction;
    // release if something focussed on the node
    this.releaseNode();
    if (options.locked === true) {
      this.lockedOnNodeId = options.lockedOnNode;
      this.lockedOnNodeOffset = options.offset;
    }

    // forcefully complete the old animation if it was still running
    if (this.easingTime != 0) {
      this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation.
    }

    this.sourceScale = this.body.view.scale;
    this.sourceTranslation = this.body.view.translation;
    this.targetScale = options.scale;

    // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw
    // but at least then we'll have the target transition
    this.body.view.scale = this.targetScale;
    var viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight});

    var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
      x: viewCenter.x - options.position.x,
      y: viewCenter.y - options.position.y
    };
    this.targetTranslation = {
      x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x,
      y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y
    };

    // if the time is set to 0, don't do an animation
    if (options.animation.duration === 0) {
      if (this.lockedOnNodeId != undefined) {
        this.viewFunction = this._lockedRedraw.bind(this);
        this.body.emitter.on("initRedraw", this.viewFunction);
      }
      else {
        this.body.view.scale = this.targetScale;
        this.body.view.translation = this.targetTranslation;
        this.body.emitter.emit("_requestRedraw");
      }
    }
    else {
      this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's
      this.animationEasingFunction = options.animation.easingFunction;


      this.viewFunction = this._transitionRedraw.bind(this);
      this.body.emitter.on("initRedraw", this.viewFunction);
      this.body.emitter.emit("_startRendering");
    }
  }

  /**
   * used to animate smoothly by hijacking the redraw function.
   * @private
   */
  _lockedRedraw() {
    var nodePosition = {x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y};
    var viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight});
    var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
      x: viewCenter.x - nodePosition.x,
      y: viewCenter.y - nodePosition.y
    };
    var sourceTranslation = this.body.view.translation;
    var targetTranslation = {
      x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x,
      y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y
    };

    this.body.view.translation = targetTranslation;
  }

  releaseNode() {
    if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) {
      this.body.emitter.off("initRedraw", this.viewFunction);
      this.lockedOnNodeId = undefined;
      this.lockedOnNodeOffset = undefined;
    }
  }

  /**
   *
   * @param easingTime
   * @private
   */
  _transitionRedraw(finished = false) {
    this.easingTime += this.animationSpeed;
    this.easingTime = finished === true ? 1.0 : this.easingTime;

    var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime);

    this.body.view.scale = this.sourceScale + (this.targetScale - this.sourceScale) * progress;
    this.body.view.translation = {
      x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress,
      y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress
    };

    // cleanup
    if (this.easingTime >= 1.0) {
      this.body.emitter.off("initRedraw", this.viewFunction);
      this.easingTime = 0;
      if (this.lockedOnNodeId != undefined) {
        this.viewFunction = this._lockedRedraw.bind(this);
        this.body.emitter.on("initRedraw", this.viewFunction);
      }
      this.body.emitter.emit("animationFinished");
    }
  };


  getScale() {
    return this.body.view.scale;
  }

  getViewPosition() {
    return this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight});
  }


}

export default View;