// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// A compiled regular expression for string-oriented matching.
pub struct Regex {
  priv pat : @re.Pattern
  priv mut re : @re.Regex?
}

///|
/// Compiles a regex pattern string into a `Regex` object.
///
/// The regex syntax follows MoonBit `lexmatch` regex literals.
/// The following constructs are recognized:
///
/// - `.` wildcard (matches any character, including newline)
/// - Character classes: `[abc]`, `[^abc]`, `[a-z]`, POSIX classes such as
///   `[[:digit:]]`, `[[:alpha:]]`, `[[:space:]]`, `[[:word:]]`
/// - Quantifiers: `*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}` and non-greedy forms
///   `*?`, `+?`, `??`, `{n}?`, `{n,}?`, `{n,m}?`
/// - Grouping and alternation: `( ... )`, `(?: ... )` (non-capturing),
///   `(? ... )`, `a|b`
/// - Assertions and modifiers: `^`, `$`, `\b`, `\B`, `(?i: ... )`
///
/// Escape sequences include `\n`, `\r`, `\t`, `\f`, `\v`, and escaped
/// metacharacters. In `Regex::compile`, Unicode escapes are supported:
/// `\uXXXX` and `\u{X...}`. `\xHH` is not supported in `Regex::compile`.
///
/// `^` and `$` are non-multiline anchors: they match only the beginning and
/// end of the whole input, not per-line boundaries.
///
/// `\d`, `\D`, `\s`, `\S`, `\w`, and `\W` are not supported; use POSIX
/// character classes instead.
///
/// POSIX character classes are ASCII-based.
///
/// In character classes, the dash `-` is used to specify ranges (e.g., `[a-z]`).
/// To match a literal dash, it must be escaped as `\-`. Placing a dash at the
/// start or end of a character class (e.g., `[-a]` or `[a-]`) is not supported.
///
/// For full grammar and semantics, see:
/// 
///
/// Raises when `pattern` is not a valid regex pattern.
///
/// Example:
///
/// ```mbt check
/// test {
///   let regex = re"[[:digit:]]+"
///   guard regex.execute("a12b") is Some(m) else { fail("Expected match") }
///   inspect(m.content(), content="12")
/// }
/// ```
#alias(new, deprecated="Use `Regex()` instead")
pub fn Regex::Regex(pattern : StringView) -> Regex raise {
  let pat = @regex_parser.parse(
    profile=re_profile_unicode,
    pattern,
    mode=String,
  )
  { pat, re: None }
}

///|
const MAX_REPEAT_QUANTIFIER = 256

///|
pub impl @debug.Debug for Regex with fn to_repr(self) {
  @debug.Repr::opaque_("Regex", Repr(self.pat))
}

///|
/// Result of one successful match produced by `Regex::execute`.
struct MatchResult {
  input : StringView
  group_names : ReadOnlyArray[String?]
  result : @re.MatchResult
} derive(@debug.Debug)

///|
/// Return match `before` view.
pub fn MatchResult::before(self : MatchResult) -> StringView {
  guard self.result.group(0) is Some((start, _end)) else { panic() }
  self.input[0:start]
}

///|
/// Return match `after` view.
pub fn MatchResult::after(self : MatchResult) -> StringView {
  guard self.result.group(0) is Some((_start, end)) else { panic() }
  self.input[end:self.input.length()]
}

///|
/// Return match `content` view.
pub fn MatchResult::content(self : MatchResult) -> StringView {
  guard self.result.group(0) is Some((start, end)) else { panic() }
  self.input[start:end]
}

///|
/// Access capture `group` information.
pub fn MatchResult::group(self : MatchResult, group_index : Int) -> StringView? {
  match self.result.group(group_index) {
    None => None
    Some((start, end)) => Some(self.input[start:end])
  }
}

///|
/// Access capture `named_group` information.
pub fn MatchResult::named_group(
  self : MatchResult,
  name : String,
) -> StringView? {
  match self.group_names.search(Some(name)) {
    None => None
    Some(index) => self.group(index)
  }
}

