import { useEffect, useRef, useState, useCallback } from 'react'
import { createPortal } from 'react-dom'
import * as THREE from 'three'
import type { CoasterSimulationResult } from '../types/api'
import type { TerrainCaptureData } from './useTerrainCapture'
import { geoToEnu } from './useTerrainCapture'
import styles from './RideRenderer.module.css'

// ── Types ──────────────────────────────────────────────────────────────────────

type CameraMode = 'first' | 'third'

interface Props {
  simResult: CoasterSimulationResult
  captureData: TerrainCaptureData[]
  onStop: () => void
  /** Called at ~4 Hz with the current track-fraction position [0,1]. */
  onRideProgress?: (sFrac: number) => void
}

// ── Helpers ───────────────────────────────────────────────────────────────────

/** Largest index i such that arr[i] <= t (arr must be non-decreasing). */
function bisect(arr: number[], t: number): number {
  let lo = 0, hi = arr.length - 2
  while (lo < hi) {
    const mid = (lo + hi + 1) >> 1
    if (arr[mid] <= t) lo = mid
    else hi = mid - 1
  }
  return lo
}

function formatRideTime(s: number): string {
  const m   = Math.floor(s / 60)
  const sec = Math.floor(s % 60)
  return `${m}:${sec.toString().padStart(2, '0')}`
}

const UP = new THREE.Vector3(0, 1, 0)

// ── Precomputed ride data in Three.js ENU coords ───────────────────────────────

interface RideData {
  timeArray:    number[]
  sFrac:        number[]    // profile s_frac at each sample — parallel to timeArray
  centerline:   THREE.Vector3[]
  rail1:        THREE.Vector3[]
  rail2:        THREE.Vector3[]
  totalDuration: number
  count:        number
}

function buildRideData(simResult: CoasterSimulationResult): RideData {
  const { profile, rail_1, rail_2, origin } = simResult
  const n = Math.min(profile.s_frac.length, rail_1.length, rail_2.length)

  const r1 = (rail_1 as [number, number, number][]).slice(0, n)
    .map(([lon, lat, alt]) => geoToEnu(lon, lat, alt, origin))
  const r2 = (rail_2 as [number, number, number][]).slice(0, n)
    .map(([lon, lat, alt]) => geoToEnu(lon, lat, alt, origin))
  const cl = r1.map((a, i) => a.clone().lerp(r2[i], 0.5))

  // Cumulative time using same 1 m/s velocity floor as backend
  const timeArray: number[] = [0]
  for (let i = 1; i < n; i++) {
    const ds = (profile.s_frac[i] - profile.s_frac[i - 1]) * profile.total_length_m
    const v  = Math.max(profile.velocity_ms[i - 1], 1.0)
    timeArray.push(timeArray[i - 1] + ds / v)
  }

  const sFrac = profile.s_frac.slice(0, n)

  return { timeArray, sFrac, centerline: cl, rail1: r1, rail2: r2,
           totalDuration: timeArray[n - 1], count: n }
}

// ── Camera target computation (zero allocation per call) ──────────────────────
//
// All scratch objects are pre-allocated at module level so the hot rAF path
// never triggers the garbage collector.

const _mat     = new THREE.Matrix4()
const _dummy   = new THREE.PerspectiveCamera()   // PerspectiveCamera.lookAt → −Z toward target

// Scratch vectors reused every frame
const _scPos   = new THREE.Vector3()
const _scR1    = new THREE.Vector3()
const _scR2    = new THREE.Vector3()
const _scTang  = new THREE.Vector3()
const _scSide  = new THREE.Vector3()
const _scTUp   = new THREE.Vector3()
const _scLook  = new THREE.Vector3()
// Output objects — callers must copy out before the next call
const _outPos  = new THREE.Vector3()
const _outQuat = new THREE.Quaternion()
// Scratch for user drag rotation offset (first-person only)
const _userOffsetQuat = new THREE.Quaternion()
const _userOffsetEuler = new THREE.Euler(0, 0, 0, 'YXZ')

interface CameraTarget { pos: THREE.Vector3; quat: THREE.Quaternion }

