// Copyright (c) 2026 colmugx
// SPDX-License-Identifier: Apache-2.0

///|
/// Opaque compiled pattern. No external regexp type leaks through this API.
pub struct CompiledPattern {
  priv expression : @regexp.Regexp
  priv assertions : Array[(Bool, @regexp.Regexp)]
  priv anchored : Bool
}

///|
pub(all) enum PatternSupport {
  Supported(CompiledPattern)
  Unsupported(reason~ : String)
}

///|
pub(all) suberror PatternError {
  NotSupported(String)
} derive(Debug)

///|
pub extend PatternError with @debug.Debug::{to_repr}

///|
fn char_at(source : String, index : Int32) -> Char {
  source.get_char(index).unwrap_or_else(() => source[index].unsafe_to_char())
}

///|
fn char_width(c : Char) -> Int32 {
  if c.to_int() > 65535 {
    2
  } else {
    1
  }
}

///|
/// Two lowercase hexadecimal digits for a control code point (0..=0x1F).
fn control_hex(code : Int32) -> String {
  let digits = "0123456789abcdef"
  let high = (code >> 4) & 0xF
  let low = code & 0xF
  let builder = StringBuilder()
  builder.write_char(digits[high].unsafe_to_char())
  builder.write_char(digits[low].unsafe_to_char())
  builder.to_string()
}