///|
/// DO NOT call this function even in this package.
#internal(internal, "not intended for public use")
#doc(hidden)
pub fn Regex::internal_compile_pattern(pat : Pattern) -> Regex {
  let lowered_pat = re_lower_to_utf16(pat.0)
  { pat: pat.0, re: Some(@re.compile(profile=re_profile_utf16, lowered_pat)) }
}

///|
/// DO NOT call this function even in this package.
#internal(internal, "not intended for public use")
#doc(hidden)
pub fn Regex::internal_from_string(pat : String) -> Regex {
  try! Regex(pat)
}

///|
fn Regex::re(self : Regex) -> @re.Regex {
  match self.re {
    Some(re) => re
    None => {
      let lowered_pat = re_lower_to_utf16(self.pat)
      let re = @re.compile(profile=re_profile_utf16, lowered_pat)
      self.re = Some(re)
      re
    }
  }
}

///|
/// Compiles a regex pattern string into a `Regex` object, panicking on invalid patterns.
///
/// This function is equivalent to `Regex(pattern)` but converts any compilation
/// error into a panic instead of raising an exception.
///
/// The regex syntax follows the same rules as `Regex`.
///
/// Panics when `pattern` is not a valid regex pattern.
///
/// Example:
///
/// ```mbt check
/// test {
///   let regex = @string.Regex::unsafe_from_string("[[:digit:]]+")
///   guard regex.execute("a12b") is Some(m) else { fail("Expected match") }
///   inspect(m.content(), content="12")
/// }
/// ```
pub fn Regex::unsafe_from_string(pattern : StringView) -> Regex {
  try! Regex(pattern)
}

///|
/// Builds a regex that matches `str` literally.
///
/// This is equivalent to `Regex(Regex::escape(str))`.
///
/// Example:
///
/// ```mbt check
/// test {
///   let regex = @string.Regex::string("a+b(c)")
///   inspect(regex.execute("a+b(c)") is Some(_), content="true")
///   inspect(regex.execute("abcc") is Some(_), content="false")
/// }
/// ```
pub fn Regex::string(str : StringView) -> Regex {
  let pat = @re.seq(
    ReadOnlyArray::from_array(
      str.to_array().map(b => @re.char(@re.RecharSet::char(b.to_int()))),
    ),
  )
  { pat, re: None }
}

///|
/// Repeats this regex with a quantifier and returns a new regex.
///
/// - `min` is the minimum number of repetitions (default `0`)
/// - `max` is the optional maximum number of repetitions
/// - `greedy` controls whether matching is greedy or non-greedy
/// - Panics if `min < 0`, `min > 256`, `max < min`, or `max > 256`
///
/// Example:
///
/// ```mbt check
/// test {
///   let greedy = re"[[:digit:]]".repeat(min=2, max=4)
///   guard greedy.execute("a12345") is Some(m1) else { fail("Expected match") }
///   inspect(m1.content(), content="1234")
///
///   let nongreedy = re"[[:digit:]]".repeat(min=2, max=4, greedy=false)
///   guard nongreedy.execute("a12345") is Some(m2) else { fail("Expected match") }
///   inspect(m2.content(), content="12")
/// }
/// ```
pub fn Regex::repeat(
  self : Regex,
  min? : Int = 0,
  max? : Int,
  greedy? : Bool = true,
) -> Regex {
  guard min >= 0 && min <= MAX_REPEAT_QUANTIFIER else { panic() }
  guard max is None ||
    (max is Some(max) && max >= min && max <= MAX_REPEAT_QUANTIFIER) else {
    panic()
  }
  {
    pat: @re.quantifier(self.pat, {
      min,
      max,
      mode: if greedy {
        Greedy
      } else {
        NonGreedy
      },
    }),
    re: None,
  }
}

///|
/// Concatenates two regexes in sequence.
///
/// Example:
///
/// ```mbt check
/// test {
///   let regex = @string.Regex::string("ab") + @string.Regex::string("cd")
///   guard regex.execute("xabcd") is Some(m) else { fail("Expected match") }
///   inspect(m.content(), content="abcd")
/// }
/// ```
#intrinsic("%regex.seq")
pub impl Add for Regex with fn add(self, other) -> Regex {
  { pat: @re.seq([self.pat, other.pat]), re: None }
}