/** Writes result into _outPos / _outQuat — no heap allocation. */
function computeCameraTarget(
  t: number,
  data: RideData,
  mode: CameraMode,
): CameraTarget {
  const { timeArray, centerline, rail1, rail2, count } = data

  const ct = Math.max(0, Math.min(t, timeArray[count - 1]))
  const i  = bisect(timeArray, ct)
  const i1 = Math.min(i + 1, count - 1)
  const alpha = (timeArray[i1] > timeArray[i])
    ? (ct - timeArray[i]) / (timeArray[i1] - timeArray[i])
    : 0

  _scPos.lerpVectors(centerline[i], centerline[i1], alpha)
  _scR1.lerpVectors(rail1[i], rail1[i1], alpha)
  _scR2.lerpVectors(rail2[i], rail2[i1], alpha)

  // Wider stencil for smoother forward tangent
  const pi = Math.max(0, i - 2)
  const pj = Math.min(count - 1, i1 + 2)
  _scTang.subVectors(centerline[pj], centerline[pi]).normalize()

  // Track-local "up": cross(tangent, rail1→rail2) → binormal
  _scSide.subVectors(_scR1, _scR2).normalize()
  _scTUp.crossVectors(_scTang, _scSide).normalize()
  if (_scTUp.dot(UP) < 0) _scTUp.negate()

  if (mode === 'first') {
    // Sit 1.5 m above centreline, look 10 m ahead
    _outPos.copy(_scPos).addScaledVector(UP, 1.5)
    _scLook.copy(_outPos).addScaledVector(_scTang, 10)
    _dummy.up.copy(_scTUp)
  } else {
    // 40 m behind + 12 m above, looking at the car
    _outPos.copy(_scPos).addScaledVector(_scTang, -40).addScaledVector(UP, 12)
    _scLook.copy(_scPos)
    _dummy.up.copy(UP)
  }

  _dummy.position.copy(_outPos)
  _dummy.lookAt(_scLook)
  _dummy.updateMatrixWorld(true)
  _mat.extractRotation(_dummy.matrixWorld)
  _outQuat.setFromRotationMatrix(_mat)

  return { pos: _outPos, quat: _outQuat }
}

// ── Terrain mesh builder (one per patch) ──────────────────────────────────────

function buildTerrainMesh(
  patch:    TerrainCaptureData,
  renderer: THREE.WebGLRenderer,
  visible:  boolean,
): { mesh: THREE.Mesh; geo: THREE.BufferGeometry; mat: THREE.Material; tex: THREE.Texture } {
  const GRID  = patch.gridSize
  const verts = patch.terrainVertices

  const posArr = new Float32Array(GRID * GRID * 3)
  const uvArr  = new Float32Array(GRID * GRID * 2)

  for (let j = 0; j < GRID; j++) {
    for (let i = 0; i < GRID; i++) {
      const idx = j * GRID + i
      const v   = verts[idx]
      posArr[idx * 3]     = v.x
      posArr[idx * 3 + 1] = v.y - 0.5
      posArr[idx * 3 + 2] = v.z
      uvArr[idx * 2]      = i / (GRID - 1)
      uvArr[idx * 2 + 1]  = j / (GRID - 1)
    }
  }

  const idxArr: number[] = []
  for (let j = 0; j < GRID - 1; j++) {
    for (let i = 0; i < GRID - 1; i++) {
      const a = j * GRID + i,  b = j * GRID + i + 1
      const c = (j + 1) * GRID + i + 1,  d = (j + 1) * GRID + i
      idxArr.push(a, b, d,  b, c, d)
    }
  }

  const geo = new THREE.BufferGeometry()
  geo.setAttribute('position', new THREE.BufferAttribute(posArr, 3))
  geo.setAttribute('uv',       new THREE.BufferAttribute(uvArr,  2))
  geo.setIndex(idxArr)
  geo.computeVertexNormals()

  const tex = new THREE.Texture(patch.imageBitmap)
  tex.colorSpace  = THREE.SRGBColorSpace
  tex.anisotropy  = renderer.capabilities.getMaxAnisotropy()
  tex.minFilter   = THREE.LinearMipmapLinearFilter
  tex.magFilter   = THREE.LinearFilter
  tex.needsUpdate = true

  const mat  = new THREE.MeshLambertMaterial({ map: tex, side: THREE.FrontSide })
  const mesh = new THREE.Mesh(geo, mat)
  mesh.visible = visible
  return { mesh, geo, mat, tex }
}

// ── Three.js scene builder ─────────────────────────────────────────────────────

