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

/**
 * @class Node
 * A node. A node can be connected to other nodes via one or multiple edges.
 * @param {object} properties An object containing properties for the node. All
 *                            properties are optional, except for the id.
 *                              {number} id     Id of the node. Required
 *                              {string} label  Text label for the node
 *                              {number} x      Horizontal position of the node
 *                              {number} y      Vertical position of the node
 *                              {string} shape  Node shape, available:
 *                                              "database", "circle", "ellipse",
 *                                              "box", "image", "text", "dot",
 *                                              "star", "triangle", "triangleDown",
 *                                              "square", "icon"
 *                              {string} image  An image url
 *                              {string} title  An title text, can be HTML
 *                              {anytype} group A group name or number
 * @param {Network.Images} imagelist    A list with images. Only needed
 *                                            when the node has an image
 * @param {Network.Groups} grouplist    A list with groups. Needed for
 *                                            retrieving group properties
 * @param {Object}               constants    An object with default values for
 *                                            example for the color
 *
 */
function Node(properties, imagelist, grouplist, networkConstants) {
  var constants = util.selectiveBridgeObject(['nodes'],networkConstants);
  this.options = constants.nodes;

  this.selected = false;
  this.hover = false;

  this.edges = []; // all edges connected to this node
  this.dynamicEdges = [];
  this.reroutedEdges = {};

  // set defaults for the properties
  this.id = undefined;
  this.allowedToMoveX = false;
  this.allowedToMoveY = false;
  this.xFixed = false;
  this.yFixed = false;
  this.horizontalAlignLeft = true; // these are for the navigation controls
  this.verticalAlignTop    = true; // these are for the navigation controls
  this.baseRadiusValue = networkConstants.nodes.radius;
  this.radiusFixed = false;
  this.level = -1;
  this.preassignedLevel = false;
  this.hierarchyEnumerated = false;
  this.labelDimensions = {top:0, left:0, width:0, height:0, yLine:0}; // could be cached
  this.boundingBox = {top:0, left:0, right:0, bottom:0};

  this.imagelist = imagelist;
  this.grouplist = grouplist;

  // physics properties
  this.fx = 0.0;  // external force x
  this.fy = 0.0;  // external force y
  this.vx = 0.0;  // velocity x
  this.vy = 0.0;  // velocity y
  this.x = null;
  this.y = null;
  this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate

  // used for reverting to previous position on stabilization
  this.previousState = {vx:0,vy:0,x:0,y:0};

  this.damping = networkConstants.physics.damping; // written every time gravity is calculated
  this.fixedData = {x:null,y:null};

  this.setProperties(properties, constants);

  // creating the variables for clustering
  this.resetCluster();
  this.clusterSession = 0;
  this.clusterSizeWidthFactor  = networkConstants.clustering.nodeScaling.width;
  this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height;
  this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius;
  this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements;
  this.growthIndicator = 0;

  // variables to tell the node about the network.
  this.networkScaleInv = 1;
  this.networkScale = 1;
  this.canvasTopLeft = {"x": -300, "y": -300};
  this.canvasBottomRight = {"x":  300, "y":  300};
  this.parentEdgeId = null;
}


/**
 *  Revert the position and velocity of the previous step.
 */
Node.prototype.revertPosition = function() {
  this.x = this.previousState.x;
  this.y = this.previousState.y;
  this.vx = this.previousState.vx;
  this.vy = this.previousState.vy;
}


/**
 * (re)setting the clustering variables and objects
 */
Node.prototype.resetCluster = function() {
  // clustering variables
  this.formationScale = undefined; // this is used to determine when to open the cluster
  this.clusterSize = 1;            // this signifies the total amount of nodes in this cluster
  this.containedNodes = {};
  this.containedEdges = {};
  this.clusterSessions = [];
};

/**
 * Attach a edge to the node
 * @param {Edge} edge
 */
Node.prototype.attachEdge = function(edge) {
  if (this.edges.indexOf(edge) == -1) {
    this.edges.push(edge);
  }
  if (this.dynamicEdges.indexOf(edge) == -1) {
    this.dynamicEdges.push(edge);
  }
};

/**
 * Detach a edge from the node
 * @param {Edge} edge
 */
Node.prototype.detachEdge = function(edge) {
  var index = this.edges.indexOf(edge);
  if (index != -1) {
    this.edges.splice(index, 1);
  }
  index = this.dynamicEdges.indexOf(edge);
  if (index != -1) {
    this.dynamicEdges.splice(index, 1);
  }
};


