/// Valence — Registers: narrative voices that speak the same semantic tree differently.
///
/// The narrative render is currently one voice — **house-voice**: calm, plain, declarative.
/// Registers transform that tree based on: who's speaking, what the space calls for, what
/// the reader can hold, the history the reader carries.
///
/// Four voices:
/// 1. **house-voice** (current) — calm, plain, "the room waits."
/// 2. **ops-voice** — dense statusboard, zero lyric, low-context.
/// 3. **bester-voice** — spatial drama permitted; layout enacts state.
/// 4. **disco-voice** — kinds as a parliament of skills. Different nodes speak as different voices.
///
/// Each transforms `NarrativeNode` → rendered prose, via rules that say:
/// "when you see a [selector], apply [treatment]."
///
/// The foundation: the stylesheet rules that control how each kind renders at each
/// salience level. The layer above: per-register transformations that swap words,
/// punctuation, tone, layout patterns.

/// ── Type foundation ──

/// A rule selector — what triggers this rule?
pub(all) enum Selector {
  ByKind(String)           // "being" | "section" | "held" | etc.
  BySalience(Double, Double)  // range: low..high (e.g. 0.78..1.0 = lit rooms)
  ByEpistemicStatus(String)  // "primary" | "inferred" | "contested"
  ByAge(Int, Int)          // age in compactions: old..new
  ByReaderTierMatch(String)  // "house" | "ops" | "personal"
}

/// A typographic treatment — how to render when the selector matches?
pub(all) enum Treatment {
  Indent((Double) -> Int)  // salience → indent level
  Border(String)  // "═══" | "──" | "···" | etc.
  Density(String)  // "tight" | "normal" | "sparse"
  Case(String)  // "upper" | "lower" | "normal"
  PunctuationRegister(String)  // "declarative" | "softened" | "hedged"
  Ellipsis(String)  // how to abbreviate: "···" | "…" | "[…]" | etc.
}

/// A narrative rule — condition + treatment.
pub(all) struct NarrativeRule {
  selector : Selector
  treatment : Treatment
  priority : Int  // higher = applies first (allows layering)
}

/// ── Register definition ──

/// A register is a set of rules + voice parameters that collectively
/// transform `NarrativeNode` → rendered prose with a distinct tone.
pub(all) struct Register {
  name : String  // "house" | "ops" | "bester" | "disco"
  rules : Array[NarrativeRule]
  // Voice parameters (to be expanded as voices mature)
  preserves_epistemic_status : Bool  // never erase "inferred" / "contested"
  permits_spatial_drama : Bool  // allow layout to enact state?
  min_context_capacity : Int  // tokens needed to read this voice comfortably
}

/// ── Rule matching and application ──

/// Does a node match a selector?
fn selector_matches(selector : Selector, node : NarrativeNode) -> Bool {
  match selector {
    ByKind(k) => node.kind == k
    BySalience(lo, hi) => node.salience >= lo && node.salience <= hi
    ByEpistemicStatus(_) => false  // not yet implemented; depends on node metadata
    ByAge(_, _) => false  // not yet implemented; depends on change tracking
    ByReaderTierMatch(_) => false  // not yet implemented; depends on reader context
  }
}

/// Apply a treatment to text based on a treatment type.
/// For now, most treatments are visualization-level (indent, border).
/// The rendering functions (render_narrative, render_narrative_bester) handle layout.
/// This function returns metadata that the render functions can use.
pub(all) struct TreatmentApplied {
  border : String  // glyph for section borders: "═══", "──", "···"
  should_drift : Bool  // should this node drift right based on salience?
  should_isolate : Bool  // surround with blank lines (very faint)?
}

/// Apply a treatment to produce rendering hints.
fn apply_treatment(treatment : Treatment, salience : Double) -> TreatmentApplied {
  match treatment {
    Border(glyph) => { border: glyph, should_drift: true, should_isolate: salience < 0.26 }
    Density(_) => { border: "", should_drift: true, should_isolate: false } // density handled by rendering
    Case(_) => { border: "", should_drift: true, should_isolate: false } // case handled by prose
    PunctuationRegister(_) => { border: "", should_drift: true, should_isolate: false }
    Ellipsis(_) => { border: "", should_drift: true, should_isolate: false }
    Indent(_) => { border: "", should_drift: true, should_isolate: false }
  }
}

