///////////////////////////////////////////////////////////////////////////////
// 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, Raycaster, Object3D, MathUtils } from "three";

interface WalkControlsEventMap {
  change: { type: "change" };
  walkspeedchange: { type: "walkspeedchange"; data: number };
}

export class WalkControls extends Controls<WalkControlsEventMap> {
  readonly EYE_HEIGHT = 1.7;
  readonly FAILING_DISTANCE = 2;
  readonly GROUND_FOLLOWING_SPEED = 0.05;
  readonly LOOK_SPEED = 0.1;
  readonly WALK_SPEED_DELIMITER = 4;
  readonly WHEEL_SPEED_DELIMITER = 15000;

  public movementSpeed = 0.1;
  public multiplier = 3;

  private raycaster: Raycaster;
  private groundObjects: Object3D[];

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

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

  private camera: Camera;

  constructor(camera: Camera, canvas: HTMLElement, groundObjects: Object3D[]) {
    super(camera, canvas);
    this.camera = camera;

    this.groundObjects = groundObjects;
    this.raycaster = new Raycaster();
    this.raycaster.near = 0;
    this.raycaster.far = this.EYE_HEIGHT + this.FAILING_DISTANCE;

    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: "walkspeedchange", data: this.multiplier });
        }
        break;

      case "NumpadAdd":
      case "Equal":
        if (this.multiplier < 10) {
          this.multiplier = this.multiplier + 1;
          this.dispatchEvent({ type: "walkspeedchange", 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();
  };

  private updateGroundFollowing() {
    const up = new Vector3().copy(this.camera.up);
    this.raycaster.set(this.object.position, up.negate());

    this.raycaster.params = this.raycaster.params = {
      Mesh: {},
      Line: { threshold: 0 },
      Line2: { threshold: 0 },
      LOD: { threshold: 0 },
      Points: { threshold: 0 },
      Sprite: { threshold: 0 },
    };

    const intersects = this.raycaster.intersectObjects(this.groundObjects, false);
    if (intersects.length > 0) {
      const groundY = intersects[0].point.y;
      const targetY = groundY + this.EYE_HEIGHT;

      // Smoothly interpolate the camera's y position to the target height
      this.object.position.y = MathUtils.lerp(this.object.position.y, targetY, this.GROUND_FOLLOWING_SPEED);
    }
  }

  override update() {
    let moved = false;
    let upgradeGroundFollowing = false;
    const forward = new Vector3();
    const sideways = new Vector3();

    if (this.moveKeys.size > 0) {
      upgradeGroundFollowing = true;
      const timeDelta = this.moveClock.getDelta();
      const moveDelta = (timeDelta * this.multiplier * this.movementSpeed) / this.WALK_SPEED_DELIMITER;

      this.object.getWorldDirection(forward);
      forward.y = 0;
      forward.normalize();

      sideways.setFromMatrixColumn(this.object.matrix, 0);
      sideways.y = 0;
      sideways.normalize();

      if (this.moveKeys.has("KeyW")) {
        this.object.position.addScaledVector(forward, moveDelta);
      }
      if (this.moveKeys.has("KeyS")) {
        this.object.position.addScaledVector(forward, -moveDelta);
      }

      if (this.moveKeys.has("KeyA")) {
        this.object.position.addScaledVector(sideways, -moveDelta);
      }
      if (this.moveKeys.has("KeyD")) {
        this.object.position.addScaledVector(sideways, moveDelta);
      }

      if (this.moveKeys.has("KeyQ")) {
        this.object.translateY(moveDelta);
        upgradeGroundFollowing = false;
      }
      if (this.moveKeys.has("KeyE")) {
        this.object.translateY(-moveDelta);
        upgradeGroundFollowing = false;
      }

      const lookDelta = this.LOOK_SPEED + (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;
      moved = true;
    }

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

      this.object.getWorldDirection(forward);
      forward.y = 0;
      forward.normalize();
      this.object.position.addScaledVector(forward, -moveDelta);

      this.moveWheel += -1 * Math.sign(this.moveWheel);
      moved = true;
    }

    if (upgradeGroundFollowing) this.updateGroundFollowing();

    if (moved) {
      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);
  }
}
