///////////////////////////////////////////////////////////////////////////////
// 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 { OrbitAction } from "../Actions/OrbitAction";
import { PanAction } from "../Actions/PanAction";
import { ZoomAction } from "../Actions/ZoomAction";
import { Point2d } from "./Geometry";
import { OdBaseDragger } from "./OdBaseDragger";

export enum GestureAction {
  None,
  Orbit,
  Pan,
  Zoom,
}

export class GestureManager extends OdBaseDragger {
  private _previousEvents = new Map<number, PointerEvent>();
  private _currentEvents = new Map<number, PointerEvent>();
  private _lastGestureAction = GestureAction.None;
  private _orbitAction: OrbitAction;
  private _panAction: PanAction;
  private _zoomAction: ZoomAction;

  private _initialDistance: number;
  private readonly _maxInitialDistanceDifference = 30 * window.devicePixelRatio;

  private _isSingleTouchEnabled = false;

  public get isSingleTouchEnabled(): boolean {
    return this._isSingleTouchEnabled;
  }

  public set isSingleTouchEnabled(value: boolean) {
    this._isSingleTouchEnabled = value;
  }

  public constructor(subject: Viewer) {
    super(subject);

    this._orbitAction = new OrbitAction(this.m_module, this.subject, this.beginInteractivity, this.endInteractivity);
    this._panAction = new PanAction(
      this.m_module,
      this.subject,
      this.beginInteractivity,
      this.endInteractivity,
      this.getViewParams,
      this.setViewParams
    );
    this._zoomAction = new ZoomAction(this.m_module, this.subject);
  }

  private getMiddlePoint(events: Map<number, PointerEvent>): Point2d {
    if (events.size !== 2) {
      return undefined;
    }

    const keys = this.getKeys(events);
    const point0 = this.relativeCoords(events.get(keys[0]));
    const point1 = this.relativeCoords(events.get(keys[1]));

    return {
      x: Math.floor((point0.x + point1.x) / 2),
      y: Math.floor((point0.y + point1.y) / 2),
    };
  }

  private getFirstPoint(events: Map<number, PointerEvent>): Point2d {
    if (events.size < 1) {
      return undefined;
    }

    const keys = this.getKeys(events);
    return this.relativeCoords(events.get(keys[0]));
  }

  private getDistance(events: Map<number, PointerEvent>): number {
    if (events.size !== 2) {
      return -1;
    }

    const keys = this.getKeys(events);
    const point0 = this.relativeCoords(events.get(keys[0]));
    const point1 = this.relativeCoords(events.get(keys[1]));

    return Math.hypot(point0.x - point1.x, point0.y - point1.y);
  }

  private updateEvent(event: PointerEvent) {
    const eventNotInCurrentEvents = !this._currentEvents.get(event.pointerId);
    if (eventNotInCurrentEvents && this._currentEvents.size === 2) {
      return;
    }

    const previousEvent = this._currentEvents.get(event.pointerId);
    if (previousEvent) {
      this._previousEvents.set(previousEvent.pointerId, previousEvent);
    }

    this._currentEvents.set(event.pointerId, event);

    if (eventNotInCurrentEvents) {
      this._initialDistance = this.getDistance(this._currentEvents);
    }
  }

  private removeEvent(event: PointerEvent) {
    this._currentEvents.delete(event.pointerId);
    this._previousEvents.delete(event.pointerId);

    if (this._currentEvents.size < 2) {
      this._initialDistance = -1;
    }
  }

  private getKeys(map: Map<number, PointerEvent>): number[] {
    return Array.from(map.keys());
  }

  private analyzeGesture() {
    if (this._currentEvents.size === 2) {
      const currentDistance = this.getDistance(this._currentEvents);
      const previousDistance = this.getDistance(this._previousEvents);

      const currentDistanceEqualsInitialDistance =
        Math.abs(this._initialDistance - currentDistance) <= this._maxInitialDistanceDifference;
      if (currentDistanceEqualsInitialDistance) {
        this.executePanAction(this.getMiddlePoint(this._currentEvents));
      } else {
        if (previousDistance !== -1 && currentDistance !== previousDistance) {
          this.executeZoomAction(currentDistance, previousDistance);
        }
      }
    } else if (this._currentEvents.size === 1 && this.isSingleTouchEnabled) {
      this.executeOrbitAction(this.getFirstPoint(this._currentEvents));
    }
  }

  private executeZoomAction(currentDistance: number, previousDistance: number) {
    if (this._lastGestureAction !== GestureAction.Zoom) {
      this.executeEndAction(this._lastGestureAction);
      this._lastGestureAction = GestureAction.Zoom;
      OdBaseDragger.isGestureActive = true;
    }

    const zoomSpeed = (currentDistance - previousDistance) / 700;
    const zoomFactor = 1 + zoomSpeed;
    const middlePoint = this.getMiddlePoint(this._currentEvents);
    this._zoomAction.action(middlePoint.x, middlePoint.y, zoomFactor, middlePoint.x, middlePoint.y);
    this.subject.update();
  }

  private executePanAction(currentPoint: Point2d) {
    if (this._lastGestureAction !== GestureAction.Pan) {
      this.executeEndAction(this._lastGestureAction);
      this._lastGestureAction = GestureAction.Pan;
      OdBaseDragger.isGestureActive = true;

      this._panAction.beginAction(currentPoint.x, currentPoint.y, currentPoint.x, currentPoint.y);
    }

    this._panAction.action(currentPoint.x, currentPoint.y, currentPoint.x, currentPoint.y);
    this.subject.update();
  }

  private executeOrbitAction(currentPoint: Point2d) {
    if (this._lastGestureAction !== GestureAction.Orbit) {
      this.executeEndAction(this._lastGestureAction);
      this._lastGestureAction = GestureAction.Orbit;
      OdBaseDragger.isGestureActive = true;

      this._orbitAction.beginAction(currentPoint.x, currentPoint.y);
    }

    this._orbitAction.action(currentPoint.x, currentPoint.y);
    this.subject.update();
  }

  private executeEndAction(gestureAction: GestureAction) {
    if (gestureAction === GestureAction.Orbit) this._orbitAction.endAction();
    if (gestureAction === GestureAction.Pan) this._panAction.endAction();
    OdBaseDragger.isGestureActive = false;
  }

  private needIgnoreEvent(event: PointerEvent): boolean {
    return (
      !this.subject.options.enableZoomWheel || !this.subject.options.enableGestures || !this.eventIsTouchEvent(event)
    );
  }

  private eventIsTouchEvent(event: PointerEvent): boolean {
    return event.pointerType === "touch" || event.pointerType === "";
  }

  override pointerdown(event: PointerEvent) {
    if (this.needIgnoreEvent(event)) {
      return;
    }

    this.updateEvent(event);
  }

  override pointermove(event: PointerEvent) {
    if (this.needIgnoreEvent(event)) {
      return;
    }

    this.updateEvent(event);
    this.analyzeGesture();
  }

  override pointerup(event: PointerEvent) {
    if (this.needIgnoreEvent(event)) {
      return;
    }

    this.removeEvent(event);

    if (this._currentEvents.size < 2) {
      this.executeEndAction(this._lastGestureAction);
      OdBaseDragger.isGestureActive = false;
    }

    this._lastGestureAction = GestureAction.None;
  }

  override pointercancel(event: PointerEvent) {
    if (this.needIgnoreEvent(event)) {
      return;
    }

    this.pointerup(event);
  }

  pointerleave(event: PointerEvent) {
    if (this.needIgnoreEvent(event)) {
      return;
    }

    this.pointerup(event);
  }
}
