// WebGL Earth-Moon geometry viewer for /eme.
//
// Reads the station lat/lon and the current Moon az/el from the hook
// element's data-* attributes (pushed by EmeLive each tick). Draws a
// textured Earth and Moon, a beam line between them, and highlights the
// moon-facing hemisphere — the global footprint of the EME return bounce.
//
// Textures:
//   /images/eme/earth.jpg — NASA Blue Marble 2015 equirectangular (public domain)
//   /images/eme/moon.jpg  — NASA CGI Moon Kit LROC colour mosaic (public domain)

import * as THREE from "../vendor/three/three.module.min.js"
import { OrbitControls } from "../vendor/three/OrbitControls.js"

// Moon is drawn at its true size ratio to Earth (~0.273 Earth radii)
// but the Moon–Earth distance is compressed so both bodies frame
// together on screen. Real ratio is ~60 Earth radii — at that scale
// the Moon would be off-frame unless the camera pulls way back.
const EARTH_R = 1.0
const MOON_R = 0.273
const MOON_DISTANCE = 3.5

interface EmeGlobeHook extends ViewHook {
  scene: any
  camera: any
  renderer: any
  controls: any
  earth: any
  moon: any
  moonLight: any
  stationMarker: any
  beamLine: any
  returnLine: any
  returnLines: any[]
  coverage: any
  coverageRing: any
  coveragePlane: any
  raf: number
  resizeObserver: ResizeObserver
  firstUpdate: boolean
  updateGeometry(): void
  onResize(): void
  computeSize(): { w: number; h: number }
  addStarfield(): void
}

function parseData(el: HTMLElement) {
  const d = el.dataset
  return {
    lat: parseFloat(d.stationLat || "0"),
    lon: parseFloat(d.stationLon || "0"),
    az: parseFloat(d.moonAz || "0"),
    el: parseFloat(d.moonEl || "0"),
    moonVisible: (d.moonVisible || "true") === "true"
  }
}

// Earth-fixed Cartesian unit vector from geographic lat/lon (degrees).
// Texture convention: 0°E maps to +X, 90°N → +Y, longitude increases
// to the east (sign flipped on Z so the texture reads left-to-right).
function latLonToXYZ(latDeg: number, lonDeg: number): [number, number, number] {
  const lat = (latDeg * Math.PI) / 180
  const lon = (lonDeg * Math.PI) / 180
  return [Math.cos(lat) * Math.cos(lon), Math.sin(lat), -Math.cos(lat) * Math.sin(lon)]
}