///|
/// Builds an alternation that matches either regex.
///
/// Example:
///
/// ```mbt check
/// test {
///   let regex = @string.Regex::string("cat") | @string.Regex::string("dog")
///   inspect(regex.execute("dog") is Some(_), content="true")
///   inspect(regex.execute("cow") is Some(_), content="false")
/// }
/// ```
#intrinsic("%regex.alt")
pub impl BitOr for Regex with fn lor(self, other) -> Regex {
  { pat: @re.alt([self.pat, other.pat]), re: None }
}

///|
/// Executes this regex on `input` and returns the first match found.
///
/// Search starts at `last_index` (default `0`). The returned match, when
/// present, starts at or after that index.
///
/// `last_index` must satisfy `0 <= last_index <= input.length()`.
///
/// For inputs containing supplementary Unicode characters, `last_index` must
/// also be a valid UTF-16 character boundary (that is, not the second code
/// unit of a surrogate pair).
///
/// Passing a `last_index` in the middle of a surrogate pair may produce match
/// offsets that later cause `MatchResult::before`, `MatchResult::content`, or
/// `MatchResult::after` to panic when slicing.
///
/// `last_index` only controls where searching starts. It does **not** change
/// anchor semantics:
///
/// - `^` still matches only the beginning of `input`
/// - `$` still matches only the end of `input`
///
/// This parameter is needed by iterative operations such as
/// `Regex::find`, `Regex::replace_by`, and `Regex::split`, which repeatedly
/// resume searching from the end of the previous match while keeping anchor
/// behavior relative to the full `input`.
///
/// Returns `None` when there is no match from `last_index` to the end of
/// `input`.
///
/// Example:
///
/// ```mbt check
/// test {
///   let regex = re"[[:digit:]]+"
///   let input = "a12b34"
///
///   guard regex.execute(input) is Some(first) else {
///     fail("Expected first match")
///   }
///   inspect(first.content(), content="12")
///
///   let next = first.before().length() + first.content().length()
///   guard regex.execute(input, last_index=next) is Some(second) else {
///     fail("Expected second match")
///   }
///   inspect(second.content(), content="34")
/// }
/// ```
///
/// ```mbt check
/// test {
///   let anchored = re"^ab$"
///   inspect(anchored.execute("ab", last_index=0) is Some(_), content="true")
///   inspect(anchored.execute("xaby", last_index=1) is Some(_), content="false")
/// }
/// ```
pub fn Regex::execute(
  self : Regex,
  input : StringView,
  last_index? : Int = 0,
) -> MatchResult? {
  match self.re().execute(input, last_index) {
    None => None
    Some(result) =>
      Some({ input, group_names: self.re().group_names(), result })
  }
}

///|
/// Wraps this regex in a named capture group.
///
/// Returns a new regex that captures the entire match of `self` with the
/// specified `group_name`.
///
/// Example:
///
/// ```mbt check
/// test {
///   let digit = re"[[:digit:]]+".capture("number")
///   let regex = @string.Regex::string("ID: ") + digit
///   guard regex.execute("ID: 12345") is Some(m) else { fail("Expected match") }
///   debug_inspect(
///     m.named_group("number"),
///     content=(
///       #|Some()
///     ),
///   )
/// }
/// ```
///
/// ```mbt check
/// test {
///   let user = re"[[:alpha:]]+".capture("user")
///   let domain = re"[[:alpha:]]+".capture("domain")
///   let tld = re"[[:alpha:]]+".capture("tld")
///   let email = user +
///     @string.Regex::string("@") +
///     domain +
///     @string.Regex::string(".") +
///     tld
///   guard email.execute("john@example.com") is Some(m) else {
///     fail("Expected match")
///   }
///   debug_inspect(
///     m.named_group("user"),
///     content=(
///       #|Some()
///     ),
///   )
///   debug_inspect(
///     m.named_group("domain"),
///     content=(
///       #|Some()
///     ),
///   )
///   debug_inspect(
///     m.named_group("tld"),
///     content=(
///       #|Some()
///     ),
///   )
/// }
/// ```
pub fn Regex::capture(self : Regex, group_name : String) -> Regex {
  { pat: @re.capture(name=group_name, self.pat), re: None }
}