///////////////////////////////////////////////////////////////////////////////
// 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 {
  EdgesGeometry,
  LineBasicMaterial,
  MeshPhongMaterial,
  Object3D,
  RGBAFormat,
  UnsignedByteType,
  Vector2,
  WebGLRenderTarget,
} from "three";
import { LineSegmentsGeometry } from "three/examples/jsm/lines/LineSegmentsGeometry.js";
import { Wireframe } from "three/examples/jsm/lines/Wireframe.js";
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";

import { IComponent, Options, ResizeEvent } from "@inweb/viewer-core";
import { Viewer } from "../Viewer";
import { HighlighterUtils } from "./HighlighterUtils";

export class HighlighterComponent implements IComponent {
  protected viewer: Viewer;
  public renderTarget: WebGLRenderTarget;
  public facesMaterial: MeshPhongMaterial;
  public edgesMaterial: LineMaterial;
  public lineMaterial: LineBasicMaterial;
  public lineGlowMaterial: LineMaterial;

  constructor(viewer: Viewer) {
    this.viewer = viewer;

    const gl2 = viewer.canvas.getContext("webgl2");
    if (gl2) {
      const size = viewer.renderer.getSize(new Vector2());
      this.renderTarget = new WebGLRenderTarget(size.x, size.y, {
        format: RGBAFormat,
        stencilBuffer: false,
        samples: 4,
        type: UnsignedByteType,
      });
    }

    this.viewer.addEventListener("databasechunk", this.geometryEnd);
    this.viewer.addEventListener("optionschange", this.optionsChange);
    this.viewer.addEventListener("resize", this.viewerResize);

    this.geometryEnd();
  }

  dispose() {
    this.viewer.removeEventListener("databasechunk", this.geometryEnd);
    this.viewer.removeEventListener("optionschange", this.optionsChange);
    this.viewer.removeEventListener("resize", this.viewerResize);
  }

  highlight(objects: Object3D | Object3D[]) {
    const { edgesVisibility } = this.viewer.options;

    if (!Array.isArray(objects)) objects = [objects];
    if (!objects.length) return;

    objects
      .filter((object) => !object.userData.isEdge) // <- filtering server generated edges
      .forEach((object: any) => {
        if (object.isHighlighted) return;

        if (object.isLine || object.isLineSegments) {
          const positions = object.geometry.attributes.position.array;
          const indices = object.geometry.index ? object.geometry.index.array : null;
          const lineGeometry = indices
            ? HighlighterUtils.fromIndexedLine(positions, indices)
            : HighlighterUtils.fromNonIndexedLine(positions, object.isLineSegments);

          const wireframe = new Wireframe(lineGeometry, this.lineGlowMaterial);
          wireframe.position.copy(object.position);
          wireframe.rotation.copy(object.rotation);
          wireframe.scale.copy(object.scale);
          wireframe.visible = edgesVisibility;

          object.parent.add(wireframe);
          object.userData.highlightWireframe = wireframe;

          object.userData.originalMaterial = object.material;
          object.material = this.lineMaterial;
          object.isHighlighted = true;
        } else if (object.isMesh) {
          const edgesGeometry = new EdgesGeometry(object.geometry, 89);
          const lineGeometry = new LineSegmentsGeometry().fromEdgesGeometry(edgesGeometry);

          const wireframe = new Wireframe(lineGeometry, this.edgesMaterial);
          wireframe.position.copy(object.position);
          wireframe.rotation.copy(object.rotation);
          wireframe.scale.copy(object.scale);
          wireframe.visible = edgesVisibility;

          object.parent.add(wireframe);
          object.userData.highlightWireframe = wireframe;

          object.userData.originalMaterial = object.material;
          object.material = this.facesMaterial;
          object.isHighlighted = true;
        }
      });
  }

  unhighlight(objects: Object3D | Object3D[]) {
    if (!Array.isArray(objects)) objects = [objects];
    if (!objects.length) return;

    objects.forEach((object: any) => {
      if (!object.isHighlighted) return;

      object.isHighlighted = false;
      object.material = object.userData.originalMaterial;
      object.userData.highlightWireframe.removeFromParent();

      delete object.userData.originalMaterial;
      delete object.userData.highlightWireframe;
    });
  }

  geometryEnd = () => {
    this.facesMaterial = new MeshPhongMaterial({
      transparent: true,
      specular: 0x222222,
      shininess: 10,
      reflectivity: 0.05,
      polygonOffset: true,
      polygonOffsetFactor: 1,
      polygonOffsetUnits: 1,
    });

    this.edgesMaterial = new LineMaterial({
      linewidth: 1.5,
      resolution: new Vector2(window.innerWidth, window.innerHeight),
    });

    this.lineMaterial = new LineBasicMaterial({
      transparent: true,
      depthTest: true,
      depthWrite: true,
    });

    this.lineGlowMaterial = new LineMaterial({
      linewidth: 1.5,
      transparent: true,
      opacity: 0.8,
      resolution: new Vector2(window.innerWidth, window.innerHeight),
    });

    this.syncHighlightColors();
  };

  optionsChange = () => {
    this.syncHighlightColors();
    this.viewer.update();
  };

  syncHighlightColors() {
    const options = this.viewer.options.enableCustomHighlight ? this.viewer.options : Options.defaults();

    const { facesColor, facesTransparancy, facesOverlap } = options;
    const { edgesColor, edgesVisibility, edgesOverlap } = options;

    this.facesMaterial.color.setRGB(facesColor.r / 255, facesColor.g / 255, facesColor.b / 255);
    this.facesMaterial.opacity = (255 - facesTransparancy) / 255;
    this.facesMaterial.depthTest = !facesOverlap;
    this.facesMaterial.depthWrite = !facesOverlap;

    this.edgesMaterial.color.setRGB(edgesColor.r / 255, edgesColor.g / 255, edgesColor.b / 255);
    this.edgesMaterial.depthTest = !edgesOverlap;
    this.edgesMaterial.depthWrite = !edgesOverlap;

    this.lineMaterial.color.setRGB(facesColor.r / 255, facesColor.g / 255, facesColor.b / 255);

    this.lineGlowMaterial.color.setRGB(edgesColor.r / 255, edgesColor.g / 255, edgesColor.b / 255);
    this.lineGlowMaterial.depthTest = !edgesOverlap;
    this.lineGlowMaterial.depthWrite = !edgesOverlap;

    this.viewer.selected.forEach((selected) => {
      const wireframe = selected.userData.highlightWireframe;
      if (wireframe) wireframe.visible = edgesVisibility;
    });
  }

  viewerResize(event: ResizeEvent) {
    this.renderTarget?.setSize(event.width, event.height);
    this.edgesMaterial?.resolution.set(event.width, event.height);
    this.lineGlowMaterial?.resolution.set(event.width, event.height);
  }
}
