///|
const INFO_NAMECOUNT : UInt = 17

///|
const INFO_NAMEENTRYSIZE : UInt = 18

///|
const INFO_NAMETABLE : UInt = 19

///|
const INFO_NEWLINE : UInt = 20

///|
const NEWLINE_CR : UInt = 1

///|
const NEWLINE_LF : UInt = 2

///|
const NEWLINE_CRLF : UInt = 3

///|
const NEWLINE_ANY : UInt = 4

///|
const NEWLINE_ANYCRLF : UInt = 5

///|
const NEWLINE_NUL : UInt = 6

///|
const SUBSTITUTE_GLOBAL : UInt = 0x00000100U

///|
const SUBSTITUTE_OVERFLOW_LENGTH : UInt = 0x00001000U

///|
const ERROR_NOMATCH : Int = -1

///|
const NOTEMPTY_ATSTART : UInt = 0x00000008U

///|
const ANCHORED : UInt = 0x80000000U

///|
priv enum Pcre2Newline {
  CR = 1
  LF = 2
  CRLF = 3
  Any = 4
  AnyCRLF = 5
  Nul = 6
}

///|
fn Pcre2Newline::of_uint(value : UInt) -> Pcre2Newline {
  match value {
    NEWLINE_CR => Pcre2Newline::CR
    NEWLINE_LF => Pcre2Newline::LF
    NEWLINE_CRLF => Pcre2Newline::CRLF
    NEWLINE_ANY => Pcre2Newline::Any
    NEWLINE_ANYCRLF => Pcre2Newline::AnyCRLF
    NEWLINE_NUL | _ => Pcre2Newline::Nul
  }
}

///|
fn Code::newline(self : Code) -> Pcre2Newline raise Pcre2Error {
  let newline = Ref::new(0U)
  let result = pcre2_pattern_info_16_uint(self.0, INFO_NEWLINE, newline)
  if result != 0 {
    raise Pcre2Error::Code(result)
  }
  Pcre2Newline::of_uint(newline.val)
}

///|
fn Code::name_count(self : Code) -> UInt raise Pcre2Error {
  let name_count = Ref::new(0U)
  let result = pcre2_pattern_info_16_uint(self.0, INFO_NAMECOUNT, name_count)
  if result != 0 {
    raise Pcre2Error::Code(result)
  }
  name_count.val
}

///|
fn Code::name_table(self : Code) -> @c.Pointer[UInt16] raise Pcre2Error {
  let name_table = Ref::new(@c.Pointer::null())
  let result = pcre2_pattern_info_16_uint16_pointer(
    self.0,
    INFO_NAMETABLE,
    name_table,
  )
  if result != 0 {
    raise Pcre2Error::Code(result)
  }
  name_table.val
}

///|
fn Code::name_entry_size(self : Code) -> UInt raise Pcre2Error {
  let name_entry_size = Ref::new(0U)
  let result = pcre2_pattern_info_16_uint(
    self.0,
    INFO_NAMEENTRYSIZE,
    name_entry_size,
  )
  if result != 0 {
    raise Pcre2Error::Code(result)
  }
  name_entry_size.val
}

///|
priv type Pcre2Code

///|
fn Pcre2Code::is_null(self : Pcre2Code) -> Bool {
  @pointer.unsafe_borrow(self, ptr => ptr.is_null())
}

///|
#external
priv type Pcre2CompileContext

///|
fn Pcre2CompileContext::null() -> Pcre2CompileContext {
  @c.Pointer::unsafe_into(@c.Pointer::null())
}

///|
#external
priv type Pcre2GeneralContext

///|
fn Pcre2GeneralContext::null() -> Pcre2GeneralContext {
  @c.Pointer::unsafe_into(@c.Pointer::null())
}

///|
priv type Pcre2MatchData

///|
#external
priv type Pcre2MatchContext

///|
fn Pcre2MatchContext::null() -> Pcre2MatchContext {
  @c.Pointer::unsafe_into(@c.Pointer::null())
}

///|
type Pcre2Size = UInt64

///|
#owned(buffer)
extern "c" fn pcre2_get_error_message_16(
  error_code : Int,
  buffer : String,
  buffer_length : Pcre2Size,
) -> Int = "moonbit_pcre2_get_error_message_16"

///|
#owned(pattern, error_code, error_offset)
extern "c" fn pcre2_compile_16(
  pattern : String,
  length : Pcre2Size,
  options : UInt,
  error_code : Ref[Int],
  error_offset : Ref[Pcre2Size],
  context : Pcre2CompileContext,
) -> Pcre2Code = "moonbit_pcre2_compile_16"

