// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// Which way a lookaround looks, and whether it wants to find something.
pub enum Look {
  /// `(?=…)` / `(?!…)`
  Ahead(positive~ : Bool)
  /// `(?<=…)` / `(?…)`: once through, no backtracking into it.
  Atomic(node~ : Node)
  /// A lookaround.
  Lookaround(node~ : Node, look~ : Look)
  /// A backreference to what a group captured, case-folded when `icase`.
  Backref(slot~ : Int32, icase~ : Bool)
  /// A conditional: test, then one branch or the other.
  Cond(probe~ : Cond, yes~ : Node, no~ : Node?)
  /// An assertion.
  Anchor(anchor~ : Anchor)
  /// `\K`: everything before here stops counting towards the match.
  Keep
  /// `\X`: one grapheme cluster.
  Grapheme
  /// The enclosed node, matched with a budget of edits.
  Fuzzy(spec~ : FuzzySpec, node~ : Node)
  /// `(?(DEFINE)…)`: declare subroutines, match empty. The body's groups are
  /// registered (and compiled into sub-programs) but never matched inline.
  Define(node~ : Node)
  /// A recursive call back into the pattern.
  Recurse(target~ : Recurse)
  /// A control verb: `(*ACCEPT)`, `(*FAIL)`, `(*MARK:name)`, `(*PRUNE)`,
  /// `(*SKIP)`.
  Verb(kind~ : VerbKind)
} derive(Eq, Debug)

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

///|
pub extend Node with Debug::{to_repr}

///|
/// A parsed pattern: the tree, and what the parse learned about it.
///
/// `flags` is the compile-time flags with the pattern's own global inline
/// flags merged in — what `Pattern::flags` answers.
pub struct Ast {
  root : Node
  /// How many capture groups the pattern has, group 0 included.
  groups : Int32
  /// Group name to the slots carrying it. An array, because ECMA-262 since
  /// 2025 allows the same name in different branches.
  groupindex : Map[String, Array[Int32]]
  /// `\L` named lists, expanded at compile time.
  named_lists : Map[String, Array[String]]
  /// The effective flags, global inline flags merged in.
  flags : Flags
}

///|
/// The tree on its own, for the executor.
pub fn Ast::root(self : Ast) -> Node {
  self.root
}

///|
/// How many capture groups the pattern has.
pub fn Ast::groups(self : Ast) -> Int32 {
  self.groups
}

///|
/// Group name to the slots carrying it.
pub fn Ast::groupindex(self : Ast) -> Map[String, Array[Int32]] {
  self.groupindex
}

///|
/// The named lists the pattern declared with `\L`.
pub fn Ast::named_lists(self : Ast) -> Map[String, Array[String]] {
  self.named_lists
}

///|
/// The effective flags.
pub fn Ast::flags(self : Ast) -> Flags {
  self.flags
}

///|
/// The syntax tree as text: what `Pattern::dump` answers.
///
/// Not the pattern — a parse and re-print loses spacing and comments, and
/// folds flags into the nodes — but close enough to read a pattern back from,
/// which is what tests and debugging want.
pub fn Ast::dump(self : Ast) -> String {
  let buf = StringBuilder::StringBuilder()
  dump_node(self.root, buf)
  buf.to_string()
}

