import { ResourcesC } from "@24tools/playable_template";
import { Easing, Group, Tween } from "@tweenjs/tween.js";
import {
  Material,
  Mesh,
  Object3D,
  Vector3,
} from "three";
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
import { CameraC } from "../CameraC";
import { ResourcesType } from "../Presets/Enums/ResourcesType";
import { ThreeC } from "../ThreeC";
import { ResourceType } from "./ResourceType";

export type ScreenPoint = {
  x: number;
  y: number;
  z: number;
};

const PICKUP_SIZE = 1;

export class ResourceScreenFly {
  static createPickup(type: ResourceType) {
    const prefab = ResourcesC.getResource<GLTF>(ResourcesType.Mesh, type);
    if (!prefab?.scene) {
      console.warn(`Resource mesh not found: ${type}`);
      return null;
    }

    const meshSource = this.findFirstMesh(prefab.scene);
    if (!meshSource) {
      console.warn(`Resource mesh has no geometry: ${type}`);
      return null;
    }

    const mesh = meshSource.clone();
    mesh.position.set(0, 0, 0);
    mesh.rotation.set(0, 0, 0);
    mesh.scale.set(1, 1, 1);
    this.cloneMaterial(mesh);

    const pickup = new Object3D();
    pickup.add(mesh);

    mesh.geometry.computeBoundingBox();
    const size = new Vector3();
    mesh.geometry.boundingBox!.getSize(size);
    const maxDim = Math.max(size.x, size.y, size.z);
    if (maxDim > 0) {
      pickup.scale.setScalar(PICKUP_SIZE / maxDim);
    }

    this.orientToCamera(pickup);
    return pickup;
  }

  static flyAlongScreen(
    pickup: Object3D,
    startScreen: ScreenPoint,
    endScreen: ScreenPoint,
    tweenGroup: Group,
    durationMs: number,
    endScale: number,
    onComplete: () => void,
  ) {
    const startScale = pickup.scale.clone();
    pickup.position.copy(this.screenToWorld(startScreen));
    this.orientToCamera(pickup);
    ThreeC.addToScene(pickup);

    const state = { t: 0 };

    const flyTween = new Tween(state)
      .to({ t: 1 }, durationMs)
      .easing(Easing.Cubic.InOut)
      .onUpdate(() => {
        const screen = this.lerpScreenPoint(startScreen, endScreen, state.t);
        pickup.position.copy(this.screenToWorld(screen));
        const scale = this.lerp(1, endScale, state.t);
        pickup.scale.set(
          startScale.x * scale,
          startScale.y * scale,
          startScale.z * scale,
        );
        this.orientToCamera(pickup);
      })
      .onComplete(() => {
        ThreeC.removeFromScene(pickup);
        onComplete();
      });

    tweenGroup.add(flyTween);
    flyTween.start(performance.now());
  }

  static orientToCamera(object: Object3D) {
    object.quaternion.copy(CameraC.camera.quaternion);
  }

  static worldToScreen(worldPos: Vector3): ScreenPoint {
    const canvasRect = this.getCanvasRect();
    if (!canvasRect) {
      return { x: 0, y: 0, z: 0.5 };
    }

    const projected = worldPos.clone().project(CameraC.camera);

    return {
      x: (projected.x * 0.5 + 0.5) * canvasRect.width + canvasRect.left,
      y: (-projected.y * 0.5 + 0.5) * canvasRect.height + canvasRect.top,
      z: projected.z,
    };
  }

  static screenPointFromClient(clientX: number, clientY: number, ndcZ: number): ScreenPoint {
    return { x: clientX, y: clientY, z: ndcZ };
  }

  static screenToWorld(screen: ScreenPoint) {
    const canvasRect = this.getCanvasRect();
    const camera = CameraC.camera;
    if (!canvasRect) {
      return new Vector3();
    }

    const ndc = new Vector3(
      ((screen.x - canvasRect.left) / canvasRect.width) * 2 - 1,
      -((screen.y - canvasRect.top) / canvasRect.height) * 2 + 1,
      screen.z,
    );
    ndc.unproject(camera);
    return ndc;
  }

  private static lerp(from: number, to: number, t: number) {
    return from + (to - from) * t;
  }

  private static lerpScreenPoint(from: ScreenPoint, to: ScreenPoint, t: number): ScreenPoint {
    return {
      x: this.lerp(from.x, to.x, t),
      y: this.lerp(from.y, to.y, t),
      z: this.lerp(from.z, to.z, t),
    };
  }

  private static getCanvasRect() {
    const canvas = document.querySelector("canvas");
    return canvas?.getBoundingClientRect() ?? null;
  }

  private static findFirstMesh(root: Object3D): Mesh | null {
    let result: Mesh | null = null;
    root.traverse((child) => {
      if (!result && (child as Mesh).isMesh) {
        result = child as Mesh;
      }
    });
    return result;
  }

  private static cloneMaterial(mesh: Mesh) {
    const applyPickupMaterial = (material: Material) => {
      const cloned = material.clone();
      cloned.depthTest = true;
      cloned.depthWrite = true;
      cloned.polygonOffset = true;
      cloned.polygonOffsetFactor = -2;
      cloned.polygonOffsetUnits = -2;
      return cloned;
    };

    const { material } = mesh;
    if (Array.isArray(material)) {
      mesh.material = material.map((entry) => applyPickupMaterial(entry));
      return;
    }

    if (material) {
      mesh.material = applyPickupMaterial(material);
    }
  }
}