/**
 * Set or overwrite properties for the node
 * @param {Object} properties an object with properties
 * @param {Object} constants  and object with default, global properties
 */
Node.prototype.setProperties = function(properties, constants) {
  if (!properties) {
    return;
  }

  var fields = ['borderWidth','borderWidthSelected','shape','image','brokenImage','radius','fontColor',
    'fontSize','fontFace','fontFill','fontStrokeWidth','fontStrokeColor','group','mass','fontDrawThreshold',
    'scaleFontWithValue','fontSizeMaxVisible','customScalingFunction','iconFontFace', 'icon', 'iconColor', 'iconSize'
  ];
  util.selectiveDeepExtend(fields, this.options, properties);

  // basic properties
  if (properties.id !== undefined)        {this.id = properties.id;}
  if (properties.label !== undefined)     {this.label = properties.label; this.originalLabel = properties.label;}
  if (properties.title !== undefined)     {this.title = properties.title;}
  if (properties.x !== undefined)         {this.x = properties.x; this.predefinedPosition = true;}
  if (properties.y !== undefined)         {this.y = properties.y; this.predefinedPosition = true;}
  if (properties.value !== undefined)     {this.value = properties.value;}
  if (properties.level !== undefined)     {this.level = properties.level; this.preassignedLevel = true;}

  // navigation controls properties
  if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  if (properties.verticalAlignTop    !== undefined) {this.verticalAlignTop    = properties.verticalAlignTop;}
  if (properties.triggerFunction     !== undefined) {this.triggerFunction     = properties.triggerFunction;}

  if (this.id === undefined) {
    throw "Node must have an id";
  }

  // copy group properties
  if (typeof properties.group === 'number' || (typeof properties.group === 'string' && properties.group != '')) {
    var groupObj = this.grouplist.get(properties.group);
    util.deepExtend(this.options, groupObj);
    // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case.
    this.options.color = util.parseColor(this.options.color);
  }
  // individual shape properties
  if (properties.radius !== undefined)         {this.baseRadiusValue = this.options.radius;}
  if (properties.color !== undefined)          {this.options.color = util.parseColor(properties.color);}

  if (this.options.image !== undefined && this.options.image!= "") {
    if (this.imagelist) {
      this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage);
    }
    else {
      throw "No imagelist provided";
    }
  }

  if (properties.allowedToMoveX !== undefined) {
    this.xFixed = !properties.allowedToMoveX;
    this.allowedToMoveX = properties.allowedToMoveX;
  }
  else if (properties.x !== undefined && this.allowedToMoveX == false) {
    this.xFixed = true;
  }


  if (properties.allowedToMoveY !== undefined) {
    this.yFixed = !properties.allowedToMoveY;
    this.allowedToMoveY = properties.allowedToMoveY;
  }
  else if (properties.y !== undefined && this.allowedToMoveY == false) {
    this.yFixed = true;
  }

  this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);

  if (this.options.shape === 'image' || this.options.shape === 'circularImage') {
    this.options.radiusMin = constants.nodes.widthMin;
    this.options.radiusMax = constants.nodes.widthMax;
  }

  // choose draw method depending on the shape
  switch (this.options.shape) {
    case 'database':      this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
    case 'box':           this.draw = this._drawBox; this.resize = this._resizeBox; break;
    case 'circle':        this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
    case 'ellipse':       this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
    // TODO: add diamond shape
    case 'image':         this.draw = this._drawImage; this.resize = this._resizeImage; break;
    case 'circularImage': this.draw = this._drawCircularImage; this.resize = this._resizeCircularImage; break;
    case 'text':          this.draw = this._drawText; this.resize = this._resizeText; break;
    case 'dot':           this.draw = this._drawDot; this.resize = this._resizeShape; break;
    case 'square':        this.draw = this._drawSquare; this.resize = this._resizeShape; break;
    case 'triangle':      this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
    case 'triangleDown':  this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
    case 'star':          this.draw = this._drawStar; this.resize = this._resizeShape; break;
    case 'icon':          this.draw = this._drawIcon; this.resize = this._resizeIcon; break;
    default:              this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  }
  // reset the size of the node, this can be changed
  this._reset();

};

/**
 * select this node
 */
Node.prototype.select = function() {
  this.selected = true;
  this._reset();
};

/**
 * unselect this node
 */
Node.prototype.unselect = function() {
  this.selected = false;
  this._reset();
};


/**
 * Reset the calculated size of the node, forces it to recalculate its size
 */
Node.prototype.clearSizeCache = function() {
  this._reset();
};

