// Links written bare: `http://example.com`, `www.example.com` and
// `someone@example.com`. The first two are recognised while scanning, the last
// one by a pass over the text nodes once emphasis has been settled, because an
// address is not introduced by any character of its own.

///|
/// Whether a character may appear in a host name.
fn is_valid_hostchar(c : Char) -> Bool {
  !is_unicode_space(c) && !is_unicode_punctuation_only(c)
}

///|
/// How much of the text from `start` is a host name, counting the segments and
/// refusing a host whose last two segments hold an underscore.
fn check_domain(
  text : StringView,
  start : Int32,
  size : Int32,
  allow_short : Bool,
) -> Int32 {
  let mut i = 1
  let mut segments = 0
  let mut underscore_before = 0
  let mut underscore_last = 0
  while i + 1 < size {
    if cu(text, start + i) == 0x5C && i + 2 < size {
      i = i + 1
    }
    let c = cu(text, start + i)
    if c == 0x5F {
      underscore_last = underscore_last + 1
    } else if c == 0x2E {
      underscore_before = underscore_last
      underscore_last = 0
      segments = segments + 1
    } else if !is_valid_hostchar(char_at(text, start + i)) && c != 0x2D {
      break
    }
    i = i + 1
  }
  if (underscore_before > 0 || underscore_last > 0) && segments <= 10 {
    return 0
  }
  if allow_short {
    i
  } else if segments > 0 {
    i
  } else {
    0
  }
}

///|
/// Cut the trailing punctuation a sentence leaves on a bare link, keeping the
/// parentheses balanced.
fn autolink_delim(text : StringView, start : Int32, limit : Int32) -> Int32 {
  let mut link_end = limit
  let mut opening = 0
  let mut closing = 0
  let mut i = 0
  while i < link_end {
    let c = cu(text, start + i)
    if c == 0x3C {
      link_end = i
      break
    } else if c == 0x28 {
      opening = opening + 1
    } else if c == 0x29 {
      closing = closing + 1
    }
    i = i + 1
  }
  while link_end > 0 {
    let c = cu(text, start + link_end - 1)
    if c == 0x29 {
      if closing <= opening {
        return link_end
      }
      closing = closing - 1
      link_end = link_end - 1
    } else if c == 0x3F ||
      c == 0x21 ||
      c == 0x2E ||
      c == 0x2C ||
      c == 0x3A ||
      c == 0x2A ||
      c == 0x5F ||
      c == 0x7E ||
      c == 0x27 ||
      c == 0x22 {
      link_end = link_end - 1
    } else if c == 0x3B {
      let mut candidate = link_end - 2
      while candidate > 0 && is_alpha(cu(text, start + candidate)) {
        candidate = candidate - 1
      }
      if candidate < link_end - 2 && cu(text, start + candidate) == 0x26 {
        link_end = candidate
      } else {
        link_end = link_end - 1
      }
    } else {
      return link_end
    }
  }
  link_end
}

///|
/// `http://`, `https://` or `ftp://` right before the `:` being looked at.
fn is_safe_scheme(text : StringView, from : Int32, to : Int32) -> Bool {
  let length = to - from
  for scheme in ["http://", "https://", "ftp://"] {
    let scheme_view : StringView = scheme[:]
    if length == scheme_view.length() &&
      text.clamped_view(start=from, end=to).equal_ignore_ascii_case(scheme_view) {
      return is_valid_hostchar(char_at(text, to))
    }
  }
  false
}

///|
/// Recognise `scheme://host` starting at the `:` under the cursor.
fn Subject::try_url_autolink(self : Subject) -> Bool {
  let n = self.input.length()
  if self.pos + 3 > n ||
    self.peek(self.pos + 1) != 0x2F ||
    self.peek(self.pos + 2) != 0x2F {
    return false
  }
  let text = self.input[:]
  let mut rewind = 0
  while rewind < self.pos && is_alpha(self.peek(self.pos - rewind - 1)) {
    rewind = rewind + 1
  }
  let start = self.pos - rewind
  if !is_safe_scheme(text, start, self.pos + 3) {
    return false
  }
  let domain = check_domain(text, self.pos + 3, n - (self.pos + 3), true)
  if domain == 0 {
    return false
  }
  let mut link_end = 3 + domain
  while self.pos + link_end < n &&
        !is_unicode_space(char_at(text, self.pos + link_end)) &&
        cu(text, self.pos + link_end) != 0x3C {
    link_end = link_end + 1
  }
  link_end = autolink_delim(text, self.pos, link_end)
  if link_end == 0 {
    return false
  }
  let destination = text
    .clamped_view(start~, end=self.pos + link_end)
    .to_owned()
  self.unput(rewind)
  let link = self.tree.make(LinkNode, destination, destination.length(), "")
  self.tree.append_child(self.root, link)
  let label = self.tree.make(TextNode, destination, destination.length(), "")
  self.tree.append_child(link, label)
  self.pos = self.pos + link_end
  true
}