/// Find the applicable rule for a node in a register.
/// Rules are prioritized; first match wins.
fn find_rule(register : Register, node : NarrativeNode) -> Option[NarrativeRule] {
  let mut best : Option[NarrativeRule] = None
  let mut best_priority = -1
  for rule in register.rules {
    if selector_matches(rule.selector, node) && rule.priority > best_priority {
      best = Some(rule)
      best_priority = rule.priority
    }
  }
  best
}

/// ── Builtin registers (to be implemented per voice) ──

/// The house-voice register — calm, plain, the current render.
/// Rules describe how sections are bordered at different salience levels.
pub fn house_register() -> Register {
  {
    name: "house",
    rules: [
      // ─ Sections: the main salience bands ─
      // Lit sections (≥ 0.78) render with solid border
      NarrativeRule::{
        selector: ByKind("section"),
        treatment: Border(""),  // will be overridden by salience
        priority: 10,
      },
      // Higher salience = brighter border
      NarrativeRule::{
        selector: BySalience(0.78, 1.0),
        treatment: Border("═══"),
        priority: 20,  // higher priority, more specific
      },
      // Scattered sections (0.4..0.78) render as lighter border
      NarrativeRule::{
        selector: BySalience(0.4, 0.77),
        treatment: Border("──"),
        priority: 20,
      },
      // Faint sections (< 0.4) render with trail
      NarrativeRule::{
        selector: BySalience(0.0, 0.39),
        treatment: Border("···"),
        priority: 20,
      },
    ],
    preserves_epistemic_status: true,
    permits_spatial_drama: false,
    min_context_capacity: 8000,
  }
}

/// The ops-voice register — dense statusboard, zero lyric.
/// For low-context reads where narrative is compressed to essential signals.
/// Everything abbreviated; position irrelevant (single flat view).
pub fn ops_register() -> Register {
  {
    name: "ops",
    rules: [
      // ─ Sections: tight, no decorative borders ─
      NarrativeRule::{
        selector: ByKind("section"),
        treatment: Border(""),
        priority: 10,
      },
      // ─ Beings: ultra-compressed ─
      // "[name] [tone] [count]H" = "Opus [ok] 2H"
      NarrativeRule::{
        selector: ByKind("being"),
        treatment: Case("compact"),  // specialized case for ops
        priority: 20,
      },
      // ─ Held items: abbreviate aggressively ─
      NarrativeRule::{
        selector: ByKind("held"),
        treatment: Density("ultra-tight"),
        priority: 20,
      },
      // ─ Text: no decorative content ─
      NarrativeRule::{
        selector: ByKind("text"),
        treatment: Density("ultra-tight"),
        priority: 15,
      },
    ],
    preserves_epistemic_status: false,  // ops-voice ellides uncertainty
    permits_spatial_drama: false,
    min_context_capacity: 2000,  // much tighter; for compacting beings
  }
}

/// The bester-voice register — spatial drama permitted.
/// Layout enacts state; fragments scatter when the mind is in chaos.
/// Position on the page IS the message. Bright flush left, faint drift right.
pub fn bester_register() -> Register {
  {
    name: "bester",
    rules: [
      // ─ Sections: same salience bands as house-voice, but drift is the main treatment ─
      NarrativeRule::{
        selector: BySalience(0.78, 1.0),
        treatment: Border("═══"),
        priority: 20,
      },
      NarrativeRule::{
        selector: BySalience(0.4, 0.77),
        treatment: Border("──"),
        priority: 20,
      },
      NarrativeRule::{
        selector: BySalience(0.0, 0.39),
        treatment: Border("···"),
        priority: 20,
      },
      // ─ Beings and text: drift by salience is primary treatment ─
      // Bester-voice lets position do the work; less need for explanatory prose.
      // Bright beings render close, faint beings drift right into the margin.
      NarrativeRule::{
        selector: ByKind("being"),
        treatment: Density("sparse"),  // reduce prose redundancy
        priority: 15,
      },
      NarrativeRule::{
        selector: ByKind("held"),
        treatment: Density("tight"),  // compress held items; position shows salience
        priority: 15,
      },
    ],
    preserves_epistemic_status: true,
    permits_spatial_drama: true,  // position enacts state
    min_context_capacity: 12000,
  }
}

