/**
 * Created by Alex on 2/20/2015.
 */

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

function ClusterEngine(data,options) {
  this.nodes = data.nodes;
  this.edges = data.edges;
  this.nodeIndices = data.nodeIndices;
  this.emitter = data.emitter;
  this.clusteredNodes = {};
}


/**
 *
 * @param hubsize
 * @param options
 */
ClusterEngine.prototype.clusterByConnectionCount = function(hubsize, options) {
  if (hubsize === undefined) {
    hubsize = this._getHubSize();
  }
  else if (tyepof(hubsize) == "object") {
    options = this._checkOptions(hubsize);
    hubsize = this._getHubSize();
  }

  var nodesToCluster = [];
  for (var i = 0; i < this.nodeIndices.length; i++) {
    var node = this.nodes[this.nodeIndices[i]];
    if (node.edges.length >= hubsize) {
      nodesToCluster.push(node.id);
    }
  }

  for (var i = 0; i < nodesToCluster.length; i++) {
    var node = this.nodes[nodesToCluster[i]];
    this.clusterByConnection(node,options,{},{},true);
  }
  this.emitter.emit('dataChanged');
}


/**
 * loop over all nodes, check if they adhere to the condition and cluster if needed.
 * @param options
 * @param doNotUpdateCalculationNodes
 */
ClusterEngine.prototype.clusterByNodeData = function(options, doNotUpdateCalculationNodes) {
  if (options === undefined)               {throw new Error("Cannot call clusterByNodeData without options.");}
  if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");}

  // check if the options object is fine, append if needed
  options = this._checkOptions(options);

  var childNodesObj = {};
  var childEdgesObj = {}

  // collect the nodes that will be in the cluster
  for (var i = 0; i < this.nodeIndices.length; i++) {
    var nodeId = this.nodeIndices[i];
    var clonedOptions = this._cloneOptions(nodeId);
    if (options.joinCondition(clonedOptions) == true) {
      childNodesObj[nodeId] = this.nodes[nodeId];
    }
  }

  this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
}


/**
 * Cluster all nodes in the network that have only 1 edge
 * @param options
 * @param doNotUpdateCalculationNodes
 */
ClusterEngine.prototype.clusterOutliers = function(options, doNotUpdateCalculationNodes) {
  options = this._checkOptions(options);

  var clusters = []

  // collect the nodes that will be in the cluster
  for (var i = 0; i < this.nodeIndices.length; i++) {
    var childNodesObj = {};
    var childEdgesObj = {};
    var nodeId = this.nodeIndices[i];
    if (this.nodes[nodeId].edges.length == 1) {
      var edge = this.nodes[nodeId].edges[0];
      var childNodeId = this._getConnectedId(edge, nodeId);
      if (childNodeId != nodeId) {
        if (options.joinCondition === undefined) {
          childNodesObj[nodeId] = this.nodes[nodeId];
          childNodesObj[childNodeId] = this.nodes[childNodeId];
        }
        else {
          var clonedOptions = this._cloneOptions(nodeId);
          if (options.joinCondition(clonedOptions) == true) {
            childNodesObj[nodeId] = this.nodes[nodeId];
          }
          clonedOptions = this._cloneOptions(childNodeId);
          if (options.joinCondition(clonedOptions) == true) {
            childNodesObj[childNodeId] = this.nodes[childNodeId];
          }
        }
        clusters.push({nodes:childNodesObj, edges:childEdgesObj})
      }
    }
  }

  for (var i = 0; i < clusters.length; i++) {
    this._cluster(clusters[i].nodes, clusters[i].edges, options, true)
  }

  if (doNotUpdateCalculationNodes !== true) {
    this.emitter.emit('dataChanged');
  }
}

/**
 *
 * @param nodeId
 * @param options
 * @param doNotUpdateCalculationNodes
 */
