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

import NavigationHandler from './components/NavigationHandler'
import Popup             from './components/Popup'

class InteractionHandler {
  constructor(body, canvas, selectionHandler) {
    this.body = body;
    this.canvas = canvas;
    this.selectionHandler = selectionHandler;
    this.navigationHandler = new NavigationHandler(body,canvas);

    // bind the events from hammer to functions in this object
    this.body.eventListeners.onTap        = this.onTap.bind(this);
    this.body.eventListeners.onTouch      = this.onTouch.bind(this);
    this.body.eventListeners.onDoubleTap  = this.onDoubleTap.bind(this);
    this.body.eventListeners.onHold       = this.onHold.bind(this);
    this.body.eventListeners.onDragStart  = this.onDragStart.bind(this);
    this.body.eventListeners.onDrag       = this.onDrag.bind(this);
    this.body.eventListeners.onDragEnd    = this.onDragEnd.bind(this);
    this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this);
    this.body.eventListeners.onPinch      = this.onPinch.bind(this);
    this.body.eventListeners.onMouseMove  = this.onMouseMove.bind(this);
    this.body.eventListeners.onRelease    = this.onRelease.bind(this);
    this.body.eventListeners.onContext    = this.onContext.bind(this);

    this.touchTime = 0;
    this.drag = {};
    this.pinch = {};
    this.popup = undefined;
    this.popupObj = undefined;
    this.popupTimer = undefined;

    this.body.functions.getPointer = this.getPointer.bind(this);

    this.options = {};
    this.defaultOptions = {
      dragNodes:true,
      dragView: true,
      hover: false,
      keyboard: {
        enabled: false,
        speed: {x: 10, y: 10, zoom: 0.02},
        bindToWindow: true
      },
      navigationButtons: false,
      tooltipDelay: 300,
      zoomView: true
    };
    util.extend(this.options,this.defaultOptions);

