// Load custom shapes into CanvasRenderingContext2D
require('./shapes');

var Emitter = require('emitter-component');
var Hammer = require('../module/hammer');
var util = require('../util');
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var dotparser = require('./dotparser');
var gephiParser = require('./gephiParser');
var Images = require('./Images');
var Activator = require('../shared/Activator');

import  Groups              from './modules/Groups';
import  NodesHandler        from './modules/NodesHandler';
import  EdgesHandler        from './modules/EdgesHandler';
import  PhysicsEngine       from './modules/PhysicsEngine';
import  ClusterEngine       from './modules/Clustering';
import  CanvasRenderer      from './modules/CanvasRenderer';
import  Canvas              from './modules/Canvas';
import  View                from './modules/View';
import  InteractionHandler  from './modules/InteractionHandler';
import  SelectionHandler    from "./modules/SelectionHandler";
import  LayoutEngine        from "./modules/LayoutEngine";
import  ManipulationSystem  from "./modules/ManipulationSystem";
import  ConfigurationSystem from "./modules/ConfigurationSystem";
import  Validator           from "./modules/Validator";
import  {printStyle}        from "./modules/Validator";
import  {allOptions, configureOptions} from './modules/components/AllOptions.js';

/**
 * @constructor Network
 * Create a network visualization, displaying nodes and edges.
 *
 * @param {Element} container   The DOM element in which the Network will
 *                                  be created. Normally a div element.
 * @param {Object} data         An object containing parameters
 *                              {Array} nodes
 *                              {Array} edges
 * @param {Object} options      Options
 */
function Network(container, data, options) {
  if (!(this instanceof Network)) {
    throw new SyntaxError('Constructor must be called with the new operator');
  }

  // set constant values
  this.options = {};
  this.defaultOptions = {
    clickToUse: false
  };
  util.extend(this.options, this.defaultOptions);

  // containers for nodes and edges
  this.body = {
    nodes: {},
    nodeIndices: [],
    edges: {},
    edgeIndices: [],
    data: {
      nodes: null,      // A DataSet or DataView
      edges: null       // A DataSet or DataView
    },
    functions: {
      createNode: function() {},
      createEdge: function() {},
      getPointer: function() {}
    },
    emitter: {
      on:   this.on.bind(this),
      off:  this.off.bind(this),
      emit: this.emit.bind(this),
      once: this.once.bind(this)
    },
    eventListeners: {
      onTap:        function() {},
      onTouch:      function() {},
      onDoubleTap:  function() {},
      onHold:       function() {},
      onDragStart:  function() {},
      onDrag:       function() {},
      onDragEnd:    function() {},
      onMouseWheel: function() {},
      onPinch:      function() {},
      onMouseMove:  function() {},
      onRelease:    function() {},
      onContext:    function() {}
    },
    container: container,
    view: {
      scale: 1,
      translation: {x: 0, y: 0}
    }
  };

  // bind the event listeners
  this.bindEventListeners();

  // setting up all modules
  this.images              = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images
  this.groups              = new Groups(); // object with groups
  this.canvas              = new Canvas(this.body);                         // DOM handler
  this.selectionHandler    = new SelectionHandler(this.body, this.canvas);  // Selection handler
  this.interactionHandler  = new InteractionHandler(this.body, this.canvas, this.selectionHandler);  // Interaction handler handles all the hammer bindings (that are bound by canvas), key
  this.view                = new View(this.body, this.canvas);              // camera handler, does animations and zooms
  this.renderer            = new CanvasRenderer(this.body, this.canvas);    // renderer, starts renderloop, has events that modules can hook into
  this.physics             = new PhysicsEngine(this.body);                  // physics engine, does all the simulations
  this.layoutEngine        = new LayoutEngine(this.body);                   // layout engine for inital layout and hierarchical layout
  this.clustering          = new ClusterEngine(this.body);                  // clustering api
  this.manipulation        = new ManipulationSystem(this.body, this.canvas, this.selectionHandler); // data manipulation system

  this.nodesHandler        = new NodesHandler(this.body, this.images, this.groups, this.layoutEngine);   // Handle adding, deleting and updating of nodes as well as global options
  this.edgesHandler        = new EdgesHandler(this.body, this.images, this.groups);   // Handle adding, deleting and updating of edges as well as global options

  // create the DOM elements
  this.canvas._create();

  // setup configuration system
  this.configurationSystem = new ConfigurationSystem(this, this.body.container, configureOptions, this.canvas.pixelRatio);

  // apply options
  this.setOptions(options);

  // load data (the disable start variable will be the same as the enabled clustering)
  this.setData(data);

}

