text: /* global THREE */
  var registerComponent = require('../core/component').registerComponent;
  var utils = require('../utils/');
  
  var bind = utils.bind;
  
  var EVENTS = {
    CLICK: 'click',
    FUSING: 'fusing',
    MOUSEENTER: 'mouseenter',
    MOUSEDOWN: 'mousedown',
    MOUSELEAVE: 'mouseleave',
    MOUSEUP: 'mouseup'
  };
  
  var STATES = {
    FUSING: 'cursor-fusing',
    HOVERING: 'cursor-hovering',
    HOVERED: 'cursor-hovered'
  };
  
  var CANVAS_EVENTS = {
    DOWN: ['mousedown', 'touchstart'],
    UP: ['mouseup', 'touchend']
  };
  
  /**
   * Cursor component. Applies the raycaster component specifically for starting the raycaster
   * from the camera and pointing from camera's facing direction, and then only returning the
   * closest intersection. Cursor can be fine-tuned by setting raycaster properties.
   *
   * @member {object} fuseTimeout - Timeout to trigger fuse-click.
   * @member {Element} cursorDownEl - Entity that was last mousedowned during current click.
   * @member {object} intersection - Attributes of the current intersection event, including
   *         3D- and 2D-space coordinates. See: http://threejs.org/docs/api/core/Raycaster.html
   * @member {Element} intersectedEl - Currently-intersected entity. Used to keep track to
   *         emit events when unintersecting.
   */
  module.exports.Component = registerComponent('cursor', {
    dependencies: ['raycaster'],
  
    schema: {
      downEvents: {default: []},
      fuse: {default: utils.device.isMobile()},
      fuseTimeout: {default: 1500, min: 0},
      upEvents: {default: []},
      rayOrigin: {default: 'entity', oneOf: ['mouse', 'entity']}
    },
  
    init: function () {
      var self = this;
  
      this.fuseTimeout = undefined;
      this.cursorDownEl = null;
      this.intersectedEl = null;
      this.canvasBounds = document.body.getBoundingClientRect();
  
      // Debounce.
      this.updateCanvasBounds = utils.debounce(function updateCanvasBounds () {
        self.canvasBounds = self.el.sceneEl.canvas.getBoundingClientRect();
      }, 1000);
  
      this.eventDetail = {};
      this.intersectedEventDetail = {cursorEl: this.el};
  
      // Bind methods.
      this.onCursorDown = bind(this.onCursorDown, this);
      this.onCursorUp = bind(this.onCursorUp, this);
      this.onIntersection = bind(this.onIntersection, this);
      this.onIntersectionCleared = bind(this.onIntersectionCleared, this);
      this.onMouseMove = bind(this.onMouseMove, this);
    },
  
    update: function (oldData) {
      if (this.data.rayOrigin === oldData.rayOrigin) { return; }
      this.updateMouseEventListeners();
    },
  
    play: function () {
      this.addEventListeners();
    },
  
    pause: function () {
      this.removeEventListeners();
    },
  
    remove: function () {
      var el = this.el;
      el.removeState(STATES.HOVERING);
      el.removeState(STATES.FUSING);
      clearTimeout(this.fuseTimeout);
      if (this.intersectedEl) { this.intersectedEl.removeState(STATES.HOVERED); }
      this.removeEventListeners();
    },
  
    addEventListeners: function () {
      var canvas;
      var data = this.data;
      var el = this.el;
      var self = this;
  
      function addCanvasListeners () {
        canvas = el.sceneEl.canvas;
        if (data.downEvents.length || data.upEvents.length) { return; }
        CANVAS_EVENTS.DOWN.forEach(function (downEvent) {
          canvas.addEventListener(downEvent, self.onCursorDown);
        });
        CANVAS_EVENTS.UP.forEach(function (upEvent) {
          canvas.addEventListener(upEvent, self.onCursorUp);
        });
      }
  
      canvas = el.sceneEl.canvas;
      if (canvas) {
        addCanvasListeners();
      } else {
        el.sceneEl.addEventListener('render-target-loaded', addCanvasListeners);
      }
  
      data.downEvents.forEach(function (downEvent) {
        el.addEventListener(downEvent, self.onCursorDown);
      });
      data.upEvents.forEach(function (upEvent) {
        el.addEventListener(upEvent, self.onCursorUp);
      });
      el.addEventListener('raycaster-intersection', this.onIntersection);
      el.addEventListener('raycaster-intersection-cleared', this.onIntersectionCleared);
  
      window.addEventListener('resize', this.updateCanvasBounds);
    },
  
    removeEventListeners: function () {
      var canvas;
      var data = this.data;
      var el = this.el;
      var self = this;
  
      canvas = el.sceneEl.canvas;
      if (canvas && !data.downEvents.length && !data.upEvents.length) {
        CANVAS_EVENTS.DOWN.forEach(function (downEvent) {
          canvas.removeEventListener(downEvent, self.onCursorDown);
        });
        CANVAS_EVENTS.UP.forEach(function (upEvent) {
          canvas.removeEventListener(upEvent, self.onCursorUp);
        });
      }
  
      data.downEvents.forEach(function (downEvent) {
        el.removeEventListener(downEvent, self.onCursorDown);
      });
      data.upEvents.forEach(function (upEvent) {
        el.removeEventListener(upEvent, self.onCursorUp);
      });
      el.removeEventListener('raycaster-intersection', this.onIntersection);
      el.removeEventListener('raycaster-intersection-cleared', this.onIntersectionCleared);
      canvas.removeEventListener('mousemove', this.onMouseMove);
      canvas.removeEventListener('touchstart', this.onMouseMove);
      canvas.removeEventListener('touchmove', this.onMouseMove);
      canvas.removeEventListener('resize', this.updateCanvasBounds);
    },
  
    updateMouseEventListeners: function () {
      var canvas;
      var el = this.el;
  
      canvas = el.sceneEl.canvas;
      canvas.removeEventListener('mousemove', this.onMouseMove);
      canvas.removeEventListener('touchmove', this.onMouseMove);
      el.setAttribute('raycaster', 'useWorldCoordinates', false);
      if (this.data.rayOrigin !== 'mouse') { return; }
      canvas.addEventListener('mousemove', this.onMouseMove, false);
      canvas.addEventListener('touchmove', this.onMouseMove, false);
      el.setAttribute('raycaster', 'useWorldCoordinates', true);
      this.updateCanvasBounds();
    },
  
    onMouseMove: (function () {
      var direction = new THREE.Vector3();
      var mouse = new THREE.Vector2();
      var origin = new THREE.Vector3();
      var rayCasterConfig = {origin: origin, direction: direction};
  
      return function (evt) {
        var bounds = this.canvasBounds;
        var camera = this.el.sceneEl.camera;
        var left;
        var point;
        var top;
  
        camera.parent.updateMatrixWorld();
  
        // Calculate mouse position based on the canvas element
        if (evt.type === 'touchmove' || evt.type === 'touchstart') {
          // Track the first touch for simplicity.
          point = evt.touches.item(0);
        } else {
          point = evt;
        }
  
        left = point.clientX - bounds.left;
        top = point.clientY - bounds.top;
        mouse.x = (left / bounds.width) * 2 - 1;
        mouse.y = -(top / bounds.height) * 2 + 1;
  
        origin.setFromMatrixPosition(camera.matrixWorld);
        direction.set(mouse.x, mouse.y, 0.5).unproject(camera).sub(origin).normalize();
        this.el.setAttribute('raycaster', rayCasterConfig);
        if (evt.type === 'touchmove') { evt.preventDefault(); }
      };
    })(),
  
    /**
     * Trigger mousedown and keep track of the mousedowned entity.
     */
    onCursorDown: function (evt) {
      // Raycast again for touch.
      if (this.data.rayOrigin === 'mouse' && evt.type === 'touchstart') {
        this.onMouseMove(evt);
        this.el.components.raycaster.checkIntersections();
        evt.preventDefault();
      }
  
      this.twoWayEmit(EVENTS.MOUSEDOWN);
      this.cursorDownEl = this.intersectedEl;
    },
  
    /**
     * Trigger mouseup if:
     * - Not fusing (mobile has no mouse).
     * - Currently intersecting an entity.
     * - Currently-intersected entity is the same as the one when mousedown was triggered,
     *   in case user mousedowned one entity, dragged to another, and mouseupped.
     */
    onCursorUp: function (evt) {
      this.twoWayEmit(EVENTS.MOUSEUP);
  
      // If intersected entity has changed since the cursorDown, still emit mouseUp on the
      // previously cursorUp entity.
      if (this.cursorDownEl && this.cursorDownEl !== this.intersectedEl) {
        this.intersectedEventDetail.intersection = null;
        this.cursorDownEl.emit(EVENTS.MOUSEUP, this.intersectedEventDetail);
      }
  
      if (!this.data.fuse && this.intersectedEl && this.cursorDownEl === this.intersectedEl) {
        this.twoWayEmit(EVENTS.CLICK);
      }
  
      this.cursorDownEl = null;
      if (evt.type === 'touchend') { evt.preventDefault(); }
    },
  
    /**
     * Handle intersection.
     */
    onIntersection: function (evt) {
      var currentIntersection;
      var cursorEl = this.el;
      var index;
      var intersectedEl;
      var intersection;
  
      // Select closest object, excluding the cursor.
      index = evt.detail.els[0] === cursorEl ? 1 : 0;
      intersection = evt.detail.intersections[index];
      intersectedEl = evt.detail.els[index];
  
      // If cursor is the only intersected object, ignore the event.
      if (!intersectedEl) { return; }
  
      // Already intersecting this entity.
      if (this.intersectedEl === intersectedEl) { return; }
  
      // Ignore events further away than active intersection.
      if (this.intersectedEl) {
        currentIntersection = this.el.components.raycaster.getIntersection(this.intersectedEl);
        if (currentIntersection.distance <= intersection.distance) { return; }
      }
  
      // Unset current intersection.
      this.clearCurrentIntersection(true);
  
      this.setIntersection(intersectedEl, intersection);
    },
  
    /**
     * Handle intersection cleared.
     */
    onIntersectionCleared: function (evt) {
      var clearedEls = evt.detail.clearedEls;
      // Check if the current intersection has ended
      if (clearedEls.indexOf(this.intersectedEl) === -1) { return; }
      this.clearCurrentIntersection();
    },
  
    setIntersection: function (intersectedEl, intersection) {
      var cursorEl = this.el;
      var data = this.data;
      var self = this;
  
      // Already intersecting.
      if (this.intersectedEl === intersectedEl) { return; }
  
      // Set new intersection.
      this.intersectedEl = intersectedEl;
  
      // Hovering.
      cursorEl.addState(STATES.HOVERING);
      intersectedEl.addState(STATES.HOVERED);
      this.twoWayEmit(EVENTS.MOUSEENTER);
  
      // Begin fuse if necessary.
      if (data.fuseTimeout === 0 || !data.fuse) { return; }
      cursorEl.addState(STATES.FUSING);
      this.twoWayEmit(EVENTS.FUSING);
      this.fuseTimeout = setTimeout(function fuse () {
        cursorEl.removeState(STATES.FUSING);
        self.twoWayEmit(EVENTS.CLICK);
      }, data.fuseTimeout);
    },
  
    clearCurrentIntersection: function (ignoreRemaining) {
      var index;
      var intersection;
      var intersections;
      var cursorEl = this.el;
  
      // Nothing to be cleared.
      if (!this.intersectedEl) { return; }
  
      // No longer hovering (or fusing).
      this.intersectedEl.removeState(STATES.HOVERED);
      cursorEl.removeState(STATES.HOVERING);
      cursorEl.removeState(STATES.FUSING);
      this.twoWayEmit(EVENTS.MOUSELEAVE);
  
      // Unset intersected entity (after emitting the event).
      this.intersectedEl = null;
  
      // Clear fuseTimeout.
      clearTimeout(this.fuseTimeout);
  
      // Set intersection to another raycasted element if any.
      if (ignoreRemaining === true) { return; }
      intersections = this.el.components.raycaster.intersections;
      if (intersections.length === 0) { return; }
      // Exclude the cursor.
      index = intersections[0].object.el === cursorEl ? 1 : 0;
      intersection = intersections[index];
      if (!intersection) { return; }
      this.setIntersection(intersection.object.el, intersection);
    },
  
    /**
     * Helper to emit on both the cursor and the intersected entity (if exists).
     */
    twoWayEmit: function (evtName) {
      var el = this.el;
      var intersectedEl = this.intersectedEl;
      var intersection;
  
      intersection = this.el.components.raycaster.getIntersection(intersectedEl);
      this.eventDetail.intersectedEl = intersectedEl;
      this.eventDetail.intersection = intersection;
      el.emit(evtName, this.eventDetail);
  
      if (!intersectedEl) { return; }
  
      this.intersectedEventDetail.intersection = intersection;
      intersectedEl.emit(evtName, this.intersectedEventDetail);
    }
  });
  