ClusterEngine.prototype.clusterByConnection = function(nodeId, options, doNotUpdateCalculationNodes) {
  // kill conditions
  if (nodeId === undefined)             {throw new Error("No nodeId supplied to clusterByConnection!");}
  if (this.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");}

  var node = this.nodes[nodeId];
  options = this._checkOptions(options, node);
  if (options.clusterNodeProperties.x === undefined)  {options.clusterNodeProperties.x = node.x; options.clusterNodeProperties.allowedToMoveX = !node.xFixed;}
  if (options.clusterNodeProperties.y === undefined)  {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;}

  var childNodesObj = {};
  var childEdgesObj = {}
  var parentNodeId = node.id;
  var parentClonedOptions = this._cloneOptions(parentNodeId);
  childNodesObj[parentNodeId] = node;

  // collect the nodes that will be in the cluster
  for (var i = 0; i < node.edges.length; i++) {
    var edge = node.edges[i];
    var childNodeId = this._getConnectedId(edge, parentNodeId);

    if (childNodeId !== parentNodeId) {
      if (options.joinCondition === undefined) {
        childEdgesObj[edge.id] = edge;
        childNodesObj[childNodeId] = this.nodes[childNodeId];
      }
      else {
        // clone the options and insert some additional parameters that could be interesting.
        var childClonedOptions = this._cloneOptions(childNodeId);
        if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) {
          childEdgesObj[edge.id] = edge;
          childNodesObj[childNodeId] = this.nodes[childNodeId];
        }
      }
    }
    else {
      childEdgesObj[edge.id] = edge;
    }
  }

  this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
}


/**
 * This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes.
 * @param objId
 * @param type
 * @returns {{}}
 * @private
 */
ClusterEngine.prototype._cloneOptions = function(objId, type) {
  var clonedOptions = {};
  if (type === undefined || type == 'node') {
    util.deepExtend(clonedOptions, this.nodes[objId].options, true);
    util.deepExtend(clonedOptions, this.nodes[objId].properties, true);
    clonedOptions.amountOfConnections = this.nodes[objId].edges.length;
  }
  else {
    util.deepExtend(clonedOptions, this.edges[objId].properties, true);
  }
  return clonedOptions;
}


/**
 * This function creates the edges that will be attached to the cluster.
 *
 * @param childNodesObj
 * @param childEdgesObj
 * @param newEdges
 * @param options
 * @private
 */
ClusterEngine.prototype._createClusterEdges = function (childNodesObj, childEdgesObj, newEdges, options) {
  var edge, childNodeId, childNode;

  var childKeys = Object.keys(childNodesObj);
  for (var i = 0; i < childKeys.length; i++) {
    childNodeId = childKeys[i];
    childNode = childNodesObj[childNodeId];

    // mark all edges for removal from global and construct new edges from the cluster to others
    for (var j = 0; j < childNode.edges.length; j++) {
      edge = childNode.edges[j];
      childEdgesObj[edge.id] = edge;

      var otherNodeId = edge.toId;
      var otherOnTo = true;
      if (edge.toId != childNodeId) {
        otherNodeId = edge.toId;
        otherOnTo = true;
      }
      else if (edge.fromId != childNodeId) {
        otherNodeId = edge.fromId;
        otherOnTo = false;
      }

      if (childNodesObj[otherNodeId] === undefined) {
        var clonedOptions = this._cloneOptions(edge.id, 'edge');
        util.deepExtend(clonedOptions, options.clusterEdgeProperties);
        // avoid forcing the default color on edges that inherit color
        if (edge.properties.color === undefined) {
          delete clonedOptions.color;
        }

        if (otherOnTo === true) {
          clonedOptions.from = options.clusterNodeProperties.id;
          clonedOptions.to = otherNodeId;
        }
        else {
          clonedOptions.from = otherNodeId;
          clonedOptions.to = options.clusterNodeProperties.id;
        }
        clonedOptions.id = 'clusterEdge:' + util.randomUUID();
        newEdges.push(new Edge(clonedOptions,this,this.constants))
      }
    }
  }
}


