///|
priv struct Hold {
  mut depth : Int32
  mut quote : Int32
  mut escape : Bool
  mut open_at : Int32
  mut index : Int32
}

///|
fn Hold::new() -> Hold {
  { depth: 0, quote: 0, escape: false, open_at: -1, index: 0, }
}

///|
fn Hold::feed(self : Hold, current : Int32) -> Unit {
  self.index += 1
  if self.escape {
    self.escape = false
    return
  }
  if self.quote != 0 {
    if current == '\\'.to_int() {
      self.escape = true
    } else if current == self.quote {
      self.quote = 0
      if self.depth == 0 {
        self.open_at = -1
      }
    }
    return
  }
  if current == '"'.to_int() || current == '\''.to_int() {
    self.quote = current
    if self.depth == 0 {
      self.open_at = self.index - 1
    }
  } else if current == '('.to_int() ||
    current == '['.to_int() ||
    current == '{'.to_int() {
    if self.depth == 0 {
      self.open_at = self.index - 1
    }
    self.depth += 1
  } else if current == ')'.to_int() ||
    current == ']'.to_int() ||
    current == '}'.to_int() {
    if self.depth > 0 {
      self.depth -= 1
    }
    if self.depth == 0 {
      self.open_at = -1
    }
  }
}

///|
fn Hold::result(self : Hold) -> Int32 {
  if self.quote != 0 || self.depth > 0 {
    self.open_at
  } else {
    -1
  }
}

///|
/// Plain text is one `lexscan` match. Quotes and brackets are fed one by one.
fn skip_plain(hold : Hold, skipped : StringView) -> Unit {
  hold.index += skipped.length()
}

///|
fn scan_scanner(scanner : @lexbuf.StringScanner) -> Int32 {
  let hold = Hold::new()
  while true {
    if hold.quote != 0 && !hold.escape {
      lexscan scanner {
        re"^[^\"'\\\\]+" as skipped => skip_plain(hold, skipped)
        re"^." as mark => hold.feed(mark.to_int())
        _ => break
      }
    } else {
      lexscan scanner {
        re"^[^\"'(){}]+" as skipped => skip_plain(hold, skipped)
        re"^." as mark => hold.feed(mark.to_int())
        _ => break
      }
    }
  }
  hold.result()
}

///|
fn scan_lexbuf(input : @lexbuf.Lexbuf) -> Int32 {
  let hold = Hold::new()
  while true {
    if hold.quote != 0 && !hold.escape {
      lexscan input {
        re"^[^\"'\\\\]+" as skipped => skip_plain(hold, skipped)
        re"^." as mark => hold.feed(mark.to_int())
        _ => break
      }
    } else {
      lexscan input {
        re"^[^\"'(){}]+" as skipped => skip_plain(hold, skipped)
        re"^." as mark => hold.feed(mark.to_int())
        _ => break
      }
    }
  }
  hold.result()
}

///|
/// Outermost unclosed quote or bracket, using `lexscan` on the whole source.
fn hold_from(src : String) -> Int32 {
  let scanner = @lexbuf.StringScanner::{ data: src[:], cursor: 0, }
  scan_scanner(scanner)
}

///|
/// Same scan over chunks. `Lexbuf` refills when a token crosses a chunk boundary.
fn hold_from_chunks(chunks : Array[String]) -> Int32 {
  let mut next = 0
  let input = @lexbuf.Lexbuf::from_fn(fn() {
    if next >= chunks.length() {
      None
    } else {
      let chunk = chunks[next]
      next += 1
      Some(chunk)
    }
  })
  scan_lexbuf(input)
}

///|
/// Classify one JSON escape with `lexmatch`. Unicode escapes stay with the hex reader.
fn write_schema_escape(buf : StringBuilder, escaped : Char) -> Bool {
  lexmatch escaped.to_string() {
    re"^\"$" | re"^\\\\$" | re"^/$" => {
      buf.write_char(escaped)
      true
    }
    re"^b$" => {
      buf.write_char('\u{0008}')
      true
    }
    re"^f$" => {
      buf.write_char('\u{000c}')
      true
    }
    re"^n$" => {
      buf.write_char('\n')
      true
    }
    re"^r$" => {
      buf.write_char('\r')
      true
    }
    re"^t$" => {
      buf.write_char('\t')
      true
    }
    _ => false
  }
}