///|
#owned(code)
extern "c" fn pcre2_match_data_create_from_pattern_16(
  code : Pcre2Code,
  context : Pcre2GeneralContext,
) -> Pcre2MatchData = "moonbit_pcre2_match_data_create_from_pattern_16"

///|
#owned(match_data)
extern "c" fn pcre2_get_ovector_pointer_16(
  match_data : Pcre2MatchData,
) -> @c.Pointer[Pcre2Size] = "moonbit_pcre2_get_ovector_pointer_16"

///|
#owned(code, subject, match_data, replacement, output_buffer, output_length)
extern "c" fn pcre2_substitute_16(
  code : Pcre2Code,
  subject : String,
  subject_offset : Pcre2Size,
  subject_length : Pcre2Size,
  start_offset : Pcre2Size,
  options : UInt,
  match_data : Pcre2MatchData,
  match_context : Pcre2MatchContext,
  replacement : String,
  replacement_offset : Pcre2Size,
  replacement_length : Pcre2Size,
  output_buffer : String,
  output_length : Ref[Pcre2Size],
) -> Int = "moonbit_pcre2_substitute_16"

///|
#owned(match_data)
extern "c" fn pcre2_get_startchar_16(match_data : Pcre2MatchData) -> Pcre2Size = "moonbit_pcre2_get_startchar_16"

///|
#owned(code, subject, match_data, context)
extern "c" fn pcre2_match_16(
  code : Pcre2Code,
  subject : String,
  length : Pcre2Size,
  start_offset : Pcre2Size,
  options : UInt,
  match_data : Pcre2MatchData,
  context : Pcre2MatchContext,
) -> Int = "moonbit_pcre2_match_16"

///|
#owned(code, where_)
extern "c" fn pcre2_pattern_info_16_uint(
  code : Pcre2Code,
  what : UInt,
  where_ : Ref[UInt],
) -> Int = "moonbit_pcre2_pattern_info_16_uint32"

///|
#owned(code, where_)
extern "c" fn pcre2_pattern_info_16_uint16_pointer(
  code : Pcre2Code,
  what : UInt,
  where_ : Ref[@c.Pointer[UInt16]],
) -> Int = "moonbit_pcre2_pattern_info_16_uint16_pointer"

///|
struct Code(Pcre2Code)

///|
pub fn compile(pattern : String) -> Code raise Pcre2Error {
  let error_code = Ref::new(0)
  let error_offset = Ref::new((0 : Pcre2Size))
  let code = pcre2_compile_16(
    pattern,
    pattern.length().to_uint64(),
    0,
    error_code,
    error_offset,
    Pcre2CompileContext::null(),
  )
  if code.is_null() {
    if error_code.val > 0 {
      raise Pcre2Error::Compile(pattern, error_code.val, error_offset.val)
    } else {
      raise Pcre2Error::Code(error_code.val)
    }
  } else {
    code
  }
}

///|
struct Match {
  subject : StringView
  code_unit_offsets : FixedArray[Int]
  name_table : Map[String, Int]
}

///|
trait MatchIndex {
  get(Match, Self) -> StringView?
}

///|
pub impl MatchIndex for Int with get(self : Match, index : Int) -> StringView? {
  if index < 0 || index * 2 + 1 >= self.code_unit_offsets.length() {
    return None
  }
  let start = self.code_unit_offsets[index * 2]
  let end = self.code_unit_offsets[index * 2 + 1]
  Some(self.subject.view(start_offset=start, end_offset=end))
}

///|
pub impl MatchIndex for String with get(self : Match, index : String) -> StringView? {
  guard self.name_table.get(index) is Some(index) else { None }
  let start = self.code_unit_offsets[index * 2]
  let end = self.code_unit_offsets[index * 2 + 1]
  Some(self.subject.view(start_offset=start, end_offset=end))
}

///|
pub impl MatchIndex for StringView with get(self : Match, index : StringView) -> StringView? {
  guard self.name_table.get(index.to_string()) is Some(index) else { None }
  let start = self.code_unit_offsets[index * 2]
  let end = self.code_unit_offsets[index * 2 + 1]
  Some(self.subject.view(start_offset=start, end_offset=end))
}

///|
pub fn[Index : MatchIndex] Match::op_get(
  self : Match,
  index : Index,
) -> StringView {
  Index::get(self, index).unwrap()
}

