/-
  Topolei.Render.Probe
  ====================
  Empirical check for `render_faithful` — the axiom in `Topolei.GPU.Spec`
  that claims the GPU's runtime output equals the compiled shader's
  `ShaderSemantic` at every pixel and uniform setting.

  The Rust FFI `topolei_canvas_render_probe_path_pixel` accepts a Lean
  `EMLPath` inductive structurally, builds the fragment shader as a
  `naga::Module` directly via `emit_naga::build_probe_module` (no GLSL
  text intermediary), submits the module to wgpu's SPIR-V backend, and
  reads back a single pixel from an `Rgba32Float` offscreen target.
  This module binds that FFI and exercises it on the demo paths
  `plotExp` / `plotLn` / `plotTransp`.

  ## Tolerance

  IEEE 754 rounding differs slightly between the CPU (Lean-side
  `ShaderSemantic` evaluation via `Float.exp`, `Float.log`, `Float.cos`)
  and the GPU (wgpu → Vulkan → driver → shader unit).  The comparison
  therefore uses an absolute tolerance rather than bit-exact equality.
  `5e-3` is generous; a bit-exact probe on known IEEE 754 drivers
  (e.g. SwiftShader) might tighten it to 1e-6 or better.

  ## Running

  The probe requires a Vulkan/Metal/DX12 adapter (or a software
  rasterizer like SwiftShader) on the host.  When no adapter is
  available the FFI returns a sentinel `RGBA { r := -1, g := -1, ... }`
  and the test skips that comparison rather than asserting failure.

  Run with:  `lake build probe-test && ./.lake/build/bin/probe-test`.
-/

import Topolei.EML
import Topolei.EML.Path
import Topolei.GPU.Spec

namespace TopoleiProbe

/-- Rendered-pixel RGBA as four Lean `Float` values.  Layout matches
    the `topolei_canvas_shim_io_ok_rgba` helper in `canvas-rs/shim.c`:
    tag 0, 4 boxed-Float fields. -/
structure RGBA where
  r : Float
  g : Float
  b : Float
  a : Float
deriving Repr, Inhabited

/-- **Structured probe.**  `renderProbePath path width height time
    pathParam x y` — passes the `EMLPath` inductive *structurally* to
    the Rust side, which walks the object tree
    (`eml::emlpath_from_lean`), builds a `naga::Module` directly via
    `emit_naga::build_probe_module`, and runs the offscreen probe.

    No shader-source string crosses the FFI boundary.  See
    `NAGA_IR_PLAN.md` for the construction; `compileEMLPath_correct`
    in `Topolei.GPU.Spec` is the contract the IR builder must satisfy. -/
@[extern "topolei_canvas_render_probe_path_pixel"]
opaque renderProbePath
    (path : @& EMLPath)
    (width height : UInt32)
    (time pathParam : Float)
    (x y : UInt32) : IO RGBA

/-- True when `renderProbePath` returned its GPU-unavailable sentinel.
    A channel of `-1.0` is outside the valid RGBA range `[0, 1]`, so
    tests can use this as a reliable skip signal. -/
def RGBA.isSentinel (c : RGBA) : Bool :=
  c.r < 0.0 && c.g < 0.0 && c.b < 0.0 && c.a < 0.0

/-- Bridge to `GPU/Spec.lean`: the CPU-side `PixelColor` that the GPU
    is expected to match.  One-to-one translation. -/
def RGBA.ofPixelColor (c : PixelColor) : RGBA :=
  { r := c.r, g := c.g, b := c.b, a := c.a }

/-- Absolute-tolerance comparison for `Float`.  Both arguments must
    differ by no more than `tol`. -/
def floatClose (a b tol : Float) : Bool :=
  let d := a - b
  let ad := if d < 0.0 then -d else d
  ad <= tol