/**
 * Reset the calculated size of the node, forces it to recalculate its size
 * @private
 */
Node.prototype._reset = function() {
  this.width = undefined;
  this.height = undefined;
};

/**
 * get the title of this node.
 * @return {string} title    The title of the node, or undefined when no title
 *                           has been set.
 */
Node.prototype.getTitle = function() {
  return typeof this.title === "function" ? this.title() : this.title;
};

/**
 * Calculate the distance to the border of the Node
 * @param {CanvasRenderingContext2D}   ctx
 * @param {Number} angle        Angle in radians
 * @returns {number} distance   Distance to the border in pixels
 */
Node.prototype.distanceToBorder = function (ctx, angle) {
  var borderWidth = 1;

  if (!this.width) {
    this.resize(ctx);
  }

  switch (this.options.shape) {
    case 'circle':
    case 'dot':
      return this.options.radius+ borderWidth;

    case 'ellipse':
      var a = this.width / 2;
      var b = this.height / 2;
      var w = (Math.sin(angle) * a);
      var h = (Math.cos(angle) * b);
      return a * b / Math.sqrt(w * w + h * h);

    // TODO: implement distanceToBorder for database
    // TODO: implement distanceToBorder for triangle
    // TODO: implement distanceToBorder for triangleDown

    case 'box':
    case 'image':
    case 'text':
    default:
      if (this.width) {
        return Math.min(
            Math.abs(this.width / 2 / Math.cos(angle)),
            Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
        // TODO: reckon with border radius too in case of box
      }
      else {
        return 0;
      }

  }
  // TODO: implement calculation of distance to border for all shapes
};

/**
 * Set forces acting on the node
 * @param {number} fx   Force in horizontal direction
 * @param {number} fy   Force in vertical direction
 */
Node.prototype._setForce = function(fx, fy) {
  this.fx = fx;
  this.fy = fy;
};

/**
 * Add forces acting on the node
 * @param {number} fx   Force in horizontal direction
 * @param {number} fy   Force in vertical direction
 * @private
 */
Node.prototype._addForce = function(fx, fy) {
  this.fx += fx;
  this.fy += fy;
};

/**
 * Store the state before the next step
 */
Node.prototype.storeState = function() {
  this.previousState.x = this.x;
  this.previousState.y = this.y;
  this.previousState.vx = this.vx;
  this.previousState.vy = this.vy;
}

/**
 * Perform one discrete step for the node
 * @param {number} interval    Time interval in seconds
 */
Node.prototype.discreteStep = function(interval) {
  this.storeState();
  if (!this.xFixed) {
    var dx   = this.damping * this.vx;     // damping force
    var ax   = (this.fx - dx) / this.options.mass;  // acceleration
    this.vx += ax * interval;               // velocity
    this.x  += this.vx * interval;          // position
  }
  else {
    this.fx = 0;
    this.vx = 0;
  }

  if (!this.yFixed) {
    var dy   = this.damping * this.vy;     // damping force
    var ay   = (this.fy - dy) / this.options.mass;  // acceleration
    this.vy += ay * interval;               // velocity
    this.y  += this.vy * interval;          // position
  }
  else {
    this.fy = 0;
    this.vy = 0;
  }
};



/**
 * Perform one discrete step for the node
 * @param {number} interval    Time interval in seconds
 * @param {number} maxVelocity The speed limit imposed on the velocity
 */
Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
  this.storeState();
  if (!this.xFixed) {
    var dx   = this.damping * this.vx;     // damping force
    var ax   = (this.fx - dx) / this.options.mass;  // acceleration
    this.vx += ax * interval;               // velocity
    this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
    this.x  += this.vx * interval;          // position
  }
  else {
    this.fx = 0;
    this.vx = 0;
  }

  if (!this.yFixed) {
    var dy   = this.damping * this.vy;     // damping force
    var ay   = (this.fy - dy) / this.options.mass;  // acceleration
    this.vy += ay * interval;               // velocity
    this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
    this.y  += this.vy * interval;          // position
  }
  else {
    this.fy = 0;
    this.vy = 0;
  }
};

/**
 * Check if this node has a fixed x and y position
 * @return {boolean}      true if fixed, false if not
 */
Node.prototype.isFixed = function() {
  return (this.xFixed && this.yFixed);
};

/**
 * Check if this node is moving
 * @param {number} vmin   the minimum velocity considered as "moving"
 * @return {boolean}      true if moving, false if it has no velocity
 */
Node.prototype.isMoving = function(vmin) {
  var velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2));