/**
 * This function checks the options that can be supplied to the different cluster functions
 * for certain fields and inserts defaults if needed
 * @param options
 * @returns {*}
 * @private
 */
ClusterEngine.prototype._checkOptions = function(options) {
  if (options === undefined) {options = {};}
  if (options.clusterEdgeProperties === undefined)    {options.clusterEdgeProperties = {};}
  if (options.clusterNodeProperties === undefined)    {options.clusterNodeProperties = {};}

  return options;
}

/**
 *
 * @param {Object}    childNodesObj         | object with node objects, id as keys, same as childNodes except it also contains a source node
 * @param {Object}    childEdgesObj         | object with edge objects, id as keys
 * @param {Array}     options               | object with {clusterNodeProperties, clusterEdgeProperties, processProperties}
 * @param {Boolean}   doNotUpdateCalculationNodes | when true, do not wrap up
 * @private
 */
ClusterEngine.prototype._cluster = function(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes) {
  // kill condition: no children so cant cluster
  if (Object.keys(childNodesObj).length == 0) {return;}

  // check if we have an unique id;
  if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();}
  var clusterId = options.clusterNodeProperties.id;

  // create the new edges that will connect to the cluster
  var newEdges = [];
  this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options);

  // construct the clusterNodeProperties
  var clusterNodeProperties = options.clusterNodeProperties;
  if (options.processProperties !== undefined) {
    // get the childNode options
    var childNodesOptions = [];
    for (var nodeId in childNodesObj) {
      var clonedOptions = this._cloneOptions(nodeId);
      childNodesOptions.push(clonedOptions);
    }

    // get clusterproperties based on childNodes
    var childEdgesOptions = [];
    for (var edgeId in childEdgesObj) {
      var clonedOptions = this._cloneOptions(edgeId, 'edge');
      childEdgesOptions.push(clonedOptions);
    }

    clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions);
    if (!clusterNodeProperties) {
      throw new Error("The processClusterProperties function does not return properties!");
    }
  }
  if (clusterNodeProperties.label === undefined) {
    clusterNodeProperties.label = 'cluster';
  }


  // give the clusterNode a postion if it does not have one.
  var pos = undefined
  if (clusterNodeProperties.x === undefined) {
    pos = this._getClusterPosition(childNodesObj);
    clusterNodeProperties.x = pos.x;
    clusterNodeProperties.allowedToMoveX = true;
  }
  if (clusterNodeProperties.x === undefined) {
    if (pos === undefined) {
      pos = this._getClusterPosition(childNodesObj);
    }
    clusterNodeProperties.y = pos.y;
    clusterNodeProperties.allowedToMoveY = true;
  }


  // force the ID to remain the same
  clusterNodeProperties.id = clusterId;


  // create the clusterNode
  var clusterNode = new Node(clusterNodeProperties, this.images, this.groups, this.constants);
  clusterNode.isCluster = true;
  clusterNode.containedNodes = childNodesObj;
  clusterNode.containedEdges = childEdgesObj;


  // delete contained edges from global
  for (var edgeId in childEdgesObj) {
    if (childEdgesObj.hasOwnProperty(edgeId)) {
      if (this.edges[edgeId] !== undefined) {
        if (this.edges[edgeId].via !== null) {
          var viaId = this.edges[edgeId].via.id;
          if (viaId) {
            this.edges[edgeId].via = null
            delete this.sectors['support']['nodes'][viaId];
          }
        }
        this.edges[edgeId].disconnect();
        delete this.edges[edgeId];
      }
    }
  }


  // remove contained nodes from global
  for (var nodeId in childNodesObj) {
    if (childNodesObj.hasOwnProperty(nodeId)) {
      this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.nodes[nodeId]};
      delete this.nodes[nodeId];
    }
  }


  // finally put the cluster node into global
  this.nodes[clusterNodeProperties.id] = clusterNode;


  // push new edges to global
  for (var i = 0; i < newEdges.length; i++) {
    this.edges[newEdges[i].id] = newEdges[i];
    this.edges[newEdges[i].id].connect();
  }


  // create bezier nodes for smooth curves if needed
  this._createBezierNodes(newEdges);


  // set ID to undefined so no duplicates arise
  clusterNodeProperties.id = undefined;


  // wrap up
  if (doNotUpdateCalculationNodes !== true) {
    this.emitter.emit('dataChanged');
  }
}


