///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2025, Open Design Alliance (the "Alliance").
// All rights reserved.
//
// This software and its documentation and related materials are owned by
// the Alliance. The software may only be incorporated into application
// programs owned by members of the Alliance, subject to a signed
// Membership Agreement and Supplemental Software License Agreement with the
// Alliance. The structure and organization of this software are the valuable
// trade secrets of the Alliance and its suppliers. The software is also
// protected by copyright law and international treaty provisions. Application
// programs incorporating this software must include the following statement
// with their copyright notices:
//
//   This application incorporates Open Design Alliance software pursuant to a
//   license agreement with Open Design Alliance.
//   Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance.
//   All rights reserved.
//
// By use of this software, its documentation or related materials, you
// acknowledge and accept the above terms.
///////////////////////////////////////////////////////////////////////////////

import { Clock, Camera, Controls, Quaternion, Vector2, Vector3 } from "three";

interface FlyControlsEventMap {
  change: { type: "change" };
  flyspeedchange: { type: "flyspeedchange"; data: number };
}

export class FlyControls extends Controls<FlyControlsEventMap> {
  public movementSpeed = 0.2;
  public lookSpeed = 5;
  public multiplier = 5;

  private moveKeys: Set<string>;
  private moveWheel = 0;
  private moveClock: Clock;

  private quaternion: Quaternion;
  private downPosition: Vector2;
  private mouseDragOn = false;
  public rotateDelta: Vector2;

  constructor(camera: Camera, canvas: HTMLElement) {
    super(camera, canvas);

    this.moveKeys = new Set();
    this.moveClock = new Clock();

    this.quaternion = camera.quaternion.clone();
    this.downPosition = new Vector2(0, 0);
    this.rotateDelta = new Vector2(0, 0);

    this.domElement.addEventListener("pointerdown", this.onPointerDown);
    this.domElement.addEventListener("pointermove", this.onPointerMove);
    this.domElement.addEventListener("pointerup", this.onPointerUp);
    this.domElement.addEventListener("pointercancel", this.onPointerCancel);
    this.domElement.addEventListener("wheel", this.onWheel);

    window.addEventListener("keydown", this.onKeyDown);
    window.addEventListener("keyup", this.onKeyUp);
  }

  override dispose() {
    this.domElement.removeEventListener("pointerdown", this.onPointerDown);
    this.domElement.removeEventListener("pointermove", this.onPointerMove);
    this.domElement.removeEventListener("pointerup", this.onPointerUp);
    this.domElement.removeEventListener("pointercancel", this.onPointerCancel);
    this.domElement.removeEventListener("wheel", this.onWheel);

    window.removeEventListener("keydown", this.onKeyDown);
    window.removeEventListener("keyup", this.onKeyUp);

    super.dispose();
  }

  onPointerDown = (event: PointerEvent) => {
    if (event.button !== 0) return;

    this.domElement.setPointerCapture(event.pointerId);
    this.downPosition.set(event.clientX, event.clientY);
    this.quaternion.copy(this.object.quaternion);
    this.mouseDragOn = true;
  };

  onPointerMove = (event: PointerEvent) => {
    if (!this.mouseDragOn) return;

    const movePosition = new Vector2(event.clientX, event.clientY);
    if (this.downPosition.distanceTo(movePosition) === 0) return;

    this.rotateDelta.copy(this.downPosition).sub(movePosition);
    this.rotateCamera(this.rotateDelta);
    this.dispatchEvent({ type: "change" });
  };

  onPointerUp = (event: PointerEvent) => {
    this.domElement.releasePointerCapture(event.pointerId);
    this.mouseDragOn = false;
  };

  onPointerCancel = (event: PointerEvent) => {
    this.domElement.dispatchEvent(new PointerEvent("pointerup", event));
  };

  onWheel = (event: WheelEvent) => {
    this.moveWheel = event.deltaY;
    this.update();
  };

  onKeyDown = (event: KeyboardEvent) => {
    switch (event.code) {
      case "NumpadSubtract":
      case "Minus":
        if (this.multiplier > 1) {
          this.multiplier = this.multiplier - 1;
          this.dispatchEvent({ type: "flyspeedchange", data: this.multiplier });
        }
        break;

      case "NumpadAdd":
      case "Equal":
        if (this.multiplier < 10) {
          this.multiplier = this.multiplier + 1;
          this.dispatchEvent({ type: "flyspeedchange", data: this.multiplier });
        }
        break;

      case "ArrowLeft":
      case "ArrowRight":
      case "ArrowUp":
      case "ArrowDown":
      case "KeyW":
      case "KeyS":
      case "KeyA":
      case "KeyD":
      case "KeyQ":
      case "KeyE":
        this.moveKeys.add(event.code);
        this.update();
        break;
    }
  };

  onKeyUp = (event: KeyboardEvent) => {
    if (this.moveKeys.delete(event.code)) this.update();
  };

  override update() {
    if (this.moveKeys.size > 0) {
      const timeDelta = this.moveClock.getDelta();
      const moveDelta = timeDelta * this.movementSpeed * this.multiplier;

      if (this.moveKeys.has("KeyW")) this.object.translateZ(-moveDelta);
      if (this.moveKeys.has("KeyS")) this.object.translateZ(moveDelta);

      if (this.moveKeys.has("KeyA")) this.object.translateX(-moveDelta);
      if (this.moveKeys.has("KeyD")) this.object.translateX(moveDelta);

      if (this.moveKeys.has("KeyQ")) this.object.translateY(moveDelta);
      if (this.moveKeys.has("KeyE")) this.object.translateY(-moveDelta);

      const lookDelta = this.lookSpeed + (this.multiplier - 1);

      if (this.moveKeys.has("ArrowUp")) this.rotateCamera(this.rotateDelta.add(new Vector2(0, -lookDelta / 2)));
      if (this.moveKeys.has("ArrowDown")) this.rotateCamera(this.rotateDelta.add(new Vector2(0, lookDelta / 2)));

      if (this.moveKeys.has("ArrowLeft")) this.rotateCamera(this.rotateDelta.add(new Vector2(lookDelta, 0)));
      if (this.moveKeys.has("ArrowRight")) this.rotateCamera(this.rotateDelta.add(new Vector2(-lookDelta, 0)));

      this.moveWheel = 0;
      this.dispatchEvent({ type: "change" });
    }

    if (this.moveWheel !== 0) {
      const moveDelta = this.moveWheel * 0.0001 * this.movementSpeed * this.multiplier;

      this.object.translateZ(-moveDelta);
      this.moveWheel += -1 * Math.sign(this.moveWheel);
      this.dispatchEvent({ type: "change" });
    }

    if (this.moveKeys.size === 0 && this.moveWheel === 0) {
      this.moveClock.stop();
      this.moveClock.autoStart = true;
    }
  }

  rotateCamera(delta: Vector2) {
    const rotateX = (Math.PI * delta.x) / this.domElement.clientWidth;
    const rotateY = (Math.PI * delta.y) / this.domElement.clientHeight;

    const xRotation = new Quaternion();
    xRotation.setFromAxisAngle(this.object.up, rotateX);

    const yRotation = new Quaternion();
    yRotation.setFromAxisAngle(new Vector3(1, 0, 0), rotateY);

    const quaternion = this.quaternion.clone();
    quaternion.premultiply(xRotation).multiply(yRotation).normalize();

    this.object.setRotationFromQuaternion(quaternion);
  }
}
