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

export class OrbitAction {
  private _m_module: any;
  private _subject: Viewer;
  private _beginInteractivity: () => void;
  private _endInteractivity: () => void;
  private m_viewCenter: any;
  private m_startPoint: Point2d;

  constructor(m_module: any, subject: Viewer, beginInteractivity: () => void, endInteractivity: () => void) {
    this._m_module = m_module;
    this._subject = subject;
    this._beginInteractivity = beginInteractivity;
    this._endInteractivity = endInteractivity;
  }

  public beginAction(x: number, y: number) {
    this.m_viewCenter = this.getCenter();
    this.m_startPoint = { x, y };

    const view = this.getViewer().activeView;

    view.delete();
    this._beginInteractivity();
  }

  public action(x: number, y: number) {
    const view = this.getViewer().activeView;

    const corners = view.vportRect;

    const size = Math.max(Math.abs(corners[2] - corners[0]), Math.abs(corners[3] - corners[1]));

    const distX = ((this.m_startPoint.x - x) * Math.PI) / size;
    const distY = ((this.m_startPoint.y - y) * Math.PI) / size;

    this.m_startPoint.x = x;
    this.m_startPoint.y = y;

    const xOrbit = distY;
    const yOrbit = distX;

    const viewParams = {
      position: view.viewPosition,
      target: view.viewTarget,
      upVector: view.upVector,
      viewFieldWidth: view.viewFieldWidth,
      viewFieldHeight: view.viewFieldHeight,
      perspective: view.perspective,
    };

    view.delete();

    const sideVector = this.getSideVector(viewParams);

    if (xOrbit !== 0.0) {
      this.calculateXOrbit(viewParams, -xOrbit, sideVector);
    }

    if (yOrbit !== 0.0) {
      this.calculateYOrbit(viewParams, yOrbit, sideVector);
    }

    sideVector.delete();

    const extView = this.getViewer().getActiveTvExtendedView();

    extView.setView(
      viewParams.position,
      viewParams.target,
      viewParams.upVector,
      viewParams.viewFieldWidth,
      viewParams.viewFieldHeight,
      viewParams.perspective
    );
    extView.delete();

    this._subject.activeDragger()?.updatePreview?.();
  }

  public endAction() {
    this._endInteractivity();
  }

  private getSideVector(viewParams: any): any {
    const pUpV = this.toVector(viewParams.upVector);
    const pTarget = this.toPoint(viewParams.target);
    const pPosition = this.toPoint(viewParams.position);

    const direct = pTarget.sub(pPosition);
    const vDirect = direct.asVector();

    const vCross = pUpV.crossProduct(vDirect);
    const sideVector = vCross.normalize();

    this.deleteAll([direct, pUpV, pTarget, pPosition, vDirect, vCross]);

    return sideVector;
  }

  private calculateXOrbit(viewParams: any, delta: number, sideVector: Vector3d): void {
    {
      const pPoint = this.toPoint(viewParams.position);
      const pCenter = this.toPoint(this.m_viewCenter);

      const rotatePoint = pPoint.rotateByBasePoint(delta, sideVector, pCenter);
      viewParams.position = rotatePoint.toArray();

      this.deleteAll([pPoint, pCenter, rotatePoint]);
    }

    {
      const pTarget = this.toPoint(viewParams.target);
      const pCenter = this.toPoint(this.m_viewCenter);

      const rotatePoint = pTarget.rotateByBasePoint(delta, sideVector, pCenter);
      viewParams.target = rotatePoint.toArray();

      this.deleteAll([pTarget, pCenter, rotatePoint]);
    }

    {
      const pPoint = this.toPoint(viewParams.position);
      const pTarget = this.toPoint(viewParams.target);
      const pCenter = this.toPoint(this.m_viewCenter);

      const pUp = pTarget.sub(pPoint);
      const vUp = pUp.asVector();

      const crossProduct = vUp.crossProduct(sideVector);
      const crossProductNormal = crossProduct.normalize();

      viewParams.upVector = crossProductNormal.toArray();

      this.deleteAll([pPoint, pTarget, pCenter, pUp, vUp, crossProduct, crossProductNormal]);
    }
  }

  private calculateYOrbit(viewParams: any, delta: number, sideVector: any): void {
    {
      const pPoint = this.toPoint(viewParams.position);
      const pCenter = this.toPoint(this.m_viewCenter);

      const zAxis = this.toVector(this._m_module.Vector3d.kZAxis);

      const rotatePoint = pPoint.rotateByBasePoint(delta, zAxis, pCenter);
      viewParams.position = rotatePoint.toArray();

      this.deleteAll([zAxis, pPoint, pCenter, rotatePoint]);
    }

    {
      const pTarget = this.toPoint(viewParams.target);
      const pCenter = this.toPoint(this.m_viewCenter);

      const zAxis = this.toVector(this._m_module.Vector3d.kZAxis);

      const rotatePoint = pTarget.rotateByBasePoint(delta, zAxis, pCenter);
      viewParams.target = rotatePoint.toArray();

      this.deleteAll([zAxis, pTarget, pCenter, rotatePoint]);
    }

    {
      const zAxis = this.toVector(this._m_module.Vector3d.kZAxis);
      const pTarget = this.toPoint(viewParams.target);
      const pPoint = this.toPoint(viewParams.position);

      const side = sideVector.rotateBy(delta, zAxis);

      const pUp = pTarget.sub(pPoint);
      const vUp = pUp.asVector();

      const cross = vUp.crossProduct(side);
      const crossNormal = cross.normalize();

      viewParams.upVector = crossNormal.toArray();

      this.deleteAll([zAxis, pTarget, pPoint, side, pUp, vUp, cross, crossNormal]);
    }
  }

  private getCenter(): any {
    const viewer = this.getViewer();
    let center;

    const pSet = viewer.getSelected();
    if (!pSet.isNull() && pSet.numItems() !== 0) {
      const itr = pSet.getIterator();
      let ext, entId, extSelected;
      for (; !itr.done(); itr.step()) {
        entId = itr.getEntity();
        ext = entId.getWCSExtents();
        if (extSelected) extSelected.addExt(ext);
        else extSelected = ext;
      }
      center = extSelected.center();

      extSelected.delete();
      itr.delete();
    } else {
      center = viewer.getActiveExtents().center();
    }

    return center;
  }

  private getViewer(): any {
    return this._m_module.getViewer();
  }

  private toVector(geVector): Vector3d {
    return this._m_module.Vector3d.createFromArray(geVector);
  }

  private toPoint(gePoint: number[]): Point3d {
    return this._m_module.Point3d.createFromArray(gePoint);
  }

  private deleteAll(objects): void {
    for (const obj of objects) {
      obj?.delete?.();
    }
  }
}