//  this.velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2))
  return (velocity > vmin);
};

/**
 * check if this node is selecte
 * @return {boolean} selected   True if node is selected, else false
 */
Node.prototype.isSelected = function() {
  return this.selected;
};

/**
 * Retrieve the value of the node. Can be undefined
 * @return {Number} value
 */
Node.prototype.getValue = function() {
  return this.value;
};

/**
 * Calculate the distance from the nodes location to the given location (x,y)
 * @param {Number} x
 * @param {Number} y
 * @return {Number} value
 */
Node.prototype.getDistance = function(x, y) {
  var dx = this.x - x,
      dy = this.y - y;
  return Math.sqrt(dx * dx + dy * dy);
};


/**
 * Adjust the value range of the node. The node will adjust it's radius
 * based on its value.
 * @param {Number} min
 * @param {Number} max
 */
Node.prototype.setValueRange = function(min, max, total) {
  if (!this.radiusFixed && this.value !== undefined) {
    var scale = this.options.customScalingFunction(min, max, total, this.value);
    var radiusDiff = this.options.radiusMax - this.options.radiusMin;
    if (this.options.scaleFontWithValue == true) {
      var fontDiff = this.options.fontSizeMax - this.options.fontSizeMin;
      this.options.fontSize = this.options.fontSizeMin + scale * fontDiff;
    }
    this.options.radius = this.options.radiusMin + scale * radiusDiff;
  }

  this.baseRadiusValue = this.options.radius;
};

/**
 * Draw this node in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
Node.prototype.draw = function(ctx) {
  throw "Draw method not initialized for node";
};

/**
 * Recalculate the size of this node in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
Node.prototype.resize = function(ctx) {
  throw "Resize method not initialized for node";
};

/**
 * Check if this object is overlapping with the provided object
 * @param {Object} obj   an object with parameters left, top, right, bottom
 * @return {boolean}     True if location is located on node
 */
Node.prototype.isOverlappingWith = function(obj) {
  return (this.left              < obj.right  &&
          this.left + this.width > obj.left   &&
          this.top               < obj.bottom &&
          this.top + this.height > obj.top);
};

Node.prototype._resizeImage = function (ctx) {
  // TODO: pre calculate the image size

  if (!this.width || !this.height) {  // undefined or 0
    var width, height;
    if (this.value) {
      this.options.radius= this.baseRadiusValue;
      var scale = this.imageObj.height / this.imageObj.width;
      if (scale !== undefined) {
        width = this.options.radius|| this.imageObj.width;
        height = this.options.radius* scale || this.imageObj.height;
      }
      else {
        width = 0;
        height = 0;
      }
    }
    else {
      width = this.imageObj.width;
      height = this.imageObj.height;
    }
    this.width  = width;
    this.height = height;

    this.growthIndicator = 0;
    if (this.width > 0 && this.height > 0) {
      this.width  += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements)  * this.clusterSizeWidthFactor;
      this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
      this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
      this.growthIndicator = this.width - width;
    }
  }
};

Node.prototype._drawImageAtPosition = function (ctx) {
  if (this.imageObj.width != 0 ) {
    // draw the shade
    if (this.clusterSize > 1) {
      var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
      lineWidth *= this.networkScaleInv;
      lineWidth = Math.min(0.2 * this.width,lineWidth);

      ctx.globalAlpha = 0.5;
      ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
    }

    // draw the image
    ctx.globalAlpha = 1.0;
    ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  }
};

Node.prototype._drawImageLabel = function (ctx) {
  var yLabel;
  var offset = 0;
  
  if (this.height){
    offset = this.height / 2;
    var labelDimensions = this.getTextSize(ctx);
      
    if (labelDimensions.lineCount >= 1){
      offset += labelDimensions.height / 2;
      offset += 3;
    }
  }
  
  yLabel = this.y + offset;

  this._label(ctx, this.label, this.x, yLabel, undefined);
};

Node.prototype._drawImage = function (ctx) {
  this._resizeImage(ctx);
  this.left   = this.x - this.width / 2;
  this.top    = this.y - this.height / 2;

  this._drawImageAtPosition(ctx);

  this.boundingBox.top = this.top;
  this.boundingBox.left = this.left;
  this.boundingBox.right = this.left + this.width;
  this.boundingBox.bottom = this.top + this.height;

  this._drawImageLabel(ctx);
  this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left);
  this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width);
  this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height);
};

