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

///|
/// A compiled pattern: the tree it came from, the flat program it became, and
/// the source, so it can answer what Python's `re` answers of a `Pattern` —
/// its string, its flags, how many groups, and what they are named.
pub(all) struct Pattern {
  priv pat : String
  priv flags : @parse.Flags
  priv dialect : @parse.Dialect
  priv ast : @parse.Ast
  priv program : @exec.Program
  priv partial : Bool
}

///|
/// The pattern's source text.
pub fn Pattern::pattern(self : Pattern) -> String {
  self.pat
}

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

///|
/// The dialect the pattern was read under.
pub fn Pattern::dialect(self : Pattern) -> @parse.Dialect {
  self.dialect
}

///|
/// How many capturing groups the pattern has, group 0 not counted — Python's
/// `Pattern.groups`.
pub fn Pattern::groups(self : Pattern) -> Int32 {
  self.ast.groups()
}

///|
/// Group name to slot, as Python's `Pattern.groupindex`.
pub fn Pattern::groupindex(self : Pattern) -> Map[String, Array[Int32]] {
  self.ast.groupindex()
}

///|
/// The `\L` lists the pattern used, name to items — `regex.Pattern.named_lists`.
pub fn Pattern::named_lists(self : Pattern) -> Map[String, Array[String]] {
  self.ast.named_lists()
}

///|
/// The syntax tree re-printed, as Python's `Pattern.dump` would.
pub fn Pattern::dump(self : Pattern) -> String {
  self.ast.dump()
}

///|
/// Whether the pattern was compiled for partial matching.
pub fn Pattern::partial(self : Pattern) -> Bool {
  self.partial
}

///|
/// Python's `pos` and `endpos` as the region they name: both are brought inside
/// the text rather than refused, and `-1` means "to the end". A region whose
/// end falls before its start stays empty, which the engine reads as no match.
fn region(text : String, pos : Int32, endpos : Int32) -> (Int32, Int32) {
  let n = cp_length(text)
  let start = if pos < 0 { 0 } else if pos > n { n } else { pos }
  let end = if endpos < 0 { n } else if endpos > n { n } else { endpos }
  (start, end)
}

///|
/// Scan `text` for the leftmost match.
pub fn Pattern::search(
  self : Pattern,
  text : String,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  limit~ : Int32 = 10000000,
) -> Match? raise @parse.Refused {
  self.search_from(text, pos, endpos, false, limit)
}

///|
/// A search that may be told it has to consume something, which is how the
/// scan steps past an empty match without losing a non-empty one at the same
/// place.
fn Pattern::search_from(
  self : Pattern,
  text : String,
  pos : Int32,
  endpos : Int32,
  must_advance : Bool,
  limit : Int32,
) -> Match? raise @parse.Refused {
  let (start, end) = region(text, pos, endpos)
  match
    self.program.search(
      text,
      pos=start,
      endpos=end,
      limit~,
      must_advance~,
      partial=self.partial,
    ) {
    None => None
    Some(r) =>
      Some(Match::{ pattern: self, text, result: r, pos: start, endpos: end, })
  }
}

///|
/// Match only at `pos`.
pub fn Pattern::prefix(
  self : Pattern,
  text : String,
  pos~ : Int32 = 0,
  limit~ : Int32 = 10000000,
) -> Match? raise @parse.Refused {
  let (start, end) = region(text, pos, -1)
  match self.program.prefix(text, pos=start, limit~, partial=self.partial) {
    None => None
    Some(r) =>
      Some(Match::{ pattern: self, text, result: r, pos: start, endpos: end, })
  }
}

///|
/// Match the whole region `pos..endpos`.
pub fn Pattern::full(
  self : Pattern,
  text : String,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  limit~ : Int32 = 10000000,
) -> Match? raise @parse.Refused {
  let (start, end) = region(text, pos, endpos)
  match
    self.program.full(text, pos=start, endpos=end, limit~, partial=self.partial) {
    None => None
    Some(r) =>
      Some(Match::{ pattern: self, text, result: r, pos: start, endpos: end, })
  }
}

///|
/// All non-overlapping matches, as `findall` at the module level.
pub fn Pattern::findall(
  self : Pattern,
  text : String,
  group~ : Group = Whole,
  overlapped~ : Bool = false,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  limit~ : Int32 = 10000000,
) -> Array[String?] raise @parse.Refused {
  let ng = self.groups()
  // Python answers a tuple once there are two or more groups, which a
  // statically typed list cannot say, so the caller has to pick one.
  if group is Whole && ng >= 2 {
    raise @parse.Ambiguous(groups=ng)
  }
  let use_group = if group is Whole {
    if ng == 0 {
      Whole
    } else {
      Num(n=1)
    }
  } else {
    group
  }
  let out : Array[String?] = []
  for m in self.walk(text, pos~, endpos~, overlapped~, limit~) {
    // A group that took no part answers the empty string, which is what
    // Python's `findall` collects — `split` and `group` keep the difference.
    out.push(
      match m.group(use_group) {
        None => Some("")
        Some(g) => Some(g)
      },
    )
  }
  out
}

///|
/// All matches as `Match` objects, as `finditer` at the module level.
pub fn Pattern::finditer(
  self : Pattern,
  text : String,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  overlapped~ : Bool = false,
  limit~ : Int32 = 10000000,
) -> Array[Match] raise @parse.Refused {
  self.walk(text, pos~, endpos~, overlapped~, limit~)
}

