/**
 * HUD class
 * @param {PhotoSphereViewer} psv
 * @constructor
 * @extends module:components.PSVComponent
 * @memberof module:components
 */
function PSVHUD(psv) {
  PSVComponent.call(this, psv);

  /**
   * @member {SVGElement}
   * @readonly
   */
  this.svgContainer = null;

  /**
   * @summary All registered markers
   * @member {Object.<string, PSVMarker>}
   */
  this.markers = {};

  /**
   * @summary Last selected marker
   * @member {PSVMarker}
   * @readonly
   */
  this.currentMarker = null;

  /**
   * @summary Marker under the cursor
   * @member {PSVMarker}
   * @readonly
   */
  this.hoveringMarker = null;

  /**
   * @member {Object}
   * @private
   */
  this.prop = {
    panelOpened: false,
    panelOpening: false,
    markersButton: this.psv.navbar.getNavbarButton('markers', true)
  };

  this.create();
}

PSVHUD.prototype = Object.create(PSVComponent.prototype);
PSVHUD.prototype.constructor = PSVHUD;

PSVHUD.className = 'psv-hud';
PSVHUD.publicMethods = [
  'addMarker',
  'removeMarker',
  'updateMarker',
  'clearMarkers',
  'getMarker',
  'getCurrentMarker',
  'gotoMarker',
  'hideMarker',
  'showMarker',
  'toggleMarker',
  'toggleMarkersList',
  'showMarkersList',
  'hideMarkersList'
];

/**
 * @override
 */
PSVHUD.prototype.create = function() {
  PSVComponent.prototype.create.call(this);

  this.svgContainer = document.createElementNS(PSVUtils.svgNS, 'svg');
  this.svgContainer.setAttribute('class', 'psv-hud-svg-container');
  this.container.appendChild(this.svgContainer);

  // Markers events via delegation
  this.container.addEventListener('mouseenter', this, true);
  this.container.addEventListener('mouseleave', this, true);
  this.container.addEventListener('mousemove', this, true);

  // Viewer events
  this.psv.on('click', this);
  this.psv.on('dblclick', this);
  this.psv.on('render', this);
  this.psv.on('open-panel', this);
  this.psv.on('close-panel', this);
};

/**
 * @override
 */
PSVHUD.prototype.destroy = function() {
  this.clearMarkers(false);

  this.container.removeEventListener('mouseenter', this);
  this.container.removeEventListener('mouseleave', this);
  this.container.removeEventListener('mousemove', this);

  this.psv.off('click', this);
  this.psv.off('dblclick', this);
  this.psv.off('render', this);
  this.psv.off('open-panel', this);
  this.psv.off('close-panel', this);

  delete this.svgContainer;

  PSVComponent.prototype.destroy.call(this);
};

/**
 * @summary Handles events
 * @param {Event} e
 * @private
 */
PSVHUD.prototype.handleEvent = function(e) {
  switch (e.type) {
    // @formatter:off
    case 'mouseenter':  this._onMouseEnter(e);        break;
    case 'mouseleave':  this._onMouseLeave(e);        break;
    case 'mousemove':   this._onMouseMove(e);         break;
    case 'click':       this._onClick(e.args[0], e, false); break;
    case 'dblclick':    this._onClick(e.args[0], e, true);  break;
    case 'render':      this.renderMarkers();         break;
    case 'open-panel':  this._onPanelOpened();        break;
    case 'close-panel': this._onPanelClosed();        break;
    // @formatter:on
  }
};

/**
 * @summary Adds a new marker to viewer
 * @param {Object} properties - see {@link http://photo-sphere-viewer.js.org/markers.html#config}
 * @param {boolean} [render=true] - renders the marker immediately
 * @returns {PSVMarker}
 * @throws {PSVError} when the marker's id is missing or already exists
 */
PSVHUD.prototype.addMarker = function(properties, render) {
  if (!properties.id) {
    throw new PSVError('missing marker id');
  }

  if (this.markers[properties.id]) {
    throw new PSVError('marker "' + properties.id + '" already exists');
  }

  var marker = new PSVMarker(properties, this.psv);

  if (marker.isNormal()) {
    this.container.appendChild(marker.$el);
  }
  else {
    this.svgContainer.appendChild(marker.$el);
  }

  this.markers[marker.id] = marker;

  if (render !== false) {
    this.renderMarkers();
  }

  return marker;
};

/**
 * @summary Returns the internal marker object for a marker id
 * @param {*} markerId
 * @returns {PSVMarker}
 * @throws {PSVError} when the marker cannot be found
 */
