///|
// Incremental (scoped) cascade — Phases 1–4. See
// docs/incremental-cascade-design.md. Beyond the base owner_id + ancestor-clean
// model: Phase 2 = sibling combinators via the preceding-sibling gate; Phase 3 =
// positional-from-start `:first-child` / `:nth-child` via owner_id keying;
// Phase 4 = `:empty`/`:blank` and positional-from-end / only / of-type via the
// child-structure signature. Only `:has`, `:focus-within` and pseudo-elements
// remain disqualified (see `css_allows_cascade_reuse`).
//
// A dynamic re-render rebuilds the styled node tree from the mutated document
// and re-runs the full O(n) cascade even when almost nothing changed. When the
// shell installs a prior-style map (via `cascade_reuse_begin`) for a reflow
// whose stylesheet is *reuse-safe* (`css_allows_cascade_reuse`), this lets an
// element whose own cascade inputs are unchanged reuse its prior computed style
// instead of re-running selector matching + StyleBuilder.
//
// Correctness (proved inductively):
//   - key: `owner_id`, the structural path already threaded through the build
//     (matches the anonymous-node uid scheme), so a stable position ⇒ stable key;
//   - a node is *clean* iff the stylesheet is reuse-safe, its input signature is
//     unchanged, its parent is clean (root is the base case), and — when the
//     sheet uses a sibling combinator (Phase 2) — every preceding sibling is
//     clean. The ancestor chain makes descendant/child combinators and inherited
//     context (style + css vars) safe; the preceding-sibling gate makes `+`/`~`
//     safe (a changed preceding sibling flips an `X + this` / `X ~ this` match);
//   - reuse-safe (`css_allows_cascade_reuse`) still excludes the selectors whose
//     match can flip without a change captured by the owner_id / ancestor /
//     preceding-sibling model: positional-from-end / only / of-type (a sibling
//     appended after an element flips its match without changing its key), `:has`
//     (descendant dependency), `:empty`/`:blank` (child/text presence), and
//     `:focus-within`. `css_uses_sibling_combinator` flags `+`/`~` so the
//     preceding-sibling gate turns on;
//   - the stylesheet itself must be unchanged — the shell only passes a prior
//     map when the CSS signature matches (a CSS edit resets the map).
//
// Everything degrades to a normal full cascade when a node isn't clean, so a
// miss is never wrong — only slower.

///|
priv struct CascadeReuseCtx {
  prior_styles : Map[String, @style.Style]
  prior_sigs : Map[String, String]
  new_styles : Map[String, @style.Style]
  new_sigs : Map[String, String]
  clean : Map[String, Bool]
  // Phase 2: when the stylesheet uses a sibling combinator (`+`/`~`), an
  // element's match can depend on a preceding sibling, so it may only reuse when
  // every preceding sibling (same parent) is also clean. `preceding_clean` is the
  // running AND of the direct children's cleanliness, keyed by parent owner_id;
  // children are entered left-to-right so it is complete for each next sibling.
  uses_sibling : Bool
  preceding_clean : Map[String, Bool]
  // Phase 4: when the sheet uses `:empty`/`:blank` or a positional-from-end /
  // only / of-type selector, each element's signature includes a summary of its
  // direct children (see `cascade_reuse_sig`).
  uses_child_structure : Bool
  mut reused : Int
}

///|
let cascade_reuse_ctx : Ref[CascadeReuseCtx?] = { val: None }

///|
/// Start a cascade-reuse pass. `prior_styles` / `prior_sigs` are keyed by
/// `owner_id` from the previous render (pass empty maps to record-only, i.e. no
/// reuse). The build records the new styles + signatures; retrieve them with
/// `cascade_reuse_end` to seed the next reflow. Only call around a
/// `build_render_root_node` when the stylesheet is unchanged and reuse-safe.
pub fn cascade_reuse_begin(
  prior_styles : Map[String, @style.Style],
  prior_sigs : Map[String, String],
  uses_sibling? : Bool = false,
  uses_child_structure? : Bool = false,
) -> Unit {
  cascade_reuse_ctx.val = Some({
    prior_styles,
    prior_sigs,
    new_styles: {},
    new_sigs: {},
    clean: {},
    uses_sibling,
    preceding_clean: {},
    uses_child_structure,
    reused: 0,
  })
}

