///////////////////////////////////////////////////////////////////////////////
// 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 { Viewer } from "../Viewer";
import { Point2d, Vector3 } from "./Common/Geometry";
import { OdBaseDragger } from "./Common/OdBaseDragger";

const FocalLengthConst = 42.0;

const calcFocalLength = (lensLength: number, fieldWidth: number, fieldHeight: number): number => {
  return (lensLength / FocalLengthConst) * Math.sqrt(fieldWidth * fieldWidth + fieldHeight * fieldHeight);
};

export class OdaWalkDragger extends OdBaseDragger {
  protected lastCoord: Point2d;
  protected speed: number;
  protected delta: number;
  protected keyPressMap: Set<string>;
  protected oldWCSEnableValue: boolean;
  protected viewParams: any;
  protected cameraId: any;
  protected cameraWalker: any;
  protected viewer: any;
  protected multiplier: number;
  protected lastFrameTS: number;
  protected animationId: any;
  protected deltaAngle: number;
  protected enableZoomWheelPreviousValue: boolean;
  protected dragPosition: Point2d;

  constructor(subject: Viewer) {
    super(subject);
    this.viewer = undefined;

    this.multiplier = 5;
    this.speed = 1;

    this.keyPressMap = new Set();
    this.keydown = this.keydown.bind(this);
    this.keyup = this.keyup.bind(this);

    this.lastFrameTS = 0;
    this.animationId = undefined;
    this.processMovement = this.processMovement.bind(this);

    this.deltaAngle = Math.PI / 3600;
    this.autoSelect = true;
  }

  override initialize() {
    super.initialize();
    this.viewer = this.getViewer();

    window.addEventListener("keydown", this.keydown, false);
    window.addEventListener("keyup", this.keyup, false);

    this.oldWCSEnableValue = this.viewer.getEnableWCS();
    this.viewer.setEnableWCS(false);

    const view = this.viewer.activeView;
    const maxDimension = this.getMaxDimension(view);
    this.speed = maxDimension / 30000;

    this.subject.emitEvent({ type: "walkstart" });

    this.viewParams = this.getViewParams();
    //this.viewParams.lensLength = view.lensLength;

    this.setViewParams(this.viewParams);
    //view.lensLength = (view.lensLength * 42) / 120;

    const model = this.viewer.getActiveModel();
    this.cameraId = model.appendCamera("Camera0");
    this.setupCamera(view);
    model.delete();

    //pCamera.setAdjustLensLength(true);
    this.cameraWalker = new this.m_module.OdTvCameraWalker();
    this.cameraWalker.setCamera(this.cameraId);

    this.subject.update();
    this.enableZoomWheelPreviousValue = this.subject.options.enableZoomWheel;
    this.subject.options.enableZoomWheel = false;
  }

  override dispose() {
    this.oldWCSEnableValue =
      this.oldWCSEnableValue !== undefined ? this.oldWCSEnableValue : this.subject.options.showWCS;
    this.viewer.setEnableWCS(this.oldWCSEnableValue);

    super.dispose();

    this.keyPressMap.clear();

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

    if (this.animationId) {
      window.cancelAnimationFrame(this.animationId);
      this.animationId = undefined;
    }

    if (this.cameraId) {
      const model = this.viewer.getActiveModel();
      model.removeEntity(this.cameraId);
      model.delete();
      this.cameraWalker?.delete();
    }

    if (this.viewParams) {
      this.setViewParams(this.viewParams);

      const avp = this.viewer.activeView;
      //avp.lensLength = this.viewParams.lensLength;
      avp.delete();
    }

    // CLOUD-5359 Demo Viewer crashes after the Walk Mode
    this.subject.update(true);
    this.subject.options.enableZoomWheel = this.enableZoomWheelPreviousValue;
  }

  keydown(ev) {
    switch (ev.code) {
      case "NumpadSubtract":
      case "Minus":
        if (this.multiplier > 1) {
          this.multiplier = this.multiplier - 1;
          this.subject.emitEvent({ type: "walkspeedchange", data: this.multiplier });
        }
        break;
      case "NumpadAdd":
      case "Equal":
        if (this.multiplier < 10) {
          this.multiplier = this.multiplier + 1;
          this.subject.emitEvent({ type: "walkspeedchange", data: this.multiplier });
        }
        break;
      case "KeyW":
      case "KeyA":
      case "KeyS":
      case "KeyD":
      case "KeyQ":
      case "KeyE":
        this.keyPressMap.add(ev.code);
        if (!this.animationId) this.processMovement(0);
        break;
    }
  }