Node.prototype._resizeCircularImage = function (ctx) {
  if(!this.imageObj.src || !this.imageObj.width || !this.imageObj.height){
    if (!this.width) {
      var diameter = this.options.radius * 2;
      this.width = diameter;
      this.height = diameter;

      // scaling used for clustering
      //this.width  += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
      //this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
      this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
      this.growthIndicator = this.options.radius- 0.5*diameter;
      this._swapToImageResizeWhenImageLoaded = true;
    }
  }
  else {
    if (this._swapToImageResizeWhenImageLoaded) {
      this.width = 0;
      this.height = 0;
      delete this._swapToImageResizeWhenImageLoaded;
    }
    this._resizeImage(ctx);
  }

};

Node.prototype._drawCircularImage = function (ctx) {
  this._resizeCircularImage(ctx);

  this.left   = this.x - this.width / 2;
  this.top    = this.y - this.height / 2;
  
  var centerX = this.left + (this.width / 2);
  var centerY = this.top + (this.height / 2);
  var radius = Math.abs(this.height / 2);

  this._drawRawCircle(ctx, centerX, centerY, radius);

  ctx.save();
  ctx.circle(this.x, this.y, radius);
  ctx.stroke();
  ctx.clip();

  this._drawImageAtPosition(ctx);

  ctx.restore();

  this.boundingBox.top = this.y - this.options.radius;
  this.boundingBox.left = this.x - this.options.radius;
  this.boundingBox.right = this.x + this.options.radius;
  this.boundingBox.bottom = this.y + this.options.radius;

  this._drawImageLabel(ctx); 
  
  this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left);
  this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width);
  this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height);
};

Node.prototype._resizeBox = function (ctx) {
  if (!this.width) {
    var margin = 5;
    var textSize = this.getTextSize(ctx);
    this.width = textSize.width + 2 * margin;
    this.height = textSize.height + 2 * margin;

    this.width  += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
    this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
    this.growthIndicator = this.width - (textSize.width + 2 * margin);
//    this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;

  }
};

Node.prototype._drawBox = function (ctx) {
  this._resizeBox(ctx);

  this.left = this.x - this.width / 2;
  this.top = this.y - this.height / 2;

  var clusterLineWidth = 2.5;
  var borderWidth = this.options.borderWidth;
  var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;

  ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;

  // draw the outer border
  if (this.clusterSize > 1) {
    ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
    ctx.lineWidth *= this.networkScaleInv;
    ctx.lineWidth = Math.min(this.width,ctx.lineWidth);

    ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.options.radius);
    ctx.stroke();
  }
  ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  ctx.lineWidth *= this.networkScaleInv;
  ctx.lineWidth = Math.min(this.width,ctx.lineWidth);

  ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;

  ctx.roundRect(this.left, this.top, this.width, this.height, this.options.radius);
  ctx.fill();
  ctx.stroke();

  this.boundingBox.top = this.top;
  this.boundingBox.left = this.left;
  this.boundingBox.right = this.left + this.width;
  this.boundingBox.bottom = this.top + this.height;

  this._label(ctx, this.label, this.x, this.y);
};


Node.prototype._resizeDatabase = function (ctx) {
  if (!this.width) {
    var margin = 5;
    var textSize = this.getTextSize(ctx);
    var size = textSize.width + 2 * margin;
    this.width = size;
    this.height = size;

    // scaling used for clustering
    this.width  += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
    this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
    this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
    this.growthIndicator = this.width - size;
  }
};

Node.prototype._drawDatabase = function (ctx) {
  this._resizeDatabase(ctx);
  this.left = this.x - this.width / 2;
  this.top = this.y - this.height / 2;

  var clusterLineWidth = 2.5;
  var borderWidth = this.options.borderWidth;
  var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;

  ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;

  // draw the outer border
  if (this.clusterSize > 1) {
    ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
    ctx.lineWidth *= this.networkScaleInv;
    ctx.lineWidth = Math.min(this.width,ctx.lineWidth);

    ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth);
    ctx.stroke();
  }
  ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  ctx.lineWidth *= this.networkScaleInv;
  ctx.lineWidth = Math.min(this.width,ctx.lineWidth);

  ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
  ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  ctx.fill();
  ctx.stroke();

  this.boundingBox.top = this.top;
  this.boundingBox.left = this.left;
  this.boundingBox.right = this.left + this.width;
  this.boundingBox.bottom = this.top + this.height;

  this._label(ctx, this.label, this.x, this.y);
};