PSVHUD.prototype.getMarker = function(markerId) {
  var id = typeof markerId === 'object' ? markerId.id : markerId;

  if (!this.markers[id]) {
    throw new PSVError('cannot find marker "' + id + '"');
  }

  return this.markers[id];
};

/**
 * @summary Returns the last marker selected by the user
 * @returns {PSVMarker}
 */
PSVHUD.prototype.getCurrentMarker = function() {
  return this.currentMarker;
};

/**
 * @summary Updates the existing marker with the same id
 * @description Every property can be changed but you can't change its type (Eg: `image` to `html`).
 * @param {Object|PSVMarker} properties
 * @param {boolean} [render=true] - renders the marker immediately
 * @returns {PSVMarker}
 */
PSVHUD.prototype.updateMarker = function(properties, render) {
  var marker = this.getMarker(properties);

  marker.update(properties);

  if (render !== false) {
    this.renderMarkers();
  }

  return marker;
};

/**
 * @summary Removes a marker from the viewer
 * @param {*} marker
 * @param {boolean} [render=true] - renders the marker immediately
 */
PSVHUD.prototype.removeMarker = function(marker, render) {
  marker = this.getMarker(marker);

  if (marker.isNormal()) {
    this.container.removeChild(marker.$el);
  }
  else {
    this.svgContainer.removeChild(marker.$el);
  }

  if (this.hoveringMarker === marker) {
    this.psv.tooltip.hideTooltip();
  }

  marker.destroy();
  delete this.markers[marker.id];

  if (render !== false) {
    this.renderMarkers();
  }
};

/**
 * @summary Removes all markers
 * @param {boolean} [render=true] - renders the markers immediately
 */
PSVHUD.prototype.clearMarkers = function(render) {
  Object.keys(this.markers).forEach(function(marker) {
    this.removeMarker(marker, false);
  }, this);

  if (render !== false) {
    this.renderMarkers();
  }
};

/**
 * @summary Rotate the view to face the marker
 * @param {*} marker
 * @param {string|int} [duration] - rotates smoothy, see {@link PhotoSphereViewer#animate}
 * @fires module:components.PSVHUD.goto-marker-done
 * @return {Promise}  A promise that will be resolved when the animation finishes
 */
PSVHUD.prototype.gotoMarker = function(marker, duration) {
  marker = this.getMarker(marker);

  return this.psv.animate(marker, duration)
    .then(function() {
      /**
       * @event goto-marker-done
       * @memberof module:components.PSVHUD
       * @summary Triggered when the animation to a marker is done
       * @param {PSVMarker} marker
       */
      this.psv.trigger('goto-marker-done', marker);
    }.bind(this));
};

/**
 * @summary Hides a marker
 * @param {*} marker
 */
PSVHUD.prototype.hideMarker = function(marker) {
  this.getMarker(marker).visible = false;
  this.renderMarkers();
};

/**
 * @summary Shows a marker
 * @param {*} marker
 */
PSVHUD.prototype.showMarker = function(marker) {
  this.getMarker(marker).visible = true;
  this.renderMarkers();
};

/**
 * @summary Toggles a marker
 * @param {*} marker
 */
PSVHUD.prototype.toggleMarker = function(marker) {
  this.getMarker(marker).visible ^= true;
  this.renderMarkers();
};

/**
 * @summary Toggles the visibility of markers list
 */
PSVHUD.prototype.toggleMarkersList = function() {
  if (this.prop.panelOpened) {
    this.hideMarkersList();
  }
  else {
    this.showMarkersList();
  }
};

/**
 * @summary Opens side panel with list of markers
 * @fires module:components.PSVHUD.filter:render-markers-list
 */
PSVHUD.prototype.showMarkersList = function() {
  var markers = [];
  PSVUtils.forEach(this.markers, function(marker) {
    markers.push(marker);
  });

  /**
   * @event filter:render-markers-list
   * @memberof module:components.PSVHUD
   * @summary Used to alter the list of markers displayed on the side-panel
   * @param {PSVMarker[]} markers
   * @returns {PSVMarker[]}
   */
  var html = this.psv.config.templates.markersList({
    markers: this.psv.change('render-markers-list', markers),
    config: this.psv.config
  });

  this.prop.panelOpening = true;
  this.psv.panel.showPanel(html, true);

  this.psv.panel.container.querySelector('.psv-markers-list').addEventListener('click', this._onClickItem.bind(this));
};

/**
 * @summary Closes side panel if it contains the list of markers
 */
PSVHUD.prototype.hideMarkersList = function() {
  if (this.prop.panelOpened) {
    this.psv.panel.hidePanel();
  }
};