///|
/// Every match in the region, the way the two Python engines walk it.
///
/// After an empty match the next attempt at the same position has to consume
/// something — that is what makes `|a` on `a` answer an empty match at 0, then
/// `a`, then an empty match at 1, and what keeps `x*` from looping.
fn Pattern::walk(
  self : Pattern,
  text : String,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  overlapped~ : Bool = false,
  limit~ : Int32 = 10000000,
) -> Array[Match] raise @parse.Refused {
  let out : Array[Match] = []
  let (start, end) = region(text, pos, endpos)
  let mut p = start
  let mut must_advance = false
  while p <= end {
    match self.search_from(text, p, end, must_advance, limit) {
      None =>
        if must_advance {
          must_advance = false
          p = p + 1
        } else {
          break
        }
      Some(m) => {
        out.push(m)
        // A partial match is what the text has of a match that would go on, so
        // nothing can follow it.
        if m.partial() {
          break
        }
        if overlapped {
          p = m.start(0) + 1
          must_advance = false
        } else if m.end(0) == m.start(0) {
          // After an empty match Python retries the same position for a
          // non-empty one; ECMA-262's `lastIndex` just moves on by one.
          if self.dialect is @parse.Ecma262 {
            p = m.end(0) + 1
            must_advance = false
          } else {
            p = m.end(0)
            must_advance = true
          }
        } else {
          p = m.end(0)
          must_advance = false
        }
      }
    }
  }
  out
}

///|
/// Split `text` on the pattern, as `split` at the module level.
pub fn Pattern::split(
  self : Pattern,
  text : String,
  maxsplit~ : Int32 = 0,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  limit~ : Int32 = 10000000,
) -> Array[String?] raise @parse.Refused {
  let ng = self.groups()
  let (start, end) = region(text, pos, endpos)
  let out : Array[String?] = []
  let mut last = start
  let mut count = 0
  for m in self.walk(text, pos~, endpos~, overlapped=false, limit~) {
    if maxsplit != 0 && count >= maxsplit {
      break
    }
    out.push(Some(slice_cp(text, last, m.start(0))))
    let mut g = 1
    while g <= ng {
      out.push(m.group(Num(n=g)))
      g += 1
    }
    last = m.end(0)
    count += 1
  }
  out.push(Some(slice_cp(text, last, end)))
  out
}

///|
/// Replace matches with `repl`, as `sub` at the module level.
pub fn Pattern::sub(
  self : Pattern,
  text : String,
  repl : String,
  count~ : Int32 = 0,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  limit~ : Int32 = 10000000,
) -> String raise @parse.Refused {
  self.subn(text, repl, count~, pos~, endpos~, limit~).0
}

///|
/// Replace matches with `repl`, returning the new text and the count, as `subn`
/// at the module level.
pub fn Pattern::subn(
  self : Pattern,
  text : String,
  repl : String,
  count~ : Int32 = 0,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  limit~ : Int32 = 10000000,
) -> (String, Int32) raise @parse.Refused {
  self.replace(text, m => expand(repl, m), count~, pos~, endpos~, limit~)
}

///|
/// Replace matches with `f(match)`, as `subn_with` at the module level.
pub fn Pattern::subn_with(
  self : Pattern,
  text : String,
  f : (Match) -> String raise @parse.Refused,
  count~ : Int32 = 0,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  limit~ : Int32 = 10000000,
) -> (String, Int32) raise @parse.Refused {
  self.replace(text, f, count~, pos~, endpos~, limit~)
}

///|
/// Replace matches with `f(match)`, as `sub_with` at the module level.
pub fn Pattern::sub_with(
  self : Pattern,
  text : String,
  f : (Match) -> String raise @parse.Refused,
  count~ : Int32 = 0,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  limit~ : Int32 = 10000000,
) -> String raise @parse.Refused {
  self.replace(text, f, count~, pos~, endpos~, limit~).0
}

///|
/// Replace matches with `repl`, as `regex.subf` — `repl` is a format template
/// whose fields name groups (`{}`, `{0}`, `{name}`, `{0[0]}`), with an optional
/// `:spec`.
pub fn Pattern::subf(
  self : Pattern,
  text : String,
  repl : String,
  count~ : Int32 = 0,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  limit~ : Int32 = 10000000,
) -> String raise @parse.Refused {
  self.replace(text, m => expandf(repl, m), count~, pos~, endpos~, limit~).0
}

///|
/// Replace matches with `repl`, returning the new text and the count, as
/// `regex.subfn`.
pub fn Pattern::subfn(
  self : Pattern,
  text : String,
  repl : String,
  count~ : Int32 = 0,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  limit~ : Int32 = 10000000,
) -> (String, Int32) raise @parse.Refused {
  self.replace(text, m => expandf(repl, m), count~, pos~, endpos~, limit~)
}

///|
/// The one substitution loop: walk the matches, keep the text between them,
/// and put whatever the replacement answers in their place.
fn Pattern::replace(
  self : Pattern,
  text : String,
  f : (Match) -> String raise @parse.Refused,
  count~ : Int32 = 0,
  pos~ : Int32 = 0,
  endpos~ : Int32 = -1,
  limit~ : Int32 = 10000000,
) -> (String, Int32) raise @parse.Refused {
  let (start, end) = region(text, pos, endpos)
  let buf = StringBuilder::StringBuilder()
  let mut last = start
  let mut n = 0
  for m in self.walk(text, pos~, endpos~, overlapped=false, limit~) {
    if count != 0 && n >= count {
      break
    }
    buf.write_string(slice_cp(text, last, m.start(0)))
    buf.write_string(f(m))
    last = m.end(0)
    n += 1
  }
  buf.write_string(slice_cp(text, last, end))
  (buf.to_string(), n)
}