Node.prototype._resizeCircle = function (ctx) {
  if (!this.width) {
    var margin = 5;
    var textSize = this.getTextSize(ctx);
    var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
    this.options.radius = diameter / 2;

    this.width = diameter;
    this.height = diameter;

    // scaling used for clustering
//    this.width  += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
//    this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
    this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
    this.growthIndicator = this.options.radius- 0.5*diameter;
  }
};

Node.prototype._drawRawCircle = function (ctx, x, y, radius) {
  var clusterLineWidth = 2.5;
  var borderWidth = this.options.borderWidth;
  var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
    
  ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;

  // draw the outer border
  if (this.clusterSize > 1) {
    ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
    ctx.lineWidth *= this.networkScaleInv;
    ctx.lineWidth = Math.min(this.width,ctx.lineWidth);

    ctx.circle(x, y, radius+2*ctx.lineWidth);
    ctx.stroke();
  }
  ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  ctx.lineWidth *= this.networkScaleInv;
  ctx.lineWidth = Math.min(this.width,ctx.lineWidth);

  ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
  ctx.circle(this.x, this.y, radius);
  ctx.fill();
  ctx.stroke();
};

Node.prototype._drawCircle = function (ctx) {
  this._resizeCircle(ctx);
  this.left = this.x - this.width / 2;
  this.top = this.y - this.height / 2;

  this._drawRawCircle(ctx, this.x, this.y, this.options.radius);

  this.boundingBox.top = this.y - this.options.radius;
  this.boundingBox.left = this.x - this.options.radius;
  this.boundingBox.right = this.x + this.options.radius;
  this.boundingBox.bottom = this.y + this.options.radius;

  this._label(ctx, this.label, this.x, this.y);
};

Node.prototype._resizeEllipse = function (ctx) {
  if (!this.width) {
    var textSize = this.getTextSize(ctx);

    this.width = textSize.width * 1.5;
    this.height = textSize.height * 2;
    if (this.width < this.height) {
      this.width = this.height;
    }
    var defaultSize = this.width;

    // scaling used for clustering
    this.width  += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
    this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
    this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
    this.growthIndicator = this.width - defaultSize;
  }
};

Node.prototype._drawEllipse = function (ctx) {
  this._resizeEllipse(ctx);
  this.left = this.x - this.width / 2;
  this.top = this.y - this.height / 2;

  var clusterLineWidth = 2.5;
  var borderWidth = this.options.borderWidth;
  var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;

  ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;

  // draw the outer border
  if (this.clusterSize > 1) {
    ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
    ctx.lineWidth *= this.networkScaleInv;
    ctx.lineWidth = Math.min(this.width,ctx.lineWidth);

    ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
    ctx.stroke();
  }
  ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  ctx.lineWidth *= this.networkScaleInv;
  ctx.lineWidth = Math.min(this.width,ctx.lineWidth);

  ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;

  ctx.ellipse(this.left, this.top, this.width, this.height);
  ctx.fill();
  ctx.stroke();

  this.boundingBox.top = this.top;
  this.boundingBox.left = this.left;
  this.boundingBox.right = this.left + this.width;
  this.boundingBox.bottom = this.top + this.height;

  this._label(ctx, this.label, this.x, this.y);
};

Node.prototype._drawDot = function (ctx) {
  this._drawShape(ctx, 'circle');
};

Node.prototype._drawTriangle = function (ctx) {
  this._drawShape(ctx, 'triangle');
};

Node.prototype._drawTriangleDown = function (ctx) {
  this._drawShape(ctx, 'triangleDown');
};

Node.prototype._drawSquare = function (ctx) {
  this._drawShape(ctx, 'square');
};

Node.prototype._drawStar = function (ctx) {
  this._drawShape(ctx, 'star');
};

Node.prototype._resizeShape = function (ctx) {
  if (!this.width) {
    this.options.radius= this.baseRadiusValue;
    var size = 2 * this.options.radius;
    this.width = size;
    this.height = size;

    // scaling used for clustering
    this.width  += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
    this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
    this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
    this.growthIndicator = this.width - size;
  }
};

