//! Sixth part of the first pass: delimiter runs, attribute blocks, LUT.

///|
/// Determines whether the delimiter run starting at given index is
/// left-flanking, as defined by the commonmark spec (and isn't intraword
/// for _ delims).
fn delim_run_can_open(
  s : Bytes,
  start : Int,
  ix : Int,
  run_len : Int,
  mode : TableParseMode,
  options : Options,
) -> Bool {
  let suffix = s.view(start=ix)
  guard decode_char(s, ix + run_len) is Some(next_char) else { return false }
  guard !next_char.is_whitespace() else { return false }
  let delim = suffix.unsafe_get(0)
  // `^` with punctuation content (e.g. `^+^`) requires a preceding alphanumeric
  // or delimiter character (e.g. `H^+^`, `NH~4~^+^`).
  if delim == b'^' && is_punctuation(next_char.to_int()) {
    guard ix - start != 0 else { return false }
    if options.contains(enable_superscript()) {
      match decode_char(s, prev_char_ix(s, ix)) {
        Some(prev_char) =>
          if is_alphanumeric_char(prev_char) ||
            prev_char == '~' ||
            prev_char == '^' {
            return true
          }
        None => ()
      }
    }
    // Punctuation content without qualifying predecessor -- don't open
    return false
  }
  guard decode_char(s, prev_char_ix(s, ix)) is Some(prev_char) else {
    return true
  }
  let in_table = mode is Active
  if in_table {
    guard !ends_with_pipe(s, ix) else { return true }
    guard next_char != '|' else { return false }
  }
  let is_cjk_delim = options.contains(enable_cjk_friendly_emphasis()) &&
    is_cjk_friendly_delim(delim, run_len, options)
  guard !is_cjk_delim else {
    return cjk_friendly_delim_run_flanking(
        prev_char,
        fn() { decode_char(s, prev_char_ix(s, prev_char_ix(s, ix))) },
        next_char,
        delim,
      ).can_open
  }
  // `*`, `~~`, and `^` can be intraword, `~` can only be interword if it's subscript
  guard !(delim == b'*' && !is_punctuation(next_char.to_int())) else {
    return true
  }
  // `^` with non-punctuation content can open intraword
  guard delim != b'^' else { return true }
  guard !(delim == b'~' && run_len > 1) else { return true }
  guard !(delim == b'=' && run_len == 2) else { return true }
  let can_open_tilde = delim == b'~' &&
    (run_len > 1 || options.contains(enable_subscript())) &&
    !is_punctuation(next_char.to_int())
  guard !can_open_tilde else { return true }

  prev_char.is_whitespace() ||
  (
    is_punctuation(prev_char.to_int()) &&
    (delim != b'\'' || !(prev_char == ']' || prev_char == ')'))
  )
}

///|
/// Determines whether the delimiter run starting at given index is
/// right-flanking, as defined by the commonmark spec.
fn delim_run_can_close(
  s : Bytes,
  start : Int,
  ix : Int,
  run_len : Int,
  mode : TableParseMode,
  options : Options,
) -> Bool {
  guard ix - start != 0 else { return false }
  guard decode_char(s, prev_char_ix(s, ix)) is Some(prev_char) else {
    return false
  }
  guard !prev_char.is_whitespace() else { return false }
  guard decode_char(s, ix + run_len) is Some(next_char) else { return true }
  let in_table = mode is Active
  if in_table {
    guard !ends_with_pipe(s, ix) else { return false }
    guard next_char != '|' else { return true }
  }
  let delim = s.view(start=ix).unsafe_get(0)
  let is_cjk_delim = options.contains(enable_cjk_friendly_emphasis()) &&
    is_cjk_friendly_delim(delim, run_len, options)
  guard !is_cjk_delim else {
    return cjk_friendly_delim_run_flanking(
        prev_char,
        fn() { decode_char(s, prev_char_ix(s, prev_char_ix(s, ix))) },
        next_char,
        delim,
      ).can_close
  }
  // `*`, `~~`, and `^` can be intraword, `~` can only be interword if it's subscript
  let is_intraword = (
      delim == b'*' ||
      (delim == b'~' && run_len > 1) ||
      (delim == b'=' && run_len == 2)
    ) &&
    !is_punctuation(prev_char.to_int())
  guard !is_intraword else { return true }
  guard !(delim == b'^' && !is_punctuation(prev_char.to_int())) else {
    return true
  }
  guard !(delim == b'~' && (run_len > 1 || options.contains(enable_subscript()))) else {
    return true
  }

  next_char.is_whitespace() || is_punctuation(next_char.to_int())
}

///|
fn is_cjk_friendly_delim(
  delim : Byte,
  run_len : Int,
  options : Options,
) -> Bool {
  // two-tilde strikethrough, subscripts, superscript, and highlighter don't use flanking
  delim == b'*' ||
  delim == b'_' ||
  (delim == b'~' && run_len == 1 && !options.contains(enable_subscript()))
}