/**
 * @summary Updates the visibility and the position of all markers
 */
PSVHUD.prototype.renderMarkers = function() {
  var rotation = !this.psv.isGyroscopeEnabled() ? 0 : THREE.Math.radToDeg(this.psv.camera.rotation.z);

  PSVUtils.forEach(this.markers, function(marker) {
    var isVisible = marker.visible;

    if (isVisible && marker.isPoly()) {
      var positions = this._getPolyPositions(marker);
      isVisible = positions.length > (marker.isPolygon() ? 2 : 1);

      if (isVisible) {
        marker.position2D = this._getPolyDimensions(marker, positions);

        var points = positions.map(function(pos) {
          return pos.x + ',' + pos.y;
        }).join(' ');

        marker.$el.setAttributeNS(null, 'points', points);
      }
    }
    else if (isVisible) {
      var position = this._getMarkerPosition(marker);
      isVisible = this._isMarkerVisible(marker, position);

      if (isVisible) {
        marker.position2D = position;

        var scale = marker.getScale(this.psv.getZoomLevel());

        if (marker.isSvg()) {
          marker.$el.setAttributeNS(null, 'transform',
            'translate(' + position.x + ', ' + position.y + ')' +
            (scale !== 1 ? ' scale(' + scale + ', ' + scale + ')' : '') +
            (!marker.lockRotation && rotation ? ' rotate(' + rotation + ')' : '')
          );
        }
        else {
          marker.$el.style.transform = 'translate3D(' + position.x + 'px, ' + position.y + 'px, 0px)' +
            (scale !== 1 ? ' scale(' + scale + ', ' + scale + ')' : '') +
            (!marker.lockRotation && rotation ? ' rotateZ(' + rotation + 'deg)' : '');
        }
      }
    }

    PSVUtils.toggleClass(marker.$el, 'psv-marker--visible', isVisible);
  }.bind(this));
};

/**
 * @summary Determines if a point marker is visible<br>
 * It tests if the point is in the general direction of the camera, then check if it's in the viewport
 * @param {PSVMarker} marker
 * @param {PhotoSphereViewer.Point} position
 * @returns {boolean}
 * @private
 */
PSVHUD.prototype._isMarkerVisible = function(marker, position) {
  return marker.position3D.dot(this.psv.prop.direction) > 0 &&
    position.x + marker.width >= 0 &&
    position.x - marker.width <= this.psv.prop.size.width &&
    position.y + marker.height >= 0 &&
    position.y - marker.height <= this.psv.prop.size.height;
};

/**
 * @summary Computes HUD coordinates of a marker
 * @param {PSVMarker} marker
 * @returns {PhotoSphereViewer.Point}
 * @private
 */
PSVHUD.prototype._getMarkerPosition = function(marker) {
  if (marker._dynamicSize) {
    // make the marker visible to get it's size
    PSVUtils.toggleClass(marker.$el, 'psv-marker--transparent', true);
    var transform = marker.$el.style.transform;
    marker.$el.style.transform = null;

    var rect = marker.$el.getBoundingClientRect();

    marker.$el.style.transform = transform;
    PSVUtils.toggleClass(marker.$el, 'psv-marker--transparent', false);

    marker.width = rect.right - rect.left;
    marker.height = rect.bottom - rect.top;
  }

  var position = this.psv.vector3ToViewerCoords(marker.position3D);

  position.x -= marker.width * marker.anchor.left;
  position.y -= marker.height * marker.anchor.top;

  return position;
};

/**
 * @summary Computes HUD coordinates of each point of a polygon/polyline<br>
 * It handles points behind the camera by creating intermediary points suitable for the projector
 * @param {PSVMarker} marker
 * @returns {PhotoSphereViewer.Point[]}
 * @private
 */
PSVHUD.prototype._getPolyPositions = function(marker) {
  var nbVectors = marker.positions3D.length;

  // compute if each vector is visible
  var positions3D = marker.positions3D.map(function(vector) {
    return {
      vector: vector,
      visible: vector.dot(this.psv.prop.direction) > 0
    };
  }, this);

  // get pairs of visible/invisible vectors for each invisible vector connected to a visible vector
  var toBeComputed = [];
  positions3D.forEach(function(pos, i) {
    if (!pos.visible) {
      var neighbours = [
        i === 0 ? positions3D[nbVectors - 1] : positions3D[i - 1],
        i === nbVectors - 1 ? positions3D[0] : positions3D[i + 1]
      ];

      neighbours.forEach(function(neighbour) {
        if (neighbour.visible) {
          toBeComputed.push({
            visible: neighbour,
            invisible: pos,
            index: i
          });
        }
      });
    }
  });

  // compute intermediary vector for each pair (the loop is reversed for splice to insert at the right place)
  toBeComputed.reverse().forEach(function(pair) {
    positions3D.splice(pair.index, 0, {
      vector: this._getPolyIntermediaryPoint(pair.visible.vector, pair.invisible.vector),
      visible: true
    });
  }, this);

  // translate vectors to screen pos
  return positions3D
    .filter(function(pos) {
      return pos.visible;
    })
    .map(function(pos) {
      return this.psv.vector3ToViewerCoords(pos.vector);
    }, this);
};

