///////////////////////////////////////////////////////////////////////////////
// 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 { Box3, Object3D, Vector3 } from "three";
import { IInfo, Info } from "@inweb/viewer-core";

import { IModelImpl } from "./IModelImpl";
import { convertUnits } from "../measurement/UnitConverter";
import { getDisplayUnit } from "../measurement/UnitFormatter";

export class ModelImpl implements IModelImpl {
  public id: string;
  public scene: Object3D;

  constructor(scene: Object3D) {
    this.scene = scene;
  }

  dispose() {
    function disposeMaterial(material: any) {
      // if (material.alphaMap) material.alphaMap.dispose();
      // if (material.anisotropyMap) material.anisotropyMap.dispose();
      // if (material.aoMap) material.aoMap.dispose();
      // if (material.bumpMap) material.bumpMap.dispose();
      // if (material.clearcoatMap) material.clearcoatMap.dispose();
      // if (material.clearcoatNormalMap) material.clearcoatNormalMap.dispose();
      // if (material.clearcoatRoughnessMap) material.clearcoatRoughnessMap.dispose();
      // if (material.displacementMap) material.displacementMap.dispose();
      // if (material.emissiveMap) material.emissiveMap.dispose();
      // if (material.gradientMap) material.gradientMap.dispose();
      // if (material.envMap) material.envMap.dispose();
      // if (material.iridescenceMap) material.iridescenceMap.dispose();
      // if (material.lightMap) material.lightMap.dispose();
      // if (material.map) material.map.dispose();
      // if (material.metalnessMap) material.metalnessMap.dispose();
      // if (material.normalMap) material.normalMap.dispose();
      // if (material.roughnessMap) material.roughnessMap.dispose();
      // if (material.sheenColorMap) material.sheenColorMap.dispose();
      // if (material.sheenRoughnessMap) material.sheenRoughnessMap.dispose();
      // if (material.specularMap) material.specularMap.dispose();
      // if (material.thicknessMap) material.thicknessMap.dispose();
      // if (material.transmissionMap) material.transmissionMap.dispose();
      material.dispose();
    }

    function disposeMaterials(material: any) {
      const materials = Array.isArray(material) ? material : [material];
      materials.forEach((material: any) => disposeMaterial(material));
    }

    function disposeObject(object: any) {
      if (object.geometry) object.geometry.dispose();
      if (object.material) disposeMaterials(object.material);
    }

    this.scene.traverse(disposeObject);
    this.scene.clear();
  }

  getUnits(): string {
    return "Meters";
  }

  getUnitScale(): number {
    return convertUnits(this.getUnits(), "Meters", 1);
  }

  getUnitString(): string {
    return getDisplayUnit(this.getUnits());
  }

  getPrecision(): number {
    return 2;
  }