///|
priv struct CjkFriendlyDelimiterRunFlanking {
  can_open : Bool
  can_close : Bool
}

///|
fn cjk_friendly_delim_run_flanking(
  prev_char : Char,
  get_prev_prev_char : () -> Char?,
  next_char : Char,
  delim : Byte,
) -> CjkFriendlyDelimiterRunFlanking {
  if delim == b'_' || delim == b'~' {
    return cjk_friendly_underscore_delim_run_flanking(
      prev_char, get_prev_prev_char, next_char,
    )
  }

  let prev_sequence = classify_preceding_cjk_friendly_sequence(
    prev_char, get_prev_prev_char,
  )
  let before_cjk_or_ivs = prev_sequence.is_cjk ||
    prev_sequence.is_ideographic_variation_selector
  let before_space_or_punctuation = prev_char.is_whitespace() ||
    prev_sequence.is_punctuation

  let after_cjk = next_char.is_cjk_character()
  let after_punctuation = is_punctuation(next_char.to_int())
  let after_space_or_punctuation = next_char.is_whitespace() ||
    after_punctuation

  let open = !next_char.is_whitespace() &&
    (
      !after_punctuation ||
      before_space_or_punctuation ||
      before_cjk_or_ivs ||
      after_cjk ||
      next_char == '*' ||
      next_char == '_'
    )
  let close = !prev_char.is_whitespace() &&
    (
      !prev_sequence.is_punctuation ||
      after_space_or_punctuation ||
      prev_sequence.is_cjk ||
      after_cjk ||
      prev_char == '*' ||
      prev_char == '_'
    )

  { can_open: open, can_close: close }
}

///|
fn cjk_friendly_underscore_delim_run_flanking(
  prev_char : Char,
  get_prev_prev_char : () -> Char?,
  next_char : Char,
) -> CjkFriendlyDelimiterRunFlanking {
  let before_punctuation = is_preceding_cjk_friendly_punctuation(
    prev_char, get_prev_prev_char,
  )
  let before_space_or_punctuation = prev_char.is_whitespace() ||
    before_punctuation

  let after_punctuation = is_punctuation(next_char.to_int())
  let after_space_or_punctuation = next_char.is_whitespace() ||
    after_punctuation

  let open = !next_char.is_whitespace() &&
    (
      !after_punctuation ||
      before_space_or_punctuation ||
      next_char == '*' ||
      next_char == '_'
    )
  let close = !prev_char.is_whitespace() &&
    (
      !before_punctuation ||
      after_space_or_punctuation ||
      prev_char == '*' ||
      prev_char == '_'
    )

  {
    can_open: open && (before_space_or_punctuation || !close),
    can_close: close && (after_space_or_punctuation || !open),
  }
}

///|
fn is_alphanumeric_char(c : Char) -> Bool {
  c.is_ascii_alphabetic() || c.is_ascii_digit()
}

///|
/// Decodes the char starting at byte offset `ix`. Returns None if out of range.
fn decode_char(s : Bytes, ix : Int) -> Char? {
  guard ix < s.length() else { return None }
  let b0 = s.unsafe_get(ix).to_int()
  let len = if b0 < 0x80 {
    1
  } else if b0 < 0xE0 {
    2
  } else if b0 < 0xF0 {
    3
  } else {
    4
  }
  guard ix + len <= s.length() else { return None }
  let sub = s.view(start=ix, end=ix + len)
  @utf8.decode_lossy(sub).get_char(0)
}

///|
/// Returns the byte index of the char before byte offset `ix`.
fn prev_char_ix(s : Bytes, ix : Int) -> Int {
  guard ix > 0 else { return 0 }
  let mut i = ix - 1
  while i > 0 && (s.unsafe_get(i).to_int() & 0xC0) == 0x80 {
    i -= 1
  }
  i
}

///|
fn ends_with_pipe(s : Bytes, ix : Int) -> Bool {
  let prefix = s.view(start=0, end=ix)
  prefix.has_suffix(b"|".view()) && !prefix.has_suffix(b"\\|".view())
}

///|
fn special_bytes(options : Options) -> Array[Bool] {
  let bytes : Array[Bool] = Array::make(256, false)
  let standard_bytes = [
    b'\n', b'\r', b'*', b'_', b'&', b'\\', b'[', b']', b'<', b'!', b'`', b'\x00',
  ]

  for byte in standard_bytes {
    bytes[byte.to_int()] = true
  }
  if options.contains(enable_tables()) {
    bytes[b'|'.to_int()] = true
  }
  if options.contains(enable_strikethrough()) ||
    options.contains(enable_subscript()) {
    bytes[b'~'.to_int()] = true
  }
  if options.contains(enable_superscript()) {
    bytes[b'^'.to_int()] = true
  }
  if options.contains(enable_highlight()) {
    bytes[b'='.to_int()] = true
  }
  if options.contains(enable_math()) {
    bytes[b'$'.to_int()] = true
    bytes[b'{'.to_int()] = true
    bytes[b'}'.to_int()] = true
  }
  if options.contains(enable_smart_punctuation()) {
    for byte in [b'.', b'-', b'"', b'\''] {
      bytes[byte.to_int()] = true
    }
  }

  bytes
}

