import {
  Object3D,
  PerspectiveCamera,
  Quaternion,
  Vector2,
  Vector3,
} from 'three';
import { clamp } from 'three/src/math/MathUtils';
import { deg2rad } from '../../common/convert';

export class ThirdPersonCamera {
  private currentPosition = new Vector3();
  private currentLookAt = new Vector3();
  private offsetFromPlayer = new Vector3();
  private mousePos = new Vector2();
  private prevMousePos = new Vector2();

  private angleAroundPlayer = 180;
  private pitch = -45;
  private distance = 3;

  private panning = false;
  private pinching = false;
  private locked = false;
  private previousPinchLength = 0;

  private _moveFn?: (x: number, y: number) => void;

  constructor(
    private camera: PerspectiveCamera,
    private target: Object3D,
    private eventTarget: HTMLElement,
  ) {}

  private dragEvent = (x: number, y: number) => {
    this.prevMousePos.copy(this.mousePos);
    this.mousePos = this.mousePos.fromArray([x, y]);
    const offset = this.prevMousePos.clone().sub(this.mousePos);

    if (this.panning) {
      if (this.locked && this._moveFn) {
        this._moveFn(offset.x, offset.y);
      } else {
        this.angleAroundPlayer =
          this.angleAroundPlayer + ((offset.x * 0.3) % 360);
      }

      this.pitch = clamp(this.pitch + offset.y * 0.3, -90, 90);
      this.calculateCameraOffset();
    }
  };

  events = {
    contextmenu: (e: MouseEvent) => e.preventDefault(),
    mousedown: (e: MouseEvent) => {
      if (e.button === 2) {
        this.panning = true;
      }
    },
    mouseup: (e: MouseEvent) => {
      const wasPanning = this.panning === true;
      if (e.button === 2) {
        this.panning = false;
      }

      if (wasPanning && this.locked && this._moveFn && !this.panning) {
        this._moveFn(0, 0);
      }
    },
    mouseleave: (e: MouseEvent) => {
      const wasPanning = this.panning === true;
      this.panning = false;
      if (wasPanning && this.locked && this._moveFn && !this.panning) {
        this._moveFn(0, 0);
      }
    },
    mousemove: (e: MouseEvent) => this.dragEvent(e.clientX, e.clientY),
    wheel: (e: WheelEvent) => {
      e.deltaY < 0 ? (this.distance /= 1.2) : (this.distance *= 1.2);
      this.distance = clamp(this.distance, 3, 20);
      this.calculateCameraOffset();
    },
    // mobile
    touchstart: (ev: TouchEvent) => {
      ev.preventDefault();
      const touch = ev.touches[0] || ev.changedTouches[0];
      this.mousePos.fromArray([touch.pageX, touch.pageY]);
      this.panning = true;

      if (ev.touches.length === 2) {
        this.pinching = true;
      }
    },
    touchmove: (ev: TouchEvent) => {
      ev.preventDefault();

      if (ev.touches.length === 2 && this.pinching) {
        const pinchLength = Math.hypot(
          ev.touches[0].pageX - ev.touches[1].pageX,
          ev.touches[0].pageY - ev.touches[1].pageY,
        );

        if (this.previousPinchLength) {
          const delta = pinchLength / this.previousPinchLength;
          delta > 0 ? (this.distance *= delta) : (this.distance /= delta);
          this.distance = clamp(this.distance, 3, 20);
          this.calculateCameraOffset();
        }

        this.previousPinchLength = pinchLength;
      } else if (this.panning) {
        this.dragEvent(ev.touches[0].clientX, ev.touches[0].clientY);
      }
    },
    touchend: (ev: TouchEvent) => {
      const wasPanning = this.panning === true;
      this.pinching = false;
      this.panning = false;
      this.previousPinchLength = 0;

      if (wasPanning && this.locked && this._moveFn && !this.panning) {
        this._moveFn(0, 0);
      }
    },
  };

  initialize() {
    Object.keys(this.events).forEach((key) => {
      this.eventTarget.addEventListener(key, this.events[key]);
    });
    this.calculateCameraOffset();
  }

  dispose() {
    Object.keys(this.events).forEach((key) => {
      this.eventTarget.removeEventListener(key, this.events[key]);
    });
  }

  update(dt: number) {
    const offset = this.getTargetOffset();
    const lookAt = this.getTargetLookAt();

    // https://www.youtube.com/watch?v=UuNPHOJ_V5o
    const factor = 1.0 - Math.pow(0.001, dt);

    this.currentPosition.lerp(offset, factor);
    this.currentLookAt.lerp(lookAt, factor);

    this.camera.position.copy(this.currentPosition);
    this.camera.lookAt(this.currentLookAt);
  }

  public setLock(isLocked: boolean) {
    this.locked = isLocked;
  }

  public registerAltMoveFunction(fn: (x: number, y: number) => void) {
    this._moveFn = fn;
  }

  private calculateCameraOffset() {
    const hdist = this.distance * Math.cos(deg2rad(this.pitch));
    const vdist = this.distance * Math.sin(deg2rad(this.pitch));

    this.offsetFromPlayer.set(
      hdist * Math.sin(deg2rad(this.angleAroundPlayer)),
      -vdist,
      hdist * Math.cos(deg2rad(this.angleAroundPlayer)),
    );
  }

  private getTargetOffset(): Vector3 {
    const offset = this.offsetFromPlayer.clone();
    const quat = new Quaternion();
    this.target.getWorldQuaternion(quat);
    offset.applyQuaternion(quat);
    offset.add(this.target.position);
    return offset;
  }

  private getTargetLookAt(): Vector3 {
    const offset = new Vector3(0, 1.5, 0.5);
    const quat = new Quaternion();
    this.target.getWorldQuaternion(quat);
    offset.applyQuaternion(quat);
    offset.add(this.target.position);
    return offset;
  }
}