///|
/// End the pass: returns (new styles by owner_id, new signatures by owner_id,
/// reused count) and clears the active context. The maps become the next
/// reflow's prior maps.
pub fn cascade_reuse_end() -> (
  Map[String, @style.Style],
  Map[String, String],
  Int,
) {
  match cascade_reuse_ctx.val {
    Some(ctx) => {
      cascade_reuse_ctx.val = None
      (ctx.new_styles, ctx.new_sigs, ctx.reused)
    }
    None => ({}, {}, 0)
  }
}

///|
/// The cascade-input signature of an element: everything a reuse-safe cascade
/// keys on (tag, id, class list, inline style, attributes). Two renders with the
/// same signature at the same `owner_id` and a clean parent compute the same
/// style. Attributes are serialized in sorted order for stability.
fn cascade_reuse_sig(
  elem : @html.Element,
  include_child_structure : Bool,
) -> String {
  let buf = StringBuilder::new()
  buf.write_string(elem.tag)
  buf.write_char('|')
  match elem.id {
    Some(id) => buf.write_string(id)
    None => ()
  }
  buf.write_char('|')
  for c in elem.classes {
    buf.write_string(c)
    buf.write_char(' ')
  }
  buf.write_char('|')
  match elem.style {
    Some(s) => buf.write_string(s)
    None => ()
  }
  buf.write_char('|')
  let keys = elem.attributes.keys().collect()
  keys.sort()
  for k in keys {
    buf.write_string(k)
    buf.write_char('=')
    match elem.attributes.get(k) {
      Some(v) => buf.write_string(v)
      None => ()
    }
    buf.write_char(';')
  }
  // Phase 4: when the sheet uses child-structure-dependent selectors, append a
  // summary of this element's direct children. It captures the two things those
  // selectors key on: (a) the element's own emptiness / blankness (`:empty` /
  // `:blank`), and (b) — through the *parent's* summary, which reaches children
  // via the ancestor-clean chain — the child list a positional-from-end / only /
  // of-type match reads (count, order, tags). A non-whitespace text edit that
  // keeps the same shape (`T`) leaves the summary unchanged, so text-edit reuse
  // is preserved.
  if include_child_structure {
    buf.write_char('|')
    for child in elem.children {
      match child {
        @html.Node::Element(e) => {
          buf.write_string("E:")
          buf.write_string(e.tag)
          buf.write_char(';')
        }
        @html.Node::Text(t) =>
          if t.trim().is_empty() {
            buf.write_char('W')
          } else {
            buf.write_char('T')
          }
      }
    }
  }
  buf.to_string()
}

///|
/// The `owner_id` of the parent, i.e. the path with the last `/segment` removed.
/// Returns None for a top-level key (no `/`).
fn cascade_reuse_parent_key(owner_id : String) -> String? {
  match owner_id.rev_find("/") {
    Some(idx) => Some(owner_id.substring(start=0, end=idx))
    None => None
  }
}

///|
/// If element `elem` at `owner_id` can reuse its prior computed style, return it
/// (and mark the node clean so its children may reuse too). Records the
/// signature. Returns None when there is no active reuse pass or the node isn't
/// clean — the caller then computes the style normally and must call
/// `cascade_reuse_record` with the result.
fn cascade_reuse_try(owner_id : String, elem : @html.Element) -> @style.Style? {
  match cascade_reuse_ctx.val {
    None => None
    Some(ctx) => {
      let sig = cascade_reuse_sig(elem, ctx.uses_child_structure)
      ctx.new_sigs[owner_id] = sig
      let parent = cascade_reuse_parent_key(owner_id)
      let parent_clean = match parent {
        None => true // root: no parent constraint
        Some(pk) => ctx.clean.get(pk) is Some(true)
      }
      // Own inputs unchanged and a prior style exists to reuse.
      let self_clean = match ctx.prior_sigs.get(owner_id) {
        Some(prior_sig) =>
          prior_sig == sig && ctx.prior_styles.get(owner_id) is Some(_)
        None => false
      }
      // Sibling gate (Phase 2): with `+`/`~` in the sheet, this element may only
      // reuse if every preceding sibling is clean (a changed preceding sibling
      // can flip an `X + this` / `X ~ this` match).
      let preceding_ok = if ctx.uses_sibling {
        match parent {
          None => true
          Some(pk) => not(ctx.preceding_clean.get(pk) is Some(false))
        }
      } else {
        true
      }
      let is_clean = self_clean && parent_clean && preceding_ok
      // Fold this element into its parent's running preceding-clean AND.
      match parent {
        Some(pk) => {
          let prev = match ctx.preceding_clean.get(pk) {
            Some(v) => v
            None => true
          }
          ctx.preceding_clean[pk] = prev && is_clean
        }
        None => ()
      }
      if is_clean {
        ctx.clean[owner_id] = true
        match ctx.prior_styles.get(owner_id) {
          Some(style) => {
            ctx.new_styles[owner_id] = style
            ctx.reused += 1
            return Some(style)
          }
          None => ()
        }
      }
      None
    }
  }
}

