const VIRTUAL_JOYSTICK_HEIGHT = 0.8;
const HORIZONTAL_LOOK_SPEED = 0.005;
const VERTICAL_LOOK_SPEED = 0.003;
const PI_4 = Math.PI / 4;

export default class TouchEventsHandler {
  constructor() {
    this.cursor = null;
    this.lookControls = null;
    this.pinchEmitter = null;
    this.touches = [];
    this.touchReservedForCursor = null;
    this.touchesReservedForPinch = [];
    this.touchReservedForLookControls = null;
    this.needsPinch = false;
    this.pinchTouchId1 = -1;
    this.pinchTouchId2 = -1;

    this.registerCursor = this.registerCursor.bind(this);
    this.registerLookControls = this.registerLookControls.bind(this);
    this.isReady = this.isReady.bind(this);
    this.addEventListeners = this.addEventListeners.bind(this);
    this.handleTouchStart = this.handleTouchStart.bind(this);
    this.singleTouchStart = this.singleTouchStart.bind(this);
    this.handleTouchMove = this.handleTouchMove.bind(this);
    this.singleTouchMove = this.singleTouchMove.bind(this);
    this.handleTouchEnd = this.handleTouchEnd.bind(this);
    this.singleTouchEnd = this.singleTouchEnd.bind(this);
    this.pinch = this.pinch.bind(this);
    this.look = this.look.bind(this);
  }

  registerCursor(cursor) {
    this.cursor = cursor;
    if (this.isReady()) {
      this.addEventListeners();
    }
  }

  registerLookControls(lookControls) {
    this.lookControls = lookControls;
    if (this.isReady()) {
      this.addEventListeners();
    }
  }

  registerPinchEmitter(pinchEmitter) {
    this.pinchEmitter = pinchEmitter;
    if (this.isReady()) {
      this.addEventListeners();
    }
  }

  isReady() {
    return this.cursor && this.lookControls && this.pinchEmitter;
  }

  addEventListeners() {
    document.addEventListener("touchstart", this.handleTouchStart);
    document.addEventListener("touchmove", this.handleTouchMove);
    document.addEventListener("touchend", this.handleTouchEnd);
    document.addEventListener("touchcancel", this.handleTouchEnd);
  }

  handleTouchStart(e) {
    Array.prototype.forEach.call(e.touches, this.singleTouchStart);
  }

  singleTouchStart(touch) {
    if (touch.clientY / window.innerHeight >= VIRTUAL_JOYSTICK_HEIGHT) return;
    if (!this.touchReservedForCursor && this.cursor.handleTouchStart(touch)) {
      this.touchReservedForCursor = touch;
    }
    this.touches.push(touch);
  }

  handleTouchMove(e) {
    Array.prototype.forEach.call(e.touches, this.singleTouchMove);
    if (this.needsPinch) {
      this.pinch();
      this.needsPinch = false;
    }
  }

  singleTouchMove(touch) {
    if (this.touchReservedForCursor && touch.identifier === this.touchReservedForCursor.identifier) {
      this.cursor.handleTouchMove(touch);
      return;
    }
    if (touch.clientY / window.innerHeight >= VIRTUAL_JOYSTICK_HEIGHT) return;
    if (!this.touches.some(t => touch.identifier === t.identifier)) return;

    let pinchIndex = this.touchesReservedForPinch.findIndex(t => touch.identifier === t.identifier);
    if (pinchIndex !== -1) {
      this.touchesReservedForPinch[pinchIndex] = touch;
    } else if (this.touchesReservedForPinch.length < 2) {
      this.touchesReservedForPinch.push(touch);
      pinchIndex = this.touchesReservedForPinch.length - 1;
    }
    if (this.touchesReservedForPinch.length == 2 && pinchIndex !== -1) {
      if (this.touchReservedForLookControls && touch.identifier === this.touchReservedForLookControls.identifier) {
        this.touchReservedForLookControls = null;
      }
      this.needsPinch = true;
      return;
    }

    if (!this.touchReservedForLookControls) {
      this.touchReservedForLookControls = touch;
    }
    if (touch.identifier === this.touchReservedForLookControls.identifier) {
      if (!this.touchReservedForCursor) {
        this.cursor.handleTouchMove(touch);
      }
      this.look(this.touchReservedForLookControls, touch);
      this.touchReservedForLookControls = touch;
      return;
    }
  }

  pinch() {
    const t1 = this.touchesReservedForPinch[0];
    const t2 = this.touchesReservedForPinch[1];
    const isNewPinch = t1.identifier !== this.pinchTouchId1 || t2.identifier !== this.pinchTouchId2;
    const pinchDistance = TouchEventsHandler.distance(t1.clientX, t1.clientY, t2.clientX, t2.clientY);
    this.pinchEmitter.emit("pinch", { isNewPinch: isNewPinch, distance: pinchDistance });
    this.pinchTouchId1 = t1.identifier;
    this.pinchTouchId2 = t2.identifier;
  }

  look(prevTouch, touch) {
    const dX = touch.clientX - prevTouch.clientX;
    const dY = touch.clientY - prevTouch.clientY;

    this.lookControls.yawObject.rotation.y -= dX * HORIZONTAL_LOOK_SPEED;
    this.lookControls.pitchObject.rotation.x -= dY * VERTICAL_LOOK_SPEED;
    this.lookControls.pitchObject.rotation.x = Math.max(
      -PI_4,
      Math.min(PI_4, this.lookControls.pitchObject.rotation.x)
    );
  }

  handleTouchEnd(e) {
    Array.prototype.forEach.call(e.changedTouches, this.singleTouchEnd);
  }

  singleTouchEnd(touch) {
    if (this.touchReservedForCursor && touch.identifier === this.touchReservedForCursor.identifier) {
      this.cursor.handleTouchEnd(touch);
      this.touchReservedForCursor = null;
      return;
    }

    const touchIndex = this.touches.findIndex(t => touch.identifier === t.identifier);
    if (touchIndex === -1) return;
    this.touches.splice(touchIndex, 1);

    const pinchIndex = this.touchesReservedForPinch.findIndex(t => touch.identifier === t.identifier);
    if (pinchIndex !== -1) {
      this.touchesReservedForPinch.splice(pinchIndex, 1);
      this.pinchTouchId1 = -1;
      this.pinchTouchId2 = -1;
    }

    if (this.touchReservedForLookControls && touch.identifier === this.touchReservedForLookControls.identifier) {
      this.touchReservedForLookControls = null;
    }
  }

  static distance = (x1, y1, x2, y2) => {
    const x = x1 - x2;
    const y = y1 - y2;
    return Math.sqrt(x * x + y * y);
  };
}