/-- Compare two RGBA values channel-wise under `tol`. -/
def rgbaClose (expected actual : RGBA) (tol : Float) : Bool :=
  floatClose expected.r actual.r tol &&
  floatClose expected.g actual.g tol &&
  floatClose expected.b actual.b tol &&
  floatClose expected.a actual.a tol

/-- Convert framebuffer pixel `(x, y)` at resolution `(w, h)` into the
    `uv ∈ [0, 1]²` coordinates the probe shader sees at that pixel's
    fragment center.  Framebuffer y is top-down (wgpu origin) while
    the vertex shader's uv tracks NDC, so we flip: `uv.y = 1 -
    (y+0.5)/h`.  `uv.x = (x+0.5)/w`.  The `+0.5` lands us on the pixel
    center where the rasterizer samples. -/
def pixelUV (x y width height : UInt32) : PixelCoord :=
  let w := width.toNat.toFloat
  let h := height.toNat.toFloat
  let px := (x.toNat.toFloat + 0.5) / w
  let py := 1.0 - (y.toNat.toFloat + 0.5) / h
  { x := px, y := py }

/-- Run a single probe: pass the `EMLPath` directly to the Rust
    offscreen probe; compare the GPU-returned pixel `(x, y)` against
    Lean's `EMLPath.toColor path (pixelUV ...) u`. -/
def runProbe
    (path : EMLPath) (label : String)
    (width height : UInt32)
    (time pathParam : Float)
    (x y : UInt32)
    (tol : Float := 5e-3) : IO Bool := do
  let actual ← renderProbePath path width height time pathParam x y
  if actual.isSentinel then
    IO.println s!"  ⏭  SKIP [{label}] at ({x}, {y}) — no GPU adapter"
    return true
  let p : PixelCoord := pixelUV x y width height
  let u : FrameUniforms :=
    { time, resWidth := width.toNat.toFloat, resHeight := height.toNat.toFloat,
      pathParam }
  let expectedColor := EMLPath.toColor path p u
  let expected := RGBA.ofPixelColor expectedColor
  if rgbaClose expected actual tol then
    IO.println s!"  ✅ [{label}] pixel ({x}, {y}): GPU matches Lean (r={expected.r} g={expected.g} b={expected.b})"
    return true
  else
    IO.println s!"  ❌ [{label}] pixel ({x}, {y}) divergent"
    IO.println s!"       expected: r={expected.r} g={expected.g} b={expected.b} a={expected.a}"
    IO.println s!"       actual:   r={actual.r} g={actual.g} b={actual.b} a={actual.a}"
    return false

/-- Full probe battery.  Returns number of failures.

    Each entry asserts `EMLPath.toColor path p u ≈ GPU_pixel(p, u)`
    within tolerance — i.e. the inhabited form of `render_faithful`
    on the naga-IR pipeline. -/
def runProbes : IO UInt32 := do
  IO.println "── Topolei render_faithful probes ──"
  IO.println "  Naga-IR pixel probes (EMLPath → naga::Module → SPIR-V → GPU):"
  -- Constant path (dim variable absent from body): `pathParam` is
  -- irrelevant and the two endpoints coincide.
  let n1 ← runProbe plotExp.toEMLPath "plotExp" 128 128 0.0 0.0 64 64
  let n2 ← runProbe plotExp.toEMLPath "plotExp" 128 128 0.0 1.0 64 64
  let n3 ← runProbe plotLn.toEMLPath  "plotLn"  128 128 0.0 0.5 32 64
  -- Parametric path: `pathParam` changes the color — sample across
  -- values to cover the sweep.
  let n4 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 0.0 64 64
  let n5 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 0.5 64 64
  let n6 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 1.0 64 64
  let n7 ← runProbe plotTransp.toEMLPath "plotTransp" 128 128 0.0 0.5 32 96
  let mut fails : UInt32 := 0
  for ok in [n1, n2, n3, n4, n5, n6, n7] do
    if !ok then fails := fails + 1
  IO.println s!"── {7 - fails.toNat} / 7 probes passed ──"
  return fails

end TopoleiProbe