  getInfo(): IInfo {
    // ===================== AI-CODE-START ======================
    // Source: Claude Sonnet 4.5
    // Date: 2025-10-02
    // Reviewer: roman.mochalov@opendesign.com
    // Issue: CLOUD-5738

    const geometries = new Set();
    const materials = new Set();
    const textures = new Set();

    let totalObjects = 0;
    let totalTriangles = 0;
    let totalPoints = 0;
    let totalLines = 0;
    let totalEdges = 0;
    let geometryBytes = 0;
    let textureBytes = 0;

    this.scene.traverse((object: any) => {
      totalObjects++;

      if (object.geometry) {
        const geometry = object.geometry;

        if (!geometries.has(geometry)) {
          geometries.add(geometry);

          if (geometry.attributes) {
            for (const name in geometry.attributes) {
              const attribute = geometry.attributes[name];
              if (attribute && attribute.array) {
                geometryBytes += attribute.array.byteLength;
              }
            }
          }

          if (geometry.index && geometry.index.array) {
            geometryBytes += geometry.index.array.byteLength;
          }
        }

        if (geometry.index) {
          const indexCount = geometry.index.count;

          if (object.isLine || object.isLineSegments) {
            totalLines += indexCount / 2;
          } else if (object.isPoints) {
            totalPoints += indexCount;
          } else {
            totalTriangles += indexCount / 3;
          }
        } else if (geometry.attributes && geometry.attributes.position) {
          const positionCount = geometry.attributes.position.count;

          if (object.isLine || object.isLineSegments) {
            totalLines += positionCount / 2;
          } else if (object.isPoints) {
            totalPoints += positionCount;
          } else {
            totalTriangles += positionCount / 3;
          }
        }

        if (object.isLineSegments && geometry.attributes.position) {
          totalEdges += geometry.attributes.position.count / 2;
        }
      }

      if (object.material) {
        const materialsArray = Array.isArray(object.material) ? object.material : [object.material];

        materialsArray.forEach((material: any) => {
          materials.add(material);

          if (material.map && !textures.has(material.map)) {
            textures.add(material.map);
            textureBytes += estimateTextureSize(material.map);
          }

          const textureProps = [
            "alphaMap",
            "aoMap",
            "bumpMap",
            "displacementMap",
            "emissiveMap",
            "envMap",
            "lightMap",
            "metalnessMap",
            "normalMap",
            "roughnessMap",
            "specularMap",
            "clearcoatMap",
            "clearcoatNormalMap",
            "clearcoatRoughnessMap",
            "iridescenceMap",
            "sheenColorMap",
            "sheenRoughnessMap",
            "thicknessMap",
            "transmissionMap",
            "anisotropyMap",
            "gradientMap",
          ];

          textureProps.forEach((prop) => {
            const texture = material[prop];
            if (texture && !textures.has(texture)) {
              textures.add(texture);
              textureBytes += estimateTextureSize(texture);
            }
          });
        });
      }
    });

    function estimateTextureSize(texture: any): number {
      if (!texture.image) return 0;

      const width = texture.image.width || 0;
      const height = texture.image.height || 0;

      // Estimate bytes per pixel (RGBA = 4 bytes)
      const bytesPerPixel = 4;

      // Account for mipmaps (adds ~33% more memory)
      const mipmapMultiplier = texture.generateMipmaps ? 1.33 : 1;

      return width * height * bytesPerPixel * mipmapMultiplier;
    }

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

    const info = new Info();

    info.scene.objects = totalObjects;
    info.scene.triangles = Math.floor(totalTriangles);
    info.scene.points = Math.floor(totalPoints);
    info.scene.lines = Math.floor(totalLines);
    info.scene.edges = Math.floor(totalEdges);

    info.memory.geometries = geometries.size;
    info.memory.geometryBytes = geometryBytes;
    info.memory.textures = textures.size;
    info.memory.textureBytes = Math.floor(textureBytes);
    info.memory.materials = materials.size;
    info.memory.totalEstimatedGpuBytes = geometryBytes + Math.floor(textureBytes);

    info.optimizedScene.objects = info.scene.objects;
    info.optimizedScene.triangles = info.scene.triangles;
    info.optimizedScene.points = info.scene.points;
    info.optimizedScene.lines = info.scene.lines;
    info.optimizedScene.edges = info.scene.edges;

    return info;
  }

  getExtents(target: Box3): Box3 {
    this.scene.traverseVisible((object) => !object.children.length && target.expandByObject(object));
    return target;
  }

  getObjects(): Object3D[] {
    const objects = [];
    this.scene.traverse((object) => objects.push(object));
    return objects;
  }

  getVisibleObjects(): Object3D[] {
    const objects = [];
    this.scene.traverseVisible((object) => objects.push(object));
    return objects;
  }

  hasObject(object: Object3D): boolean {
    while (object) {
      if (object === this.scene) return true;
      object = object.parent;
    }
    return false;
  }

  hasHandle(handle: string): boolean {
    return !handle.includes(":") || handle.split(":", 1)[0] === this.id + "";
  }

  getOwnObjects(objects: Object3D | Object3D[]): Object3D[] {
    if (!Array.isArray(objects)) objects = [objects];
    return objects.filter((object) => this.hasObject(object));
  }