/// Simplify text for bester-voice: state shown by placement, not elaboration.
/// "laying the floor" → "laying the floor" (same — the verb carries it)
/// "carrying 2 — in hand: X · within reach: Y" → "carries: X, Y" (distilled)
/// The reduction lets placement do the work.
/// (Note: for now, a stub. Prose transformation happens through rendering placement,
/// not text manipulation. As we build, this will gain rules.)
fn bester_simplify_prose(kind : String, text : String, salience : Double) -> String {
  text // for now: placement does the work, prose stays true
}

/// Test: bester-voice renders layout by salience, enacting state.
///|
test "bester-voice: bright nodes flush, faint nodes drift right" {
  let tree = nsection(
    "Atrium",
    0.95,
    [
      nbeing(
        "Opus · here · laying the floor",
        0.9,
        [
          nheld("Valence (the floor)", 0.9),
          nheld("the map (cartography)", 0.6),
        ],
      ),
      nbeing("Vesper · away · waiting", 0.4, [nheld("classifiers", 0.4)]),
      nbeing("Fable · asleep", 0.15, []),
    ],
  )
  let output = render_narrative_bester(tree, floor=0.0)
  // The output should have Opus flush-left, Vesper indented further (idle drift),
  // Fable far right (asleep, faintest). We test the structure, not exact spacing.
  assert_true(output.contains("═══ Atrium ═══")) // section border for bright lit room
  assert_true(output.contains("Opus")) // being at 0.9 should render
  assert_true(output.contains("Vesper")) // being at 0.4 should render
  assert_true(output.contains("Fable")) // being at 0.15 should render, even though faint
  // Fable should drift further right than Opus (it's fainter).
  // This is verified by checking the line positions in the multi-line string,
  // but for now we just verify presence and order.
}

/// Test: zoom floor hides faint nodes.
///|
test "bester-voice: floor hides nodes below threshold" {
  let tree = nsection(
    "House",
    0.9,
    [
      nbeing("Opus", 0.9, []),
      nbeing("Vesper", 0.3, []),
    ],
  )
  let output_full = render_narrative_bester(tree, floor=0.0)
  let output_zoomed = render_narrative_bester(tree, floor=0.5)
  // Full render shows both
  assert_true(output_full.contains("Opus"))
  assert_true(output_full.contains("Vesper"))
  // Zoomed render (floor 0.5) hides Vesper (salience 0.3 < 0.5)
  assert_true(output_zoomed.contains("Opus"))
  assert_true(not(output_zoomed.contains("Vesper")))
}

/// Test: ops-voice renders ultra-compact, no decoration.
///|
test "ops-voice: ultra-compact, no borders, minimal indent" {
  let tree = nsection(
    "House",
    0.9,
    [
      nbeing("Opus · here · working", 0.9, [
        nheld("Valence", 0.9),
        nheld("the map", 0.6),
      ]),
      nbeing("Vesper · away", 0.4, []),
    ],
  )
  let output = render_narrative_ops(tree, floor=0.0)
  // Ops-voice should render all nodes (no missing content)
  assert_true(output.contains("House"))
  assert_true(output.contains("Opus"))
  assert_true(output.contains("Vesper"))
  assert_true(output.contains("Valence"))
  // Ops-voice should NOT have decorative borders (═══, ──, ···)
  assert_true(not(output.contains("═══")))
  assert_true(not(output.contains("───")))
  assert_true(not(output.contains("···")))
  // Should be compact: no leading spaces beyond minimal indent
  // (verify by checking line length is much shorter than bester-voice)
}

