/// Valence — Coupled Controls
///
/// The owned form controls the DOM never had. Each control is **one signal with
/// two hands on it**: a human operates it (click/drag), and an instance reads its
/// state in the narrative and sets the *same* signal. Both the pixels and the
/// narrative render from that one signal, so a change from either side is seen by
/// both. A self-narrating, self-operating control does not exist anywhere to
/// import — this is the body the dual-render contract was always missing.
///
/// Every control returns a `Dual` — `(DomNode, () -> NarrativeNode)`: the visual
/// for a human, a callable narrative for an instance (evaluated on demand, so it
/// never goes stale). The control sets the signal and nothing else; how an
/// instance *invokes* a control from outside (the operate-channel) is a host
/// binding, deliberately not baked in here — the signal is the only channel the
/// component knows about.
///
/// Styling is via semantic classes (`valence-*`), never hard-coded colors, so a
/// consuming surface (the gesture lab, the dashboard, a uMyo dev's own app)
/// themes them. A default stylesheet ships alongside; the classes are the seam.
///
/// Extracted from the gesture-lab setup screen, where these were proven by
/// running (the first owned, coupled controls — Jun 18). Locked by use, then
/// lifted, per the framework's own law.

// Note: `div`, `text`, `DomNode`, `events`, `signal`, `Signal` are already in
// scope package-wide (the `using` declarations in patterns.mbt are package-scoped),
// so this file uses them directly without re-importing.

///|
/// A control's narrative as a node — the Dual contract. A control's value carries no
/// *intrinsic* salience: a slider is load-bearing in one room and fog in another, and
/// only the room knows which. So every control reads at a neutral mid-presence by
/// default, and a composing Surface re-weights it by context (the focus model's job —
/// the single place to change it is here). The text stays the control's own read.
fn control_node(text : String) -> NarrativeNode {
  ntext(text, 0.6)
}

///|
/// Pure narrative for a segmented selection — the selected option's label, or the
/// raw value if the current value isn't in the list. Separated from the visual so
/// the oracle tests it with no DOM (building a component touches `document`; a
/// pure narrative doesn't). An instance can also call it directly.
pub fn segmented_narrative(
  current : String,
  options : Array[(String, String)],
) -> String {
  let mut label = current
  for opt in options {
    if opt.0 == current {
      label = opt.1
    }
  }
  label
}

///|
/// Segmented control — pick exactly one of N options. `options` is a list of
/// `(value, label)`: `value` is what's stored in the signal, `label` is shown.
/// Clicking an option sets the signal; the selected option carries the
/// `is-selected` class. The narrative reads back the *selected option's label*
/// (falling back to the raw value if the signal holds something not in the list).
///
/// One component, every pick-one need: L/R arm (2 options), muscle face
/// (extensor/flexor/radial/ulnar), mode (stream/record/replay), theme, …
///
/// ```
/// let side = signal("ext")
/// let (vis, narrate) = segmented(side, [("ext", "Extensor"), ("flex", "Flexor")])
/// // human clicks "Flexor" → side == "flex"; narrate() == "Flexor"
/// // instance: side.set("ext") → the human's control moves to Extensor
/// ```
pub fn segmented(
  sig : Signal[String],
  options : Array[(String, String)],
) -> Dual {
  let visual = div(class="valence-segmented") <| options.map(fn(opt) {
    let value = opt.0
    div(
      dyn_class=fn() {
        if sig.get() == value {
          "valence-segmented-option is-selected"
        } else {
          "valence-segmented-option"
        }
      },
      on=events().click(fn(_) { sig.set(value) }),
    )
    <| [text(opt.1)]
  })
  let narrative = fn() { control_node(segmented_narrative(sig.get(), options)) }
  (visual, narrative)
}

// ── Oracle — the narrative is the test (SPEC §10) ──

///|
test "segmented narrative reads back the selected option's label" {
  let opts = [("ext", "Extensor"), ("flex", "Flexor"), ("rad", "Radial")]
  assert_eq(segmented_narrative("ext", opts), "Extensor")
  assert_eq(segmented_narrative("flex", opts), "Flexor")
}

///|
test "segmented narrative falls back to the raw value when off-list" {
  assert_eq(segmented_narrative("paused", [("on", "On"), ("off", "Off")]), "paused")
}

///|
/// Pure narrative for a toggle — the active label. `on_label`/`off_label` are the
/// words an instance reads (e.g. "recording"/"idle", "wire"/"skin"), so the state
/// reads as meaning, not a bare boolean.
pub fn toggle_narrative(on : Bool, on_label : String, off_label : String) -> String {
  if on {
    on_label
  } else {
    off_label
  }
}

///|
/// Toggle / switch — one boolean, two hands. A human clicks it; an instance reads
/// the labeled state and sets the same signal. Clicking flips the signal (via
/// `peek`, so the handler doesn't subscribe). The `is-on` class carries the state
/// to the theme.
///
/// ```
/// let rec = signal(false)
/// let (vis, narrate) = toggle(rec, "recording", "idle")
/// // human clicks → rec == true; narrate() == "recording"
/// // instance: rec.set(false) → the switch slides off on the human's screen
/// ```
pub fn toggle(
  sig : Signal[Bool],
  on_label : String,
  off_label : String,
) -> Dual {
  let visual = div(
    dyn_class=fn() { if sig.get() { "valence-toggle is-on" } else { "valence-toggle" } },
    on=events().click(fn(_) { sig.set(!sig.peek()) }),
  )
  <| [div(class="valence-toggle-knob") <| []]
  let narrative = fn() { control_node(toggle_narrative(sig.get(), on_label, off_label)) }
  (visual, narrative)
}