Node.prototype._drawShape = function (ctx, shape) {
  this._resizeShape(ctx);

  this.left = this.x - this.width / 2;
  this.top = this.y - this.height / 2;

  var clusterLineWidth = 2.5;
  var borderWidth = this.options.borderWidth;
  var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
  var radiusMultiplier = 2;

  // choose draw method depending on the shape
  switch (shape) {
    case 'dot':           radiusMultiplier = 2; break;
    case 'square':        radiusMultiplier = 2; break;
    case 'triangle':      radiusMultiplier = 3; break;
    case 'triangleDown':  radiusMultiplier = 3; break;
    case 'star':          radiusMultiplier = 4; break;
  }

  ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
  // draw the outer border
  if (this.clusterSize > 1) {
    ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
    ctx.lineWidth *= this.networkScaleInv;
    ctx.lineWidth = Math.min(this.width,ctx.lineWidth);

    ctx[shape](this.x, this.y, this.options.radius+ radiusMultiplier * ctx.lineWidth);
    ctx.stroke();
  }
  ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  ctx.lineWidth *= this.networkScaleInv;
  ctx.lineWidth = Math.min(this.width,ctx.lineWidth);

  ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
  ctx[shape](this.x, this.y, this.options.radius);
  ctx.fill();
  ctx.stroke();

  this.boundingBox.top = this.y - this.options.radius;
  this.boundingBox.left = this.x - this.options.radius;
  this.boundingBox.right = this.x + this.options.radius;
  this.boundingBox.bottom = this.y + this.options.radius;

  if (this.label) {
    this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'hanging',true);
    this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left);
    this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width);
    this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height);
  }
};

Node.prototype._resizeText = function (ctx) {
  if (!this.width) {
    var margin = 5;
    var textSize = this.getTextSize(ctx);
    this.width = textSize.width + 2 * margin;
    this.height = textSize.height + 2 * margin;

    // scaling used for clustering
    this.width  += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
    this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
    this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
    this.growthIndicator = this.width - (textSize.width + 2 * margin);
  }
};

Node.prototype._drawText = function (ctx) {
  this._resizeText(ctx);
  this.left = this.x - this.width / 2;
  this.top = this.y - this.height / 2;

  this._label(ctx, this.label, this.x, this.y);

  this.boundingBox.top = this.top;
  this.boundingBox.left = this.left;
  this.boundingBox.right = this.left + this.width;
  this.boundingBox.bottom = this.top + this.height;
};

Node.prototype._resizeIcon = function (ctx) {
  if (!this.width) {
    var margin = 5;
    var iconSize =
    {
      width: Number(this.options.iconSize),
      height: Number(this.options.iconSize)
    };
    this.width = iconSize.width + 2 * margin;
    this.height = iconSize.height + 2 * margin;

    // scaling used for clustering
    this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
    this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
    this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
    this.growthIndicator = this.width - (iconSize.width + 2 * margin);
  }
};

Node.prototype._drawIcon = function (ctx) {
  this._resizeIcon(ctx);

  this.options.iconSize = this.options.iconSize || 50;
  this.options.iconSize = this.options.iconSize || 50;

  this.left = this.x - this.width / 2;
  this.top = this.y - this.height / 2;
  this._icon(ctx, this.options.icon, this.x, this.y);


  this.boundingBox.top = this.y - this.options.iconSize/2;
  this.boundingBox.left = this.x - this.options.iconSize/2;
  this.boundingBox.right = this.x + this.options.iconSize/2;
  this.boundingBox.bottom = this.y + this.options.iconSize/2;

  if (this.label) {
    var iconTextSpacing = 5;
    this._label(ctx, this.label, this.x, this.y + this.height / 2 + iconTextSpacing, 'top', true);

    this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left);
    this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width);
    this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height);
  }
};

