///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2026, 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-2026 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 { Plane, Quaternion, Vector2, Vector3 } from "three";
import { TransformControls } from "three/examples/jsm/controls/TransformControls.js";

import type { Viewer } from "../Viewer";
import { STATE } from "../controls/OrbitControls.js";
import { PlaneHelper2 } from "../helpers/PlaneHelper2";
import { Snapper } from "../measurement/Snapper";
import { OrbitDragger } from "./OrbitDragger";

export class CuttingPlaneDragger extends OrbitDragger {
  protected helpers: PlaneHelper2[] = [];
  protected activeHelper: PlaneHelper2 | undefined = undefined;
  protected snapper: Snapper;
  protected downPosition: Vector2;
  protected position0: Vector3;
  protected quaternion0: Quaternion;
  protected translate: TransformControls;
  protected rotate: TransformControls;

  constructor(viewer: Viewer) {
    super(viewer);

    viewer.clippingPlanes.forEach((plane) => this.addHelper(plane));

    const extentsSize = viewer.extents.getSize(new Vector3()).length() || 1;

    this.snapper = new Snapper(viewer.camera, viewer.clippingPlanes, viewer.canvas);
    this.snapper.threshold = extentsSize / 10000;

    this.downPosition = new Vector2();
    this.position0 = new Vector3();
    this.quaternion0 = new Quaternion();

    this.translate = new TransformControls(viewer.camera, viewer.canvas);
    this.translate.setMode("translate");
    this.translate.setSpace("local");
    this.translate.showX = false;
    this.translate.showY = false;
    this.translate.showZ = true;
    this.translate.addEventListener("change", this.transformUpdate);
    this.translate.addEventListener("objectChange", this.transformChange);
    this.translate.addEventListener("dragging-changed", this.translateDrag);
    this.viewer.helpers.add(this.translate.getHelper());

    this.rotate = new TransformControls(viewer.camera, viewer.canvas);
    this.rotate.setMode("rotate");
    this.rotate.setSpace("local");
    this.rotate.showX = true;
    this.rotate.showY = true;
    this.rotate.showZ = false;
    this.rotate.addEventListener("change", this.transformUpdate);
    this.rotate.addEventListener("objectChange", this.transformChange);
    this.rotate.addEventListener("dragging-changed", this.rotateDrag);
    this.viewer.helpers.add(this.rotate.getHelper());

    this.setActiveHelper(this.helpers[this.helpers.length - 1]);

    this.viewer.addEventListener("explode", this.syncHelpers);
    this.viewer.addEventListener("hide", this.syncHelpers);
    this.viewer.addEventListener("isolate", this.syncHelpers);
    this.viewer.addEventListener("show", this.syncHelpers);
    this.viewer.addEventListener("showall", this.syncHelpers);
    this.viewer.addEventListener("clearslices", this.clearHelpers);
    this.viewer.addEventListener("changecameramode", this.updateTransformCamera);

    this.viewer.canvas.addEventListener("pointerdown", this.onPointerDown, true);
    this.viewer.canvas.addEventListener("pointerup", this.onPointerUp, true);
    this.viewer.canvas.addEventListener("pointercancel", this.onPointerCancel, true);
    this.viewer.canvas.addEventListener("dblclick", this.onDoubleClick, true);

    window.addEventListener("keydown", this.onKeyDown);
    window.addEventListener("keyup", this.onKeyUp);

    this.viewer.update();
  }

  override dispose() {
    this.viewer.removeEventListener("explode", this.syncHelpers);
    this.viewer.removeEventListener("hide", this.syncHelpers);
    this.viewer.removeEventListener("isolate", this.syncHelpers);
    this.viewer.removeEventListener("show", this.syncHelpers);
    this.viewer.removeEventListener("showall", this.syncHelpers);
    this.viewer.removeEventListener("clearslices", this.clearHelpers);
    this.viewer.removeEventListener("changecameramode", this.updateTransformCamera);

    this.viewer.canvas.removeEventListener("pointerdown", this.onPointerDown, true);
    this.viewer.canvas.removeEventListener("pointerup", this.onPointerUp, true);
    this.viewer.canvas.removeEventListener("pointercancel", this.onPointerCancel, true);
    this.viewer.canvas.removeEventListener("dblclick", this.onDoubleClick, true);

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

    this.translate.removeEventListener("change", this.transformUpdate);
    this.translate.removeEventListener("objectChange", this.transformChange);
    this.translate.removeEventListener("dragging-changed", this.translateDrag);
    this.translate.getHelper().removeFromParent();
    this.translate.detach();
    this.translate.dispose();

    this.rotate.removeEventListener("change", this.transformUpdate);
    this.rotate.removeEventListener("objectChange", this.transformChange);
    this.rotate.removeEventListener("dragging-changed", this.rotateDrag);
    this.rotate.getHelper().removeFromParent();
    this.rotate.detach();
    this.rotate.dispose();

    this.helpers.forEach((helper) => {
      helper.removeFromParent();
      helper.dispose();
    });
    this.helpers.length = 0;
    this.activeHelper = undefined;

    super.dispose();
  }

  transformUpdate = () => {
    this.viewer.update();
  };

  transformChange = () => {
    if (!this.activeHelper) return;

    const plane = this.activeHelper.plane;
    plane.normal.copy(new Vector3(0, 0, -1)).applyQuaternion(this.activeHelper.quaternion);
    plane.constant = -this.activeHelper.position.dot(plane.normal);

    this.viewer.emitEvent({ type: "changecuttingplanes" });
    this.viewer.update();

    this.changed = true; // <- cancel context menu
  };

  translateDrag = (event) => {
    this.orbit.enabled = !event.value;
    this.rotate.enabled = !event.value;
  };