///|
/// Recognise `www.host` starting at the `w` under the cursor.
fn Subject::try_www_autolink(self : Subject) -> Bool {
  let n = self.input.length()
  if self.pos > 0 {
    let before = char_before(self.input[:], self.pos)
    if !"*_~("[:].contains_char(before) && !is_unicode_space(before) {
      return false
    }
  }
  if n - self.pos < 4 {
    return false
  }
  let text = self.input[:]
  if !text
    .clamped_view(start=self.pos, end=self.pos + 4)
    .equal_ignore_ascii_case("www."[:]) {
    return false
  }
  let domain = check_domain(text, self.pos, n - self.pos, false)
  if domain == 0 {
    return false
  }
  let mut link_end = domain
  while self.pos + link_end < n &&
        !is_unicode_space(char_at(text, self.pos + link_end)) &&
        cu(text, self.pos + link_end) != 0x3C {
    link_end = link_end + 1
  }
  link_end = autolink_delim(text, self.pos, link_end)
  if link_end == 0 {
    return false
  }
  let written = text
    .clamped_view(start=self.pos, end=self.pos + link_end)
    .to_owned()
  let destination = "http://" + written
  let link = self.tree.make(LinkNode, destination, destination.length(), "")
  self.tree.append_child(self.root, link)
  let label = self.tree.make(TextNode, written, written.length(), "")
  self.tree.append_child(link, label)
  self.pos = self.pos + link_end
  true
}

///|
/// Take `count` characters back out of the text already written, which is what
/// a scheme recognised late needs.
fn Subject::unput(self : Subject, count : Int32) -> Unit {
  let mut remaining = count
  while remaining > 0 {
    let last = self.tree.nodes[self.root].last_child
    if last < 0 || self.tree.nodes[last].kind != TextNode {
      return
    }
    let length = self.tree.nodes[last].text_len
    if length <= remaining {
      remaining = remaining - length
      self.tree.detach(last)
    } else {
      self.tree.nodes[last].text_len = length - remaining
      remaining = 0
    }
  }
}

///|
/// Merge the runs of text that ended up next to each other, so that the email
/// pass and the reader both see whole words.
fn consolidate_text(tree : InlineTree, parent : Int32) -> Unit {
  let mut node = tree.nodes[parent].first_child
  while node >= 0 {
    let next = tree.nodes[node].next
    if tree.nodes[node].kind == TextNode {
      if tree.nodes[node].text_len == 0 {
        tree.detach(node)
      } else {
        let mut sibling = tree.nodes[node].next
        while sibling >= 0 && tree.nodes[sibling].kind == TextNode {
          let following = tree.nodes[sibling].next
          let merged = tree.literal(node).to_owned() +
            tree.literal(sibling).to_owned()
          tree.nodes[node].text = merged
          tree.nodes[node].text_len = merged.length()
          tree.detach(sibling)
          sibling = following
        }
      }
    } else {
      consolidate_text(tree, node)
    }
    node = next
  }
}

///|
/// Where an address starts: alphanumerics and the few punctuation marks an
/// address may hold, or the `mailto:` and `xmpp:` protocols in front of them.
fn email_local_char(c : Int32) -> Bool {
  is_alnum(c) || c == 0x2E || c == 0x2B || c == 0x2D || c == 0x5F
}