///|
fn dump_node(node : Node, buf : StringBuilder) -> Unit {
  match node {
    Empty => buf.write_string("")
    Char(code~) => dump_char(code, buf)
    Any(dotall~) => buf.write_string(if dotall { "\\n" } else { "." })
    Class(set~) => dump_class(set, buf)
    Seq(items~) =>
      for item in items {
        dump_node(item, buf)
      }
    Alt(items~) => {
      let mut first = true
      for item in items {
        if !first {
          buf.write_string("|")
        }
        first = false
        dump_node(item, buf)
      }
    }
    Repeat(node~, min~, max~, greedy~) => {
      dump_node(node, buf)
      match max {
        None =>
          if min == 0 {
            buf.write_string("*")
          } else if min == 1 {
            buf.write_string("+")
          } else {
            buf.write_string("{")
            buf.write_string(min.to_string())
            buf.write_string(",}")
          }
        Some(max) =>
          if min == max {
            buf.write_string("{")
            buf.write_string(min.to_string())
            buf.write_string("}")
          } else if min == 0 && max == 1 {
            buf.write_string("?")
          } else if min == 0 {
            buf.write_string("{")
            buf.write_string(max.to_string())
            buf.write_string("}")
          } else {
            buf.write_string("{")
            buf.write_string(min.to_string())
            buf.write_string(",")
            buf.write_string(max.to_string())
            buf.write_string("}")
          }
      }
      if !greedy {
        buf.write_string("?")
      }
    }
    Group(slot~, node~) => {
      ignore(slot)
      buf.write_string("(")
      dump_node(node, buf)
      buf.write_string(")")
    }
    Atomic(node~) => {
      buf.write_string("(?>")
      dump_node(node, buf)
      buf.write_string(")")
    }
    Lookaround(node~, look~) => {
      buf.write_string(
        match look {
          Ahead(positive=true) => "(?="
          Ahead(positive=false) => "(?!"
          Behind(positive=true) => "(?<="
          Behind(positive=false) => "(? {
      buf.write_string("\\")
      buf.write_string(slot.to_string())
    }
    Cond(probe~, yes~, no~) => {
      buf.write_string("(?(")
      match probe {
        ByGroup(slot~) => buf.write_string("\{slot}")
        ByLook(..) => buf.write_string("?")
        ByRecurse(target~) =>
          buf.write_string(
            match target {
              AnyRecursion => "R"
              GroupRecursion(slot~) => "R\{slot}"
              NamedRecursion(name~) => "R&" + name
            },
          )
      }
      dump_node(yes, buf)
      match no {
        None => ()
        Some(no) => {
          buf.write_string("|")
          dump_node(no, buf)
        }
      }
      buf.write_string(")")
    }
    Anchor(anchor~) =>
      buf.write_string(
        match anchor {
          StartLine(multiline=true) => "^"
          StartLine(multiline=false) => "\\A"
          EndLine(multiline=true, ..) => "$"
          EndLine(multiline=false, before_final_newline=true) => "$"
          EndLine(multiline=false, before_final_newline=false) => "\\Z"
          TextStart => "\\A"
          TextEnd => "\\Z"
          WordBoundary(negate=false, ..) => "\\b"
          WordBoundary(negate=true, ..) => "\\B"
          WordStart => "\\m"
          WordEnd => "\\M"
          SearchAnchor => "\\G"
        },
      )
    Keep => buf.write_string("\\K")
    Grapheme => buf.write_string("\\X")
    Fuzzy(spec~, node~) => {
      dump_node(node, buf)
      buf.write_string(dump_fuzzy(spec))
    }
    Define(node~) => {
      buf.write_string("(?(DEFINE)")
      dump_node(node, buf)
      buf.write_string(")")
    }
    Recurse(target~) =>
      match target {
        Whole => buf.write_string("(?R)")
        Group(slot~) => {
          buf.write_string("(?")
          buf.write_string(slot.to_string())
          buf.write_string(")")
        }
        Named(name~) => {
          buf.write_string("(?&")
          buf.write_string(name)
          buf.write_string(")")
        }
      }
    Verb(kind~) =>
      buf.write_string(
        match kind {
          Accept => "(*ACCEPT)"
          Fail => "(*FAIL)"
          Commit => "(*COMMIT)"
          Mark(name~) => "(*MARK:" + name + ")"
          Prune(name~) => verb_arg("(*PRUNE", name)
          Skip(name~) => verb_arg("(*SKIP", name)
          Then(name~) => verb_arg("(*THEN", name)
          LimitMatch(n~) => "(*LIMIT_MATCH:\{n})"
          LimitRecursion(n~) => "(*LIMIT_RECURSION:\{n})"
          Version => "(*VERSION)"
          Fence => "(*FENCE)"
        },
      )
  }
}

///|
fn verb_arg(verb : String, name : String?) -> String {
  match name {
    None => verb + ")"
    Some(n) => verb + ":" + n + ")"
  }
}

///|
fn dump_fuzzy(spec : FuzzySpec) -> String {
  let parts : Array[String] = []
  match spec.insertions {
    Some(n) => parts.push("i<=\{n}")
    None => ()
  }
  match spec.deletions {
    Some(n) => parts.push("d<=\{n}")
    None => ()
  }
  match spec.substitutions {
    Some(n) => parts.push("s<=\{n}")
    None => ()
  }
  match spec.errors {
    Some(n) => parts.push("e<=\{n}")
    None => ()
  }
  match spec.cost {
    None => ()
    Some((i, d, s)) =>
      if i == 1 && d == 1 && s == 1 {
        parts.push("e<=1")
      } else {
        parts.push("\{i}i+\{d}d+\{s}s<=1")
      }
  }
  "{" + parts.join(",") + "}"
}

///|
fn hex_of(code : Int32) -> String {
  let table = "0123456789ABCDEF"
  let buf = StringBuilder::StringBuilder()
  let mut n = code
  if n == 0 {
    buf.write_char('0')
    return buf.to_string()
  }
  // Collect the digits backwards, then write them in order.
  let digs : Array[Char] = []
  while n > 0 {
    digs.push(table[n % 16].to_int().unsafe_to_char())
    n = n / 16
  }
  let mut i = digs.length() - 1
  while i >= 0 {
    buf.write_char(digs[i])
    i -= 1
  }
  buf.to_string()
}

///|
fn dump_char(code : Int32, buf : StringBuilder) -> Unit {
  // Printable ASCII stands for itself; everything else is spelled out, so the
  // dump is unambiguous about what the tree holds.
  if code >= 0x20 && code < 0x7F {
    buf.write_char(code.unsafe_to_char())
  } else {
    buf.write_string("\\x")
    buf.write_string(hex_of(code))
  }
}

///|
fn dump_class(set : @prop.CharSet, buf : StringBuilder) -> Unit {
  buf.write_string("[")
  for (lo, hi) in set.ranges() {
    if lo == hi {
      dump_char(lo, buf)
    } else {
      dump_char(lo, buf)
      buf.write_string("-")
      dump_char(hi, buf)
    }
  }
  buf.write_string("]")
}

// ---------------------------------------------------------------------------
// Building a tree by hand
// ---------------------------------------------------------------------------

///|
/// An `Ast` assembled from parts rather than parsed, so patterns can be
/// composed in code and still be the same kind of value as a parsed one.
pub fn Ast::built(
  root : Node,
  groups~ : Int32,
  groupindex~ : Map[String, Array[Int32]],
  flags~ : Flags,
) -> Ast {
  { root, groups, groupindex, named_lists: {}, flags, }
}

///|
/// The tree with every capture slot moved up by `by`.
///
/// Composing two patterns renumbers the right-hand one's groups so the result
/// is numbered as one written pattern would be — the left operand keeps
/// `1..n`, the right one continues at `n+1`.
pub fn Node::shift_slots(self : Node, by : Int32) -> Node {
  if by == 0 {
    return self
  }
  shift(self, by)
}

///|
fn shift(node : Node, by : Int32) -> Node {
  match node {
    Empty
    | Char(_)
    | Any(_)
    | Class(_)
    | Anchor(_)
    | Keep
    | Grapheme
    | Verb(_) => node
    Seq(items~) => Seq(items=items.map(item => shift(item, by)))
    Alt(items~) => Alt(items=items.map(item => shift(item, by)))
    Repeat(node~, min~, max~, greedy~) =>
      Repeat(node=shift(node, by), min~, max~, greedy~)
    Group(slot~, node~) => Group(slot=slot + by, node=shift(node, by))
    Atomic(node~) => Atomic(node=shift(node, by))
    Lookaround(node~, look~) => Lookaround(node=shift(node, by), look~)
    Backref(slot~, icase~) => Backref(slot=slot + by, icase~)
    Cond(probe~, yes~, no~) =>
      Cond(
        probe=match probe {
          ByGroup(slot~) => ByGroup(slot=slot + by)
          ByLook(node~, look~) => ByLook(node=shift(node, by), look~)
          ByRecurse(target~) => ByRecurse(target~)
        },
        yes=shift(yes, by),
        no=match no {
          None => None
          Some(n) => Some(shift(n, by))
        },
      )
    Fuzzy(spec~, node~) => Fuzzy(spec~, node=shift(node, by))
    Define(node~) => Define(node=shift(node, by))
    Recurse(target~) =>
      Recurse(
        target=match target {
          Whole => Whole
          Group(slot~) => Group(slot=slot + by)
          Named(name~) => Named(name~)
        },
      )
  }
}