    this.bindEventListeners()
  }

  bindEventListeners() {
    this.body.emitter.on('destroy', () => {
      clearTimeout(this.popupTimer);
      delete this.body.functions.getPointer;
    })
  }

  setOptions(options) {
    if (options !== undefined) {
      // extend all but the values in fields
      let fields = ['hideEdgesOnDrag','hideNodesOnDrag','keyboard','multiselect','selectable','selectConnectedEdges'];
      util.selectiveNotDeepExtend(fields, this.options, options);

      // merge the keyboard options in.
      util.mergeOptions(this.options, options, 'keyboard');

      if (options.tooltip) {
        util.extend(this.options.tooltip, options.tooltip);
        if (options.tooltip.color) {
          this.options.tooltip.color = util.parseColor(options.tooltip.color);
        }
      }
    }

    this.navigationHandler.setOptions(this.options);
  }


  /**
   * Get the pointer location from a touch location
   * @param {{x: Number, y: Number}} touch
   * @return {{x: Number, y: Number}} pointer
   * @private
   */
  getPointer(touch) {
    return {
      x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas),
      y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas)
    };
  }


  /**
   * On start of a touch gesture, store the pointer
   * @param event
   * @private
   */
  onTouch(event) {
    if (new Date().valueOf() - this.touchTime > 50) {
      this.drag.pointer = this.getPointer(event.center);
      this.drag.pinched = false;
      this.pinch.scale = this.body.view.scale;
      // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame)
      this.touchTime = new Date().valueOf();
    }
  }

  /**
   * handle tap/click event: select/unselect a node
   * @private
   */
  onTap(event) {
    let pointer = this.getPointer(event.center);
    let multiselect = this.selectionHandler.options.multiselect &&
        (event.changedPointers[0].ctrlKey || event.changedPointers[0].metaKey);

    this.checkSelectionChanges(pointer, event, multiselect);
    this.selectionHandler._generateClickEvent('click', event, pointer);
  }


  /**
   * handle doubletap event
   * @private
   */
  onDoubleTap(event) {
    let pointer = this.getPointer(event.center);
    this.selectionHandler._generateClickEvent('doubleClick', event, pointer);
  }



  /**
   * handle long tap event: multi select nodes
   * @private
   */
  onHold(event) {
    let pointer = this.getPointer(event.center);
    let multiselect = this.selectionHandler.options.multiselect;

    this.checkSelectionChanges(pointer, event, multiselect);

    this.selectionHandler._generateClickEvent('click', event, pointer);
    this.selectionHandler._generateClickEvent('hold', event, pointer);
  }


  /**
   * handle the release of the screen
   *
   * @private
   */
  onRelease(event) {
    if (new Date().valueOf() - this.touchTime > 10) {
      let pointer = this.getPointer(event.center);
      this.selectionHandler._generateClickEvent('release', event, pointer);
      // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame)
      this.touchTime = new Date().valueOf();
    }
  }

  onContext(event) {
    let pointer = this.getPointer({x:event.clientX, y:event.clientY});
    this.selectionHandler._generateClickEvent('oncontext', event, pointer);
  }


  /**
   *
   * @param pointer
   * @param add
   */
  checkSelectionChanges(pointer, event, add = false) {
    let previouslySelectedEdgeCount = this.selectionHandler._getSelectedEdgeCount();
    let previouslySelectedNodeCount = this.selectionHandler._getSelectedNodeCount();
    let previousSelection = this.selectionHandler.getSelection();
    let selected;
    if (add === true) {
      selected = this.selectionHandler.selectAdditionalOnPoint(pointer);
    }
    else {
      selected = this.selectionHandler.selectOnPoint(pointer);
    }
    let selectedEdgesCount = this.selectionHandler._getSelectedEdgeCount();
    let selectedNodesCount = this.selectionHandler._getSelectedNodeCount();
    let currentSelection = this.selectionHandler.getSelection();

    let {nodesChanges, edgesChanges} = this._determineIfDifferent(previousSelection, currentSelection);
    let nodeSelected = false;

    if (selectedNodesCount - previouslySelectedNodeCount > 0) { // node was selected
      this.selectionHandler._generateClickEvent('selectNode', event, pointer);
      selected = true;
      nodeSelected = true;
    }
    else if (selectedNodesCount - previouslySelectedNodeCount < 0) { // node was deselected
      this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection);
      selected = true;
    }
    else if (selectedNodesCount === previouslySelectedNodeCount && nodesChanges === true) {
      this.selectionHandler._generateClickEvent('deselectNode', event, pointer, previousSelection);
      this.selectionHandler._generateClickEvent('selectNode', event, pointer);
      nodeSelected = true;
      selected = true;
    }

    // handle the selected edges
    if (selectedEdgesCount - previouslySelectedEdgeCount > 0 && nodeSelected === false) { // edge was selected
      this.selectionHandler._generateClickEvent('selectEdge', event, pointer);
      selected = true;
    }
    else if (selectedEdgesCount - previouslySelectedEdgeCount < 0) { // edge was deselected
      this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection);
      selected = true;
    }
    else if (selectedEdgesCount === previouslySelectedEdgeCount && edgesChanges === true) {
      this.selectionHandler._generateClickEvent('deselectEdge', event, pointer, previousSelection);
      this.selectionHandler._generateClickEvent('selectEdge', event, pointer);
      selected = true;
    }

    // fire the select event if anything has been selected or deselected
    if (selected === true) { // select or unselect
      this.selectionHandler._generateClickEvent('select', event, pointer);
    }
  }


  /**
   * This function checks if the nodes and edges previously selected have changed.
   * @param previousSelection
   * @param currentSelection
   * @returns {{nodesChanges: boolean, edgesChanges: boolean}}
   * @private
   */
  _determineIfDifferent(previousSelection,currentSelection) {
    let nodesChanges = false;
    let edgesChanges = false;

    for (let i = 0; i < previousSelection.nodes.length; i++) {
      if (currentSelection.nodes.indexOf(previousSelection.nodes[i]) === -1) {
        nodesChanges = true;
      }
    }
    for (let i = 0; i < currentSelection.nodes.length; i++) {
      if (previousSelection.nodes.indexOf(previousSelection.nodes[i]) === -1) {
        nodesChanges = true;
      }
    }
    for (let i = 0; i < previousSelection.edges.length; i++) {
      if (currentSelection.edges.indexOf(previousSelection.edges[i]) === -1) {
        edgesChanges = true;
      }
    }
    for (let i = 0; i < currentSelection.edges.length; i++) {
      if (previousSelection.edges.indexOf(previousSelection.edges[i]) === -1) {
        edgesChanges = true;
      }
    }

    return {nodesChanges, edgesChanges};
  }


  /**
   * This function is called by onDragStart.
   * It is separated out because we can then overload it for the datamanipulation system.
   *
   * @private
   */
  onDragStart(event) {
    //in case the touch event was triggered on an external div, do the initial touch now.
    if (this.drag.pointer === undefined) {
      this.onTouch(event);
    }

    // note: drag.pointer is set in onTouch to get the initial touch location
    let node = this.selectionHandler.getNodeAt(this.drag.pointer);

    this.drag.dragging = true;
    this.drag.selection = [];
    this.drag.translation = util.extend({},this.body.view.translation); // copy the object
    this.drag.nodeId = undefined;

    if (node !== undefined && this.options.dragNodes === true) {
      this.drag.nodeId = node.id;
      // select the clicked node if not yet selected
      if (node.isSelected() === false) {
        this.selectionHandler.unselectAll();
        this.selectionHandler.selectObject(node);
      }

      // after select to contain the node
      this.selectionHandler._generateClickEvent('dragStart', event, this.drag.pointer);

      let selection = this.selectionHandler.selectionObj.nodes;
      // create an array with the selected nodes and their original location and status
      for (let nodeId in selection) {
        if (selection.hasOwnProperty(nodeId)) {
          let object = selection[nodeId];
          let s = {
            id: object.id,
            node: object,

            // store original x, y, xFixed and yFixed, make the node temporarily Fixed
            x: object.x,
            y: object.y,
            xFixed: object.options.fixed.x,
            yFixed: object.options.fixed.y
          };

          object.options.fixed.x = true;
          object.options.fixed.y = true;

          this.drag.selection.push(s);
        }
      }
    }
    else {
      // fallback if no node is selected and thus the view is dragged.
      this.selectionHandler._generateClickEvent('dragStart', event, this.drag.pointer, undefined, true);
    }
  }


  /**
   * handle drag event
   * @private
   */
  onDrag(event) {
    if (this.drag.pinched === true) {
      return;
    }

    // remove the focus on node if it is focussed on by the focusOnNode
    this.body.emitter.emit('unlockNode');

    let pointer = this.getPointer(event.center);

    let selection = this.drag.selection;
    if (selection && selection.length && this.options.dragNodes === true) {
      this.selectionHandler._generateClickEvent('dragging', event, pointer);

      // calculate delta's and new location
      let deltaX = pointer.x - this.drag.pointer.x;
      let deltaY = pointer.y - this.drag.pointer.y;

      // update position of all selected nodes
      selection.forEach((selection) => {
        let node = selection.node;
        // only move the node if it was not fixed initially
        if (selection.xFixed === false) {
          node.x = this.canvas._XconvertDOMtoCanvas(this.canvas._XconvertCanvasToDOM(selection.x) + deltaX);
        }
        // only move the node if it was not fixed initially
        if (selection.yFixed === false) {
          node.y = this.canvas._YconvertDOMtoCanvas(this.canvas._YconvertCanvasToDOM(selection.y) + deltaY);
        }
      });

      // start the simulation of the physics
      this.body.emitter.emit('startSimulation');
    }
    else {
      // move the network
      if (this.options.dragView === true) {
        this.selectionHandler._generateClickEvent('dragging', event, pointer, undefined, true);

        // if the drag was not started properly because the click started outside the network div, start it now.
        if (this.drag.pointer === undefined) {
          this.onDragStart(event);
          return;
        }
        let diffX = pointer.x - this.drag.pointer.x;
        let diffY = pointer.y - this.drag.pointer.y;

        this.body.view.translation = {x:this.drag.translation.x + diffX, y:this.drag.translation.y + diffY};
        this.body.emitter.emit('_redraw');
      }
    }
  }


  /**
   * handle drag start event
   * @private
   */
  onDragEnd(event) {
    this.drag.dragging = false;
    let selection = this.drag.selection;
    if (selection && selection.length) {
      selection.forEach(function (s) {
        // restore original xFixed and yFixed
        s.node.options.fixed.x = s.xFixed;
        s.node.options.fixed.y = s.yFixed;
      });
      this.selectionHandler._generateClickEvent('dragEnd', event, this.getPointer(event.center));
      this.body.emitter.emit('startSimulation');
    }
    else {
      this.selectionHandler._generateClickEvent('dragEnd', event, this.getPointer(event.center), undefined, true);
      this.body.emitter.emit('_requestRedraw');
    }
  }



  /**
   * Handle pinch event
   * @param event
   * @private
   */
  onPinch(event) {
    let pointer = this.getPointer(event.center);

    this.drag.pinched = true;
    if (this.pinch['scale'] === undefined) {
      this.pinch.scale = 1;
    }

    // TODO: enabled moving while pinching?
    let scale = this.pinch.scale * event.scale;
    this.zoom(scale, pointer)
  }


  /**
   * Zoom the network in or out
   * @param {Number} scale a number around 1, and between 0.01 and 10
   * @param {{x: Number, y: Number}} pointer    Position on screen
   * @return {Number} appliedScale    scale is limited within the boundaries
   * @private
   */
  zoom(scale, pointer) {
    if (this.options.zoomView === true) {
      let scaleOld = this.body.view.scale;
      if (scale < 0.00001) {
        scale = 0.00001;
      }
      if (scale > 10) {
        scale = 10;
      }

      let preScaleDragPointer = undefined;
      if (this.drag !== undefined) {
        if (this.drag.dragging === true) {
          preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer);
        }
      }
      // + this.canvas.frame.canvas.clientHeight / 2
      let translation = this.body.view.translation;

      let scaleFrac = scale / scaleOld;
      let tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
      let ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;

      this.body.view.scale = scale;
      this.body.view.translation = {x:tx, y:ty};

      if (preScaleDragPointer != undefined) {
        let postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer);
        this.drag.pointer.x = postScaleDragPointer.x;
        this.drag.pointer.y = postScaleDragPointer.y;
      }

      this.body.emitter.emit('_requestRedraw');

      if (scaleOld < scale) {
        this.body.emitter.emit('zoom', {direction: '+', scale: this.body.view.scale});
      }
      else {
        this.body.emitter.emit('zoom', {direction: '-', scale: this.body.view.scale});
      }
    }
  }


  /**
   * Event handler for mouse wheel event, used to zoom the timeline
   * See http://adomas.org/javascript-mouse-wheel/
   *     https://github.com/EightMedia/hammer.js/issues/256
   * @param {MouseEvent}  event
   * @private
   */
  onMouseWheel(event) {
    if (this.options.zoomView === true) {
      // retrieve delta
      let delta = 0;
      if (event.wheelDelta) { /* IE/Opera. */
        delta = event.wheelDelta / 120;
      }
      else if (event.detail) { /* Mozilla case. */
        // In Mozilla, sign of delta is different than in IE.
        // Also, delta is multiple of 3.
        delta = -event.detail / 3;
      }

      // If delta is nonzero, handle it.
      // Basically, delta is now positive if wheel was scrolled up,
      // and negative, if wheel was scrolled down.
      if (delta !== 0) {

        // calculate the new scale
        let scale = this.body.view.scale;
        let zoom = delta / 10;
        if (delta < 0) {
          zoom = zoom / (1 - zoom);
        }
        scale *= (1 + zoom);

        // calculate the pointer location
        let pointer = this.getPointer({x: event.clientX, y: event.clientY});

        // apply the new scale
        this.zoom(scale, pointer);
      }

      // Prevent default actions caused by mouse wheel.
      event.preventDefault();
    }
  }


  /**
   * Mouse move handler for checking whether the title moves over a node with a title.
   * @param  {Event} event
   * @private
   */
  onMouseMove(event) {
    let pointer = this.getPointer({x:event.clientX, y:event.clientY});
    let popupVisible = false;

    // check if the previously selected node is still selected
    if (this.popup !== undefined) {
      if (this.popup.hidden === false) {
        this._checkHidePopup(pointer);
      }

      // if the popup was not hidden above
      if (this.popup.hidden === false) {
        popupVisible = true;
        this.popup.setPosition(pointer.x + 3, pointer.y - 5);
        this.popup.show();
      }
    }

    // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over.
    if (this.options.keyboard.bindToWindow === false && this.options.keyboard.enabled === true) {
      this.canvas.frame.focus();
    }

    // start a timeout that will check if the mouse is positioned above an element
    if (popupVisible === false) {
      if (this.popupTimer !== undefined) {
        clearInterval(this.popupTimer); // stop any running calculationTimer
        this.popupTimer = undefined;
      }
      if (!this.drag.dragging) {
        this.popupTimer = setTimeout(() => this._checkShowPopup(pointer), this.options.tooltipDelay);
      }
    }

    /**
    * Adding hover highlights
    */
    if (this.options.hover === true) {
      // adding hover highlights
      let obj = this.selectionHandler.getNodeAt(pointer);
      if (obj === undefined) {
        obj = this.selectionHandler.getEdgeAt(pointer);
      }
      this.selectionHandler.hoverObject(obj);
    }
  }



  /**
   * Check if there is an element on the given position in the network
   * (a node or edge). If so, and if this element has a title,
   * show a popup window with its title.
   *
   * @param {{x:Number, y:Number}} pointer
   * @private
   */
 _checkShowPopup(pointer) {
    let x = this.canvas._XconvertDOMtoCanvas(pointer.x);
    let y = this.canvas._YconvertDOMtoCanvas(pointer.y);
    let pointerObj = {
      left:   x,
      top:    y,
      right:  x,
      bottom: y
    };

    let previousPopupObjId = this.popupObj === undefined ? undefined : this.popupObj.id;
    let nodeUnderCursor = false;
    let popupType = 'node';

    // check if a node is under the cursor.
    if (this.popupObj === undefined) {
      // search the nodes for overlap, select the top one in case of multiple nodes
      let nodeIndices = this.body.nodeIndices;
      let nodes = this.body.nodes;
      let node;
      let overlappingNodes = [];
      for (let i = 0; i < nodeIndices.length; i++) {
        node = nodes[nodeIndices[i]];
        if (node.isOverlappingWith(pointerObj) === true) {
          if (node.getTitle() !== undefined) {
            overlappingNodes.push(nodeIndices[i]);
          }
        }
      }

      if (overlappingNodes.length > 0) {
        // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others
        this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]];
        // if you hover over a node, the title of the edge is not supposed to be shown.
        nodeUnderCursor = true;
      }
    }

    if (this.popupObj === undefined && nodeUnderCursor === false) {
      // search the edges for overlap
      let edgeIndices = this.body.edgeIndices;
      let edges = this.body.edges;
      let edge;
      let overlappingEdges = [];
      for (let i = 0; i < edgeIndices.length; i++) {
        edge = edges[edgeIndices[i]];
        if (edge.isOverlappingWith(pointerObj) === true) {
          if (edge.connected === true && edge.getTitle() !== undefined) {
            overlappingEdges.push(edgeIndices[i]);
          }
        }
      }

      if (overlappingEdges.length > 0) {
        this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]];
        popupType = 'edge';
      }
    }

    if (this.popupObj !== undefined) {
      // show popup message window
      if (this.popupObj.id !== previousPopupObjId) {
        if (this.popup === undefined) {
          this.popup = new Popup(this.canvas.frame);
        }

        this.popup.popupTargetType = popupType;
        this.popup.popupTargetId = this.popupObj.id;

        // adjust a small offset such that the mouse cursor is located in the
        // bottom left location of the popup, and you can easily move over the
        // popup area
        this.popup.setPosition(pointer.x + 3, pointer.y - 5);
        this.popup.setText(this.popupObj.getTitle());
        this.popup.show();
        this.body.emitter.emit('showPopup',this.popupObj.id);
      }
    }
    else {
      if (this.popup !== undefined) {
        this.popup.hide();
        this.body.emitter.emit('hidePopup');
      }
    }
  }


  /**
   * Check if the popup must be hidden, which is the case when the mouse is no
   * longer hovering on the object
   * @param {{x:Number, y:Number}} pointer
   * @private
   */
 _checkHidePopup(pointer) {
    let pointerObj = this.selectionHandler._pointerToPositionObject(pointer);

    let stillOnObj = false;
    if (this.popup.popupTargetType === 'node') {
      if (this.body.nodes[this.popup.popupTargetId] !== undefined) {
        stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj);

        // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it.
        // we initially only check stillOnObj because this is much faster.
        if (stillOnObj === true) {
          let overNode = this.selectionHandler.getNodeAt(pointer);
          stillOnObj = overNode.id === this.popup.popupTargetId;
        }
      }
    }
    else {
      if (this.selectionHandler.getNodeAt(pointer) === undefined) {
        if (this.body.edges[this.popup.popupTargetId] !== undefined) {
          stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj);
        }
      }
    }


    if (stillOnObj === false) {
      this.popupObj = undefined;
      this.popup.hide();
      this.body.emitter.emit('hidePopup');
    }
  }

}

export default InteractionHandler;
