// Template and capture-group evaluation for the matching engine.
//
// Semantics mirror the uap-python snapshot (uap-python/ 6dd8c39, read-only
// authority) exactly:
//
// - `utils.get` (utils.py:8-9): a group reference yields `None` when the
//   index is out of range or the group did not participate in the match,
//   and also `None` when the group captured the empty string
//   (`m[idx] or None`).
// - `utils.replacer` (utils.py:12-33): absent/empty template -> `None`;
//   otherwise every `$N` is substituted with `get(m, N) or ""` (a missing
//   group contributes the empty string), then the WHOLE substituted result
//   is stripped once, and an empty result becomes `None`. Strip is applied
//   to the final template result only — never to individual group values.
// - The UA (browser) family has bespoke semantics (utils.py:23-27,
//   matchers.py:41-45): the family template is a literal or `$1`
//   substitution with the raw group value; it is NOT stripped and an empty
//   result stays empty (no `None` transition). Generated data keeps the
//   invariant that UA family templates only ever reference `Group(1)`.
//
// Whitespace note: Python `str.strip()` removes Unicode whitespace; MoonBit
// `StringView::trim` removes the ASCII set "\t\n\r ". Every group value and
// template in uap-core is ASCII around its whitespace, so the two agree on
// all real data.

///|
/// Read capture group `n` of `m` with uap-python `utils.get` semantics:
/// `None` when the group is out of range, did not participate, or captured
/// the empty string; the owned captured text otherwise.
fn get_group(m : @regexp.MatchResult, n : Int) -> String? {
  match m.get(n) {
    Some(view) => if view.is_empty() { None } else { Some(view.to_owned()) }
    None => None
  }
}

///|
/// Evaluate a present replacement template, mirroring `utils.replacer`:
/// substitute each `Group(n)` with the raw captured text (a missing or
/// empty group contributes the empty string), strip the whole result once,
/// and return `None` when nothing remains.
fn eval_template(
  parts : Array[@rules.TemplatePart],
  m : @regexp.MatchResult,
) -> String? {
  let buf = StringBuilder()
  for part in parts {
    match part {
      @rules.Literal(text) => buf.write_string(text)
      @rules.Group(n) =>
        // `get` returns `None` for missing/empty groups -> empty string,
        // exactly like uap-python's `get(m, n) or ""` inside `re.sub`.
        match m.get(n) {
          Some(view) => buf.write_stringview(view)
          None => ()
        }
    }
  }
  let stripped = buf.to_string().trim()
  if stripped.is_empty() {
    None
  } else {
    Some(stripped.to_owned())
  }
}

///|
/// Evaluate one replacement field that falls back to a capture group when
/// the template was absent in the rule data (`None` template -> `Group(
/// fallback_group)`), both under `utils.replacer` semantics. This is the
/// flavor used for every OS field and for the device family and model.
fn eval_field(
  parts : Array[@rules.TemplatePart]?,
  fallback_group : Int,
  m : @regexp.MatchResult,
) -> String? {
  match parts {
    Some(ps) => eval_template(ps, m)
    // Absent template: the upstream default template is the bare group
    // reference `"$\{fallback_group}"` (matchers.py:94-98,153-155), i.e.
    // replacer evaluation over a single group part — strip once, empty
    // -> None. (The `TemplatePart` constructors are read-only outside the
    // generated `rules` package, so this case is spelled out instead of
    // building `[@rules.Group(fallback_group)]`.)
    None =>
      match m.get(fallback_group) {
        Some(view) => {
          let stripped = view.trim()
          if stripped.is_empty() {
            None
          } else {
            Some(stripped.to_owned())
          }
        }
        None => None
      }
  }
}

///|
/// Evaluate the UA (browser) family with the bespoke upstream semantics
/// (matchers.py:41-45): literal parts are copied as-is and `Group(n)` parts
/// are substituted with the raw captured text (missing group contributes
/// the empty string; upstream would raise on a non-participating group 1,
/// which no uap-core rule can trigger). The result is NOT stripped and is
/// never converted to `None` — an empty family is returned as `""`, because
/// the modern upstream matcher reports the first regex match unconditionally
/// (`Resolver.__call__` filters only `None` results, and a populated
/// `UserAgent` record is always truthy).
fn eval_ua_family(
  parts : Array[@rules.TemplatePart]?,
  m : @regexp.MatchResult,
) -> String {
  let buf = StringBuilder()
  match parts {
    Some(ps) =>
      for part in ps {
        match part {
          @rules.Literal(text) => buf.write_string(text)
          @rules.Group(n) =>
            match m.get(n) {
              Some(view) => buf.write_stringview(view)
              None => ()
            }
        }
      }
    // No family template: fall back to raw capture group 1 (`family or
    // "$1"` in matchers.py:33, substituted via `replace("$1", m[1])`).
    None =>
      match m.get(1) {
        Some(view) => buf.write_stringview(view)
        None => ()
      }
  }
  buf.to_string()
}

///|
/// Evaluate one UA (browser) version field. Upstream semantics
/// (matchers.py:47-50): a static `v1/v2/v3_replacement` is used verbatim,
/// otherwise the field falls back to capture group 2/3/4 under `utils.get`
/// semantics (`None` when the group is missing or empty). Generated UA
/// version templates are literal-only, so evaluating them through
/// `eval_template` (strip + empty -> `None`) is indistinguishable from the
/// upstream verbatim use on all real data.
fn eval_ua_version(
  parts : Array[@rules.TemplatePart]?,
  fallback_group : Int,
  m : @regexp.MatchResult,
) -> String? {
  match parts {
    Some(ps) => eval_template(ps, m)
    None => get_group(m, fallback_group)
  }
}