// Extend Network with an Emitter mixin
Emitter(Network.prototype);


/**
 * Set options
 * @param {Object} options
 */
Network.prototype.setOptions = function (options) {
  if (options !== undefined) {

    let errorFound = Validator.validate(options, allOptions);
    if (errorFound === true) {
      options = {};
      console.log('%cErrors have been found in the supplied options object. None of the options will be used.', printStyle);
    }

    // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system.
    options = this.layoutEngine.setOptions(options.layout, options);

    // pass the options to the modules
    this.groups.setOptions(options.groups);
    this.nodesHandler.setOptions(options.nodes);
    this.edgesHandler.setOptions(options.edges);
    this.physics.setOptions(options.physics);
    this.canvas.setOptions(options.canvas);
    this.renderer.setOptions(options.rendering);
    this.view.setOptions(options.view);
    this.interactionHandler.setOptions(options.interaction);
    this.selectionHandler.setOptions(options.selection);
    this.clustering.setOptions(options.clustering);
    this.manipulation.setOptions(options.manipulation);

    this.configurationSystem.setOptions(options.configure);

    // if the configuration system is enabled, copy all options and put them into the config system
    if (this.configurationSystem.options.enabled === true) {
      let networkOptions = {nodes:{},edges:{},layout:{},interaction:{},manipulation:{},physics:{},selection:{},rendering:{}};
      util.deepExtend(networkOptions.nodes,        this.nodesHandler.options);
      util.deepExtend(networkOptions.edges,        this.edgesHandler.options);
      util.deepExtend(networkOptions.layout,       this.layoutEngine.options);
      util.deepExtend(networkOptions.interaction,  this.interactionHandler.options);
      util.deepExtend(networkOptions.manipulation, this.manipulation.options);
      util.deepExtend(networkOptions.physics,      this.physics.options);
      util.deepExtend(networkOptions.selection,    this.selectionHandler.options);
      util.deepExtend(networkOptions.rendering,    this.renderer.options);
      this.configurationSystem.setModuleOptions(networkOptions);
    }

    // handle network global options
    if (options.clickToUse !== undefined) {
      if (options.clickToUse === true) {
        if (this.activator === undefined) {
          this.activator = new Activator(this.frame);
          this.activator.on('change', this._createKeyBinds.bind(this));
        }
      }
      else {
        if (this.activator !== undefined) {
          this.activator.destroy();
          delete this.activator;
        }
        this.body.emitter.emit("activate");
      }
    }
    else {
      this.body.emitter.emit("activate");
    }

    this.canvas.setSize();

    // start the physics simulation. Can be safely called multiple times.
    this.body.emitter.emit("startSimulation");
  }
};


/**
 * Update the this.body.nodeIndices with the most recent node index list
 * @private
 */
Network.prototype._updateVisibleIndices = function () {
  let nodes = this.body.nodes;
  let edges = this.body.edges;
  this.body.nodeIndices = [];
  this.body.edgeIndices = [];

  for (let nodeId in nodes) {
    if (nodes.hasOwnProperty(nodeId)) {
      if (nodes[nodeId].options.hidden === false) {
        this.body.nodeIndices.push(nodeId);
      }
    }
  }

  for (let edgeId in edges) {
    if (edges.hasOwnProperty(edgeId)) {
      if (edges[edgeId].options.hidden === false) {
        this.body.edgeIndices.push(edgeId);
      }
    }
  }
};


/**
 * Bind all events
 */
Network.prototype.bindEventListeners = function () {
  // this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed.
  this.body.emitter.on("_dataChanged", () => {
    // update shortcut lists
    this._updateVisibleIndices();
    this.physics.updatePhysicsIndices();

    // call the dataUpdated event because the only difference between the two is the updating of the indices
    this.body.emitter.emit("_dataUpdated");
  });

  // this is called when options of EXISTING nodes or edges have changed.
  this.body.emitter.on("_dataUpdated", () => {
    // update values
    this._updateValueRange(this.body.nodes);
    this._updateValueRange(this.body.edges);
    // start simulation (can be called safely, even if already running)
    this.body.emitter.emit("startSimulation");
  });
};