function buildScene(captureData: TerrainCaptureData[], rideData: RideData, renderer: THREE.WebGLRenderer) {
  const scene = new THREE.Scene()
  scene.background = new THREE.Color(0x87ceeb)

  scene.add(new THREE.AmbientLight(0xffffff, 0.7))
  const sun = new THREE.DirectionalLight(0xfffbe6, 0.85)
  sun.position.set(200, 500, -300)
  scene.add(sun)

  const geos:  THREE.BufferGeometry[] = []
  const mats:  THREE.Material[]       = []
  const texes: THREE.Texture[]        = []

  // ── Terrain patches — one mesh per patch, only the active one visible ────
  const terrainMeshes = captureData.map((patch, i) => {
    const { mesh, geo, mat, tex } = buildTerrainMesh(patch, renderer, i === 0)
    geos.push(geo)
    mats.push(mat)
    texes.push(tex)
    scene.add(mesh)
    // Pre-upload texture to GPU so visibility swaps have zero hitch
    renderer.initTexture(tex)
    return mesh
  })

  function setActivePatch(idx: number) {
    const clamped = Math.max(0, Math.min(idx, terrainMeshes.length - 1))
    terrainMeshes.forEach((m, i) => { m.visible = i === clamped })
  }

  // ── Coaster rails ──────────────────────────────────────────────────────────
  function addRail(pts: THREE.Vector3[]) {
    const step  = Math.max(1, Math.floor(pts.length / 200))
    const dpts  = pts.filter((_, i) => i % step === 0 || i === pts.length - 1)
    const curve = new THREE.CatmullRomCurve3(dpts)
    const geo = new THREE.TubeGeometry(curve, Math.min(dpts.length * 3, 600), 0.075, 6, false)
    const mat = new THREE.MeshLambertMaterial({ color: 0xef4444 })
    geos.push(geo)
    mats.push(mat)
    scene.add(new THREE.Mesh(geo, mat))
  }
  addRail(rideData.rail1)
  addRail(rideData.rail2)

  return {
    scene,
    setActivePatch,
    disposeAll: () => {
      geos.forEach(g => g.dispose())
      mats.forEach(m => m.dispose())
      texes.forEach(t => t.dispose())
    },
  }
}

// ── Component ─────────────────────────────────────────────────────────────────