  keyup(ev) {
    this.keyPressMap.delete(ev.code);
    if (this.keyPressMap.size < 1 && this.animationId) {
      window.cancelAnimationFrame(this.animationId);
      this.animationId = undefined;
      this.lastFrameTS = 0;
    }
  }

  processMovement(timestamp) {
    this.animationId = requestAnimationFrame(this.processMovement);

    if (this.lastFrameTS !== 0) {
      const deltaTS = timestamp - this.lastFrameTS;
      const currentDelta = this.multiplier * deltaTS * this.speed;

      for (const keyCode of this.keyPressMap) {
        switch (keyCode) {
          case "KeyW":
            this.cameraWalker.moveForward(currentDelta);
            break;
          case "KeyS":
            this.cameraWalker.moveBackward(currentDelta);
            break;
          case "KeyA":
            this.cameraWalker.moveLeft(currentDelta);
            break;
          case "KeyD":
            this.cameraWalker.moveRight(currentDelta);
            break;
          case "KeyQ":
            this.cameraWalker.moveUp(currentDelta);
            break;
          case "KeyE":
            this.cameraWalker.moveDown(currentDelta);
            break;
        }
      }
      this.subject.update();
    }

    this.lastFrameTS = timestamp;
  }

  override start(x: number, y: number): void {
    this.dragPosition = { x, y };
  }

  override drag(x: number, y: number) {
    if (this.cameraId && this.isDragging) {
      const dltX = x - this.dragPosition.x;
      const dltY = y - this.dragPosition.y;
      this.dragPosition = { x, y };

      if (dltX !== 0.0) this.turnLeft(-dltX * this.deltaAngle);
      if (dltY !== 0.0) this.cameraWalker.turnDown(dltY * this.deltaAngle);
      this.subject.update();
    }
  }

  turnLeft(angle) {
    //TODO: migrate to VisualizeJS
    const pCamera = this.cameraWalker.camera().openObjectAsCamera();
    const dir = this.toVector(pCamera.direction());
    const up = this.toVector(pCamera.upVector());

    const pos = pCamera.position();

    const rotMatrix = this.createMatrix3d();
    const zAxisVector: Vector3 = [0, 0, 1];
    rotMatrix.setToRotation(angle, zAxisVector, pos);
    dir.transformBy(rotMatrix);
    up.transformBy(rotMatrix);
    pCamera.setupCameraByDirection(pos, dir.toArray(), up.toArray());
    pCamera.delete();
  }

  setupCamera(view) {
    const pCamera = this.cameraId.openObjectAsCamera();
    const target = view.viewTarget;

    pCamera.setDisplayGlyph(false);
    pCamera.setDisplayTarget(false);
    pCamera.setAutoAdjust(true);
    pCamera.setupCamera(view.viewPosition, target, view.upVector);
    pCamera.setNearClip(false, 1.0);
    pCamera.setFarClip(false, 0);
    pCamera.setViewParameters(view.viewFieldWidth, view.viewFieldHeight, true);

    const focalL = calcFocalLength(view.lensLength, view.viewFieldWidth, view.viewFieldHeight);

    const pTarget = this.toPoint(view.viewTarget);
    const viewDir = this.toPoint(view.viewPosition);
    const viewDirSub = viewDir.sub(pTarget);
    const viewDirVec = viewDirSub.asVector();
    const viewDirVecNormal = viewDirVec.normalize();

    const geViewDir = this.toGeVector(viewDirVecNormal);
    const newGeViewDir = [geViewDir[0] * focalL, geViewDir[1] * focalL, geViewDir[2] * focalL];

    const pTarget2 = this.toPoint(view.viewTarget);
    const newGeViewDirPt = this.toPoint(newGeViewDir);

    const newPos = pTarget2.add(newGeViewDirPt);
    pCamera.setupCamera(this.toGePoint(newPos), view.viewTarget, view.upVector);

    this.deleteAll([pTarget, viewDir, viewDirSub, viewDirVec, viewDirVecNormal, pTarget2, newGeViewDirPt, newPos]);

    pCamera.assignView(view);
    pCamera.delete();
  }

  getMaxDimension(view) {
    const [xmax, ymax, zmax] = view.sceneExtents.max();
    const [xmin, ymin, zmin] = view.sceneExtents.min();
    const volume = [xmax - xmin, ymax - ymin, zmax - zmin];
    return Math.max(...volume);
  }
}