///|
/// Computes the number of header columns in a table line.
fn count_header_cols(
  bytes : BytesView,
  pipes : Int,
  start : Int,
  last_pipe_ix : Int,
) -> Int {
  let mut pipes = pipes
  let mut start = start
  // was first pipe preceded by whitespace? if so, subtract one
  start += scan_whitespace_no_nl(bytes.view(start~))
  if bytes.length() > start && bytes.unsafe_get(start) == b'|' {
    pipes -= 1
  }

  // was last pipe followed by whitespace? if so, sub one
  if scan_blank_line(bytes.view(start=last_pipe_ix + 1)) is Some(_) {
    pipes
  } else {
    pipes + 1
  }
}

///|
/// Split the usual heading content range and the content inside the trailing attribute block.
fn extract_attribute_block_content_from_header_text(
  heading : BytesView,
) -> (Int, (Int, Int)?) {
  let heading_len = heading.length()
  let mut ix = heading_len
  ix -= scan_rev_while(heading, fn(b) {
    b == b'\n' || b == b'\r' || b == b' ' || b == b'\t'
  })
  guard ix != 0 else { return (heading_len, None) }

  let attr_block_close = ix - 1
  if heading.length() <= attr_block_close ||
    heading.unsafe_get(attr_block_close) != b'}' {
    // The last character is not `}`. No attribute blocks found.
    return (heading_len, None)
  }
  // move cursor before the closing right brace (`}`)
  ix -= 1

  ix -= scan_rev_while(heading.view(start=0, end=ix), fn(b) {
    !(b == b'{' ||
    b == b'}' ||
    b == b'<' ||
    b == b'>' ||
    b == b'\\' ||
    b == b'\n' ||
    b == b'\r')
  })
  if ix == 0 {
    // `{` is not found. No attribute blocks available.
    return (heading_len, None)
  }
  let attr_block_open = ix - 1
  if heading.unsafe_get(attr_block_open) != b'{' {
    // `{` is not found. No attribute blocks available.
    return (heading_len, None)
  }

  (attr_block_open, Some((ix, attr_block_close)))
}

///|
/// Parses an attribute block content, such as `.class1 #id .class2`.
fn parse_inside_attribute_block(
  inside_attr_block : String,
) -> HeadingAttributes? {
  let mut id : String? = None
  let classes : Array[String] = []
  let attrs : Array[(String, String?)] = []

  for attr in split_ascii_whitespace(inside_attr_block) {
    let attr_bytes = @utf8.encode(attr)
    if attr_bytes.length() > 1 {
      let first_byte = attr_bytes.unsafe_get(0)
      if first_byte == b'#' {
        id = Some(
          replace_nuls(
            @utf8.decode_lossy(
              attr_bytes.view(start=1, end=attr_bytes.length()),
            ),
          ),
        )
      } else if first_byte == b'.' {
        classes.push(
          replace_nuls(
            @utf8.decode_lossy(
              attr_bytes.view(start=1, end=attr_bytes.length()),
            ),
          ),
        )
      } else {
        match attr.split_once("=") {
          Some((key, value)) =>
            attrs.push(
              (
                replace_nuls(key.to_owned()),
                Some(replace_nuls(value.to_owned())),
              ),
            )
          None => attrs.push((replace_nuls(attr), None))
        }
      }
    }
  }

  Some(HeadingAttributes::{ id, classes, attrs })
}

///|
fn split_ascii_whitespace(s : String) -> Array[String] {
  let bytes = @utf8.encode(s)
  let result : Array[String] = []
  let mut start = -1
  for i in 0..<=bytes.length() {
    if i < bytes.length() && !bytes.unsafe_get(i).is_ascii_whitespace_std() {
      if start < 0 {
        start = i
      }
    } else if start >= 0 {
      result.push(@utf8.decode_lossy(bytes.view(start~, end=i)))
      start = -1
    }
  }
  result
}

///|
/// Rust std `u8::is_ascii_whitespace` (used by `str::split_ascii_whitespace`):
/// tab, LF, FF, CR, space — notably NOT vertical tab.
fn Byte::is_ascii_whitespace_std(c : Byte) -> Bool {
  let b = c.to_int()
  b == 0x09 || b == 0x0a || b == 0x0c || b == 0x0d || b == 0x20
}

///|
fn is_active_mode(mode : TableParseMode) -> Bool {
  mode is Active
}

///|
fn is_scan_mode(mode : TableParseMode) -> Bool {
  mode is Scan
}