///////////////////////////////////////////////////////////////////////////////
// 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 {
  Camera,
  EdgesGeometry,
  Intersection,
  Line3,
  MathUtils,
  Object3D,
  Plane,
  Raycaster,
  Vector2,
  Vector3,
} from "three";

const DESKTOP_SNAP_DISTANCE = 10;
const MOBILE_SNAP_DISTANCE = 50;

const _vertex = new Vector3();
const _start = new Vector3();
const _end = new Vector3();
const _line = new Line3();
const _center = new Vector3();
const _projection = new Vector3();

export class Snapper {
  public camera: Camera;
  public clippingPlanes: Plane[];
  public canvas: HTMLCanvasElement;
  public threshold: number;
  private raycaster: Raycaster;
  private detectRadiusInPixels: number;
  private edgesCache: WeakMap<any, EdgesGeometry>;

  constructor(camera: Camera, clippingPlanes: Plane[], canvas: HTMLCanvasElement) {
    this.camera = camera;
    this.clippingPlanes = clippingPlanes;
    this.canvas = canvas;

    this.threshold = 0.0001;

    this.raycaster = new Raycaster();
    this.detectRadiusInPixels = this.isMobile() ? MOBILE_SNAP_DISTANCE : DESKTOP_SNAP_DISTANCE;
    this.edgesCache = new WeakMap();
  }

  isMobile(): boolean {
    if (typeof navigator === "undefined") return false;

    // ===================== AI-CODE-START ======================
    // Source: Claude Sonnet 4
    // Date: 2025-09-09
    // Reviewer: roman.mochalov@opendesign.com
    // Issue: CLOUD-5799

    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);

    // ===================== AI-CODE-END ======================
  }

  getMousePosition(event: MouseEvent, target: Vector2): Vector2 {
    return target.set(event.clientX, event.clientY);
  }

  getPointerIntersects(
    mouse: Vector2,
    objects: Object3D[],
    recursive = false,
    clip = true
  ): Array<Intersection<Object3D>> {
    const rect = this.canvas.getBoundingClientRect();
    const x = ((mouse.x - rect.left) / rect.width) * 2 - 1;
    const y = (-(mouse.y - rect.top) / rect.height) * 2 + 1;

    const coords = new Vector2(x, y);
    this.raycaster.setFromCamera(coords, this.camera);

    this.raycaster.params = {
      Mesh: {},
      Line: { threshold: this.threshold },
      Line2: { threshold: this.threshold },
      LOD: {},
      Points: { threshold: this.threshold },
      Sprite: {},
    };

    let intersects = this.raycaster.intersectObjects(objects, recursive);

    if (clip) {
      const clippingPlanes = this.clippingPlanes;
      clippingPlanes.forEach((plane: Plane) => {
        intersects = intersects.filter((intersect) => plane.distanceToPoint(intersect.point) >= 0);
      });
    }

    return intersects;
  }

  getDetectRadius(point: Vector3): number {
    const camera: any = this.camera;

    // ===================== AI-CODE-START ======================
    // Source: Gemini 2.5 Pro
    // Date: 2025-08-27
    // Reviewer: roman.mochalov@opendesign.com
    // Issue: CLOUD-5799
    // Notes: Originally AI-generated, modified manually

    if (camera.isOrthographicCamera) {
      const fieldHeight = (camera.top - camera.bottom) / camera.zoom;

      const canvasHeight = this.canvas.height;
      const worldUnitsPerPixel = fieldHeight / canvasHeight;

      return this.detectRadiusInPixels * worldUnitsPerPixel;
    }

    if (camera.isPerspectiveCamera) {
      const distance = camera.position.distanceTo(point);
      const fieldHeight = 2 * Math.tan(MathUtils.degToRad(camera.fov * 0.5)) * distance;

      const canvasHeight = this.canvas.height;
      const worldUnitsPerPixel = fieldHeight / canvasHeight;

      return this.detectRadiusInPixels * worldUnitsPerPixel;
    }

    // ===================== AI-CODE-END ======================

    return 0.1;
  }

  getSnapPoint(mouse: Vector2, objects: Object3D[]): Vector3 | undefined {
    const intersections = this.getPointerIntersects(mouse, objects);
    if (intersections.length === 0) return undefined;

    // ===================== AI-CODE-START ======================
    // Source: Gemini 2.5 Pro
    // Date: 2025-08-20
    // Reviewer: roman.mochalov@opendesign.com
    // Issue: CLOUD-5799
    // Notes: Originally AI-generated, modified manually

    const object: any = intersections[0].object;
    const intersectionPoint = intersections[0].point;
    const localPoint = object.worldToLocal(intersectionPoint.clone());

    let snapPoint: Vector3 | undefined = undefined;
    let snapDistance = this.getDetectRadius(intersectionPoint);

    const geometry = object.geometry;

    const positions = geometry.attributes.position.array;
    for (let i = 0; i < positions.length; i += 3) {
      _vertex.set(positions[i], positions[i + 1], positions[i + 2]);
      const distance = _vertex.distanceTo(localPoint);
      if (distance < snapDistance) {
        snapDistance = distance;
        snapPoint = _vertex.clone();
      }
    }
    if (snapPoint) return object.localToWorld(snapPoint);

    let edges = this.edgesCache.get(geometry);
    if (!edges) {
      edges = new EdgesGeometry(geometry);
      this.edgesCache.set(geometry, edges);
    }

    const edgePositions = edges.attributes.position.array;
    for (let i = 0; i < edgePositions.length; i += 6) {
      _start.set(edgePositions[i], edgePositions[i + 1], edgePositions[i + 2]);
      _end.set(edgePositions[i + 3], edgePositions[i + 4], edgePositions[i + 5]);
      _line.set(_start, _end);

      _line.getCenter(_center);
      const centerDistance = _center.distanceTo(localPoint);
      if (centerDistance < snapDistance) {
        snapDistance = centerDistance;
        snapPoint = _center.clone();
        continue;
      }

      _line.closestPointToPoint(localPoint, true, _projection);
      const lineDistance = _projection.distanceTo(localPoint);
      if (lineDistance < snapDistance) {
        snapDistance = lineDistance;
        snapPoint = _projection.clone();
      }
    }
    if (snapPoint) return object.localToWorld(snapPoint);

    // ===================== AI-CODE-END ======================

    return intersectionPoint.clone();
  }
}