/**
 * Given one point in the same direction of the camera and one point behind the camera,
 * computes an intermediary point on the great circle delimiting the half sphere visible by the camera.
 * The point is shifted by .01 rad because the projector cannot handle points exactly on this circle.
 * {@link http://math.stackexchange.com/a/1730410/327208}
 * @param P1 {THREE.Vector3}
 * @param P2 {THREE.Vector3}
 * @returns {THREE.Vector3}
 * @private
 */
PSVHUD.prototype._getPolyIntermediaryPoint = function(P1, P2) {
  var C = this.psv.prop.direction.clone().normalize();
  var N = new THREE.Vector3().crossVectors(P1, P2).normalize();
  var V = new THREE.Vector3().crossVectors(N, P1).normalize();
  var H = new THREE.Vector3().addVectors(P1.clone().multiplyScalar(-C.dot(V)), V.clone().multiplyScalar(C.dot(P1))).normalize();
  var a = new THREE.Vector3().crossVectors(H, C);
  return H.applyAxisAngle(a, 0.01).multiplyScalar(PhotoSphereViewer.SPHERE_RADIUS);
};

/**
 * @summary Computes the boundaries positions of a polygon/polyline marker
 * @param {PSVMarker} marker - alters width and height
 * @param {PhotoSphereViewer.Point[]} positions
 * @returns {PhotoSphereViewer.Point}
 * @private
 */
PSVHUD.prototype._getPolyDimensions = function(marker, positions) {
  var minX = +Infinity;
  var minY = +Infinity;
  var maxX = -Infinity;
  var maxY = -Infinity;

  positions.forEach(function(pos) {
    minX = Math.min(minX, pos.x);
    minY = Math.min(minY, pos.y);
    maxX = Math.max(maxX, pos.x);
    maxY = Math.max(maxY, pos.y);
  });

  marker.width = maxX - minX;
  marker.height = maxY - minY;

  return {
    x: minX,
    y: minY
  };
};

/**
 * @summary Handles mouse enter events, show the tooltip for non polygon markers
 * @param {MouseEvent} e
 * @fires module:components.PSVHUD.over-marker
 * @private
 */
PSVHUD.prototype._onMouseEnter = function(e) {
  var marker;
  if (e.target && (marker = e.target.psvMarker) && !marker.isPoly()) {
    this.hoveringMarker = marker;

    /**
     * @event over-marker
     * @memberof module:components.PSVHUD
     * @summary Triggered when the user puts the cursor hover a marker
     * @param {PSVMarker} marker
     */
    this.psv.trigger('over-marker', marker);

    if (marker.tooltip) {
      this.psv.tooltip.showTooltip({
        content: marker.tooltip.content,
        position: marker.tooltip.position,
        left: marker.position2D.x,
        top: marker.position2D.y,
        box: {
          width: marker.width,
          height: marker.height
        }
      });
    }
  }
};

/**
 * @summary Handles mouse leave events, hide the tooltip
 * @param {MouseEvent} e
 * @fires module:components.PSVHUD.leave-marker
 * @private
 */
PSVHUD.prototype._onMouseLeave = function(e) {
  var marker;
  if (e.target && (marker = e.target.psvMarker)) {
    // do not hide if we enter the tooltip itself while hovering a polygon
    if (marker.isPoly() && e.relatedTarget && PSVUtils.hasParent(e.relatedTarget, this.psv.tooltip.container)) {
      return;
    }

    /**
     * @event leave-marker
     * @memberof module:components.PSVHUD
     * @summary Triggered when the user puts the cursor away from a marker
     * @param {PSVMarker} marker
     */
    this.psv.trigger('leave-marker', marker);

    this.hoveringMarker = null;

    this.psv.tooltip.hideTooltip();
  }
};

/**
 * @summary Handles mouse move events, refresh the tooltip for polygon markers
 * @param {MouseEvent} e
 * @fires module:components.PSVHUD.leave-marker
 * @fires module:components.PSVHUD.over-marker
 * @private
 */