export function RideRenderer({ simResult, captureData, onStop, onRideProgress }: Props) {
  const canvasRef   = useRef<HTMLCanvasElement>(null)
  const rendererRef = useRef<THREE.WebGLRenderer | null>(null)
  const cameraRef   = useRef(new THREE.PerspectiveCamera(75, 1, 0.1, 50000))
  const sceneRef    = useRef<THREE.Scene | null>(null)
  const rafRef      = useRef<number | null>(null)
  const fpsElRef    = useRef<HTMLDivElement | null>(null)

  // Ride playback refs (mutable, used inside rAF loop)
  const rideDataRef       = useRef<RideData | null>(null)
  const isPlayingRef      = useRef(false)
  const rideTimeRef       = useRef(0)
  const lastTimeUpdateRef = useRef(-1)   // throttle for ride-time display (~4 Hz)
  const lastCursorUpdateRef = useRef(-1) // throttle for plot cursor (~1 Hz — Recharts SVG is expensive)
  const cameraModeRef     = useRef<CameraMode>('first')
  const prevWallRef     = useRef(0)

  // FPS tracking (updated via DOM ref — zero React overhead)
  const fpsCountRef  = useRef(0)
  const fpsLastRef   = useRef(0)

  // Smooth camera state
  const smoothPosRef  = useRef(new THREE.Vector3())
  const smoothQuatRef = useRef(new THREE.Quaternion())
  const needsInitRef  = useRef(true)

  // Active terrain patch switching
  const setActivePatchRef  = useRef<((idx: number) => void) | null>(null)
  const activePatchIdxRef  = useRef(0)

  // First-person drag rotation state
  const userYawRef    = useRef(0)    // radians — current yaw offset from track forward
  const userPitchRef  = useRef(0)    // radians — current pitch offset
  const isDraggingRef = useRef(false)
  const lastDragXRef  = useRef(0)
  const lastDragYRef  = useRef(0)

  // React state (updated ~4 Hz)
  const [isPlaying,     setIsPlaying]     = useState(false)
  const [rideTime,      setRideTime]      = useState(0)
  const [totalDuration, setTotalDuration] = useState(0)
  const [cameraMode,    setCameraMode]    = useState<CameraMode>('first')

  useEffect(() => { cameraModeRef.current = cameraMode }, [cameraMode])

  // ── Build Three.js scene ────────────────────────────────────────────────────

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return

    // antialias: false — significant GPU cost saving; no pixelRatio scaling on retina
    const renderer = new THREE.WebGLRenderer({ canvas, antialias: false })
    renderer.setPixelRatio(1)
    rendererRef.current = renderer

    const rideData = buildRideData(simResult)
    rideDataRef.current = rideData
    setTotalDuration(rideData.totalDuration)

    const { scene, setActivePatch, disposeAll } = buildScene(captureData, rideData, renderer)
    sceneRef.current       = scene
    setActivePatchRef.current = setActivePatch
    activePatchIdxRef.current = 0

    function onResize() {
      const w = window.innerWidth, h = window.innerHeight
      renderer.setSize(w, h)
      cameraRef.current.aspect = w / h
      cameraRef.current.updateProjectionMatrix()
    }
    window.addEventListener('resize', onResize)
    onResize()

    if (rideData.count > 1) {
      const t0 = computeCameraTarget(0, rideData, 'first')
      smoothPosRef.current.copy(t0.pos)
      smoothQuatRef.current.copy(t0.quat)
      cameraRef.current.position.copy(t0.pos)
      cameraRef.current.quaternion.copy(t0.quat)
      cameraRef.current.updateMatrixWorld()
    }

    renderer.render(scene, cameraRef.current)

    return () => {
      window.removeEventListener('resize', onResize)
      if (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); rafRef.current = null }
      disposeAll()
      renderer.dispose()
      rendererRef.current = null
      sceneRef.current    = null
    }
  }, [simResult, captureData])  // eslint-disable-line react-hooks/exhaustive-deps

  // ── rAF render loop ──────────────────────────────────────────────────────────

  // Smoothing time constants (seconds)
  const POS_TAU = 0.05
  const ROT_TAU = 0.08

  function startLoop() {
    function tick(wallMs: number) {
      const renderer = rendererRef.current
      const scene    = sceneRef.current
      if (!renderer || !scene) return

      const dt = Math.min((wallMs - prevWallRef.current) / 1000, 0.1)
      prevWallRef.current = wallMs

      // ── FPS counter (DOM write, no React) ──────────────────────────────────
      fpsCountRef.current++
      if (wallMs - fpsLastRef.current >= 500) {
        const elapsed = (wallMs - fpsLastRef.current) / 1000
        const fps = Math.round(fpsCountRef.current / elapsed)
        if (fpsElRef.current) fpsElRef.current.textContent = `${fps} FPS`
        fpsCountRef.current = 0
        fpsLastRef.current  = wallMs
      }

      // ── Ride time ──────────────────────────────────────────────────────────
      let rideT = rideTimeRef.current

      if (isPlayingRef.current) {
        rideTimeRef.current += dt
        rideT = rideTimeRef.current

        // Ride time display: ~4 Hz (cheap — only updates RideRenderer itself)
        if (rideT - lastTimeUpdateRef.current > 0.25) {
          setRideTime(rideT)
          lastTimeUpdateRef.current = rideT
        }

        // Plot cursor: ~1 Hz (expensive — triggers Recharts SVG repaint in parent)
        if (onRideProgress && rideT - lastCursorUpdateRef.current > 1.0) {
          lastCursorUpdateRef.current = rideT
          const d = rideDataRef.current
          if (d) {
            const ci  = bisect(d.timeArray, rideT)
            const ci1 = Math.min(ci + 1, d.count - 1)
            const ca  = (d.timeArray[ci1] > d.timeArray[ci])
              ? (rideT - d.timeArray[ci]) / (d.timeArray[ci1] - d.timeArray[ci])
              : 0
            onRideProgress(d.sFrac[ci] + (d.sFrac[ci1] - d.sFrac[ci]) * ca)
          }
        }

        const data = rideDataRef.current
        if (data && rideT >= data.totalDuration) {
          rideTimeRef.current  = data.totalDuration
          rideT                = data.totalDuration
          isPlayingRef.current = false
          setIsPlaying(false)
          setRideTime(rideT)
        }
      }

      // ── Active terrain patch ──────────────────────────────────────────────
      if (captureData.length > 1 && setActivePatchRef.current) {
        const d = rideDataRef.current
        if (d) {
          const pi  = bisect(d.timeArray, rideT)
          const pi1 = Math.min(pi + 1, d.count - 1)
          const pa  = d.timeArray[pi1] > d.timeArray[pi]
            ? (rideT - d.timeArray[pi]) / (d.timeArray[pi1] - d.timeArray[pi])
            : 0
          const frac   = d.sFrac[pi] + (d.sFrac[pi1] - d.sFrac[pi]) * pa
          const target = Math.round(frac * (captureData.length - 1))
          if (target !== activePatchIdxRef.current) {
            activePatchIdxRef.current = target
            setActivePatchRef.current(target)
          }
        }
      }

      // ── Camera ────────────────────────────────────────────────────────────
      const data = rideDataRef.current
      if (data) {
        const target = computeCameraTarget(rideT, data, cameraModeRef.current)

        // First-person drag rotation: lerp yaw/pitch back to 0 when not dragging,
        // then bake the offset into the target quaternion.
        if (cameraModeRef.current === 'first') {
          if (!isDraggingRef.current) {
            const returnAlpha = 1 - Math.exp(-dt * 2.5)
            userYawRef.current   *= (1 - returnAlpha)
            userPitchRef.current *= (1 - returnAlpha)
            if (Math.abs(userYawRef.current)   < 0.0001) userYawRef.current   = 0
            if (Math.abs(userPitchRef.current) < 0.0001) userPitchRef.current = 0
          }
          _userOffsetEuler.set(userPitchRef.current, userYawRef.current, 0, 'YXZ')
          _userOffsetQuat.setFromEuler(_userOffsetEuler)
          target.quat.multiply(_userOffsetQuat)
        }

        if (needsInitRef.current || isDraggingRef.current) {
          // Snap instantly: on init, or while dragging so the view tracks the finger
          smoothPosRef.current.copy(target.pos)
          smoothQuatRef.current.copy(target.quat)
          needsInitRef.current = false
        } else {
          const posAlpha = 1 - Math.exp(-dt / POS_TAU)
          const rotAlpha = 1 - Math.exp(-dt / ROT_TAU)
          smoothPosRef.current.lerp(target.pos, posAlpha)
          smoothQuatRef.current.slerp(target.quat, rotAlpha)
        }

        cameraRef.current.position.copy(smoothPosRef.current)
        cameraRef.current.quaternion.copy(smoothQuatRef.current)
        cameraRef.current.updateMatrixWorld()
      }

      renderer.render(scene, cameraRef.current)
      rafRef.current = requestAnimationFrame(tick)
    }

    prevWallRef.current = performance.now()
    fpsLastRef.current  = performance.now()
    rafRef.current = requestAnimationFrame(tick)
  }

  // ── Playback controls ──────────────────────────────────────────────────────

  const play = useCallback(() => {
    if (!rideDataRef.current) return
    // Restart from beginning if ride has finished
    const restarting = rideTimeRef.current >= rideDataRef.current.totalDuration - 0.05
    if (restarting) {
      rideTimeRef.current  = 0
      userYawRef.current   = 0
      userPitchRef.current = 0
      setRideTime(0)
      needsInitRef.current = true   // snap camera to start position
      if (onRideProgress) onRideProgress(0)
    } else {
      needsInitRef.current = false  // resume with smooth camera
    }
    lastTimeUpdateRef.current   = -1
    lastCursorUpdateRef.current = -1
    isPlayingRef.current        = true
    setIsPlaying(true)
  }, [onRideProgress])

  const pause = useCallback(() => {
    isPlayingRef.current = false
    setIsPlaying(false)
  }, [])

  const stop = useCallback(() => {
    isPlayingRef.current = false
    rideTimeRef.current  = 0
    setIsPlaying(false)
    setRideTime(0)
    onStop()
  }, [onStop])

  // ── First-person drag-to-look event listeners ─────────────────────────────

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return

    const DRAG_SENSITIVITY = 0.004
    const MAX_PITCH = Math.PI / 2.5   // ±72°
    const MAX_YAW   = Math.PI         // ±180°

    function onMouseDown(e: MouseEvent) {
      if (cameraModeRef.current !== 'first') return
      e.preventDefault()   // prevent text-selection flicker during drag
      isDraggingRef.current = true
      lastDragXRef.current  = e.clientX
      lastDragYRef.current  = e.clientY
    }
    function onMouseMove(e: MouseEvent) {
      if (!isDraggingRef.current) return
      const dx = e.clientX - lastDragXRef.current
      const dy = e.clientY - lastDragYRef.current
      lastDragXRef.current = e.clientX
      lastDragYRef.current = e.clientY
      userYawRef.current   = Math.max(-MAX_YAW,   Math.min(MAX_YAW,   userYawRef.current   - dx * DRAG_SENSITIVITY))
      userPitchRef.current = Math.max(-MAX_PITCH,  Math.min(MAX_PITCH, userPitchRef.current - dy * DRAG_SENSITIVITY))
    }
    function onMouseUp() { isDraggingRef.current = false }

    function onTouchStart(e: TouchEvent) {
      if (cameraModeRef.current !== 'first' || e.touches.length === 0) return
      isDraggingRef.current = true
      lastDragXRef.current  = e.touches[0].clientX
      lastDragYRef.current  = e.touches[0].clientY
    }
    function onTouchMove(e: TouchEvent) {
      if (!isDraggingRef.current || e.touches.length === 0) return
      const dx = e.touches[0].clientX - lastDragXRef.current
      const dy = e.touches[0].clientY - lastDragYRef.current
      lastDragXRef.current = e.touches[0].clientX
      lastDragYRef.current = e.touches[0].clientY
      userYawRef.current   = Math.max(-MAX_YAW,   Math.min(MAX_YAW,   userYawRef.current   - dx * DRAG_SENSITIVITY))
      userPitchRef.current = Math.max(-MAX_PITCH,  Math.min(MAX_PITCH, userPitchRef.current - dy * DRAG_SENSITIVITY))
    }
    function onTouchEnd() { isDraggingRef.current = false }

    canvas.addEventListener('mousedown',  onMouseDown)
    window.addEventListener('mousemove',  onMouseMove)
    window.addEventListener('mouseup',    onMouseUp)
    canvas.addEventListener('touchstart', onTouchStart, { passive: true })
    window.addEventListener('touchmove',  onTouchMove,  { passive: true })
    window.addEventListener('touchend',   onTouchEnd)

    return () => {
      canvas.removeEventListener('mousedown',  onMouseDown)
      window.removeEventListener('mousemove',  onMouseMove)
      window.removeEventListener('mouseup',    onMouseUp)
      canvas.removeEventListener('touchstart', onTouchStart)
      window.removeEventListener('touchmove',  onTouchMove)
      window.removeEventListener('touchend',   onTouchEnd)
    }
  }, [])

  // Start render loop on mount; auto-play immediately
  useEffect(() => {
    needsInitRef.current = true
    startLoop()
    const data = rideDataRef.current
    if (data) {
      lastTimeUpdateRef.current   = -1
      lastCursorUpdateRef.current = -1
      isPlayingRef.current        = true
      setIsPlaying(true)
    }
    return () => {
      if (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); rafRef.current = null }
    }
  }, [])   // eslint-disable-line react-hooks/exhaustive-deps

  // ── UI ────────────────────────────────────────────────────────────────────

  const isRideDone = !isPlaying && totalDuration > 0 && rideTime >= totalDuration - 0.05

  const controls = (
    <div className={styles.rideBar}>
      <button
        className={styles.ridePlayBtn}
        onClick={isPlaying ? pause : play}
        title={isPlaying ? 'Pause' : isRideDone ? 'Restart' : 'Resume'}
      >
        {isPlaying ? '⏸' : isRideDone ? '↺' : '▶'}
      </button>
      <button className={styles.rideStopBtn} onClick={stop} title="Stop ride">
        ⏹
      </button>

      <div className={styles.rideBarDivider} />

      <span className={styles.rideCameraLabel}>Camera</span>
      <button
        className={`${styles.rideCameraBtn}${cameraMode === 'first' ? ` ${styles.rideCameraActive}` : ''}`}
        onClick={() => setCameraMode('first')}
      >
        1st Person
      </button>
      <button
        className={`${styles.rideCameraBtn}${cameraMode === 'third' ? ` ${styles.rideCameraActive}` : ''}`}
        onClick={() => setCameraMode('third')}
      >
        3rd Person
      </button>

      <div className={styles.rideBarDivider} />

      <span className={styles.rideTimeDisplay}>
        {formatRideTime(rideTime)}
        <span className={styles.rideTimeSep}>/</span>
        {formatRideTime(totalDuration)}
      </span>
    </div>
  )

  return (
    <>
      <canvas ref={canvasRef} className={styles.canvas} />
      <div ref={fpsElRef} className={styles.fpsCounter} />
      {createPortal(controls, document.body)}
    </>
  )
}
