//! Link label parsing and matching.

///|
/// Assumes the opening bracket has already been scanned.
/// The line break handler determines what happens when a linebreak
/// is found. It is passed the bytes following the line break and
/// either returns `Some(k)`, where `k` is the number of bytes to skip,
/// or `None` to abort parsing the label.
/// Returns the number of bytes read (including closing bracket) and label on success.
fn scan_link_label_rest(
  text : BytesView,
  start : Int,
  linebreak_handler : ((BytesView) -> Int?)?,
  is_in_table : Bool,
) -> (Int, String)? {
  let bytes = text.view(start~)
  let mut ix = 0
  let mut only_white_space = true
  let mut codepoints = 0
  // no worries, doesn't allocate until we push things onto it
  let label = StringBuilder::new()
  let mut mark = 0

  while true {
    guard codepoints < 1000 else { return None }
    guard ix < bytes.length() else { return None }
    let b = bytes.unsafe_get(ix)
    guard b != b'[' else { return None }
    if b == b']' {
      break
    }
    let is_pipe_escape = b == b'|' &&
      is_in_table &&
      ix != 0 &&
      bytes.unsafe_get(ix - 1) == b'\\'
    let is_table_pipe = b == b'\\' &&
      is_in_table &&
      ix + 1 < bytes.length() &&
      bytes.unsafe_get(ix + 1) == b'|'
    if is_pipe_escape {
      // only way to reach this spot is to have `\\|` (even number of `\` before `|`)
      write_range(label, bytes, mark, ix - 1)
      label.write_string("|")
      ix += 1
      only_white_space = false
      mark = ix
    } else if is_table_pipe {
      // only way to reach this spot is to have `\|` (odd number of `\` before `|`)
      write_range(label, bytes, mark, ix)
      label.write_string("|")
      ix += 2
      codepoints += 1
      only_white_space = false
      mark = ix
    } else if b == b'\x00' {
      write_range(label, bytes, mark, ix)
      label.write_string("\u{fffd}")
      ix += 1
      codepoints += 1
      only_white_space = false
      mark = ix
    } else {
      let is_backslash_escape = b == b'\\' &&
        ix + 1 < bytes.length() &&
        is_ascii_punctuation(bytes.unsafe_get(ix + 1).to_int())
      if is_backslash_escape {
        ix += 2
        codepoints += 2
        only_white_space = false
      } else if b.to_char().is_ascii_whitespace() {
        // normalize labels by collapsing whitespaces, including linebreaks
        let mut whitespaces = 0
        let mut linebreaks = 0
        let whitespace_start = ix

        while ix < bytes.length() &&
              bytes.unsafe_get(ix).to_char().is_ascii_whitespace() {
          match scan_eol(bytes.view(start=ix)) {
            Some(eol_bytes) => {
              linebreaks += 1
              guard linebreaks <= 1 else { return None }
              ix += eol_bytes
              match linebreak_handler {
                Some(handler) =>
                  match handler(bytes.view(start=ix)) {
                    Some(skip) => ix += skip
                    None => return None
                  }
                None => return None
              }
              whitespaces += 2 // indicate that we need to replace
            }
            None => {
              whitespaces += if bytes.unsafe_get(ix) == b' ' { 1 } else { 2 }
              ix += 1
            }
          }
        }
        if whitespaces > 1 {
          write_range(label, bytes, mark, whitespace_start)
          label.write_string(" ")
          mark = ix
          codepoints += ix - whitespace_start
        } else {
          codepoints += 1
        }
      } else {
        only_white_space = false
        ix += 1
        if (b.to_int() & 0b1000_0000) != 0 {
          codepoints += 1
        }
      }
    }
  }

  if only_white_space {
    None
  } else {
    let label_str = if mark == 0 {
      trim_ascii_ws(@utf8.decode_lossy(bytes.view(start=0, end=ix)))
    } else {
      write_range(label, bytes, mark, ix)
      let s = label.to_string()
      trim_ascii_ws(s)
    }
    Some((ix + 1, label_str))
  }
}

///|
fn trim_ascii_ws(s : String) -> String {
  s.trim(chars=" \r\n\t").to_owned()
}