///|
/// Find the addresses in a text node and turn them into links.
fn Subject::postprocess_emails(self : Subject, parent : Int32) -> Unit {
  let mut node = self.tree.nodes[parent].first_child
  while node >= 0 {
    let next = self.tree.nodes[node].next
    let kind = self.tree.nodes[node].kind
    if kind == LinkNode {
      ()
    } else if kind == TextNode {
      self.split_emails(node)
    } else if kind == ImageNode ||
      kind == CodeNode ||
      kind == HtmlNode ||
      kind == SoftBreakNode ||
      kind == HardBreakNode {
      ()
    } else {
      self.postprocess_emails(node)
    }
    node = next
  }
}

///|
/// One address found in a text node.
priv struct EmailMatch {
  start : Int32
  end : Int32
  auto_mailto : Bool
}

///|
/// Scan the text for addresses and replace them with links, keeping the text
/// around them.
fn Subject::split_emails(self : Subject, node : Int32) -> Unit {
  let text = self.tree.literal(node).to_owned()
  let n = text.length()
  let matches : Array[EmailMatch] = []
  let mut offset = 0
  while offset < n {
    // The next `@`.
    let mut at = offset
    while at < n && cu(text[:], at) != 0x40 {
      at = at + 1
    }
    if at >= n {
      break
    }
    // The local part, and any protocol in front of it.
    let mut start = at
    while start > 0 && email_local_char(cu(text[:], start - 1)) {
      start = start - 1
    }
    let mut auto_mailto = true
    let mut is_xmpp = false
    let rest = text.clamped_view(start=0, end=start)
    if rest.has_suffix("mailto:"[:]) && protocol_boundary(rest, 7) {
      auto_mailto = false
      start = start - 7
    } else if rest.has_suffix("xmpp:"[:]) && protocol_boundary(rest, 5) {
      auto_mailto = false
      is_xmpp = true
      start = start - 5
    }
    if start == at {
      offset = at + 1
      continue
    }
    // The domain.
    let mut link_end = 1
    let mut segments = 0
    let mut retry = false
    while at + link_end < n {
      let c = cu(text[:], at + link_end)
      if is_alnum(c) {
        link_end = link_end + 1
      } else if c == 0x40 {
        offset = at + 1
        at = at + 1
        retry = true
        break
      } else if c == 0x2E &&
        at + link_end + 1 < n &&
        is_alnum(cu(text[:], at + link_end + 1)) {
        segments = segments + 1
        link_end = link_end + 1
      } else if c == 0x2F && is_xmpp {
        link_end = link_end + 1
      } else if c == 0x2D || c == 0x5F {
        link_end = link_end + 1
      } else {
        break
      }
    }
    if retry {
      continue
    }
    let last = cu(text[:], at + link_end - 1)
    if link_end < 2 || segments == 0 || !(is_alpha(last) || last == 0x2E) {
      offset = at + link_end
      continue
    }
    let end = autolink_delim(text[:], at, link_end)
    if end == 0 {
      offset = at + 1
      continue
    }
    matches.push({ start, end: at + end, auto_mailto, })
    offset = at + end
  }
  if matches.length() == 0 {
    return
  }
  let parent = self.tree.nodes[node].parent
  let _ = parent
  let mut cursor = 0
  let mut anchor = node
  for match_ in matches {
    if match_.start > cursor {
      let piece_text = text
        .clamped_view(start=cursor, end=match_.start)
        .to_owned()
      let piece = self.tree.make(TextNode, piece_text, piece_text.length(), "")
      self.tree.insert_after(anchor, piece)
      anchor = piece
    }
    let written = text
      .clamped_view(start=match_.start, end=match_.end)
      .to_owned()
    let destination = if match_.auto_mailto {
      "mailto:" + written
    } else {
      written
    }
    let link = self.tree.make(LinkNode, destination, destination.length(), "")
    self.tree.insert_after(anchor, link)
    let label = self.tree.make(TextNode, written, written.length(), "")
    self.tree.append_child(link, label)
    anchor = link
    cursor = match_.end
  }
  if cursor < n {
    let piece = self.tree.make(
      TextNode,
      text.clamped_view(start=cursor, end=n).to_owned(),
      n - cursor,
      "",
    )
    self.tree.insert_after(anchor, piece)
  }
  self.tree.detach(node)
}

///|
/// Whether a protocol is a word of its own, not the tail of a longer one.
fn protocol_boundary(text : StringView, length : Int32) -> Bool {
  let before = text.length() - length
  before == 0 || !is_alnum(cu(text, before - 1))
}