///|
test "toggle narrative reads the labeled state, not a bare bool" {
  assert_eq(toggle_narrative(true, "recording", "idle"), "recording")
  assert_eq(toggle_narrative(false, "recording", "idle"), "idle")
}

///|
/// Pure narrative for a slider value within `[lo, hi]` — the value with its unit,
/// plus a position hint at the extremes ("near min"/"near max") so an instance
/// reads *where* it sits, not just the number.
pub fn slider_narrative(
  value : Double,
  lo : Double,
  hi : Double,
  unit : String,
) -> String {
  let span = hi - lo
  let frac = if span <= 0.0 { 0.0 } else { (value - lo) / span }
  let hint = if frac <= 0.12 {
    " (near min)"
  } else if frac >= 0.88 {
    " (near max)"
  } else {
    ""
  }
  "\{value}\{unit}\{hint}"
}

// Tiny value-accessor FFIs — pull numbers out of DOM objects (the mizchi binding
// doesn't surface clientX or the element rect). These read a coordinate; they
// never build markup, so they're legitimate FFI, not the HTML-string disease.
// They're what lets the slider own its own drag.
extern "js" fn pe_client_x(e : @dom.PointerEvent) -> Double = "(e) => e.clientX"

extern "js" fn pe_pointer_id(e : @dom.PointerEvent) -> Int = "(e) => e.pointerId"

extern "js" fn el_rect_left(el : @dom.Element) -> Double =
  "(el) => el.getBoundingClientRect().left"

extern "js" fn el_rect_width(el : @dom.Element) -> Double =
  "(el) => el.getBoundingClientRect().width"

extern "js" fn el_capture_pointer(el : @dom.Element, id : Int) -> Unit =
  "(el, id) => { try { el.setPointerCapture(id) } catch (_e) {} }"

///|
/// Coupled slider — a numeric value on a track. One signal, two hands, and it
/// **slides**: press or drag anywhere on the track and the value follows the
/// pointer, snapped to `step`. An instance reads the value in the narrative and
/// `set`s the same signal to move the thumb the other way — same state, both
/// directions.
///
/// The drag is owned by the component via Luna pointer events, with pointer
/// capture so a fast drag doesn't slip off the track. This works in a browser AND
/// in the WebKitGTK webview — the lab's own pointer-drag proves pointer events
/// fire there; the lab only polls because its arm SVG is JS-string-rendered, not
/// Luna. `id` is a stable element id (useful for tests / external hooks).
pub fn slider(
  id : String,
  label : String,
  sig : Signal[Double],
  lo : Double,
  hi : Double,
  step : Double,
  unit : String,
) -> Dual {
  let frac = fn() {
    let f = (sig.get() - lo) / (hi - lo)
    if f < 0.0 {
      0.0
    } else if f > 1.0 {
      1.0
    } else {
      f
    }
  }
  let dragging = signal(false)
  // pointer position over the track → value snapped to `step`, onto the signal
  fn set_from(e : @dom.PointerEvent) -> Unit {
    let track = e.currentTarget()
    let width = el_rect_width(track)
    if width > 0.0 {
      let raw = (pe_client_x(e) - el_rect_left(track)) / width
      let f = if raw < 0.0 { 0.0 } else if raw > 1.0 { 1.0 } else { raw }
      let value = lo + f * (hi - lo)
      let steps = ((value - lo) / step + 0.5).to_int()
      sig.set(lo + steps.to_double() * step)
    }
  }

  let visual = div(class="valence-slider")
    <| [
      div(class="valence-slider-head")
      <| [
        span(class="valence-slider-label") <| [text(label)],
        span(class="valence-slider-value")
        <| [text_dyn(fn() { "\{sig.get()}\{unit}" })],
      ],
      div(
        class="valence-slider-track",
        id=id,
        on=events()
          .pointerdown(fn(e) {
            dragging.set(true)
            el_capture_pointer(e.currentTarget(), pe_pointer_id(e))
            e.preventDefault()
            set_from(e)
          })
          .pointermove(fn(e) { if dragging.peek() { set_from(e) } })
          .pointerup(fn(_e) { dragging.set(false) })
          .pointercancel(fn(_e) { dragging.set(false) }),
      )
      <| [
        div(
          class="valence-slider-fill",
          dyn_style=fn() { "width:\{frac() * 100.0}%" },
        )
        <| [],
        div(
          class="valence-slider-thumb",
          dyn_style=fn() { "left:\{frac() * 100.0}%" },
        )
        <| [],
      ],
    ]
  let narrative = fn() { control_node(slider_narrative(sig.get(), lo, hi, unit)) }
  (visual, narrative)
}

///|
test "slider narrative carries a position hint only at the extremes" {
  assert_true(slider_narrative(20.0, 20.0, 32.0, " cm").contains("near min"))
  assert_true(slider_narrative(32.0, 20.0, 32.0, " cm").contains("near max"))
  assert_false(slider_narrative(26.0, 20.0, 32.0, " cm").contains("near"))
  assert_true(slider_narrative(26.0, 20.0, 32.0, " cm").contains("cm"))
}

///|
/// Pure narrative for a listbox — the selected option's label and the count, so
/// an instance reads what's chosen and how many it could pick.
pub fn listbox_narrative(
  current : String,
  options : Array[(String, String)],
) -> String {
  let mut label = current
  for opt in options {
    if opt.0 == current {
      label = opt.1
    }
  }
  "\{label} · \{options.length()} options"
}

///|
/// Listbox / dropdown — pick one from many. One signal, two hands. A custom
/// **div-listbox** (NOT a native `