PSVHUD.prototype._onMouseMove = function(e) {
  if (!this.psv.prop.moving) {
    var marker;

    // do not hide if we enter the tooltip itself while hovering a polygon
    if (e.target && (marker = e.target.psvMarker) && marker.isPoly() ||
      e.target && PSVUtils.hasParent(e.target, this.psv.tooltip.container) && (marker = this.hoveringMarker)) {

      if (!this.hoveringMarker) {
        this.psv.trigger('over-marker', marker);

        this.hoveringMarker = marker;
      }

      var boundingRect = this.psv.container.getBoundingClientRect();

      if (marker.tooltip) {
        this.psv.tooltip.showTooltip({
          content: marker.tooltip.content,
          position: marker.tooltip.position,
          top: e.clientY - boundingRect.top - this.psv.config.tooltip.arrow_size / 2,
          left: e.clientX - boundingRect.left - this.psv.config.tooltip.arrow_size,
          box: { // separate the tooltip from the cursor
            width: this.psv.config.tooltip.arrow_size * 2,
            height: this.psv.config.tooltip.arrow_size * 2
          }
        });
      }
    }
    else if (this.hoveringMarker && this.hoveringMarker.isPoly()) {
      this.psv.trigger('leave-marker', this.hoveringMarker);

      this.hoveringMarker = null;

      this.psv.tooltip.hideTooltip();
    }
  }
};

/**
 * @summary Handles mouse click events, select the marker and open the panel if necessary
 * @param {Object} data
 * @param {Event} e
 * @param {boolean} dblclick
 * @fires module:components.PSVHUD.select-marker
 * @fires module:components.PSVHUD.unselect-marker
 * @private
 */
PSVHUD.prototype._onClick = function(data, e, dblclick) {
  var marker;
  if (data.target && (marker = PSVUtils.getClosest(data.target, '.psv-marker')) && marker.psvMarker) {
    this.currentMarker = marker.psvMarker;

    /**
     * @event select-marker
     * @memberof module:components.PSVHUD
     * @summary Triggered when the user clicks on a marker. The marker can be retrieved from outside the event handler
     * with {@link module:components.PSVHUD.getCurrentMarker}
     * @param {PSVMarker} marker
     * @param {boolean} dblclick - the simple click is always fired before the double click
     */
    this.psv.trigger('select-marker', this.currentMarker, dblclick);

    if (this.psv.config.click_event_on_marker) {
      // add the marker to event data
      data.marker = marker.psvMarker;
    }
    else {
      e.stopPropagation();
    }
  }
  else if (this.currentMarker) {
    /**
     * @event unselect-marker
     * @memberof module:components.PSVHUD
     * @summary Triggered when a marker was selected and the user clicks elsewhere
     * @param {PSVMarker} marker
     */
    this.psv.trigger('unselect-marker', this.currentMarker);

    this.currentMarker = null;
  }

  if (marker && marker.psvMarker && marker.psvMarker.content) {
    this.psv.panel.showPanel(marker.psvMarker.content);
  }
  else if (this.psv.panel.prop.opened) {
    e.stopPropagation();
    this.psv.panel.hidePanel();
  }
};

/**
 * @summary Clicks on an item
 * @param {MouseEvent} e
 * @fires module:components.PSVHUD.select-marker-list
 * @private
 */
PSVHUD.prototype._onClickItem = function(e) {
  var li;
  if (e.target && (li = PSVUtils.getClosest(e.target, 'li')) && li.dataset.psvMarker) {
    var marker = this.getMarker(li.dataset.psvMarker);

    /**
     * @event select-marker-list
     * @memberof module:components.PSVHUD
     * @summary Triggered when a marker is selected from the side panel
     * @param {PSVMarker} marker
     */
    this.psv.trigger('select-marker-list', marker);

    this.gotoMarker(marker, 1000);
    this.psv.panel.hidePanel();
  }
};

/**
 * @summary Updates status when the panel is updated
 * @private
 */
PSVHUD.prototype._onPanelOpened = function() {
  if (this.prop.panelOpening) {
    this.prop.panelOpening = false;
    this.prop.panelOpened = true;
  }
  else {
    this.prop.panelOpened = false;
  }

  if (this.prop.markersButton) {
    this.prop.markersButton.toggleActive(this.prop.panelOpened);
  }
};

/**
 * @summary Updates status when the panel is updated
 * @private
 */
PSVHUD.prototype._onPanelClosed = function() {
  this.prop.panelOpened = false;
  this.prop.panelOpening = false;

  if (this.prop.markersButton) {
    this.prop.markersButton.toggleActive(false);
  }
};