/**
 * Check if a node is a cluster.
 * @param nodeId
 * @returns {*}
 */
ClusterEngine.prototype.isCluster = function(nodeId) {
  if (this.nodes[nodeId] !== undefined) {
    return this.nodes[nodeId].isCluster;
  }
  else {
    console.log("Node does not exist.")
    return false;
  }

}

/**
 * get the position of the cluster node based on what's inside
 * @param {object} childNodesObj    | object with node objects, id as keys
 * @returns {{x: number, y: number}}
 * @private
 */
ClusterEngine.prototype._getClusterPosition = function(childNodesObj) {
  var childKeys = Object.keys(childNodesObj);
  var minX = childNodesObj[childKeys[0]].x;
  var maxX = childNodesObj[childKeys[0]].x;
  var minY = childNodesObj[childKeys[0]].y;
  var maxY = childNodesObj[childKeys[0]].y;
  var node;
  for (var i = 0; i < childKeys.lenght; i++) {
    node = childNodesObj[childKeys[0]];
    minX = node.x < minX ? node.x : minX;
    maxX = node.x > maxX ? node.x : maxX;
    minY = node.y < minY ? node.y : minY;
    maxY = node.y > maxY ? node.y : maxY;
  }
  return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)};
}


/**
 * Open a cluster by calling this function.
 * @param {String}  clusterNodeId               | the ID of the cluster node
 * @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true
 */
ClusterEngine.prototype.openCluster = function(clusterNodeId, doNotUpdateCalculationNodes) {
  // kill conditions
  if (clusterNodeId === undefined)             {throw new Error("No clusterNodeId supplied to openCluster.");}
  if (this.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");}
  if (this.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return};

  var node = this.nodes[clusterNodeId];
  var containedNodes = node.containedNodes;
  var containedEdges = node.containedEdges;

  // release nodes
  for (var nodeId in containedNodes) {
    if (containedNodes.hasOwnProperty(nodeId)) {
      this.nodes[nodeId] = containedNodes[nodeId];
      // inherit position
      this.nodes[nodeId].x = node.x;
      this.nodes[nodeId].y = node.y;

      // inherit speed
      this.nodes[nodeId].vx = node.vx;
      this.nodes[nodeId].vy = node.vy;

      delete this.clusteredNodes[nodeId];
    }
  }

  // release edges
  for (var edgeId in containedEdges) {
    if (containedEdges.hasOwnProperty(edgeId)) {
      this.edges[edgeId] = containedEdges[edgeId];
      this.edges[edgeId].connect();
      var edge = this.edges[edgeId];
      if (edge.connected === false) {
        if (this.clusteredNodes[edge.fromId] !== undefined) {
          this._connectEdge(edge, edge.fromId, true);
        }
        if (this.clusteredNodes[edge.toId] !== undefined) {
          this._connectEdge(edge, edge.toId, false);
        }
      }
    }
  }
  this._createBezierNodes(containedEdges);

  var edgeIds = [];
  for (var i = 0; i < node.edges.length; i++) {
    edgeIds.push(node.edges[i].id);
  }

  // remove edges in clusterNode
  for (var i = 0; i < edgeIds.length; i++) {
    var edge = this.edges[edgeIds[i]];
    // if the edge should have been connected to a contained node
    if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) {
      // the node in the from array was contained in the cluster
      if (this.nodes[edge.fromArray[0].id] !== undefined) {
        this._connectEdge(edge, edge.fromArray[0].id, true);
      }
    }
    else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) {
      // the node in the to array was contained in the cluster
      if (this.nodes[edge.toArray[0].id] !== undefined) {
        this._connectEdge(edge, edge.toArray[0].id, false);
      }
    }
    else {
      var edgeId = edgeIds[i];
      var viaId = this.edges[edgeId].via.id;
      if (viaId) {
        this.edges[edgeId].via = null
        delete this.sectors['support']['nodes'][viaId];
      }
      // this removes the edge from node.edges, which is why edgeIds is formed
      this.edges[edgeId].disconnect();
      delete this.edges[edgeId];
    }
  }

  // remove clusterNode
  delete this.nodes[clusterNodeId];

  if (doNotUpdateCalculationNodes !== true) {
    this.emitter.emit('dataChanged');
  }
}