/**
 * Set nodes and edges, and optionally options as well.
 *
 * @param {Object} data              Object containing parameters:
 *                                   {Array | DataSet | DataView} [nodes] Array with nodes
 *                                   {Array | DataSet | DataView} [edges] Array with edges
 *                                   {String} [dot] String containing data in DOT format
 *                                   {String} [gephi] String containing data in gephi JSON format
 *                                   {Options} [options] Object with options
 */
Network.prototype.setData = function (data) {
  // reset the physics engine.
  this.body.emitter.emit("resetPhysics");
  this.body.emitter.emit("_resetData");

  // unselect all to ensure no selections from old data are carried over.
  this.selectionHandler.unselectAll();

  if (data && data.dot && (data.nodes || data.edges)) {
    throw new SyntaxError('Data must contain either parameter "dot" or ' +
      ' parameter pair "nodes" and "edges", but not both.');
  }

  // set options
  this.setOptions(data && data.options);
  // set all data
  if (data && data.dot) {
    // parse DOT file
    if (data && data.dot) {
      var dotData = dotparser.DOTToGraph(data.dot);
      this.setData(dotData);
      return;
    }
  }
  else if (data && data.gephi) {
    // parse DOT file
    if (data && data.gephi) {
      var gephiData = gephiParser.parseGephi(data.gephi);
      this.setData(gephiData);
      return;
    }
  }
  else {
    this.nodesHandler.setData(data && data.nodes, true);
    this.edgesHandler.setData(data && data.edges, true);
  }

  // emit change in data
  this.body.emitter.emit("_dataChanged");

  // find a stable position or start animating to a stable position
  this.body.emitter.emit("initPhysics");
};


/**
 * Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function.
 * var network = new vis.Network(..);
 * network.destroy();
 * network = null;
 */
Network.prototype.destroy = function () {
  this.body.emitter.emit("destroy");
  // clear events
  this.body.emitter.off();
  this.off();

  // delete modules
  delete this.groups;
  delete this.canvas;
  delete this.selectionHandler;
  delete this.interactionHandler;
  delete this.view;
  delete this.renderer;
  delete this.physics;
  delete this.layoutEngine;
  delete this.clustering;
  delete this.manipulation;
  delete this.nodesHandler;
  delete this.edgesHandler;
  delete this.configurationSystem;
  delete this.images;

  // delete emitter bindings
  delete this.body.emitter.emit;
  delete this.body.emitter.on;
  delete this.body.emitter.off;
  delete this.body.emitter.once;
  delete this.body.emitter;


  for (var nodeId in this.body.nodes) {
    delete this.body.nodes[nodeId];
  }
  for (var edgeId in this.body.edges) {
    delete this.body.edges[edgeId];
  }

  // remove the container and everything inside it recursively
  util.recursiveDOMDelete(this.body.container);
};


/**
 * Update the values of all object in the given array according to the current
 * value range of the objects in the array.
 * @param {Object} obj    An object containing a set of Edges or Nodes
 *                        The objects must have a method getValue() and
 *                        setValueRange(min, max).
 * @private
 */
Network.prototype._updateValueRange = function (obj) {
  var id;

  // determine the range of the objects
  var valueMin = undefined;
  var valueMax = undefined;
  var valueTotal = 0;
  for (id in obj) {
    if (obj.hasOwnProperty(id)) {
      var value = obj[id].getValue();
      if (value !== undefined) {
        valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
        valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
        valueTotal += value;
      }
    }
  }

  // adjust the range of all objects
  if (valueMin !== undefined && valueMax !== undefined) {
    for (id in obj) {
      if (obj.hasOwnProperty(id)) {
        obj[id].setValueRange(valueMin, valueMax, valueTotal);
      }
    }
  }
};


/**
 * Returns true when the Network is active.
 * @returns {boolean}
 */
Network.prototype.isActive = function () {
  return !this.activator || this.activator.active;
};


