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 ─────────────────────────────────────────────────

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

const _mat  = new THREE.Matrix4()
// PerspectiveCamera.lookAt uses camera convention: −Z axis toward target.
// A plain Object3D.lookAt points +Z toward target (opposite), which would
// make the camera look backward.
const _dummy = new THREE.PerspectiveCamera()

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

  const pos = centerline[i].clone().lerp(centerline[i1], alpha)
  const r1i = rail1[i].clone().lerp(rail1[i1], alpha)
  const r2i = rail2[i].clone().lerp(rail2[i1], alpha)

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

  // Track-local "up": cross(tangent, rail1→rail2) gives the binormal
  const side    = r1i.clone().sub(r2i).normalize()
  const trackUp = tang.clone().cross(side).normalize()
  if (trackUp.dot(UP) < 0) trackUp.negate()

  let camPos:    THREE.Vector3
  let lookTarget: THREE.Vector3
  let upVec:     THREE.Vector3

  if (mode === 'first') {
    // Sit 1.5 m above centreline, look 10 m ahead from the seat
    camPos     = pos.clone().addScaledVector(UP, 1.5)
    lookTarget = camPos.clone().addScaledVector(tang, 10)
    upVec      = trackUp
  } else {
    // 40 m behind + 12 m above, looking at the car
    camPos     = pos.clone()
      .addScaledVector(tang, -40)
      .addScaledVector(UP, 12)
    lookTarget = pos
    upVec      = UP
  }

  // Build quaternion from look-at matrix (avoids gimbal lock and is slerp-able)
  _dummy.position.copy(camPos)
  _dummy.up.copy(upVec)
  _dummy.lookAt(lookTarget)
  _dummy.updateMatrixWorld(true)
  // _dummy.matrixWorld = translation * rotation; extract rotation quaternion
  _mat.extractRotation(_dummy.matrixWorld)
  const quat = new THREE.Quaternion().setFromRotationMatrix(_mat)

  return { pos: camPos, quat }
}

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

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

  // Lighting
  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 mesh ────────────────────────────────────────────────────────────
  const GRID  = captureData.gridSize           // 64
  const verts = captureData.terrainVertices    // GRID×GRID, row-major: idx = j*GRID+i
  //   j=0 → south (lat = tileBbox[1])
  //   j=GRID-1 → north (lat = tileBbox[3])
  //   i=0 → west  (lon = tileBbox[0])
  //   i=GRID-1 → east (lon = tileBbox[2])

  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   // shift terrain 0.5 m down so tracks at height 0 sit above it
      posArr[idx * 3 + 2] = v.z

      // The stitched canvas has north at pixel row 0 (tj=0 = northernmost tile).
      // THREE.CanvasTexture has flipY=true by default, which means:
      //   texture V=0 → canvas bottom row → south latitude
      //   texture V=1 → canvas top row    → north latitude
      // So: south (j=0) → V=0, north (j=GRID-1) → V=1
      uvArr[idx * 2]     = i / (GRID - 1)          // U: west→east = 0→1
      uvArr[idx * 2 + 1] = j / (GRID - 1)          // V: south→north = 0→1 (flipY corrects canvas)
    }
  }

  // Indices — winding must produce upward normals (+Y) when viewed from above.
  // Quad corners: a=SW, b=SE, c=NE, d=NW  (j+1=north, i+1=east)
  // CCW from above: a→b→d and b→c→d
  const idxArr: number[] = []
  for (let j = 0; j < GRID - 1; j++) {
    for (let i = 0; i < GRID - 1; i++) {
      const a = j * GRID + i          // SW
      const b = j * GRID + i + 1     // SE
      const c = (j + 1) * GRID + i + 1  // NE
      const d = (j + 1) * GRID + i   // NW
      idxArr.push(a, b, d)   // SW→SE→NW  (CCW from +Y)
      idxArr.push(b, c, d)   // SE→NE→NW  (CCW from +Y)
    }
  }

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

  // Draw ImageBitmap onto a real HTMLCanvasElement so CanvasTexture works reliably
  const texCanvas = document.createElement('canvas')
  texCanvas.width  = captureData.imageBitmap.width
  texCanvas.height = captureData.imageBitmap.height
  texCanvas.getContext('2d')!.drawImage(captureData.imageBitmap, 0, 0)

  const terrainTex = new THREE.CanvasTexture(texCanvas)
  terrainTex.colorSpace = THREE.SRGBColorSpace
  texes.push(terrainTex)

  const terrainMat = new THREE.MeshLambertMaterial({ map: terrainTex, side: THREE.FrontSide })
  mats.push(terrainMat)
  scene.add(new THREE.Mesh(terrainGeo, terrainMat))

  // ── Coaster rails ───────────────────────────────────────────────────────────
  function addRail(pts: THREE.Vector3[]) {
    // Decimate: CatmullRomCurve3 doesn't need every sim point
    const step  = Math.max(1, Math.floor(pts.length / 500))
    const dpts  = pts.filter((_, i) => i % step === 0 || i === pts.length - 1)
    const curve = new THREE.CatmullRomCurve3(dpts)
    // TubeGeometry radius: 0.25 m (matches Cesium's ~35 cm shape, slightly smaller for 3D)
    const geo = new THREE.TubeGeometry(curve, Math.min(dpts.length * 4, 2000), 0.25, 10, 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,
    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)

  // Ride playback refs (mutable, used inside rAF loop)
  const rideDataRef     = useRef<RideData | null>(null)
  const isPlayingRef    = useRef(false)
  const rideTimeRef     = useRef(0)
  const startWallRef    = useRef(0)
  const lastUiUpdateRef = useRef(-1)
  const cameraModeRef   = useRef<CameraMode>('third')
  const prevWallRef     = useRef(0)           // for frame deltaTime

  // Smooth camera state (lerped/slerped each frame)
  const smoothPosRef  = useRef(new THREE.Vector3())
  const smoothQuatRef = useRef(new THREE.Quaternion())
  const needsInitRef  = useRef(true)          // skip lerp on first frame after play

  // 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>('third')

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

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

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

    const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
    rendererRef.current = renderer

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

    const { scene, disposeAll } = buildScene(captureData, rideData)
    sceneRef.current = scene

    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()

    // Position camera at track start looking forward
    if (rideData.count > 1) {
      const t0 = computeCameraTarget(0, rideData, 'third')
      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) — larger = smoother / more lag
  const POS_TAU = 0.20   // position lag
  const ROT_TAU = 0.35   // rotation lag

  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

      let rideT = rideTimeRef.current

      if (isPlayingRef.current) {
        rideT = (wallMs - startWallRef.current) / 1000
        rideTimeRef.current = rideT

        if (rideT - lastUiUpdateRef.current > 0.25) {
          setRideTime(rideT)
          lastUiUpdateRef.current = rideT

          // Emit current s_frac for the plot cursor
          const d = rideDataRef.current
          if (d && onRideProgress) {
            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) {
          rideT = data.totalDuration
          rideTimeRef.current = rideT
          isPlayingRef.current = false
          setIsPlaying(false)
          setRideTime(rideT)
        }
      }

      // Compute target camera state for current ride time
      const data = rideDataRef.current
      if (data) {
        const target = computeCameraTarget(rideT, data, cameraModeRef.current)

        if (needsInitRef.current) {
          // Snap to target on first frame after play to avoid lerping from wrong pos
          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()
    rafRef.current = requestAnimationFrame(tick)
  }

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

  const play = useCallback(() => {
    if (!rideDataRef.current) return
    startWallRef.current    = performance.now() - rideTimeRef.current * 1000
    lastUiUpdateRef.current = -1
    isPlayingRef.current    = true
    needsInitRef.current    = false  // don't re-snap if resuming from pause
    setIsPlaying(true)
  }, [])

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

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

  // Start render loop on mount; auto-play immediately
  useEffect(() => {
    needsInitRef.current = true
    startLoop()
    // Auto-play
    const data = rideDataRef.current
    if (data) {
      startWallRef.current    = performance.now()
      lastUiUpdateRef.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 controls = (
    <div className={styles.rideBar}>
      <button
        className={styles.ridePlayBtn}
        onClick={isPlaying ? pause : play}
        title={isPlaying ? 'Pause' : 'Resume'}
      >
        {isPlaying ? '⏸' : '▶'}
      </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} />
      {createPortal(controls, document.body)}
    </>
  )
}