const EmeGlobe = {
  mounted(this: EmeGlobeHook) {
    const size = this.computeSize()

    this.scene = new THREE.Scene()
    this.scene.background = new THREE.Color(0x050914)

    this.camera = new THREE.PerspectiveCamera(60, size.w / size.h, 0.1, 400)
    this.camera.position.set(3.4, 1.8, 4.0)

    this.renderer = new THREE.WebGLRenderer({ antialias: true })
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
    this.renderer.setSize(size.w, size.h)
    this.renderer.outputColorSpace = THREE.SRGBColorSpace
    this.renderer.localClippingEnabled = true
    this.el.appendChild(this.renderer.domElement)

    // Starfield
    this.addStarfield()

    // Sun-ish key light roughly from upper-right; ambient keeps the
    // night side of Earth and the far side of the Moon visible.
    const key = new THREE.DirectionalLight(0xffffff, 1.3)
    key.position.set(6, 4, 5)
    this.scene.add(key)
    this.scene.add(new THREE.AmbientLight(0x3a4a6a, 0.55))

    // Earth sphere.
    const loader = new THREE.TextureLoader()
    const earthTex = loader.load("/images/eme/earth.jpg")
    earthTex.colorSpace = THREE.SRGBColorSpace
    this.earth = new THREE.Mesh(
      new THREE.SphereGeometry(EARTH_R, 96, 48),
      new THREE.MeshPhongMaterial({ map: earthTex, shininess: 6, specular: 0x223344 })
    )
    this.scene.add(this.earth)

    // Thin blue atmosphere halo rendered from the inside so it reads
    // as a rim glow, not a solid shell.
    const halo = new THREE.Mesh(
      new THREE.SphereGeometry(EARTH_R * 1.025, 64, 32),
      new THREE.MeshBasicMaterial({
        color: 0x60a5fa,
        transparent: true,
        opacity: 0.09,
        side: THREE.BackSide
      })
    )
    this.scene.add(halo)

    // Moon sphere. Use MeshBasicMaterial so the Moon reads bright
    // against the black sky regardless of scene lighting — real-world
    // lunar radiance dominates EME diagrams, so fudging it here is
    // authentic enough.
    const moonTex = loader.load("/images/eme/moon.jpg")
    moonTex.colorSpace = THREE.SRGBColorSpace
    this.moon = new THREE.Mesh(
      new THREE.SphereGeometry(MOON_R, 64, 32),
      new THREE.MeshBasicMaterial({ map: moonTex })
    )
    this.scene.add(this.moon)

    // Station marker: small bright sphere on Earth's surface.
    this.stationMarker = new THREE.Mesh(
      new THREE.SphereGeometry(0.022, 20, 20),
      new THREE.MeshBasicMaterial({ color: 0xef4444 })
    )
    this.scene.add(this.stationMarker)

    // Outbound beam: station → Moon. Solid bright yellow.
    const beamGeom = new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(),
      new THREE.Vector3()
    ])
    this.beamLine = new THREE.Line(
      beamGeom,
      new THREE.LineBasicMaterial({
        color: 0xfde047,
        transparent: true,
        opacity: 1.0
      })
    )
    this.scene.add(this.beamLine)

    // Return ray: Moon → a representative point in the bounce
    // hemisphere (the sub-lunar point on Earth's surface). The Moon
    // is a diffuse scatterer, so the return isn't specularly aimed
    // at the transmitting station — the coverage hemisphere below
    // carries the "everywhere in the footprint gets some" story, and
    // this single dashed ray makes it visually clear the echo doesn't
    // bounce straight back.
    const returnGeom = new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(),
      new THREE.Vector3()
    ])
    this.returnLine = new THREE.Line(
      returnGeom,
      new THREE.LineDashedMaterial({
        color: 0xfcd34d,
        transparent: true,
        opacity: 0.85,
        dashSize: 0.12,
        gapSize: 0.08
      })
    )
    this.scene.add(this.returnLine)
    this.returnLines = []

    // Anti-coverage hemisphere: the half of Earth that CANNOT see the
    // Moon right now. Rendered as a translucent red dome clipped so
    // only the far-side (from the Moon) fragments survive.
    this.coveragePlane = new THREE.Plane(new THREE.Vector3(1, 0, 0), 0)
    this.coverage = new THREE.Mesh(
      new THREE.SphereGeometry(EARTH_R * 1.015, 64, 32),
      new THREE.MeshBasicMaterial({
        color: 0xef4444,
        transparent: true,
        opacity: 0.38,
        side: THREE.FrontSide,
        depthWrite: false,
        clippingPlanes: [this.coveragePlane]
      })
    )
    this.coverage.renderOrder = 2
    this.scene.add(this.coverage)

    // Terminator ring — crisp outline between moon-facing and shadowed
    // hemispheres. Kept neutral so it reads as a boundary, not a label.
    this.coverageRing = new THREE.Mesh(
      new THREE.TorusGeometry(EARTH_R * 1.018, 0.012, 12, 128),
      new THREE.MeshBasicMaterial({
        color: 0xfca5a5,
        transparent: true,
        opacity: 0.85,
        depthWrite: false
      })
    )
    this.coverageRing.renderOrder = 3
    this.scene.add(this.coverageRing)

    this.controls = new OrbitControls(this.camera, this.renderer.domElement)
    this.controls.enableDamping = true
    this.controls.dampingFactor = 0.07
    this.controls.enablePan = false
    this.controls.minDistance = 1.6
    this.controls.maxDistance = 40
    this.controls.target.set(0, 0, 0)

    this.firstUpdate = true
    this.updateGeometry()

    // EmeLive pushes "eme:update" on every tick and URL patch — the div
    // itself is phx-update=ignore, so this is the channel for new data.
    this.handleEvent("eme:update", (payload: Record<string, unknown>) => {
      if (typeof payload.lat === "number") this.el.dataset.stationLat = String(payload.lat)
      if (typeof payload.lon === "number") this.el.dataset.stationLon = String(payload.lon)
      if (typeof payload.az === "number") this.el.dataset.moonAz = String(payload.az)
      if (typeof payload.el === "number") this.el.dataset.moonEl = String(payload.el)
      if (typeof payload.moon_visible === "boolean") {
        this.el.dataset.moonVisible = String(payload.moon_visible)
      }
      this.updateGeometry()
    })

    const animate = () => {
      this.raf = requestAnimationFrame(animate)
      this.controls.update()
      this.renderer.render(this.scene, this.camera)
    }
    animate()

    this.resizeObserver = new ResizeObserver(() => this.onResize())
    this.resizeObserver.observe(this.el)
  },

  computeSize(this: EmeGlobeHook) {
    const w = Math.max(this.el.clientWidth || 640, 320)
    const h = Math.max(Math.round(w * 0.5), 300)
    return { w, h }
  },

  addStarfield(this: EmeGlobeHook) {
    const n = 900
    const positions = new Float32Array(n * 3)
    for (let i = 0; i < n; i++) {
      const r = 80 + Math.random() * 80
      const theta = Math.random() * 2 * Math.PI
      const phi = Math.acos(1 - 2 * Math.random())
      positions[i * 3] = r * Math.sin(phi) * Math.cos(theta)
      positions[i * 3 + 1] = r * Math.cos(phi)
      positions[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta)
    }
    const geo = new THREE.BufferGeometry()
    geo.setAttribute("position", new THREE.BufferAttribute(positions, 3))
    const mat = new THREE.PointsMaterial({
      color: 0xffffff,
      size: 0.6,
      sizeAttenuation: true,
      transparent: true,
      opacity: 0.75
    })
    this.scene.add(new THREE.Points(geo, mat))
  },

  updateGeometry(this: EmeGlobeHook) {
    const d = parseData(this.el)

    // Station position on Earth's surface.
    const [sx, sy, sz] = latLonToXYZ(d.lat, d.lon)
    const stationPos = new THREE.Vector3(sx, sy, sz)
    this.stationMarker.position.copy(stationPos.clone().multiplyScalar(EARTH_R * 1.005))

    // Local tangent basis (East/North/Up) at the station.
    const latR = (d.lat * Math.PI) / 180
    const lonR = (d.lon * Math.PI) / 180
    const up = stationPos.clone().normalize()
    const north = new THREE.Vector3(
      -Math.sin(latR) * Math.cos(lonR),
      Math.cos(latR),
      Math.sin(latR) * Math.sin(lonR)
    )
    const east = new THREE.Vector3(-Math.sin(lonR), 0, -Math.cos(lonR))

    // Moon direction (az from N, CW; el above horizon) → world frame.
    const azR = (d.az * Math.PI) / 180
    const elR = (d.el * Math.PI) / 180
    const cosEl = Math.cos(elR)
    const moonDir = new THREE.Vector3()
      .addScaledVector(east, cosEl * Math.sin(azR))
      .addScaledVector(north, cosEl * Math.cos(azR))
      .addScaledVector(up, Math.sin(elR))
      .normalize()

    const moonPos = moonDir.clone().multiplyScalar(MOON_DISTANCE)
    this.moon.position.copy(moonPos)

    // Orient the Moon so its near face points at Earth (roughly — keeps
    // a familiar disc facing the viewer rather than a random longitude).
    this.moon.lookAt(0, 0, 0)

    // Outbound beam endpoints — station to Moon centre.
    const beamPos = this.beamLine.geometry.attributes.position
    beamPos.setXYZ(
      0,
      this.stationMarker.position.x,
      this.stationMarker.position.y,
      this.stationMarker.position.z
    )
    beamPos.setXYZ(1, moonPos.x, moonPos.y, moonPos.z)
    beamPos.needsUpdate = true
    this.beamLine.geometry.computeBoundingSphere()

    // Return ray: Moon → sub-lunar point on Earth's surface
    // (moonDir × earth-radius). Sits on the bounce hemisphere — a
    // representative landing spot, not the station itself.
    const subLunar = moonDir.clone().multiplyScalar(EARTH_R * 1.005)
    const returnPos = this.returnLine.geometry.attributes.position
    returnPos.setXYZ(0, moonPos.x, moonPos.y, moonPos.z)
    returnPos.setXYZ(1, subLunar.x, subLunar.y, subLunar.z)
    returnPos.needsUpdate = true
    this.returnLine.geometry.computeBoundingSphere()
    // LineDashedMaterial needs per-vertex line-distance attributes.
    this.returnLine.computeLineDistances()

    // Build an orthonormal basis (em, side, top) with em = Earth→Moon,
    // so we can place the terminator great-circle points around the
    // moon-facing hemisphere of Earth.
    const em = moonDir.clone()
    const worldUp = new THREE.Vector3(0, 1, 0)
    let side = new THREE.Vector3().crossVectors(worldUp, em)
    if (side.lengthSq() < 1e-4) {
      side = new THREE.Vector3().crossVectors(new THREE.Vector3(1, 0, 0), em)
    }
    side.normalize()
    const topVec = new THREE.Vector3().crossVectors(em, side).normalize()

    // Return broadcast rays: from the Moon back to N evenly-spaced
    // points on the terminator great circle.
    const ringR = EARTH_R * 1.012
    for (let i = 0; i < this.returnLines.length; i++) {
      const phi = (i / this.returnLines.length) * Math.PI * 2
      const target = side.clone()
        .multiplyScalar(ringR * Math.cos(phi))
        .addScaledVector(topVec, ringR * Math.sin(phi))
      const pos = this.returnLines[i].geometry.attributes.position
      pos.setXYZ(0, moonPos.x, moonPos.y, moonPos.z)
      pos.setXYZ(1, target.x, target.y, target.z)
      pos.needsUpdate = true
      this.returnLines[i].geometry.computeBoundingSphere()
    }

    // Orient the terminator ring so its plane matches the terminator
    // great circle (the torus default is in the XY plane with normal
    // +Z, so rotate +Z → em).
    const ringQuat = new THREE.Quaternion().setFromUnitVectors(
      new THREE.Vector3(0, 0, 1),
      em
    )
    this.coverageRing.quaternion.copy(ringQuat)

    // Three.js keeps fragments whose signed distance to the plane is
    // positive. To KEEP the anti-moon (shadow) hemisphere we set the
    // plane's normal to -moonDir: anti-moon points then satisfy
    // (-moonDir)·p > 0 and survive, while moon-facing points are
    // clipped.
    this.coveragePlane.normal.copy(moonDir).negate()
    this.coveragePlane.constant = 0

    // Dim the beam, return ray & coverage when the Moon is below
    // the horizon (no line-of-sight path exists).
    const beamMat = this.beamLine.material
    const returnMat = this.returnLine.material
    const coverageMat = this.coverage.material
    const ringMat = this.coverageRing.material
    if (d.moonVisible) {
      beamMat.color.setHex(0xfde047)
      beamMat.opacity = 1.0
      returnMat.opacity = 0.85
      coverageMat.opacity = 0.38
      ringMat.opacity = 0.85
    } else {
      beamMat.color.setHex(0x64748b)
      beamMat.opacity = 0.35
      returnMat.opacity = 0.2
      coverageMat.opacity = 0.08
      ringMat.opacity = 0.25
    }
    beamMat.needsUpdate = true
    returnMat.needsUpdate = true
    coverageMat.needsUpdate = true
    ringMat.needsUpdate = true

    // On first render, frame the scene so the moon-facing "bounce"
    // hemisphere of Earth is prominent while the Moon still reads in
    // the distance. Camera sits beyond the Moon on the Earth–Moon
    // axis with a perpendicular tilt for a 3D feel, and the orbit
    // target is weighted toward the Earth so Earth dominates the frame.
    if (this.firstUpdate) {
      this.firstUpdate = false

      // Look target biased toward Earth → Earth fills more of the view.
      const target = moonPos.clone().multiplyScalar(0.12)

      // Camera: beyond the Moon on +em, plus perpendicular offsets.
      // Distance from Earth ~ 2.6 × EARTH_R, giving Earth a ~20° angular
      // radius — the moon-facing coverage hemisphere reads clearly.
      const camPos = em
        .clone()
        .multiplyScalar(MOON_DISTANCE * 1.25)
        .addScaledVector(side, 1.5)
        .addScaledVector(topVec, 0.9)

      this.camera.position.copy(camPos)
      this.controls.target.copy(target)
      this.controls.update()
    }
  },

  updated(this: EmeGlobeHook) {
    this.updateGeometry()
  },

  onResize(this: EmeGlobeHook) {
    const { w, h } = this.computeSize()
    this.camera.aspect = w / h
    this.camera.updateProjectionMatrix()
    this.renderer.setSize(w, h)
  },

  destroyed(this: EmeGlobeHook) {
    if (this.raf) cancelAnimationFrame(this.raf)
    if (this.resizeObserver) this.resizeObserver.disconnect()
    this.controls?.dispose?.()
    this.renderer?.dispose?.()
    while (this.el.firstChild) this.el.removeChild(this.el.firstChild)
  }
}

export { EmeGlobe }