Network.prototype.setSize             = function() {this.canvas.setSize.apply(this.canvas,arguments);};
Network.prototype.canvasToDOM         = function() {this.canvas.canvasToDOM.apply(this.canvas,arguments);};
Network.prototype.DOMtoCanvas         = function() {this.canvas.setSize.DOMtoCanvas(this.canvas,arguments);};
Network.prototype.findNode            = function() {this.clustering.findNode.apply(this.clustering,arguments);};
Network.prototype.isCluster           = function() {this.clustering.isCluster.apply(this.clustering,arguments);};
Network.prototype.openCluster         = function() {this.clustering.openCluster.apply(this.clustering,arguments);};
Network.prototype.cluster             = function() {this.clustering.cluster.apply(this.clustering,arguments);};
Network.prototype.clusterByConnection = function() {this.clustering.clusterByConnection.apply(this.clustering,arguments);};
Network.prototype.clusterByHubsize    = function() {this.clustering.clusterByHubsize.apply(this.clustering,arguments);};
Network.prototype.clusterOutliers     = function() {this.clustering.clusterOutliers.apply(this.clustering,arguments);};
Network.prototype.getSeed             = function() {this.layoutEngine.getSeed.apply(this.layoutEngine,arguments);};
Network.prototype.enableEditMode      = function() {this.manipulation.enableEditMode.apply(this.manipulation,arguments);};
Network.prototype.disableEditMode     = function() {this.manipulation.disableEditMode.apply(this.manipulation,arguments);};
Network.prototype.addNodeMode         = function() {this.manipulation.addNodeMode.apply(this.manipulation,arguments);};
Network.prototype.editNodeMode        = function() {this.manipulation.editNodeMode.apply(this.manipulation,arguments);};
Network.prototype.addEdgeMode         = function() {this.manipulation.addEdgeMode.apply(this.manipulation,arguments);};
Network.prototype.editEdgeMode        = function() {this.manipulation.editEdgeMode.apply(this.manipulation,arguments);};
Network.prototype.deleteSelected      = function() {this.manipulation.deleteSelected.apply(this.manipulation,arguments);};
Network.prototype.getPositions        = function() {this.nodesHandler.getPositions.apply(this.nodesHandler,arguments);};
Network.prototype.storePositions      = function() {this.nodesHandler.storePositions.apply(this.nodesHandler,arguments);};
Network.prototype.getBoundingBox      = function() {this.nodesHandler.getBoundingBox.apply(this.nodesHandler,arguments);};
Network.prototype.getConnectedNodes   = function() {this.nodesHandler.getConnectedNodes.apply(this.nodesHandler,arguments);};
Network.prototype.getEdges            = function() {this.nodesHandler.getEdges.apply(this.nodesHandler,arguments);};
Network.prototype.startSimulation     = function() {this.physics.startSimulation.apply(this.physics,arguments);};
Network.prototype.stopSimulation      = function() {this.physics.stopSimulation.apply(this.physics,arguments);};
Network.prototype.stabilize           = function() {this.physics.stabilize.apply(this.physics,arguments);};
Network.prototype.getSelection        = function() {this.selectionHandler.getSelection.apply(this.selectionHandler,arguments);};
Network.prototype.getSelectedNodes    = function() {this.selectionHandler.getSelectedNodes.apply(this.selectionHandler,arguments);};
Network.prototype.getSelectedEdges    = function() {this.selectionHandler.getSelectedEdges.apply(this.selectionHandler,arguments);};
Network.prototype.getNodeAt           = function() {this.selectionHandler.getNodeAt.apply(this.selectionHandler,arguments);};
Network.prototype.getEdgeAt           = function() {this.selectionHandler.getEdgeAt.apply(this.selectionHandler,arguments);};
Network.prototype.selectNodes         = function() {this.selectionHandler.selectNodes.apply(this.selectionHandler,arguments);};
Network.prototype.selectEdges         = function() {this.selectionHandler.selectEdges.apply(this.selectionHandler,arguments);};
Network.prototype.unselectAll         = function() {this.selectionHandler.unselectAll.apply(this.selectionHandler,arguments);};
Network.prototype.getScale            = function() {this.view.getScale.apply(this.view,arguments);};
Network.prototype.getPosition         = function() {this.view.getPosition.apply(this.view,arguments);};
Network.prototype.fit                 = function() {this.view.fit.apply(this.view,arguments);};
Network.prototype.moveTo              = function() {this.view.moveTo.apply(this.view,arguments);};
Network.prototype.focus               = function() {this.view.focus.apply(this.view,arguments);};
Network.prototype.releaseNode         = function() {this.view.releaseNode.apply(this.view,arguments);};

module.exports = Network;
