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

///|
/// Patterns built in code.
///
/// A pattern written as text and a pattern assembled from parts are the same
/// kind of value here — both are a syntax tree the executor runs — so `+`, `|`,
/// `repeat` and `capture` compose without ever going back through a string.

///|
/// An `Ast` matching exactly `text`, character for character.
pub fn literal(text : StringView) -> Ast {
  let nodes : Array[Node] = []
  for c in text {
    nodes.push(Char(code=c.to_int()))
  }
  let root = if nodes.length() == 0 {
    Empty
  } else if nodes.length() == 1 {
    nodes[0]
  } else {
    Seq(items=nodes)
  }
  Ast::built(root, groups=0, groupindex=Map([]), flags~)
}

///|
/// One after the other: `a + b`.
pub fn concat(a : Ast, b : Ast) -> Ast {
  let root = Seq(items=[a.root, b.root.shift_slots(a.groups)])
  Ast::built(
    root,
    groups=a.groups + b.groups,
    groupindex=merge_index(a.groupindex, b.groupindex, a.groups),
    flags=a.flags,
  )
}

///|
/// One or the other: `a | b`.
pub fn either(a : Ast, b : Ast) -> Ast {
  let root = Alt(items=[a.root, b.root.shift_slots(a.groups)])
  Ast::built(
    root,
    groups=a.groups + b.groups,
    groupindex=merge_index(a.groupindex, b.groupindex, a.groups),
    flags=a.flags,
  )
}

///|
/// `a.repeat(…)`.
pub fn repeated(
  a : Ast,
  min~ : Int32,
  max? : Int32,
  greedy~ : Bool,
) -> Ast raise Refused {
  if min < 0 {
    raise Pattern(msg="repeat count is negative", at=0, line=1, column=1)
  }
  match max {
    Some(m) =>
      if m < min {
        raise Pattern(
          msg="repeat count \{m} is below the minimum \{min}",
          at=0,
          line=1,
          column=1,
        )
      }
    None => ()
  }
  Ast::built(
    Repeat(node=a.root, min~, max~, greedy~),
    groups=a.groups,
    groupindex=a.groupindex,
    flags=a.flags,
  )
}

///|
/// `a.capture(name)` — one more group, numbered after the ones already there.
pub fn captured(a : Ast, name~ : String) -> Ast raise Refused {
  if name.length() == 0 {
    raise Pattern(msg="group name is empty", at=0, line=1, column=1)
  }
  let slot = a.groups + 1
  let index = copy_index(a.groupindex)
  match index.get(name) {
    None => index[name] = [slot]
    Some(slots) => index[name] = slots + [slot]
  }
  Ast::built(
    Group(slot~, node=a.root),
    groups=slot,
    groupindex=index,
    flags=a.flags,
  )
}

///|
fn copy_index(index : Map[String, Array[Int32]]) -> Map[String, Array[Int32]] {
  let out : Map[String, Array[Int32]] = Map([])
  for name, slots in index {
    out[name] = slots.copy()
  }
  out
}

///|
/// The right-hand pattern's group names, with its slots moved past the
/// left-hand pattern's.
fn merge_index(
  left : Map[String, Array[Int32]],
  right : Map[String, Array[Int32]],
  by : Int32,
) -> Map[String, Array[Int32]] {
  let out = copy_index(left)
  for name, slots in right {
    let moved = slots.map(slot => slot + by)
    match out.get(name) {
      None => out[name] = moved
      Some(had) => out[name] = had + moved
    }
  }
  out
}