///////////////////////////////////////////////////////////////////////////////
// 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.
///////////////////////////////////////////////////////////////////////////////

// ===================== AI-CODE-FILE ======================
// Source: Claude Sonnet 4.5
// Date: 2025-10-07
// Reviewer: vitaly.ivanov@opendesign.com
// Issue: CLOUD-5851
// Notes: Originally AI-generated, modified manually
// =========================================================

import { Camera, Clock, Controls, Vector2, Vector3, Raycaster, Object3D, MathUtils } from "three";

interface JoyStickControlsEventMap {
  change: { type: "change" };
}

export class JoyStickControls extends Controls<JoyStickControlsEventMap> {
  readonly EYE_HEIGHT = 1.7;
  readonly FAILING_DISTANCE = 2;
  readonly GROUND_FOLLOWING_SPEED = 0.05;
  readonly WALK_SPEED_DELIMITER = 4;

  public movementSpeed = 0.1;
  public multiplier = 3;

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

  private canvas: HTMLCanvasElement;
  private overlayElement: HTMLDivElement;
  private joyStickCanvas: HTMLCanvasElement;
  private context: CanvasRenderingContext2D;

  private moveClock: Clock;
  private camera: Camera;

  private joyStickPosition: Vector2;
  private isActive: boolean = false;

  private readonly MAX_JOYSTICK_DISTANCE = 100;
  private readonly INTERNAL_RADIUS = 35;
  private readonly MAX_MOVE_STICK = 40;
  private readonly EXTERNAL_RADIUS = 65;
  private readonly CANVAS_SIZE = 200;

  private centerX: number;
  private centerY: number;
  private pressed: boolean = false;

  constructor(camera: Camera, domElement: HTMLElement, canvasElement: HTMLCanvasElement, groundObjects: Object3D[]) {
    super(camera, domElement);
    this.camera = camera;
    this.canvas = canvasElement;
    this.moveClock = new Clock(false);
    this.joyStickPosition = new Vector2(0, 0);

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

    this.centerX = this.CANVAS_SIZE / 2;
    this.centerY = this.CANVAS_SIZE / 2;

    this.overlayElement = document.createElement("div");
    this.overlayElement.id = "joyStickDiv";
    this.overlayElement.style.background = "rgba(0,0,0,0)";
    this.overlayElement.style.position = "fixed";
    this.overlayElement.style.zIndex = "0";
    this.overlayElement.style.touchAction = "none";
    canvasElement.parentElement.appendChild(this.overlayElement);

    this.joyStickCanvas = document.createElement("canvas");
    this.joyStickCanvas.id = "joyStickCanvas";
    this.joyStickCanvas.width = this.CANVAS_SIZE;
    this.joyStickCanvas.height = this.CANVAS_SIZE;
    this.overlayElement.appendChild(this.joyStickCanvas);
    this.context = this.joyStickCanvas.getContext("2d")!;

    this.joyStickCanvas.addEventListener("pointerdown", this.onPointerDown);
    document.addEventListener("pointermove", this.onPointerMove);
    document.addEventListener("pointerup", this.onPointerUp);

    window.addEventListener("resize", this.onResize);
    document.addEventListener("fullscreenchange", this.onResize);

    this.updateVisibility();
    this.updatePosition();
    this.draw();
  }

  override dispose() {
    this.joyStickCanvas.removeEventListener("pointerdown", this.onPointerDown);
    document.removeEventListener("pointermove", this.onPointerMove);
    document.removeEventListener("pointerup", this.onPointerUp);

    window.removeEventListener("resize", this.onResize);
    document.removeEventListener("fullscreenchange", this.onResize);

    this.overlayElement.remove();
    super.dispose();
  }

  private onPointerDown = (event: PointerEvent) => {
    event.preventDefault();
    this.pressed = true;
  };

  private onPointerMove = (event: PointerEvent) => {
    event.preventDefault();
    if (!this.pressed) return;

    let movedX = event.pageX;
    let movedY = event.pageY;

    if (
      this.joyStickCanvas.offsetParent &&
      (this.joyStickCanvas.offsetParent as HTMLElement).tagName.toUpperCase() === "BODY"
    ) {
      movedX -= this.joyStickCanvas.offsetLeft;
      movedY -= this.joyStickCanvas.offsetTop;
    } else if (this.joyStickCanvas.offsetParent) {
      movedX -= (this.joyStickCanvas.offsetParent as HTMLElement).offsetLeft;
      movedY -= (this.joyStickCanvas.offsetParent as HTMLElement).offsetTop;
    }

    const x = 100 * ((movedX - this.centerX) / this.MAX_MOVE_STICK);
    const y = 100 * ((movedY - this.centerY) / this.MAX_MOVE_STICK) * -1;

    const distance = Math.sqrt(x * x + y * y);
    if (distance > 20) {
      this.joyStickPosition.set(x, y);
      this.isActive = true;
    } else {
      this.joyStickPosition.set(0, 0);
      this.isActive = false;
    }

    this.draw();
    this.moveClock.start();
    this.update();
  };