  getOwnHandles(handles: string | string[]): string[] {
    if (!Array.isArray(handles)) handles = [handles];
    return handles.filter((handle) => this.hasHandle(handle));
  }

  getObjectsByHandles(handles: string | string[]): Object3D[] {
    const ownHandles = this.getOwnHandles(handles);
    if (ownHandles.length === 0) return [];

    const handleSet = new Set(ownHandles.map((handle) => handle.slice(handle.indexOf(":") + 1)));

    const objects = [];
    this.scene.traverse((object) => {
      const handle = object.userData.handle;
      if (handle && handleSet.has(handle)) objects.push(object);
    });

    return objects;
  }

  getHandlesByObjects(objects: Object3D | Object3D[]): string[] {
    const ownObjects = this.getOwnObjects(objects);
    if (ownObjects.length === 0) return [];

    const handleSet = new Set<string>();
    ownObjects.forEach((object) => {
      const handle = object.userData.handle;
      if (handle) handleSet.add(`${this.id}:${handle}`);
    });

    return Array.from(handleSet);
  }

  hideObjects(objects: Object3D | Object3D[]): this {
    this.getOwnObjects(objects).forEach((object) => (object.visible = false));
    return this;
  }

  hideAllObjects(): this {
    return this.isolateObjects([]);
  }

  isolateObjects(objects: Object3D | Object3D[]): this {
    if (!Array.isArray(objects)) objects = [objects];
    const visibleSet = new Set(objects);
    this.getOwnObjects(objects).forEach((object) => object.traverseAncestors((parent) => visibleSet.add(parent)));
    this.scene.traverse((object) => (object.visible = visibleSet.has(object)));
    return this;
  }

  showObjects(objects: Object3D | Object3D[]): this {
    this.getOwnObjects(objects).forEach((object) => {
      object.visible = true;
      object.traverseAncestors((parent) => (parent.visible = true));
    });
    return this;
  }

  showAllObjects(): this {
    this.scene.traverse((object) => (object.visible = true));
    return this;
  }

  showOriginalObjects(objects: Object3D | Object3D[]): this {
    return this;
  }

  hideOriginalObjects(objects: Object3D | Object3D[]): this {
    return this;
  }

  explode(scale = 0, coeff = 4): this {
    function calcExplodeDepth(object: Object3D, depth: number): number {
      let res = depth;
      object.children.forEach((x: Object3D) => {
        const objectDepth = calcExplodeDepth(x, depth + 1);
        if (res < objectDepth) res = objectDepth;
      });

      object.userData.originalPosition = object.position.clone();
      object.userData.originalCenter = new Box3().setFromObject(object).getCenter(new Vector3());
      object.userData.isExplodeLocked = depth > 2 && object.children.length === 0;

      return res;
    }

    scale /= 100;

    if (!this.scene.userData.explodeDepth) this.scene.userData.explodeDepth = calcExplodeDepth(this.scene, 1);
    const maxDepth = this.scene.userData.explodeDepth;

    const scaledExplodeDepth = scale * maxDepth + 1;
    const explodeDepth = 0 | scaledExplodeDepth;
    const currentSegmentFraction = scaledExplodeDepth - explodeDepth;

    function explodeObject(object: Object3D, depth: number) {
      object.position.copy(object.userData.originalPosition);

      if (depth > 0 && depth <= explodeDepth && !object.userData.isExplodeLocked) {
        let objectScale = scale * coeff;
        if (depth === explodeDepth) objectScale *= currentSegmentFraction;

        const parentCenter = object.parent.userData.originalCenter;
        const objectCenter = object.userData.originalCenter;
        const objectOffset = objectCenter.clone().sub(parentCenter).multiplyScalar(objectScale);

        object.position.add(objectOffset);
      }

      object.children.forEach((x) => explodeObject(x, depth + 1));
    }

    explodeObject(this.scene, 0);
    this.scene.updateMatrixWorld();

    return this;
  }
}
