/**
 * Lane I — the `blob.body` slot, which is the only slot that is not cosmetic.
 *
 * PaintSkin stamps into a canvas texture through this mesh's UVs and derives
 * `boundingRadius` from its geometry — and that radius is read back as a
 * GAMEPLAY number (puddles.ts computes the belly contact point from it). The
 * frozen game.ts also captures `blob.mesh` by reference the instant createBlob
 * returns. So this slot is resolved SYNCHRONOUSLY from the preloaded cache, is
 * always a single leaf Mesh with a single MeshStandardMaterial, and its
 * geometry is re-centred and re-scaled to the procedural radius. Anything that
 * fails those tests keeps the procedural sphere and says why, out loud.
 */
import * as THREE from 'three'
import type { World } from '../contracts'
import { assets, collectMeshes, normalizeToRadius, paintableInfo } from './registry'
import type { SlotEntry } from './manifest'

/**
 * Bake a body asset down to ONE mesh sitting at the origin at exactly `radius`.
 *
 * Shared between `resolveBlobBody` and the ghost's borrow of `blob.body`,
 * because the fit is not decoration: `boundingRadius` is a gameplay number for
 * the blob, and for the ghost a body that skips this renders at the raw GLB's
 * size — 2.17x oversized with the farm's blobbo-base.glb, measured.
 *
 * Every transform between `root` and the mesh (and `root`'s own) is baked into
 * the geometry and then zeroed, so no ancestor can re-scale the body after it
 * has been fitted. `entry.scale` is deliberately dropped: the body is always
 * the size of its collider.
 */
export function fitBodyToRadius(
  root: THREE.Object3D,
  radius: number,
  entry?: Pick<SlotEntry, 'offset' | 'rotationDeg'>,
): THREE.Mesh | null {
  const mesh = collectMeshes(root)[0]
  if (!mesh) return null

  root.updateMatrixWorld(true)
  const geo = mesh.geometry.clone()
  // Mesh pose relative to root, so a GLB that parks its mesh under a
  // transformed node is fitted the same as one that does not.
  const rel = new THREE.Matrix4().copy(root.matrixWorld).invert().multiply(mesh.matrixWorld)
  geo.applyMatrix4(rel)

  if (entry?.offset || entry?.rotationDeg) {
    const k = Math.PI / 180
    const rot = entry.rotationDeg
      ? new THREE.Euler(entry.rotationDeg[0] * k, entry.rotationDeg[1] * k, entry.rotationDeg[2] * k)
      : new THREE.Euler()
    const off = entry.offset ?? [0, 0, 0]
    geo.applyMatrix4(new THREE.Matrix4().compose(
      new THREE.Vector3(off[0], off[1], off[2]),
      new THREE.Quaternion().setFromEuler(rot),
      new THREE.Vector3(1, 1, 1),
    ))
  }

  normalizeToRadius(geo, radius)
  mesh.geometry = geo

  for (let o: THREE.Object3D | null = mesh; o; o = o === root ? null : o.parent) {
    o.position.set(0, 0, 0)
    o.rotation.set(0, 0, 0)
    o.scale.set(1, 1, 1)
    o.updateMatrix()
  }
  root.updateMatrixWorld(true)
  return mesh
}

/**
 * Returns the paintable body mesh for the blob: the swapped-in GLB mesh when
 * `blob.body` is filled AND paint-safe, otherwise the caller's own sphere.
 */
export function resolveBlobBody(
  // `world` is unused since the idle-clip mixer was removed, but the signature
  // is called from createBlob and the editor — keep it stable.
  world: World,
  radius: number,
  buildFallback: () => THREE.Mesh,
): THREE.Mesh {
  void world
  const reg = assets()
  const inst = reg.instanceSync('blob.body')
  if (!inst) {
    if (reg.has('blob.body')) {
      console.warn(
        '[assets] blob.body was not preloaded in time — keeping the built-in blob. ' +
        'Call `await initAssets()` before installGame().',
      )
    }
    return buildFallback()
  }

  const info = paintableInfo(inst.object)
  if (!info.ok) {
    console.warn(
      '[assets] blob.body cannot be painted, so the built-in blob is being used instead:\n  - ' +
      info.problems.join('\n  - '),
    )
    return buildFallback()
  }
  if (info.problems.length) {
    console.warn('[assets] blob.body loaded with warnings:\n  - ' + info.problems.join('\n  - '))
  }

  const entry = inst.entry
  if (entry.scale !== undefined) {
    console.warn('[assets] blob.body "scale" is ignored — the body is always fitted to the blob size.')
  }
  if (entry.idleClip !== undefined) {
    console.warn(
      '[assets] blob.body "idleClip" is ignored — animated bodies are not supported yet. ' +
      'The squash-and-stretch on the blob group still plays.',
    )
  }

  // Bake the fit into the geometry rather than a parent node: feel.ts owns this
  // mesh's whole local transform and would clobber a node, and PaintSkin
  // raycasts in the mesh's own space.
  const mesh = fitBodyToRadius(inst.object, radius, entry)
  if (!mesh) return buildFallback()

  // feel.ts writes `emissiveIntensity` for the super-glow and PaintSkin forces
  // the colour white; an imported material usually has neither set up.
  const mat = mesh.material as THREE.MeshStandardMaterial
  if (!mat.emissive || mat.emissive.getHex() === 0x000000) {
    mat.emissive = new THREE.Color('#88e0ff')
  }
  mat.emissiveIntensity = 0
  mesh.castShadow = true
  return mesh
}
