import * as THREE from "three";

interface ExplodedPart {
  object: THREE.Object3D;
  originalPosition: THREE.Vector3;
  targetPosition: THREE.Vector3;
}

interface ExplodedModelOptions {
  distance?: number;
  speed?: number;
}

const _center = new THREE.Vector3();
const _direction = new THREE.Vector3();

export class ExplodedModel {
  private readonly parts: ExplodedPart[] = [];
  private readonly distance: number;
  private readonly speed: number;
  private progress = 0;
  private targetProgress = 0;

  constructor(model: THREE.Object3D, options: ExplodedModelOptions = {}) {
    this.distance = options.distance ?? 1.2;
    this.speed = options.speed ?? 6;
    this.parts = this.createParts(model);
  }

  setSplit(split: boolean): void {
    this.targetProgress = split ? 1 : 0;
  }

  update(delta: number): void {
    const diff = this.targetProgress - this.progress;
    if (Math.abs(diff) < 0.001) {
      this.progress = this.targetProgress;
    } else {
      this.progress += diff * Math.min(delta * this.speed, 1);
    }

    this.parts.forEach((part) => {
      part.object.position.lerpVectors(
        part.originalPosition,
        part.targetPosition,
        this.progress,
      );
    });
  }

  private createParts(model: THREE.Object3D): ExplodedPart[] {
    const root =
      model.children.length === 1 && model.children[0]
        ? model.children[0]
        : model;
    const directChildren = root.children.filter((child) => hasMesh(child));
    const sourceObjects =
      directChildren.length > 1 ? directChildren : getMeshes(root);

    if (sourceObjects.length === 0) return [];

    _center.set(0, 0, 0);
    sourceObjects.forEach((object) => _center.add(object.position));
    _center.divideScalar(sourceObjects.length);

    return sourceObjects.map((object, index) => {
      const originalPosition = object.position.clone();
      _direction.subVectors(originalPosition, _center);

      if (_direction.lengthSq() < 0.0001) {
        const angle = (index / sourceObjects.length) * Math.PI * 2;
        _direction.set(Math.cos(angle), 0.25, Math.sin(angle));
      }

      _direction.normalize();

      return {
        object,
        originalPosition,
        targetPosition: originalPosition
          .clone()
          .addScaledVector(_direction, this.distance),
      };
    });
  }
}

function hasMesh(object: THREE.Object3D): boolean {
  let found = false;
  object.traverse((child) => {
    if (child instanceof THREE.Mesh) {
      found = true;
    }
  });
  return found;
}

function getMeshes(object: THREE.Object3D): THREE.Object3D[] {
  const meshes: THREE.Object3D[] = [];
  object.traverse((child) => {
    if (child instanceof THREE.Mesh) {
      meshes.push(child);
    }
  });
  return meshes;
}
