///|
/// Translate the BRE/ERE operator spelling to the public MoonBit regex API.
/// Backreferences have no faithful implementation in that API and are rejected.
fn translate_regexp(
  pattern : String,
  extended : Bool,
) -> String raise GrepError {
  let chars : Array[Char] = pattern.iter().collect()
  let output = StringBuilder()
  let mut index = 0
  let mut bracket = false
  while index < chars.length() {
    let char = chars[index]
    if char == '\\' && !bracket {
      index += 1
      if index == chars.length() {
        raise GrepError("trailing backslash")
      }
      let escaped = chars[index]
      if escaped is ('1'..='9') {
        raise GrepError(
          "backreferences are not supported by the public regex API",
        )
      }
      if escaped is ('(' | ')' | '{' | '}' | '+' | '?' | '|') {
        if extended {
          output.write_char('\\')
        }
        output.write_char(escaped)
      } else if escaped is ('w' | 'W' | 's' | 'S') {
        output.write_string(
          match escaped {
            'w' => "[A-Za-z0-9_]"
            'W' => "[^A-Za-z0-9_]"
            's' => "[[:space:]]"
            _ => "[^[:space:]]"
          },
        )
      } else if escaped is ('<' | '>') {
        raise GrepError(
          "directional word assertions are not supported by the public regex API",
        )
      } else if escaped is ('b' | 'B') {
        output.write_char('\\')
        output.write_char(escaped)
      } else {
        // A backslash before an ordinary character is not a PCRE escape.
        if escaped is ('.' | '[' | ']' | '*' | '^' | '$' | '\\') {
          output.write_char('\\')
        }
        output.write_char(escaped)
      }
    } else {
      if !bracket &&
        !extended &&
        char is ('(' | ')' | '{' | '}' | '+' | '?' | '|') {
        output.write_char('\\')
      }
      output.write_char(char)
      if char == '[' {
        bracket = true
      } else if char == ']' {
        bracket = false
      }
    }
    index += 1
  }
  output.to_string()
}

///|
fn match_boundaries(
  line : String,
  start : Int,
  end : Int,
  whole : Bool,
  word : Bool,
) -> Bool {
  (!whole || (start == 0 && end == line.length())) &&
  (
    !word ||
    (
      (start == 0 || !is_word_char(line[start - 1].to_int().unsafe_to_char())) &&
      (
        end == line.length() ||
        !is_word_char(line[end].to_int().unsafe_to_char())
      )
    )
  )
}

///|
/// Choose the earliest start and then the longest match at that start.
/// Testing an exact prefix and suffix against the complete record preserves
/// anchors and word assertions, including when -o resumes partway through it.
fn Matcher::next_match(
  self : Matcher,
  line : String,
  position : Int,
) -> (Int, Int)? {
  if position > line.length() {
    return None
  }
  match self {
    Fixed(patterns, whole, word) => {
      for start in position..<=line.length() {
        let mut longest = -1
        for pattern in patterns {
          let end = start + pattern.length()
          if end <= line.length() &&
            line[start:end] == pattern &&
            match_boundaries(line, start, end, whole, word) &&
            end > longest {
            longest = end
          }
        }
        if longest >= 0 {
          return Some((start, longest))
        }
      }
      None
    }
    Regular(None, _, _) => None
    Regular(Some(regex), whole, word) => {
      let mut search = position
      while search <= line.length() {
        guard regex.execute(line, last_index=search) is Some(candidate) else {
          return None
        }
        let start = candidate.before().length()
        let prefix = Regex::string(line[:start])
        let mut end = line.length()
        while end >= start {
          if match_boundaries(line, start, end, whole, word) {
            let bounded = (try! Regex("^")) +
              prefix +
              regex +
              Regex::string(line[end:]) +
              (try! Regex("$"))
            if bounded.execute(line) is Some(_) {
              return Some((start, end))
            }
          }
          end -= 1
        }
        search = start + 1
      }
      None
    }
  }
}