///|
#deprecated("Temporary ECMA translation: recheck moonbitlang/regexp before removing", skip_current_package=true)
fn translate(pattern : String) -> String raise PatternError {
  let output = StringBuilder()
  let mut index = 0
  let mut in_class = false
  while index < pattern.length() {
    let c = char_at(pattern, index)
    if c == '\\' && index + 1 < pattern.length() {
      let next = char_at(pattern, index + 1)
      if next == 'p' || next == 'P' {
        let (replacement, after) = unicode_property(pattern, index)
        output.write_string(replacement)
        index = after
        continue
      }
      if next == 'x' {
        guard index + 3 < pattern.length() else {
          raise NotSupported("incomplete hexadecimal escape")
        }
        let hex = pattern[index + 2:index + 4].to_owned()
        for digit in hex {
          guard digit is ('0'..<='9' | 'a'..<='f' | 'A'..<='F') else {
            raise NotSupported("invalid hexadecimal escape")
          }
        }
        output.write_string("\\u00" + hex)
        index += 4
        continue
      }
      if next == 'c' {
        guard index + 2 < pattern.length() else {
          raise NotSupported("incomplete control escape")
        }
        let letter = char_at(pattern, index + 2)
        // ECMA-262 ClassControlLetter: `\c` plus exactly one ASCII letter.
        // The engine reserves `\c` as an error, so the adapter maps the letter
        // to its control code point. Non-letters stay typed-unsupported
        // instead of guessing between Annex B literal `c` and a SyntaxError.
        guard letter is ('A'..<='Z' | 'a'..<='z') else {
          raise NotSupported(
            "\\c control escapes require an ASCII letter operand",
          )
        }
        let upper = if letter is ('A'..<='Z') {
          letter.to_int()
        } else {
          letter.to_int() - 32
        }
        output.write_string("\\u00" + control_hex(upper & 0x1F))
        index += 3
        continue
      }
      // Backreferences (\1..\9, \k), shorthand classes, identity escapes
      // and control character escapes pass through: the engine implements them
      // or rejects them as a typed compile error.
      output.write_char('\\')
      output.write_char(next)
      index += 1 + char_width(next)
      continue
    }
    if c == '[' && !in_class {
      in_class = true
    } else if c == ']' && in_class {
      in_class = false
    }
    if !in_class && c == '(' && pattern[index:].has_prefix("(?") {
      if pattern[index:].has_prefix("(?=") || pattern[index:].has_prefix("(?!") {
        raise NotSupported("only a leading lookahead chain is supported")
      }
      if pattern[index:].has_prefix("(?<=") ||
        pattern[index:].has_prefix("(? Int32 raise PatternError {
  let mut index = start + 3
  let mut nesting = 1
  let mut in_class = false
  while index < pattern.length() {
    let c = char_at(pattern, index)
    if c == '\\' && index + 1 < pattern.length() {
      index += 1 + char_width(char_at(pattern, index + 1))
      continue
    }
    if c == '[' && !in_class {
      in_class = true
    } else if c == ']' && in_class {
      in_class = false
    }
    if !in_class {
      if c == '(' {
        if !pattern[index:].has_prefix("(?:") {
          raise NotSupported(
            "captures and nested lookarounds inside assertions are not supported",
          )
        }
        nesting += 1
      } else if c == ')' {
        nesting -= 1
        if nesting == 0 {
          return index
        }
      }
    }
    index += char_width(c)
  }
  raise NotSupported("unterminated lookahead assertion")
}

///|
fn has_top_alternative(pattern : String) -> Bool {
  let mut nesting = 0
  let mut in_class = false
  let mut index = 0
  while index < pattern.length() {
    let c = char_at(pattern, index)
    if c == '\\' && index + 1 < pattern.length() {
      index += 1 + char_width(char_at(pattern, index + 1))
      continue
    }
    if c == '[' && !in_class {
      in_class = true
    } else if c == ']' && in_class {
      in_class = false
    }
    if !in_class {
      if c == '(' {
        nesting += 1
      } else if c == ')' {
        nesting -= 1
      } else if c == '|' && nesting == 0 {
        return true
      }
    }
    index += char_width(c)
  }
  false
}

///|
/// Unanchored search semantics. Patterns are passed to the regexp engine after
/// only the rewrites the engine lacks (see `translate`): the ECMA dot
/// line-terminator class, `\xHH` and `\cX` escapes, and General_Category
/// property-name normalization. Backreferences, named groups, scoped flag
/// groups and shorthand classes use the engine's own ECMA semantics, and
/// dialect-invalid syntax surfaces as a typed `Unsupported` result. A leading
/// anchored positive/negative lookahead chain is evaluated with separate
/// compiled expressions; other lookarounds are explicit unsupported errors.
pub fn compile_pattern(pattern : String) -> PatternSupport {
  try {
    let anchored = pattern.has_prefix("^")
    let mut offset = if anchored { 1 } else { 0 }
    let assertions : Array[(Bool, @regexp.Regexp)] = []
    while pattern[offset:].has_prefix("(?=") ||
          pattern[offset:].has_prefix("(?!") {
      if !anchored {
        raise NotSupported(
          "leading lookahead chains must be anchored with ^ in this profile",
        )
      }
      let positive = pattern[offset:].has_prefix("(?=")
      let end = assertion_end(pattern, offset)
      let body = pattern[offset + 3:end].to_owned()
      assertions.push(
        (positive, @regexp.compile("^(?:" + translate(body) + ")")),
      )
      offset = end + 1
    }
    let expression = if assertions.is_empty() {
      @regexp.compile(translate(pattern))
    } else {
      let tail = pattern[offset:].to_owned()
      if has_top_alternative(tail) {
        raise NotSupported(
          "top-level alternatives after a lookahead chain are not supported",
        )
      }
      @regexp.compile("^(?:" + translate(tail) + ")")
    }
    Supported({ expression, assertions, anchored, })
  } catch {
    NotSupported(reason) => Unsupported(reason~)
    _ => Unsupported(reason="pattern rejected by the regexp engine")
  }
}

///|
/// Unsupported patterns cannot silently turn into a false validation result.
pub fn pattern_matches(
  support : PatternSupport,
  text : String,
) -> Bool raise PatternError {
  match support {
    Unsupported(reason~) => raise NotSupported(reason)
    Supported(compiled) => {
      if !compiled.assertions.is_empty() && !compiled.anchored {
        raise NotSupported("invalid compiled assertion origin")
      }
      for entry in compiled.assertions {
        let (positive, assertion) = entry
        if assertion.execute(text).matched() != positive {
          return false
        }
      }
      compiled.expression.execute(text).matched()
    }
  }
}