  private onPointerUp = (event: PointerEvent) => {
    event.preventDefault();
    this.pressed = false;
    this.joyStickPosition.set(0, 0);
    this.isActive = false;
    this.moveClock.stop();
    this.draw();
  };

  private onResize = () => {
    this.updateVisibility();
    this.updatePosition();
  };

  private updateVisibility() {
    const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
    const isNarrowScreen = window.innerWidth < 1024;

    if (isMobile || isNarrowScreen) {
      this.overlayElement.style.display = "block";
    } else {
      this.overlayElement.style.display = "none";
    }
  }

  private updatePosition() {
    const rect = this.canvas.getBoundingClientRect();
    this.overlayElement.style.top = `${rect.height - this.CANVAS_SIZE}px`;
    this.overlayElement.style.left = `${rect.left}px`;
    this.overlayElement.style.width = `${this.CANVAS_SIZE}px`;
    this.overlayElement.style.height = `${this.CANVAS_SIZE}px`;
  }

  private draw() {
    this.context.clearRect(0, 0, this.CANVAS_SIZE, this.CANVAS_SIZE);

    // Draw external circle
    this.context.beginPath();
    this.context.arc(this.centerX, this.centerY, this.EXTERNAL_RADIUS, 0, 2 * Math.PI, false);
    this.context.lineWidth = 2;
    this.context.strokeStyle = "#35436E";
    this.context.globalAlpha = 0.5;
    this.context.stroke();

    // Draw internal circle
    let movedX = this.centerX + (this.joyStickPosition.x * this.MAX_MOVE_STICK) / 100;
    let movedY = this.centerY - (this.joyStickPosition.y * this.MAX_MOVE_STICK) / 100;

    movedX = Math.max(this.MAX_MOVE_STICK, Math.min(this.CANVAS_SIZE - this.MAX_MOVE_STICK, movedX));
    movedY = Math.max(this.MAX_MOVE_STICK, Math.min(this.CANVAS_SIZE - this.MAX_MOVE_STICK, movedY));

    this.context.beginPath();
    this.context.arc(movedX, movedY, this.INTERNAL_RADIUS, 0, 2 * Math.PI, false);
    this.context.fillStyle = "#35436E";
    this.context.lineWidth = 2;
    this.context.strokeStyle = "#35436E";
    this.context.globalAlpha = 0.5;
    this.context.fill();
    this.context.stroke();
  }

  private updateGroundFollowing() {
    const up = new Vector3().copy(this.camera.up);
    this.raycaster.set(this.camera.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.camera.position.y = MathUtils.lerp(this.camera.position.y, targetY, this.GROUND_FOLLOWING_SPEED);
    }
  }

  override update() {
    if (!this.isActive) return;

    const forwardInput = this.joyStickPosition.y / this.MAX_JOYSTICK_DISTANCE;
    const rightInput = this.joyStickPosition.x / this.MAX_JOYSTICK_DISTANCE;

    const timeDelta = this.moveClock.getDelta();
    const moveDelta = (timeDelta * this.multiplier * this.movementSpeed) / this.WALK_SPEED_DELIMITER;

    const forward = new Vector3();
    const sideways = new Vector3();

    this.camera.getWorldDirection(forward);
    if (this.groundObjects.length > 0) {
      forward.y = 0;
    }
    forward.normalize();

    sideways.setFromMatrixColumn(this.camera.matrix, 0);
    if (this.groundObjects.length > 0) {
      sideways.y = 0;
    }
    sideways.normalize();

    if (forwardInput !== 0) {
      this.camera.position.addScaledVector(forward, moveDelta * forwardInput);
    }
    if (rightInput !== 0) {
      this.camera.position.addScaledVector(sideways, moveDelta * rightInput);
    }

    if (forwardInput !== 0 || rightInput !== 0) {
      if (this.groundObjects.length > 0) this.updateGroundFollowing();
      this.dispatchEvent({ type: "change" });
    }
  }
}