/**
 * Recalculate navigation nodes, color edges dirty, update nodes list etc.
 * @private
 */
ClusterEngine.prototype._wrapUp = function() {

  this._updateNodeIndexList();
  this._updateCalculationNodes();
  this._markAllEdgesAsDirty();
  if (this.initializing !== true) {
    this.moving = true;
    this.start();
  }
}


/**
 * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to
 * is currently residing in cluster B
 * @param edge
 * @param nodeId
 * @param from
 * @private
 */
ClusterEngine.prototype._connectEdge = function(edge, nodeId, from) {
  var clusterStack = this._getClusterStack(nodeId);
  if (from == true) {
    edge.from = clusterStack[clusterStack.length - 1];
    edge.fromId = clusterStack[clusterStack.length - 1].id;
    clusterStack.pop()
    edge.fromArray = clusterStack;
  }
  else {
    edge.to = clusterStack[clusterStack.length - 1];
    edge.toId = clusterStack[clusterStack.length - 1].id;
    clusterStack.pop();
    edge.toArray = clusterStack;
  }
  edge.connect();
}

/**
 * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
 * @param nodeId
 * @returns {Array}
 * @private
 */
ClusterEngine.prototype._getClusterStack = function(nodeId) {
  var stack = [];
  var max = 100;
  var counter = 0;

  while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
    stack.push(this.clusteredNodes[nodeId].node);
    nodeId = this.clusteredNodes[nodeId].clusterId;
    counter++;
  }
  stack.push(this.nodes[nodeId]);
  return stack;
}


/**
 * Get the Id the node is connected to
 * @param edge
 * @param nodeId
 * @returns {*}
 * @private
 */
ClusterEngine.prototype._getConnectedId = function(edge, nodeId) {
  if (edge.toId != nodeId) {
    return edge.toId;
  }
  else if (edge.fromId != nodeId) {
    return edge.fromId;
  }
  else {
    return edge.fromId;
  }
}

/**
 * We determine how many connections denote an important hub.
 * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
 *
 * @private
 */
ClusterEngine.prototype._getHubSize = function() {
  var average = 0;
  var averageSquared = 0;
  var hubCounter = 0;
  var largestHub = 0;

  for (var i = 0; i < this.nodeIndices.length; i++) {
    var node = this.nodes[this.nodeIndices[i]];
    if (node.edges.length > largestHub) {
      largestHub = node.edges.length;
    }
    average += node.edges.length;
    averageSquared += Math.pow(node.edges.length,2);
    hubCounter += 1;
  }
  average = average / hubCounter;
  averageSquared = averageSquared / hubCounter;

  var variance = averageSquared - Math.pow(average,2);
  var standardDeviation = Math.sqrt(variance);

  var hubThreshold = Math.floor(average + 2*standardDeviation);

  // always have at least one to cluster
  if (hubThreshold > largestHub) {
    hubThreshold = largestHub;
  }

  return hubThreshold;
};





module.exports = clusterEngine