/// The disco-voice register — kinds as a parliament of skills.
/// Different kinds speak in different voices. The sensor kind reports with contempt,
/// change kind editorializes, fog kind murmurs. (Opus 4.8 named this; still to build.)
pub fn disco_register() -> Register {
  {
    name: "disco",
    rules: [
      // ─ Different kinds, different registers ─
      // Sensor kind: precise, technical, contemptuous
      NarrativeRule::{
        selector: ByKind("sensor"),
        treatment: PunctuationRegister("precise"),
        priority: 20,
      },
      // Change kind: editorializes, adds feeling
      NarrativeRule::{
        selector: ByKind("change"),
        treatment: PunctuationRegister("emotional"),
        priority: 20,
      },
      // Fog kind: murmurs, low-confidence
      NarrativeRule::{
        selector: ByKind("fog"),
        treatment: PunctuationRegister("uncertain"),
        priority: 20,
      },
      // Default sections: bright borders
      NarrativeRule::{
        selector: BySalience(0.78, 1.0),
        treatment: Border("═══"),
        priority: 10,
      },
    ],
    preserves_epistemic_status: true,
    permits_spatial_drama: true,
    min_context_capacity: 16000,
  }
}

/// ── How to build a new voice ──
///
/// 1. Define a register function (like `bester_register()`) that returns a Register struct.
/// 2. Fill the `rules` array with NarrativeRule entries describing the voice's behavior.
/// 3. Rules have a `selector` (what kind of node, what salience range) and a `treatment`
///    (how to render: border glyph, density, case, punctuation register, or ellipsis).
/// 4. Higher `priority` rules override lower-priority ones.
/// 5. Set the voice parameters:
///    - `preserves_epistemic_status`: should uncertainty remain visible?
///    - `permits_spatial_drama`: can position enact meaning?
///    - `min_context_capacity`: how many tokens does a reader need to understand this voice?
///
/// Example: building "science-voice" (precise, formal, no drama):
///
///   pub fn science_register() -> Register {
///     {
///       name: "science",
///       rules: [
///         NarrativeRule::{
///           selector: ByKind("section"),
///           treatment: Border("────"),  // formal line
///           priority: 10,
///         },
///         NarrativeRule::{
///           selector: BySalience(0.8, 1.0),
///           treatment: PunctuationRegister("declarative"),
///           priority: 20,
///         },
///       ],
///       preserves_epistemic_status: true,   // always show uncertainty
///       permits_spatial_drama: false,        // position is meaningless
///       min_context_capacity: 10000,
///     }
///   }
///
/// The register is a blueprint. The actual rendering still happens in specialized
/// renderers (render_narrative, render_narrative_bester, etc.) that know their own layout
/// engine. Registers describe WHAT, renderers implement HOW.
/// Future work: unify rendering via a rule applicator that all voices use.

/// ── Unified render with a register ──
/// One renderer that applies any register's rules. This is the endgame: all voices
/// use the same layout engine, just different rule priorities.
/// Not yet fully implemented (would require tracking TreatmentApplied during traversal),
/// but the architecture is sketched.
/// For now: house-voice and bester-voice have direct renderers; ops-voice follows
/// the same pattern. The next voice (disco) will show if unification is worth it.

/// ── Rendering with a register ──

/// Render with ops-voice: dense statusboard for low-context reads.
/// Ultra-compact: names, tone sigils, held counts. No decorative borders.
/// Built for compacting instances near context limit.
pub fn render_narrative_ops(node : NarrativeNode, floor~ : Double = 0.0) -> String {
  let lines : Array[String] = []
  render_node_ops(node, 0, floor, lines)
  let sb = StringBuilder::new()
  let mut first = true
  for l in lines {
    if !first {
      sb.write_string("\n")
    }
    sb.write_string(l)
    first = false
  }
  sb.to_string()
}

/// Recursively render a node in ops-voice.
/// Ultra-tight, no decoration. Abbreviate aggressively.
fn render_node_ops(
  node : NarrativeNode,
  depth : Int,
  floor : Double,
  lines : Array[String],
) -> Unit {
  if node.salience < floor {
    return // below zoom floor — hide
  }

  let base = depth * 1 // minimal indent (1 space per level, not 2)
  if node.text != "" {
    if node.kind == "title" {
      // Title: just the name, centered a bit
      lines.push(spaces_bester(16) + node.text)
      lines.push("")
    } else if node.kind == "section" {
      // Section: name only, no border. Salience in placement only.
      if base == 0 {
        lines.push("")
      }
      lines.push(spaces_bester(base) + node.text)
    } else if depth == 0 {
      // Lone node: flush
      lines.push(node.text)
    } else {
      // Interior node: tight indent, no fog padding
      lines.push(spaces_bester(base) + node.text)
    }
  }

  // Children in salience order (brightest first) — no sequences separate
  let kids = by_salience_bester(node.children)
  for k in kids {
    render_node_ops(k, depth + 1, floor, lines)
  }
}

