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

import { ModelImpl } from "../../models/ModelImpl";
import { DynamicGltfLoader } from "./DynamicGltfLoader.js";

// Dynamic model implementation.

export class DynamicModelImpl extends ModelImpl {
  public gltfLoader: DynamicGltfLoader;

  override getInfo(): IInfo {
    const stats = this.gltfLoader.getStats();

    const info = new Info();

    info.scene.objects = stats.scene.beforeOptimization.objects;
    info.scene.triangles = stats.scene.beforeOptimization.triangles;
    info.scene.lines = stats.scene.beforeOptimization.lines;
    info.scene.edges = stats.scene.beforeOptimization.edges;

    info.optimizedScene.objects = stats.scene.afterOptimization.objects;
    info.optimizedScene.triangles = stats.scene.afterOptimization.triangles;
    info.optimizedScene.lines = stats.scene.afterOptimization.lines;
    info.optimizedScene.edges = stats.scene.afterOptimization.edges;

    info.memory.geometries = stats.memory.geometries.count;
    info.memory.geometryBytes = stats.memory.geometries.bytes;
    info.memory.textures = stats.memory.textures.count;
    info.memory.materials = stats.memory.materials.count;
    info.memory.totalEstimatedGpuBytes = stats.memory.totalEstimatedGpuBytes;

    return info;
  }

  override getExtents(target: Box3): Box3 {
    return target.union(this.gltfLoader.getTotalGeometryExtent());
  }

  override getObjects(): Object3D[] {
    const objects = [];
    this.gltfLoader.originalObjects.forEach((object: Object3D) => {
      objects.push(object);
    });
    return objects;
  }

  override getVisibleObjects(): Object3D[] {
    return this.gltfLoader.getOriginalObjectForSelect();
  }

  override getObjectsByHandles(handles: string | string[]): Object3D[] {
    if (!Array.isArray(handles)) handles = [handles];

    const handlesSet = new Set(handles);

    const objects = [];
    handlesSet.forEach((handle) => {
      objects.push(this.gltfLoader.getObjectsByHandle(handle));
    });

    return objects.flat();
  }

  override getHandlesByObjects(objects: Object3D | Object3D[]): string[] {
    if (!Array.isArray(objects)) objects = [objects];

    const handleSet = new Set<string>();
    objects
      .filter((object) => this.gltfLoader.originalObjects.has(object))
      .forEach((object) => {
        handleSet.add(object.userData.handle);
      });

    return Array.from(handleSet);
  }

  override hideObjects(objects: Object3D | Object3D[]): this {
    const handles = this.getHandlesByObjects(objects);
    this.gltfLoader.hideObjects(handles);
    return this;
  }

  override isolateObjects(objects: Object3D | Object3D[]): this {
    const handles = this.getHandlesByObjects(objects);
    this.gltfLoader.isolateObjects(new Set(handles));
    return this;
  }

  override showObjects(objects: Object3D | Object3D[]): this {
    const handles = this.getHandlesByObjects(objects);
    this.gltfLoader.showObjects(handles);
    return this;
  }

  override showAllObjects(): this {
    this.gltfLoader.showAllHiddenObjects();
    return this;
  }

  override highlightObjects(objects: Object3D | Object3D[]): this {
    this.gltfLoader.showOriginalObjects(objects);
    return this;
  }

  override unhighlightObjects(objects: Object3D | Object3D[]): this {
    this.gltfLoader.hideOriginalObjects(objects);
    return this;
  }

  override explode(scale = 0, coeff = 4): this {
    const centers = new Map();

    const calcObjectCenter = (object: Object3D, target: Vector3): Vector3 => {
      const extents = new Box3().setFromObject(object);

      const handle = object.userData.handle;
      if (!handle) return extents.getCenter(target);

      const center = centers.get(handle);
      if (center) return target.copy(center);

      const objects = this.getObjectsByHandles(handle);
      objects.forEach((x: Object3D) => extents.expandByObject(x));
      extents.getCenter(target);

      centers.set(handle, target.clone());

      return target;
    };

    function calcExplodeDepth(object: Object3D, depth: number): number {
      let result = depth;
      object.children
        .filter((x: Object3D) => !x.userData.isOptimized)
        .forEach((x: Object3D) => {
          const objectDepth = calcExplodeDepth(x, depth + 1);
          if (result < objectDepth) result = objectDepth;
        });

      object.userData.originalPosition = object.position.clone();
      object.userData.originalCenter = calcObjectCenter(object, new Vector3());

      return result;
    }

    const explodeScale = scale / 100;
    const explodeRoot = this.scene.children[0];

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

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

    const transformMap = new Map();

    function explodeObject(object, depth: number) {
      if (object.isCamera) return;
      if (object.userData.isHighlightWireframe) return;

      object.position.copy(object.userData.originalPosition);

      if (depth > 0 && depth <= explodeDepth && !object.userData.isExplodeLocked) {
        let objectScale = explodeScale * 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);

        const matrix = new Matrix4().makeTranslation(objectOffset.x, objectOffset.y, objectOffset.z);
        transformMap.set(object, matrix);
      }

      object.children
        .filter((x: Object3D) => !x.userData.isOptimized)
        .forEach((x: Object3D) => explodeObject(x, depth + 1));
    }

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

    this.gltfLoader.applyObjectTransforms(transformMap);

    return this;
  }
}
