const TARGET_TYPE_NONE = 1;
const TARGET_TYPE_INTERACTABLE = 2;
const TARGET_TYPE_UI = 4;
const TARGET_TYPE_INTERACTABLE_OR_UI = TARGET_TYPE_INTERACTABLE | TARGET_TYPE_UI;

AFRAME.registerComponent("cursor-controller", {
  dependencies: ["raycaster", "line"],
  schema: {
    cursor: { type: "selector" },
    camera: { type: "selector" },
    maxDistance: { default: 3 },
    minDistance: { default: 0 },
    cursorColorHovered: { default: "#2F80ED" },
    cursorColorUnhovered: { default: "#FFFFFF" },
    rayObject: { type: "selector" },
    useMousePos: { default: true },
    drawLine: { default: false }
  },

  init: function() {
    this.enabled = true;
    this.inVR = false;
    this.isMobile = AFRAME.utils.device.isMobile();
    this.currentTargetType = TARGET_TYPE_NONE;
    this.currentDistance = this.data.maxDistance;
    this.currentDistanceMod = 0;
    this.mousePos = new THREE.Vector2();
    this.wasCursorHovered = false;
    this.origin = new THREE.Vector3();
    this.direction = new THREE.Vector3();
    this.controllerQuaternion = new THREE.Quaternion();

    this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });

    this._handleCursorLoaded = this._handleCursorLoaded.bind(this);
    this.data.cursor.addEventListener("loaded", this._handleCursorLoaded);
  },

  enable: function() {
    this.enabled = true;
  },

  disable: function() {
    this.enabled = false;
    this.setCursorVisibility(false);
  },

  tick: (() => {
    const rayObjectRotation = new THREE.Quaternion();

    return function() {
      if (!this.enabled) {
        return;
      }

      if (this.data.useMousePos) {
        this.setRaycasterWithMousePos();
      } else {
        const rayObject = this.data.rayObject.object3D;
        rayObjectRotation.setFromRotationMatrix(rayObject.matrixWorld);
        this.direction
          .set(0, 0, 1)
          .applyQuaternion(rayObjectRotation)
          .normalize();
        this.origin.setFromMatrixPosition(rayObject.matrixWorld);
        this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction });
      }

      const isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start");
      if (isGrabbing) {
        const distance = Math.min(
          this.data.maxDistance,
          Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod)
        );
        this.direction.multiplyScalar(distance);
        this.data.cursor.object3D.position.addVectors(this.origin, this.direction);
      } else {
        this.currentDistanceMod = 0;
        this.updateDistanceAndTargetType();

        const isTarget = this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI);
        if (isTarget && !this.wasCursorHovered) {
          this.wasCursorHovered = true;
          this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered });
        } else if (!isTarget && this.wasCursorHovered) {
          this.wasCursorHovered = false;
          this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
        }
      }

      if (this.data.drawLine) {
        this.el.setAttribute("line", { start: this.origin.clone(), end: this.data.cursor.object3D.position.clone() });
      }
    };
  })(),

  setRaycasterWithMousePos: function() {
    const camera = this.data.camera.components.camera.camera;
    const raycaster = this.el.components.raycaster.raycaster;
    raycaster.setFromCamera(this.mousePos, camera);
    this.origin.copy(raycaster.ray.origin);
    this.direction.copy(raycaster.ray.direction);
    this.el.setAttribute("raycaster", { origin: raycaster.ray.origin, direction: raycaster.ray.direction });
  },

  updateDistanceAndTargetType: function() {
    let intersection = null;
    const intersections = this.el.components.raycaster.intersections;
    if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) {
      intersection = intersections[0];
      this.data.cursor.object3D.position.copy(intersection.point);
      this.currentDistance = intersections[0].distance;
    } else {
      this.currentDistance = this.data.maxDistance;
      this.direction.multiplyScalar(this.currentDistance);
      this.data.cursor.object3D.position.addVectors(this.origin, this.direction);
    }

    if (!intersection) {
      this.currentTargetType = TARGET_TYPE_NONE;
    } else if (intersection.object.el.matches(".interactable, .interactable *")) {
      this.currentTargetType = TARGET_TYPE_INTERACTABLE;
    } else if (intersection.object.el.matches(".ui, .ui *")) {
      this.currentTargetType = TARGET_TYPE_UI;
    }
  },

  _isTargetOfType: function(mask) {
    return (this.currentTargetType & mask) === this.currentTargetType;
  },

  setCursorVisibility: function(visible) {
    this.data.cursor.setAttribute("visible", visible);
    this.el.setAttribute("line", { visible: visible && this.data.drawLine });
  },

  forceCursorUpdate: function() {
    this.setRaycasterWithMousePos();
    this.el.components.raycaster.checkIntersections();
    this.updateDistanceAndTargetType();
    this.data.cursor.components["static-body"].syncToPhysics();
  },

  startInteraction: function() {
    if (this._isTargetOfType(TARGET_TYPE_INTERACTABLE_OR_UI)) {
      this.data.cursor.emit("cursor-grab", {});
      return true;
    }
    return false;
  },

  moveCursor: function(x, y) {
    this.mousePos.set(x, y);
  },

  endInteraction: function() {
    this.data.cursor.emit("cursor-release", {});
  },

  changeDistanceMod: function(delta) {
    const { minDistance, maxDistance } = this.data;
    const targetDistanceMod = this.currentDistanceMod + delta;
    const moddedDistance = this.currentDistance - targetDistanceMod;
    if (moddedDistance > maxDistance || moddedDistance < minDistance) {
      return;
    }
    this.currentDistanceMod = targetDistanceMod;
  },

  _handleCursorLoaded: function() {
    this.data.cursor.object3DMap.mesh.renderOrder = window.APP.RENDER_ORDER.CURSOR;
    this.data.cursor.removeEventListener("loaded", this._handleCursorLoaded);
  },

  remove: function() {
    this.data.cursor.removeEventListener("loaded", this._handleCursorLoaded);
  }
});