///|
/// Record a freshly-computed style for `owner_id` (the not-clean path). The
/// signature was already recorded by `cascade_reuse_try`.
fn cascade_reuse_record(owner_id : String, style : @style.Style) -> Unit {
  match cascade_reuse_ctx.val {
    Some(ctx) => ctx.new_styles[owner_id] = style
    None => ()
  }
}

///|
/// Whether the given raw CSS texts are safe for scoped cascade reuse: they must
/// use no selector whose match can flip without a change in an element's own
/// inputs or its ancestors' inputs. Conservative and cheap: a substring scan for
/// the disqualifying selector features. A false positive (e.g. `+` inside a
/// `calc()` value) only disables the optimization — never a correctness risk.
pub fn css_allows_cascade_reuse(texts : Array[String]) -> Bool {
  // NOT disqualifying:
  //   - sibling combinators (`+`/`~`) — handled by the preceding-sibling gate
  //     (`css_uses_sibling_combinator`);
  //   - `:first-child` / `:nth-child(...)` (from the *start*) — a sibling
  //     inserted before an element shifts its `owner_id` (child index) so it
  //     recomputes; one inserted after leaves its from-start position (and key)
  //     unchanged, so reuse is correct.
  //   - positional-from-*end* / only / of-type (`:last-child`, `:only-child`,
  //     `:nth-last-*`, `*-of-type`) and `:empty`/`:blank` — handled by the child
  //     structure summary (see `css_uses_child_structure` / `cascade_reuse_sig`).
  // Still disqualifying — dependencies not captured by any of the above: `:has`
  // (arbitrary-depth descendant dependency, needs a subtree-clean pass),
  // `:focus-within` (descendant focus state), and pseudo-elements.
  let unsafe_tokens = [":has", ":focus-within", "::"]
  for text in texts {
    for tok in unsafe_tokens {
      if text.contains(tok) {
        return false
      }
    }
  }
  true
}

///|
/// Whether any CSS text uses a selector whose match depends on child structure:
/// `:empty`/`:blank` (own child/text presence) or positional-from-end / only /
/// of-type (`:last-child`, `:only-child`, `:nth-last-*`, `*-of-type` — read the
/// parent's child list). When true, each element's reuse signature includes a
/// summary of its direct children. Conservative substring scan; a false positive
/// only tightens reuse.
pub fn css_uses_child_structure(texts : Array[String]) -> Bool {
  let tokens = [
    ":empty", ":blank", ":last-child", ":only-child", ":nth-last", "-of-type",
  ]
  for text in texts {
    for tok in tokens {
      if text.contains(tok) {
        return true
      }
    }
  }
  false
}

///|
/// Whether any CSS text uses a sibling combinator (`+` or `~`). Conservative: a
/// substring match (so `calc(a + b)` or `[attr~=v]` also counts) — a false
/// positive only tightens the preceding-sibling gate, never a correctness risk.
/// When true, cascade reuse requires every preceding sibling to be clean.
pub fn css_uses_sibling_combinator(texts : Array[String]) -> Bool {
  for text in texts {
    if text.contains("+") || text.contains("~") {
      return true
    }
  }
  false
}