///|
pub fn[Index : MatchIndex] Match::get(
  self : Match,
  index : Index,
) -> StringView? {
  Index::get(self, index)
}

///|
pub fn Match::groups(self : Match) -> Iter2[Int, StringView] {
  let mut i = 0
  let total = self.code_unit_offsets.length() / 2
  Iter2::new(fn() {
    if i < total {
      let start = self.code_unit_offsets[2 * i]
      let end = self.code_unit_offsets[2 * i + 1]
      let index = i
      i += 1
      Some((index, self.subject.view(start_offset=start, end_offset=end)))
    } else {
      None
    }
  })
}

///|
pub fn Match::named_groups(self : Match) -> Iter2[String, StringView] {
  let iter = self.name_table.iter2()
  Iter2::new(fn() {
    guard iter.next() is Some((name, index)) else { None }
    let start = self.code_unit_offsets[2 * index]
    let end = self.code_unit_offsets[2 * index + 1]
    Some((name, self.subject.view(start_offset=start, end_offset=end)))
  })
}

///|
pub suberror Pcre2Error {
  LookaroundBSK
  Compile(String, Int, Pcre2Size)
  Code(Int)
}

///|
pub impl Show for Pcre2Error with output(self : Pcre2Error, logger : &Logger) -> Unit {
  fn write_error_message(code : Int, logger : &Logger) -> Unit {
    let message = String::make(256, '\u{00}')
    let length = message.length().to_uint64()
    let length = pcre2_get_error_message_16(code, message, length)
    if length < 0 {
      logger.write_string("Unknown error")
    } else {
      logger.write_string([..message.view(end_offset=length)])
    }
  }