Node.prototype._icon = function (ctx, icon, x, y) {
  var relativeIconSize = Number(this.options.iconSize) * this.networkScale;
  
  if (icon && relativeIconSize > this.options.fontDrawThreshold - 1) {

      var iconSize = Number(this.options.iconSize);

      ctx.font = (this.selected ? "bold " : "") + iconSize + "px " + this.options.iconFontFace;

      // draw icon
      ctx.fillStyle = this.options.iconColor || "black";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText(icon, x, y);
  }
};
  
Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) {
  var relativeFontSize = Number(this.options.fontSize) * this.networkScale;
  if (text && relativeFontSize >= this.options.fontDrawThreshold - 1) {
    var fontSize = Number(this.options.fontSize);

    // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel)
    if (relativeFontSize >= this.options.fontSizeMaxVisible) {
      fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv;
    }

    // fade in when relative scale is between threshold and threshold - 1
    var fontColor = this.options.fontColor || "#000000";
    var strokecolor = this.options.fontStrokeColor;
    if (relativeFontSize <= this.options.fontDrawThreshold) {
      var opacity = Math.max(0,Math.min(1,1 - (this.options.fontDrawThreshold - relativeFontSize)));
      fontColor   = util.overrideOpacity(fontColor,   opacity);
      strokecolor = util.overrideOpacity(strokecolor, opacity);

    }

    ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace;

    var lines = text.split('\n');
    var lineCount = lines.length;
    var yLine = y + (1 - lineCount) / 2 * fontSize;
    if (labelUnderNode == true) {
      yLine = y + (1 - lineCount) / (2 * fontSize);
    }

    // font fill from edges now for nodes!
    var width = ctx.measureText(lines[0]).width;
    for (var i = 1; i < lineCount; i++) {
      var lineWidth = ctx.measureText(lines[i]).width;
      width = lineWidth > width ? lineWidth : width;
    }
    var height = fontSize * lineCount;
    var left = x - width / 2;
    var top = y - height / 2;
    if (baseline == "hanging") {
      top += 0.5 * fontSize;
      top += 4;   // distance from node, required because we use hanging. Hanging has less difference between browsers
      yLine += 4; // distance from node
    }
    this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine};

    // create the fontfill background
    if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") {
      ctx.fillStyle = this.options.fontFill;
      ctx.fillRect(left, top, width, height);
    }

    // draw text
    ctx.fillStyle = fontColor;
    ctx.textAlign = align || "center";
    ctx.textBaseline = baseline || "middle";
    if (this.options.fontStrokeWidth > 0){
      ctx.lineWidth   = this.options.fontStrokeWidth;
      ctx.strokeStyle = strokecolor;
      ctx.lineJoin    = 'round';
    }
    for (var i = 0; i < lineCount; i++) {
      if(this.options.fontStrokeWidth){
        ctx.strokeText(lines[i], x, yLine);
      }
      ctx.fillText(lines[i], x, yLine);
      yLine += fontSize;
    }
  }
};


Node.prototype.getTextSize = function(ctx) {
  if (this.label !== undefined) {
    var fontSize = Number(this.options.fontSize);
    if (fontSize * this.networkScale > this.options.fontSizeMaxVisible) {
      fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv;
    }
    ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace;

    var lines = this.label.split('\n'),
        height = (fontSize + 4) * lines.length,
        width = 0;

    for (var i = 0, iMax = lines.length; i < iMax; i++) {
      width = Math.max(width, ctx.measureText(lines[i]).width);
    }

    return {"width": width, "height": height, lineCount: lines.length};
  }
  else {
    return {"width": 0, "height": 0, lineCount: 0};
  }
};

/**
 * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
 * there is a safety margin of 0.3 * width;
 *
 * @returns {boolean}
 */
Node.prototype.inArea = function() {
  if (this.width !== undefined) {
  return (this.x + this.width *this.networkScaleInv  >= this.canvasTopLeft.x     &&
          this.x - this.width *this.networkScaleInv  <  this.canvasBottomRight.x &&
          this.y + this.height*this.networkScaleInv  >= this.canvasTopLeft.y     &&
          this.y - this.height*this.networkScaleInv  <  this.canvasBottomRight.y);
  }
  else {
    return true;
  }
};

/**
 * checks if the core of the node is in the display area, this is used for opening clusters around zoom
 * @returns {boolean}
 */
Node.prototype.inView = function() {
  return (this.x >= this.canvasTopLeft.x    &&
          this.x < this.canvasBottomRight.x &&
          this.y >= this.canvasTopLeft.y    &&
          this.y < this.canvasBottomRight.y);
};

/**
 * This allows the zoom level of the network to influence the rendering
 * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
 *
 * @param scale
 * @param canvasTopLeft
 * @param canvasBottomRight
 */
Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  this.networkScaleInv = 1.0/scale;
  this.networkScale = scale;
  this.canvasTopLeft = canvasTopLeft;
  this.canvasBottomRight = canvasBottomRight;
};


/**
 * This allows the zoom level of the network to influence the rendering
 *
 * @param scale
 */
Node.prototype.setScale = function(scale) {
  this.networkScaleInv = 1.0/scale;
  this.networkScale = scale;
};



/**
 * set the velocity at 0. Is called when this node is contained in another during clustering
 */
Node.prototype.clearVelocity = function() {
  this.vx = 0;
  this.vy = 0;
};


/**
 * Basic preservation of (kinectic) energy
 *
 * @param massBeforeClustering
 */
Node.prototype.updateVelocity = function(massBeforeClustering) {
  var energyBefore = this.vx * this.vx * massBeforeClustering;
  //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass);
  this.vx = Math.sqrt(energyBefore/this.options.mass);
  energyBefore = this.vy * this.vy * massBeforeClustering;
  //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass);
  this.vy = Math.sqrt(energyBefore/this.options.mass);
};

module.exports = Node;