import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import type { MeshRegistryEntry } from './cadUtils'
import type { PartMaterialMap } from '../../api/cad'
import { groupRegistryByMaterial, resolvePartMaterial } from './cadUtils'
import type { MaterialPBRMap } from '../../api/assetLibraries'
import { applyPBRToMaterial, previewColorForEntry } from './cadUtils'

interface UseGeometryMergeOpts {
  meshRegistryRef: React.RefObject<MeshRegistryEntry[]>
  partMaterials: PartMaterialMap
  pbrMap: MaterialPBRMap
  enabled: boolean
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  sceneRef: React.RefObject<any>  // THREE.Scene
}

interface MergedState {
  mergedMeshes: THREE.Mesh[]
  hiddenOriginals: THREE.Object3D[]
}

/**
 * Hook that merges meshes sharing the same material into single geometries
 * to reduce draw calls. When enabled, original meshes are hidden and merged
 * replacements are added to the scene. When disabled, originals are restored.
 */
export function useGeometryMerge({
  meshRegistryRef,
  partMaterials,
  pbrMap,
  enabled,
  sceneRef,
}: UseGeometryMergeOpts): { drawCallReduction: number } {
  const stateRef = useRef<MergedState | null>(null)
  const reductionRef = useRef(0)

  useEffect(() => {
    const scene = sceneRef.current
    const registry = meshRegistryRef.current
    if (!scene || !registry || registry.length === 0) return

    if (!enabled) {
      // Restore originals
      if (stateRef.current) {
        _restore(stateRef.current, scene)
        stateRef.current = null
        reductionRef.current = 0
      }
      return
    }

    // Already merged — skip
    if (stateRef.current) return

    const groups = groupRegistryByMaterial(registry, partMaterials)
    if (groups.size === 0) return

    const mergedMeshes: THREE.Mesh[] = []
    const hiddenOriginals: THREE.Object3D[] = []
    let meshesReplaced = 0

    for (const [materialKey, entries] of groups) {
      // Collect geometries with world transforms baked in
      const geometries: THREE.BufferGeometry[] = []
      for (const entry of entries) {
        const mesh = entry.mesh as THREE.Mesh
        if (!mesh.geometry) continue
        // Ensure world matrix is up to date
        mesh.updateWorldMatrix(true, false)
        const cloned = mesh.geometry.clone()
        cloned.applyMatrix4(mesh.matrixWorld)
        // Skip geometries with incompatible attributes (e.g. missing normals)
        if (geometries.length > 0) {
          const refAttrs = Object.keys(geometries[0].attributes).sort().join(',')
          const curAttrs = Object.keys(cloned.attributes).sort().join(',')
          if (curAttrs !== refAttrs) {
            cloned.dispose()
            continue
          }
        }
        geometries.push(cloned)
      }

      if (geometries.length < 2) {
        for (const g of geometries) g.dispose()
        continue
      }

      try {
        const merged = mergeGeometries(geometries, false)
        if (!merged) {
          for (const g of geometries) g.dispose()
          continue
        }

        // Create merged mesh with material from the first entry
        const sourceMesh = entries[0].mesh as THREE.Mesh
        const mat = (Array.isArray(sourceMesh.material)
          ? sourceMesh.material[0]
          : sourceMesh.material) as THREE.MeshStandardMaterial
        const mergedMat = mat.clone()

        // Apply PBR properties to the merged material
        const partEntry = resolvePartMaterial(entries[0].partKey, partMaterials)
        if (partEntry) {
          if (partEntry.type === 'library' && pbrMap[partEntry.value]) {
            applyPBRToMaterial(mergedMat, pbrMap[partEntry.value])
          } else {
            mergedMat.color.set(previewColorForEntry(partEntry, pbrMap))
          }
        }

        const mergedMesh = new THREE.Mesh(merged, mergedMat)
        mergedMesh.name = `__merged_${materialKey}`
        mergedMesh.userData._isMerged = true
        scene.add(mergedMesh)
        mergedMeshes.push(mergedMesh)

        // Hide originals
        for (const entry of entries) {
          const mesh = entry.mesh as THREE.Object3D
          mesh.visible = false
          mesh.raycast = () => {} // disable raycasting
          hiddenOriginals.push(mesh)
        }
        meshesReplaced += entries.length

        // Clean up cloned geometries (merged copy owns the data now)
        for (const g of geometries) g.dispose()
      } catch {
        for (const g of geometries) g.dispose()
      }
    }

    stateRef.current = { mergedMeshes, hiddenOriginals }
    reductionRef.current = meshesReplaced - mergedMeshes.length

    // Cleanup on unmount or deps change
    return () => {
      if (stateRef.current) {
        _restore(stateRef.current, scene)
        stateRef.current = null
        reductionRef.current = 0
      }
    }
  }, [enabled, partMaterials, pbrMap, meshRegistryRef, sceneRef])

  return { drawCallReduction: reductionRef.current }
}

function _restore(state: MergedState, scene: THREE.Scene): void {
  // Remove merged meshes
  for (const mesh of state.mergedMeshes) {
    scene.remove(mesh)
    mesh.geometry.dispose()
    if (Array.isArray(mesh.material)) {
      for (const m of mesh.material) m.dispose()
    } else {
      mesh.material.dispose()
    }
  }
  // Restore originals
  for (const obj of state.hiddenOriginals) {
    obj.visible = true
    obj.raycast = THREE.Mesh.prototype.raycast
  }
}