  match self {
    LookaroundBSK => logger.write_string("Lookaround BSK")
    Compile(pattern, code, offset) => {
      logger.write_string("Compilation error @ \{offset}: ")
      write_error_message(code, logger)
      logger.write_char('\n')
      let start = @cmp.maximum(0, offset.to_int() - 10)
      let end = @cmp.minimum(pattern.length(), offset.to_int() + 10)
      logger.write_substring(pattern, start, end)
      for _ in 0.. write_error_message(code, logger)
  }
}

///|
fn array_to_string(array : FixedArray[UInt16]) -> String = "%identity"

///|
struct Matches {
  code : Code
  subject : String
  subject_length : Pcre2Size
  subject_start_offset : Pcre2Size
  mut match_data : Pcre2MatchData?
  crlf_is_newline : Bool
  mut ovector : @c.Pointer[Pcre2Size]
  name_count : Int
  name_table : @c.Pointer[UInt16]
  name_entry_size : Int
}

///|
/// Retrieves the next match from the iterator, advancing the internal state to
/// find subsequent matches in the subject string.
///
/// This method implements the core pattern matching logic, handling various edge
/// cases including empty matches, lookaround assertions with `\K`, and proper
/// Unicode character boundary handling. It maintains internal state to ensure
/// all matches are found in sequence without duplicates or infinite loops.
///
/// Parameters:
///
/// * `self` : The `Matches` iterator containing the compiled pattern, subject
/// string, and matching state.
///
/// Returns `Some(Match)` containing the next match with capturing groups and
/// named groups, or `None` if no more matches are found.
///
/// Throws `Pcre2Error` in the following cases:
///
/// * `Pcre2Error::Code(Int)` for general PCRE2 matching errors
/// * `Pcre2Error::Unicode(UnicodeError)` for Unicode-related errors during
/// character offset conversion
/// * `Pcre2Error::LookaroundBSK` when encountering problematic lookaround
/// assertions with `\K`
///
/// Examples:
///
/// ```moonbit nocheck
/// let pattern = @pcre2.compile("\\d+")
/// let matches = pattern.matches("abc123def456")
/// let first = matches.next().unwrap()
/// inspect(first[0], content="123")
/// let second = matches.next().unwrap()
/// inspect(second[0], content="456")
/// assert_true(matches.next() is None)
/// ```
///
/// ```moonbit nocheck
/// let pattern = @pcre2.compile("a*")
/// let matches = pattern.matches("aabaa")
/// let first = matches.next().unwrap()
/// inspect(first[0], content="aa")
/// let second = matches.next().unwrap()
/// inspect(second[0], content="")  // empty match between 'b' and 'a'
/// ```
///
/// ```moonbit nocheck
/// let pattern = @pcre2.compile("(?P\\d+)")
/// let matches = pattern.matches("number: 42")
/// let match_result = matches.next().unwrap()
/// inspect(match_result["num"], content="42")
/// inspect(match_result[0], content="42")
/// ```
pub fn Matches::next(self : Matches) -> Match? raise Pcre2Error {
  guard self.match_data is Some(match_data) else { return None }
  fn next() -> Match? raise Pcre2Error {
    let rc = if self.ovector.is_null() {
      let rc = pcre2_match_16(
        self.code.0,
        self.subject,
        self.subject_length,
        self.subject_start_offset,
        0,
        match_data,
        Pcre2MatchContext::null(),
      )
      if rc == ERROR_NOMATCH {
        return None
      }
      self.ovector = pcre2_get_ovector_pointer_16(match_data)
      rc
    } else {
      let mut options : UInt = 0 // Normally no options
      let mut start_offset = self.ovector[1] // Start at end of previous match

      // If the previous match was for an empty string, we are finished if we are
      // at the end of the subject. Otherwise, arrange to run another match at the
      // same point to see if a non-empty match can be found.

      if self.ovector[0] == self.ovector[1] {
        if self.ovector[0] == self.subject_length {
          return None
        }
        options = NOTEMPTY_ATSTART | ANCHORED
      } else {
        // If the previous match was not an empty string, there is one tricky case to
        // consider. If a pattern contains \K within a lookbehind assertion at the
        // start, the end of the matched string can be at the offset where the match
        // started. Without special action, this leads to a loop that keeps on matching
        // the same substring. We must detect this case and arrange to move the start on
        // by one character. The pcre2_get_startchar() function returns the starting
        // offset that was passed to pcre2_match_16().
        let startchar = pcre2_get_startchar_16(match_data)
        if start_offset <= startchar {
          if startchar >= self.subject_length {
            return None
          }
          start_offset = startchar + 1
        }
      }
      let rc = pcre2_match_16(
        self.code.0,
        self.subject,
        self.subject_length,
        start_offset,
        options,
        match_data,
        Pcre2MatchContext::null(),
      )

      // This time, a result of NOMATCH isn't an error. If the value in "options"
      // is zero, it just means we have found all possible matches, so the loop ends.
      // Otherwise, it means we have failed to find a non-empty-string match at a
      // point where there was a previous empty-string match. In this case, we do
      // what Perl does: advance the matching position by one character, and
      // continue. We do this by setting the "end of previous match" offset, because
      // that is picked up at the top of the loop as the point at which to start
      // again.

      // There are two complications: (a) When CRLF is a valid newline sequence, and
      // the current position is just before it, advance by an extra byte. (b)
      // Otherwise we must ensure that we skip an entire UTF character if we are in
      // UTF mode.

      if rc == ERROR_NOMATCH {
        if options == 0U {
          return None // All matches found
        }
        self.ovector[1] = start_offset + 1 // Advance one code unit
        if self.crlf_is_newline && // If CRLF is a newline & we are at CRLF
          start_offset < self.subject_length - 1 &&
          self.subject.code_unit_at(start_offset.to_int()) == 13 &&
          self.subject.code_unit_at(start_offset.to_int() + 1) == 10 {
          self.ovector[1] += 1 // Advance by one more code unit
        }
        // NOTE(translator): The original code test if the UTF8 flag is set. However,
        // we are using UTF16, so we can skip the check.
        return self.next() // Go round the loop again
      }
      rc
    }

    // Other matching errors are not recoverable.

    if rc < 0 {
      raise Pcre2Error::Code(rc)
    }

    // The match succeeded, but the output vector wasn't big enough. This
    // should not happen.
    if rc == 0 {
      abort("Ovector was not big enough")
    }

    // We must guard against patterns such as /(?=.\K)/ that use \K in an
    // assertion to set the start of a match later than its end. In this
    // demonstration program, we just detect this case and give up.

    if self.ovector[0] > self.ovector[1] {
      raise Pcre2Error::LookaroundBSK
    }

    // As before, show substrings stored in the output vector by number, and
    // then also any named substrings.
    // NOTE(translator): The pcre2demo.c prints the matches to the console. We
    // return the match here instead.
    let matched = Match::{
      subject: self.subject,
      code_unit_offsets: FixedArray::make(2 * rc, 0),
      name_table: {},
    }
    for i in 0.. {
      self.match_data = None
      raise error
    }
  }
}

///|
/// Creates an iterator over all matches of the pattern in a subject string. Each
/// match contains both numbered and named capturing groups.
///
/// Parameters:
///
/// * `self` : A compiled regular expression pattern.
/// * `subject` : The string to search for matches.
///
/// Returns a `Matches` iterator that yields each match in sequence. Each match
/// contains information about the matched substring and any capturing groups.
///
/// Throws `Pcre2Error` if an error occurs during pattern matching.
///
/// Example:
///
/// ```moonbit nocheck
/// let pattern = @pcre2.compile("[0-9]+")
/// let matches = pattern.matches("abc123def456")
/// let first = matches.next().unwrap()
/// inspect(first[0], content="123")
/// let second = matches.next().unwrap()
/// inspect(second[0], content="456")
/// assert_true(matches.next() is None)
/// ```
pub fn Code::matches(
  self : Code,
  subject : StringView,
) -> Matches raise Pcre2Error {
  // The following code has been adapted from the pcre2demo.c file in the PCRE2
  // library.
  let match_data = pcre2_match_data_create_from_pattern_16(
    self.0,
    Pcre2GeneralContext::null(),
  )
  let subject_start_offset = subject.start_offset().to_uint64()
  let subject_length = subject_start_offset + subject.length().to_uint64()
  let crlf_is_newline = match self.newline() {
    Any | CRLF | AnyCRLF => true
    CR | LF | Nul => false
  }
  let ovector = @c.Pointer::null()
  let name_count = self.name_count().reinterpret_as_int()
  let name_table = self.name_table()
  let name_entry_size = self.name_entry_size().reinterpret_as_int()
  return Matches::{
    code: self,
    subject: subject.data(),
    subject_length,
    subject_start_offset,
    match_data: Some(match_data),
    crlf_is_newline,
    ovector,
    name_count,
    name_table,
    name_entry_size,
  }
}

///|
pub fn Code::substitute(
  self : Code,
  subject : StringView,
  replacement : StringView,
  global? : Bool = false,
) -> StringView raise {
  let match_data = pcre2_match_data_create_from_pattern_16(
    self.0,
    Pcre2GeneralContext::null(),
  )
  let subject_length = subject.length().to_uint64()
  let replacement_length = replacement.length().to_uint64()
  let output_length : Ref[UInt64] = Ref::new(subject.length().to_uint64() * 2)
  while true {
    let output = String::make(output_length.val.to_int(), '\u{00}')
    let mut options = SUBSTITUTE_OVERFLOW_LENGTH
    if global {
      options = options | SUBSTITUTE_GLOBAL
    }
    let start_offset = 0UL
    let rc = pcre2_substitute_16(
      self.0,
      subject.data(),
      subject.start_offset().to_uint64(),
      subject_length,
      start_offset,
      options,
      match_data,
      Pcre2MatchContext::null(),
      replacement.data(),
      replacement.start_offset().to_uint64(),
      replacement_length,
      output,
      output_length,
    )
    if rc < 0 {
      raise Pcre2Error::Code(rc)
    }
    break output.view(end_offset=output_length.val.to_int())
  } nobreak {
    abort("unreachable")
  }
}

///|
const CONFIG_JIT : UInt = 1

///|
#owned(where_)
extern "c" fn pcre2_config_16_int(what : UInt, where_ : Ref[Int]) -> Int = "moonbit_pcre2_config_16_int"

///|
pub fn config_jit() -> Bool raise {
  let val = Ref::new(0)
  let rc = pcre2_config_16_int(CONFIG_JIT, val)
  if rc != 0 {
    raise Pcre2Error::Code(rc)
  }
  val.val is 1
}

///|
#owned(code)
extern "c" fn pcre2_jit_compile(code : Pcre2Code, options : UInt) -> Int = "moonbit_pcre2_jit_compile_16"

///|
const PCRE2_JIT_COMPLETE : UInt = 0x00000001U

///|
const PCRE2_JIT_PARTIAL_SOFT : UInt = 0x00000002U

///|
const PCRE2_JIT_PARTIAL_HARD : UInt = 0x00000004U

///|
pub fn Code::jit_compile(
  code : Code,
  complete? : Bool = false,
  partial_soft? : Bool = false,
  partial_hard? : Bool = false,
) -> Unit raise Pcre2Error {
  let mut options : UInt = 0
  if complete {
    options = options | PCRE2_JIT_COMPLETE
  }
  if partial_soft {
    options = options | PCRE2_JIT_PARTIAL_SOFT
  }
  if partial_hard {
    options = options | PCRE2_JIT_PARTIAL_HARD
  }
  let rc = pcre2_jit_compile(code.0, options)
  if rc != 0 {
    raise Pcre2Error::Code(rc)
  }
}