///|
priv enum Symbols {
  ByteSymbols(Bytes)
  ScalarSymbols(Array[Int32])
}

///|
fn Symbols::length(self : Symbols) -> Int32 {
  match self {
    ByteSymbols(b) => b.length()
    ScalarSymbols(s) => s.length()
  }
}

///|
fn Symbols::at(self : Symbols, i : Int32) -> Int32 {
  match self {
    ByteSymbols(b) => b[i].to_int()
    ScalarSymbols(s) => s[i]
  }
}

///|
fn is_scalar(n : Int32) -> Bool {
  n >= 0 && n <= 0x10FFFF && !(n >= 0xD800 && n <= 0xDFFF)
}

///|
/// Declarative predicates: words/ranges and bounded-depth composition.
/// Words([]) is the empty language; Words([[]]) matches epsilon. Range matches
/// exactly one symbol, never epsilon. Concat([]) is epsilon; Alternative([])
/// is empty. Repetition permits unbounded languages, not executable callbacks.
pub(all) enum ProsePredicate {
  Words(Array[Array[Int32]])
  SymbolRange(Int32, Int32)
  Concat(Array[ProsePredicate])
  Alternative(Array[ProsePredicate])
  Repetition(Int32, Int32?, ProsePredicate)
} derive(Eq, Debug)

///|
/// Names equal the exact ASCII text inside <...>, case and spaces preserved.
/// Empty names are allowed: <> is a legal prose token. Duplicate and invalid
/// bindings are errors even if unused. Compiled programs own a lowered copy.
pub(all) struct ProseBinding {
  name : String
  predicate : ProsePredicate
} derive(Eq, Debug)

///|
priv struct Lowering {
  a : Analysis
  mut nodes : Int32
  max_nodes : Int32
  scalar : Bool
}

///|
fn Lowering::charge(self : Lowering, pos : Int32) -> Unit raise ParseFailure {
  self.a.tick(pos)
  if self.nodes >= self.max_nodes {
    raise ParseFailure(
      diagnostic(
        self.a.source,
        pos,
        "compile-budget",
        "expanded grammar node budget exhausted",
      ),
    )
  }
  self.nodes = self.nodes + 1
}

///|
fn Lowering::range(
  self : Lowering,
  lo : Int32,
  hi : Int32,
  pos : Int32,
) -> Expr raise ParseFailure {
  self.charge(pos)
  let valid = if self.scalar {
    lo >= 0 && hi <= 0x10FFFF && lo <= hi && !(lo >= 0xD800 && hi <= 0xDFFF)
  } else {
    lo >= 0 && hi <= 255 && lo <= hi
  }
  if !valid {
    raise ParseFailure(
      diagnostic(
        self.a.source,
        pos,
        "symbol-domain",
        "terminal range outside selected symbol domain",
      ),
    )
  }
  // Scalar input validation excludes surrogates even when a range spans them.
  Range(lo, hi, pos)
}

///|
fn Lowering::predicate(
  self : Lowering,
  predicate : ProsePredicate,
  depth~ : Int32 = 0,
) -> Expr raise ParseFailure {
  self.a.tick(0)
  if depth >= 64 {
    raise ParseFailure(
      diagnostic(
        self.a.source,
        0,
        "compile-budget",
        "prose composition depth exceeded (possibly cyclic caller structure)",
      ),
    )
  }
  match predicate {
    Concat(parts) => {
      self.charge(0)
      let es : Array[Expr] = []
      for p in parts {
        es.push(self.predicate(p, depth=depth + 1))
      }
      Sequence(es, 0)
    }
    Alternative(parts) => {
      self.charge(0)
      let es : Array[Expr] = []
      for p in parts {
        es.push(self.predicate(p, depth=depth + 1))
      }
      Choice(es, 0)
    }
    Repetition(lo, hi, p) => {
      self.charge(0)
      if lo < 0 || lo > 1000000 || (hi is Some(n) && (n < lo || n > 1000000)) {
        raise ParseFailure(
          diagnostic(
            self.a.source,
            0,
            "prose-repeat",
            "repeat bounds must be ordered in 0..1000000",
          ),
        )
      }
      Repeat(lo, hi, self.predicate(p, depth=depth + 1), 0)
    }
    SymbolRange(lo, hi) => self.range(lo, hi, 0)
    Words(words) => {
      self.charge(0)
      let alternatives : Array[Expr] = []
      for word in words {
        self.charge(0)
        let sequence : Array[Expr] = []
        for symbol in word {
          sequence.push(self.range(symbol, symbol, 0))
        }
        alternatives.push(Sequence(sequence, 0))
      }
      Choice(alternatives, 0)
    }
  }
}

///|
// Clone a compiled binding at each use so source offsets refer to the actual
// prose occurrence, not the out-of-band binding, and no caller arrays survive.
fn Lowering::at_source(
  self : Lowering,
  e : Expr,
  pos : Int32,
) -> Expr raise ParseFailure {
  self.charge(pos)
  match e {
    Range(lo, hi, _) => Range(lo, hi, pos)
    Sequence(es, _) => {
      let out : Array[Expr] = []
      for child in es {
        out.push(self.at_source(child, pos))
      }
      Sequence(out, pos)
    }
    Choice(es, _) => {
      let out : Array[Expr] = []
      for child in es {
        out.push(self.at_source(child, pos))
      }
      Choice(out, pos)
    }
    Repeat(lo, hi, child, _) => Repeat(lo, hi, self.at_source(child, pos), pos)
    _ => e // Only validated lowered binding variants are constructed.
  }
}

///|
fn Lowering::expression(
  self : Lowering,
  e : Expr,
  bindings : Map[String, Expr],
  allow_prose : Bool,
) -> Expr raise ParseFailure {
  match e {
    Range(lo, hi, pos) => self.range(lo, hi, pos)
    Prose(name, pos) => {
      self.a.tick(pos)
      if !allow_prose {
        raise ParseFailure(
          diagnostic(
            self.a.source,
            pos,
            "prose-unsupported",
            "prose-val requires an explicit binding",
          ),
        )
      }
      match bindings.get(name) {
        Some(bound) => self.at_source(bound, pos)
        None =>
          raise ParseFailure(
            diagnostic(
              self.a.source,
              pos,
              "prose-unbound",
              "no exact binding for prose value",
            ),
          )
      }
    }
    Sequence(es, pos) => {
      self.charge(pos)
      let out : Array[Expr] = []
      for child in es {
        out.push(self.expression(child, bindings, allow_prose))
      }
      Sequence(out, pos)
    }
    Choice(es, pos) => {
      self.charge(pos)
      let out : Array[Expr] = []
      for child in es {
        out.push(self.expression(child, bindings, allow_prose))
      }
      Choice(out, pos)
    }
    Repeat(lo, hi, child, pos) => {
      self.charge(pos)
      Repeat(lo, hi, self.expression(child, bindings, allow_prose), pos)
    }
    _ => {
      self.charge(e.offset())
      e
    }
  }
}

///|
pub extend ProsePredicate with Eq::{equal, not_equal}

///|
pub extend ProsePredicate with @debug.Debug::{to_repr}

///|
pub extend ProseBinding with Eq::{equal, not_equal}

///|
pub extend ProseBinding with @debug.Debug::{to_repr}