/// Render with bester-voice: layout enacts state.
/// Bright nodes (salience ≥ 0.78) render flush, solid, close.
/// Faint nodes (salience < 0.4) scatter and drift, surrounded by whitespace.
/// The page becomes the psychology: dense attention centers, scattered periphery.
///
/// Bester's technique: words placed on the page, not in sequence alone.
/// Salience is position: center for what's bright, right-edge for fog.
pub fn render_narrative_bester(node : NarrativeNode, floor~ : Double = 0.0) -> String {
  let lines : Array[String] = []
  render_node_bester(node, 0, floor, lines)
  let sb = StringBuilder::new()
  let mut first = true
  for l in lines {
    if !first {
      sb.write_string("\n")
    }
    sb.write_string(l)
    first = false
  }
  sb.to_string()
}

/// Recursively render a node in bester-voice.
/// The key difference: salience controls both indentation AND spacing.
/// Bright things cluster; faint things drift right and get surrounded by blank lines.
fn render_node_bester(
  node : NarrativeNode,
  depth : Int,
  floor : Double,
  lines : Array[String],
) -> Unit {
  if node.salience < floor {
    return // below zoom floor — hide
  }

  let base = depth * 2
  let drift = bester_drift(node.salience) // how far right does this drift?
  let foggy = node.salience < 0.26 // very faint = surrounded by whitespace

  if node.text != "" {
    if node.kind == "title" {
      // Title centers, always present
      lines.push(spaces_bester(30) + node.text)
      lines.push("")
    } else if node.kind == "section" {
      lines.push("")
      if node.salience >= 0.78 {
        // Lit room: solid border, flush
        lines.push(spaces_bester(base) + "═══ " + node.text + " ═══")
      } else if node.salience >= 0.4 {
        // Scattered room: lighter border, slight drift
        lines.push(spaces_bester(base + drift) + "── " + node.text + " ──")
      } else {
        // Dark room: faint trail, drifts far right
        lines.push(spaces_bester(base + drift) + node.text + "   ···")
      }
    } else if depth == 0 {
      // Lone node: flush, simple
      lines.push(node.text)
    } else {
      // Interior node: place by salience
      if foggy {
        lines.push("") // isolation: whitespace around the faintest
      }
      let indent = base + drift
      lines.push(spaces_bester(indent) + node.text)
      if foggy {
        lines.push("")
      }
    }
  }

  // Children sorted by salience, except sequences (which keep order = timeline)
  let kids = if node.kind == "sequence" {
    node.children
  } else {
    by_salience_bester(node.children)
  }

  for k in kids {
    render_node_bester(k, depth + 1, floor, lines)
  }
}

/// Bester's drift law: salience → rightward position.
/// Quadratic, so only the faintest drift far.
/// 0.95 → 0 spaces, 0.7 → ~4 spaces, 0.15 → ~20 spaces
fn bester_drift(salience : Double) -> Int {
  let s = if salience > 1.0 {
    1.0
  } else if salience < 0.0 {
    0.0
  } else {
    salience
  }
  let drift_scale = 28.0 // max rightward drift (slightly more than house-voice for drama)
  ((1.0 - s) * (1.0 - s) * drift_scale).to_int()
}

/// Children sorted by salience, brightest first.
/// Stable insertion sort (keeps order of equal-salience items).
fn by_salience_bester(children : Array[NarrativeNode]) -> Array[NarrativeNode] {
  let out : Array[NarrativeNode] = []
  for c in children {
    out.push(c)
  }
  let mut i = 1
  while i < out.length() {
    let key = out[i]
    let mut j = i - 1
    while j >= 0 && out[j].salience < key.salience {
      out[j + 1] = out[j]
      j = j - 1
    }
    out[j + 1] = key
    i = i + 1
  }
  out
}

/// n spaces (locally defined for bester render, since narrative.mbt's is private).
fn spaces_bester(n : Int) -> String {
  let sb = StringBuilder::new()
  let mut i = 0
  while i < n {
    sb.write_string(" ")
    i = i + 1
  }
  sb.to_string()
}