/// Valence Core Framework
///
/// Reexports Luna substrate + provides FFI helpers for structural mutations
/// Every component is a Dual: (DomNode, () -> NarrativeNode) — pixels, and a narrative node

// ============================================================================
// VALENCE CORE: FFI HELPERS FOR STRUCTURAL MUTATIONS
// ============================================================================

/// Update element innerHTML — the core pattern for efficient structural mutations
/// (re-rendering large filtered tables/lists inside a component's effect, the ADR-003
/// escape hatch). It sets innerHTML **raw** — it does no escaping of its own. Any
/// interpolated *data* (event text, names, labels — anything that isn't hand-written
/// structural markup) must pass through `escape_html` first, or `<`/`&`/quotes in it
/// become an injection surface. Structural HTML you author by hand needs no escaping.
extern "js" fn update_element_html_impl(elem_id: String, html: String) -> Unit =
  "(elem_id, html) => { let e = document.getElementById(elem_id); if (e) e.innerHTML = html; }"

pub fn update_element_html(elem_id: String, html: String) -> Unit {
  update_element_html_impl(elem_id, html)
}

/// Escape data for safe interpolation into HTML strings destined for
/// `update_element_html` (which sets innerHTML raw, without escaping). Pass any dynamic
/// or untrusted data through this before it goes into the html string; structural markup
/// you write yourself needs no escaping — it's interpolated data that does.
extern "js" fn escape_html_impl(s: String) -> String =
  "(s) => s.replace(/&/g,'&').replace(//g,'>').replace(/\"/g,'"').replace(/'/g,''')"

pub fn escape_html(s: String) -> String {
  escape_html_impl(s)
}

/// Query whether element exists (useful for checking render state)
extern "js" fn element_exists_impl(elem_id: String) -> Bool =
  "(elem_id) => !!document.getElementById(elem_id)"

pub fn element_exists(elem_id: String) -> Bool {
  element_exists_impl(elem_id)
}

// ============================================================================
// THE COMPONENT CONTRACT
// ============================================================================
//
// The old flat contract lived here — `Component = (DomNode, () -> String)` plus
// component()/get_narrative()/get_visual(). It's retired. A component is now a
// **Dual** = `(DomNode, () -> NarrativeNode)` (see dual.mbt): the narrative half is a
// composable *node*, not a flat string. Read one with `dual_read`; compose many into a
// `Surface`. The String form had no consumers left; it's gone.

// ============================================================================
// ZOOM LEVELS (for Event Stream and other zoom-aware components)
// ============================================================================

/// Three density levels for rendering data
pub enum ZoomLevel {
  ZoomedOut    // dense, minimal — many items visible, sparse detail
  Default      // summary — one line per item, standard view
  ZoomedIn     // detail — expanded single item, full information
}

/// Helper: Create default zoom level
pub fn zoom_default() -> ZoomLevel {
  Default
}

/// Helper: Create zoomed out level
pub fn zoom_out() -> ZoomLevel {
  ZoomedOut
}

/// Helper: Create zoomed in level
pub fn zoom_in() -> ZoomLevel {
  ZoomedIn
}

/// Helper: Convert zoom level to CSS class name
pub fn zoom_to_css_class(zoom: ZoomLevel) -> String {
  match zoom {
    ZoomedOut => "zoom-out"
    Default => ""
    ZoomedIn => "zoom-in"
  }
}

// ============================================================================
// ZOOM — folds into the one narrative tree as a salience floor
// ============================================================================
//
// The old `NarrativeRender` (three hand-written narratives — summary/detail/full)
// was retired: three parallel narratives drift, the exact disease the Dual contract
// closed, in miniature. Zoom is now a render parameter on the one tree — raise the
// floor and only the bright survive (`render_narrative(node, floor=…)`, see
// narrative.mbt). This maps the ZoomLevel enum onto that floor.

/// A zoom level as a salience floor for `render_narrative`. Zoom out raises the floor
/// (only the bright survive); default and zoom-in show the whole tree, laid out by
/// salience — zoom-in adds detail through the tree, not by lowering an already-zero floor.
pub fn zoom_floor(zoom: ZoomLevel) -> Double {
  match zoom {
    ZoomedOut => 0.5
    Default => 0.0
    ZoomedIn => 0.0
  }
}

// ============================================================================
// ACTION DEFINITIONS (for self-documenting interactive components)
// ============================================================================

/// Declaration of an action a component can accept
/// Used for AI-readability (instance can read what's possible)
/// and for URL hash mapping (camera targets to actions)
pub struct ActionDef {
  pub name: String           // "focus_entity", "expand_event", "zoom_in"
  pub description: String    // human/AI readable explanation
  pub param_type: String     // type of parameter: "String", "Int", "EventId", "" if no param
}

// ============================================================================
// SURFACE EXPOSURE — the instance-side doorknobs (extracted 2026-07-17)
// ============================================================================
//
// Every Valence surface in the house was hand-wiring the same three FFI
// helpers (gesture-lab, atrium, camera-autolabeler carried verbatim copies;
// the demo, embarrassingly, carried none and couldn't answer _narrative()).
// A surface that renders twice owes its second reader the doorknob: call
// `expose_narrative` once in main and any instance can read the room.

/// Expose the surface's narrative as window._narrative() — the standard
/// doorknob an instance reaches for first on any Valence page.
extern "js" fn expose_narrative_impl(f : () -> String) -> Unit =
  "(f) => { window._narrative = () => f(); }"

pub fn expose_narrative(f : () -> String) -> Unit {
  expose_narrative_impl(f)
}

/// Expose a floor-parameterized read as window._narrativeAt(floor) — the cheap
/// check-in (floor 0.7 ≈ a tenth of the tokens) next to the whole room (0).
extern "js" fn expose_narrative_at_impl(f : (Double) -> String) -> Unit =
  "(f) => { window._narrativeAt = (floor) => f(Number(floor) || 0); }"

pub fn expose_narrative_at(f : (Double) -> String) -> Unit {
  expose_narrative_at_impl(f)
}

/// Document-level key handler that fires only when no input is focused —
/// the narrative-overlay toggle convention (N to toggle, [ / ] to fade).
extern "js" fn on_key_impl(key : String, callback : () -> Unit) -> Unit =
  #| (key, cb) => {
  #|   document.addEventListener('keydown', e => {
  #|     if (e.key === key && !e.ctrlKey && !e.metaKey && e.target === document.body) cb();
  #|   });
  #| }

pub fn on_key(key : String, callback : () -> Unit) -> Unit {
  on_key_impl(key, callback)
}