/*
 * Scene3D
 * ---------------------------------------------------------------------------
 * Was: ~11.6k lines of gltfjsx-generated <mesh> components (1,372 meshes), each
 * with hardcoded castShadow/receiveShadow. That JSX shipped ~11.6k lines of JS,
 * forced React to reconcile 1,372 mesh fibers on mount (a long main-thread block),
 * and made every mesh cast + receive shadows.
 *
 * Now: the GLTF scene is rendered with a single <primitive>. Animation refs and
 * shadow/LOD policy are applied by traversing the loaded graph once.
 *
 * REF WIRING — verified against the actual GLB node tree (not the gltfjsx JSX,
 * which flattens it):
 *   tyre mesh (LCT300007_WheelStock_XX_RB1c_Tire_1k_*)
 *     └─ Object_NN              (wheel hub wrapper)
 *        └─ LCT300007_WheelStock_XX   ← wheelRef target (matches the old wheel
 *           └─ RootNode                  group's transform exactly; Y is the axle)
 *              └─ *.fbx
 *                 └─ Sketchfab_model     ← truckRef target (a scene root; its
 *                                           transform == the old truckRef group)
 * So: wheelRef = tyre.parent.parent, and truckRef = the tyre's ancestor whose
 * parent is the scene root. These point at the SAME nodes the original JSX did,
 * so TruckAnimation/wheelAnimation behave identically.
 *
 * The original generated file is preserved at Scene3D.gltfjsx.bak.txt.
 *
 * NOTE: still loads the single 31 MB GLB. Splitting + Draco/WebP compression is a
 * binary-asset step (see PR notes) and is what fixes DOWNLOAD weight; the changes
 * here reduce RUNTIME (JS bundle, mount cost, draw calls, shadow fill).
 */
import React, { useLayoutEffect } from 'react'
import { useGLTF } from '@react-three/drei'

const GLB = '/models/3d_scene_final.glb'
const DRACO_PATH = '/draco/' // self-hosted decoder (public/draco/)

// Tyre meshes in the exact order the rig expects: [FR, FL, RL, RR]. Order is
// load-bearing — animateWheels() flips spin direction by index parity.
const TYRE_ORDER = [
  /WheelStock_FR_RB1c_Tire_1k_0$/i, // FR (suffix _0, NOT .001)
  /WheelStock_FR_RB1c_Tire_1k_0\.001$/i, // FL (the .001 duplicate)
  /WheelStock_RL_RB1c_Tire/i, // RL
  /WheelStock_RR_RB1c_Tire/i, // RR
]

// Shadow policy: keep shadows ONLY on the truck (caster) and ground (receiver).
const TRUCK_NAME_RX = /^LCT300007/i
const GROUND_NAME_RX = /road|floor|slab|driveway|apron|grass|ground|pad|curb/i
const GROUND_MAT_RX = /asphalt|concrete|lane|apron|curb|pavement|tarmac|grass/i

// LOD buckets (matched against mesh name OR material name — both are reliable on
// the <primitive> graph). Hidden meshes are skipped at draw time.
const FOLIAGE_NAME_RX = /tree|foliage|atlas|bush|hedge|shrub/i
const FOLIAGE_MAT_RX = /tree|leaf|leaves|bark|foliage|shrub|grass/i
const CLUTTER_NAME_RX = /carton|cube|crate|package_box|barrel|pallet|bench/i
const CLUTTER_MAT_RX = /cardboard|pallet/i
const STREETLIGHT_NAME_RX = /street_light|streetlight|lamp/i
const STREETLIGHT_MAT_RX = /street_light/i
const BG_TREE_NAME_RX = /atlas|background_tree/i
const BG_TREE_MAT_RX = /background_tree_atlas/i

const matName = (o) => (Array.isArray(o.material) ? o.material[0]?.name : o.material?.name) || ''

export function Model({ truckRef, wheelRefs, tier = 'desktop', /* dashboardRefs (unused) */ ...props }) {
  // String arg = self-hosted Draco decoder path: the GLB is Draco-compressed
  // geometry + WebP textures (31 MB → 3.7 MB). Self-hosting (vs the gstatic CDN)
  // keeps this static-export site free of an external runtime dependency.
  const { scene } = useGLTF(GLB, DRACO_PATH)

  // useLayoutEffect: wire refs + prune before first paint so TruckAnimation /
  // CameraRig (which run in useFrame) see a fully-configured graph on frame 1.
  useLayoutEffect(() => {
    // 1) Wire animation refs by traversal (drei's `nodes` dict sanitises names
    //    like ".001" → reading the live graph avoids that mismatch) -----------
    const tyres = [null, null, null, null]
    let anyTyre = null
    scene.traverse((o) => {
      if (!o.isMesh) return
      const n = o.name || ''
      if (!/WheelStock_(FR|RL|RR)_RB1c_Tire/i.test(n)) return
      anyTyre = o
      TYRE_ORDER.forEach((rx, i) => {
        if (!tyres[i] && rx.test(n)) tyres[i] = o
      })
    })

    tyres.forEach((tyre, i) => {
      const wheelEmpty = tyre?.parent?.parent ?? null // tyre → Object_NN → WheelStock_XX
      if (wheelEmpty && wheelRefs?.[i]) wheelRefs[i].current = wheelEmpty
    })

    // Truck root = the tyre's ancestor that is a direct child of the scene root
    // (== Sketchfab_model, the node the old JSX truckRef pointed at).
    if (truckRef) {
      let t = anyTyre
      while (t && t.parent && t.parent !== scene) t = t.parent
      truckRef.current = t ?? null
      if (!t && process.env.NODE_ENV !== 'production') {
        console.warn('[Scene3D] Could not resolve truck root — truck will not animate.')
      }
    }

    // 2) Shadow pruning + per-tier LOD visibility (single traversal) ----------
    scene.traverse((o) => {
      if (!o.isMesh) return
      const n = o.name || ''
      const m = matName(o)
      const isTruck = TRUCK_NAME_RX.test(n)
      const isGround = GROUND_NAME_RX.test(n) || GROUND_MAT_RX.test(m)

      // GLTFLoader defaults cast/receive to false. Enable only where it matters.
      o.castShadow = isTruck
      o.receiveShadow = isTruck || isGround
      if (isTruck) o.frustumCulled = false // truck is the subject — never cull it

      let hidden = false
      if (tier === 'mobile') {
        hidden =
          FOLIAGE_NAME_RX.test(n) || FOLIAGE_MAT_RX.test(m) ||
          CLUTTER_NAME_RX.test(n) || CLUTTER_MAT_RX.test(m) ||
          STREETLIGHT_NAME_RX.test(n) || STREETLIGHT_MAT_RX.test(m)
      } else if (tier === 'tablet') {
        hidden = BG_TREE_NAME_RX.test(n) || BG_TREE_MAT_RX.test(m)
      }
      o.visible = !hidden
    })
  }, [scene, truckRef, wheelRefs, tier])

  return <primitive object={scene} {...props} dispose={null} />
}

useGLTF.preload(GLB, DRACO_PATH)