  rotateDrag = (event) => {
    this.orbit.enabled = !event.value;
    this.translate.enabled = !event.value;
  };

  syncHelpers = () => {
    const extentsSize = this.viewer.extents.getSize(new Vector3()).length() || 1;
    const extentsCenter = this.viewer.extents.getCenter(new Vector3());

    this.helpers.forEach((planeHelper) => {
      planeHelper.size = extentsSize;
      planeHelper.position.copy(planeHelper.plane.projectPoint(extentsCenter, new Vector3()));
    });

    this.viewer.update();
  };

  clearHelpers = () => {
    this.setActiveHelper();

    this.helpers.forEach((helper) => {
      helper.removeFromParent();
      helper.dispose();
    });
    this.helpers.length = 0;

    this.viewer.update();
  };

  updateTransformCamera = () => {
    this.translate.camera = this.viewer.camera;
    this.rotate.camera = this.viewer.camera;
    this.snapper.camera = this.viewer.camera;
  };

  onKeyDown = (event: KeyboardEvent) => {
    if (event.key === "Shift") this.rotate.setRotationSnap(Math.PI / 4);
    if (event.key === "Delete" || event.key === "Backspace") this.deleteActivePlane();
    if (event.key === "Escape" && (this.translate.dragging || this.rotate.dragging)) this.reset();
  };

  onKeyUp = (event: KeyboardEvent) => {
    if (event.key === "Shift") this.rotate.setRotationSnap(null);
  };

  onPointerDown = (event: PointerEvent) => {
    if (event.button !== 0 || !event.isPrimary) return;

    this.snapper.getMousePosition(event, this.downPosition);
    this.saveState();
  };

  onPointerUp = (event: PointerEvent) => {
    if (event.button !== 0) return;

    const upPosition = this.snapper.getMousePosition(event, new Vector2());
    if (upPosition.distanceTo(this.downPosition) !== 0) return;

    const intersects = this.snapper.getPointerIntersects(upPosition, this.helpers, true, false);
    if (intersects.length === 0) return;

    this.setActiveHelper(intersects[0].object.parent as PlaneHelper2);
  };

  onPointerCancel = (event: PointerEvent) => {
    this.viewer.canvas.dispatchEvent(new PointerEvent("pointerup", event));
  };

  onDoubleClick = (event: MouseEvent) => {
    if (!this.activeHelper) return;

    const mousePosition = this.snapper.getMousePosition(event, new Vector2());

    const intersects = this.snapper.getPointerIntersects(mousePosition, [this.activeHelper], true, false);
    if (intersects.length === 0) return;

    this.activeHelper.rotateOnAxis(new Vector3(0, 1, 0), Math.PI);
    this.transformChange();

    event.stopPropagation();
  };

  private addHelper(plane: Plane): PlaneHelper2 {
    const extentsSize = this.viewer.extents.getSize(new Vector3()).length() || 1;
    const extentsCenter = this.viewer.extents.getCenter(new Vector3());

    const helper = new PlaneHelper2(extentsSize);
    helper.plane = plane;
    helper.position.copy(plane.projectPoint(extentsCenter, new Vector3()));
    helper.quaternion.setFromUnitVectors(new Vector3(0, 0, -1), plane.normal);

    this.helpers.push(helper);
    this.viewer.helpers.add(helper);

    return helper;
  }

  private setActiveHelper(helper?: PlaneHelper2) {
    if (helper === this.activeHelper) return;

    if (this.activeHelper) {
      this.activeHelper.getLineMaterial().color.setHex(0xf0f0f0);
      this.activeHelper.getMeshMaterial().opacity = 0.15;

      this.translate.detach();
      this.rotate.detach();
    }

    this.activeHelper = helper;

    if (this.activeHelper) {
      this.activeHelper.getLineMaterial().color.setHex(0xd0d0d0);
      this.activeHelper.getMeshMaterial().opacity = 0.3;

      this.translate.attach(this.activeHelper);
      this.rotate.attach(this.activeHelper);
    }

    this.viewer.update();
  }

  private saveState() {
    if (!this.activeHelper) return;

    this.position0.copy(this.activeHelper.position);
    this.quaternion0.copy(this.activeHelper.quaternion);
  }

  private reset() {
    if (!this.activeHelper) return;

    this.translate.dragging = false;
    this.rotate.dragging = false;
    this.orbit.state = STATE.NONE;

    this.activeHelper.position.copy(this.position0);
    this.activeHelper.quaternion.copy(this.quaternion0);

    this.transformChange();
  }

  addPlane(normal: Vector3) {
    const extentsCenter = this.viewer.extents.getCenter(new Vector3());
    const constant = -extentsCenter.dot(normal);

    const plane = new Plane(normal, constant);
    this.viewer.clippingPlanes.push(plane);

    const helper = this.addHelper(plane);
    this.setActiveHelper(helper);

    this.viewer.emitEvent({ type: "changecuttingplanes" });
  }

  addPlaneX() {
    this.addPlane(new Vector3(-1, 0, 0));
  }

  addPlaneY() {
    this.addPlane(new Vector3(0, -1, 0));
  }

  addPlaneZ() {
    this.addPlane(new Vector3(0, 0, -1));
  }

  deleteActivePlane() {
    if (!this.activeHelper) return;

    const helper = this.activeHelper;

    const index = this.viewer.clippingPlanes.indexOf(helper.plane);
    if (index !== -1) this.viewer.clippingPlanes.splice(index, 1);

    this.helpers = this.helpers.filter((x) => x !== helper);
    helper.removeFromParent();
    helper.dispose();

    this.setActiveHelper(this.helpers[this.helpers.length - 1]);

    this.viewer.emitEvent({ type: "changecuttingplanes" });
  }
}
