///|
fn scan_emails(chars : Array[Char], preview : Bool) -> Array[Finding] {
  let out : Array[Finding] = []
  for at = 1; at + 3 < chars.length(); at = at + 1 {
    if chars[at] != '@' {
      continue
    }
    let mut start = at - 1
    while start > 0 &&
          (
            is_ascii_alnum(chars[start - 1]) ||
            chars[start - 1] == '.' ||
            chars[start - 1] == '_' ||
            chars[start - 1] == '+' ||
            chars[start - 1] == '-'
          ) {
      start = start - 1
    }
    let mut end = at + 1
    let mut dot = false
    while end < chars.length() &&
          (is_ascii_alnum(chars[end]) || chars[end] == '.' || chars[end] == '-') {
      if chars[end] == '.' {
        dot = true
      }
      end = end + 1
    }
    if dot &&
      at - start >= 1 &&
      end - at >= 4 &&
      is_ascii_letter(chars[end - 1]) {
      out.push(
        Finding::new(
          "email",
          Email,
          Medium,
          start,
          end,
          92,
          safe_preview(chars, start, end, preview),
          "Email address",
        ),
      )
    }
  }
  out
}

///|
fn scan_phone_numbers(chars : Array[Char], preview : Bool) -> Array[Finding] {
  let out : Array[Finding] = []
  let mut i = 0
  while i < chars.length() {
    if !is_ascii_digit(chars[i]) {
      i = i + 1
      continue
    }
    let start = i
    while i < chars.length() && is_ascii_digit(chars[i]) {
      i = i + 1
    }
    let length = i - start
    if length == 11 &&
      chars[start] == '1' &&
      chars[start + 1] >= '3' &&
      chars[start + 1] <= '9' {
      out.push(
        Finding::new(
          "cn-phone",
          Phone,
          High,
          start,
          i,
          96,
          safe_preview(chars, start, i, preview),
          "Mainland China mobile number",
        ),
      )
    }
  }
  out
}

///|
fn identity_checksum(chars : Array[Char], start : Int) -> Bool {
  let weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
  let check = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
  let mut sum = 0
  for i = 0; i < 17; i = i + 1 {
    if !is_ascii_digit(chars[start + i]) {
      return false
    }
    sum = sum + (chars[start + i].to_int() - '0'.to_int()) * weights[i]
  }
  let actual = chars[start + 17]
  actual == check[sum % 11] || (actual == 'x' && check[sum % 11] == 'X')
}

///|
fn scan_identities(chars : Array[Char], preview : Bool) -> Array[Finding] {
  let out : Array[Finding] = []
  for start = 0; start + 18 <= chars.length(); start = start + 1 {
    if start > 0 && is_ascii_alnum(chars[start - 1]) {
      continue
    }
    if start + 18 < chars.length() && is_ascii_alnum(chars[start + 18]) {
      continue
    }
    if identity_checksum(chars, start) {
      out.push(
        Finding::new(
          "cn-identity",
          Identity,
          Critical,
          start,
          start + 18,
          99,
          safe_preview(chars, start, start + 18, preview),
          "Mainland China identity number",
        ),
      )
    }
  }
  out
}

///|
fn luhn_valid(chars : Array[Char], start : Int, end : Int) -> Bool {
  let mut sum = 0
  let mut alternate = false
  let mut i = end - 1
  while i >= start {
    if !is_ascii_digit(chars[i]) {
      return false
    }
    let mut digit = chars[i].to_int() - '0'.to_int()
    if alternate {
      digit = digit * 2
      if digit > 9 {
        digit = digit - 9
      }
    }
    sum = sum + digit
    alternate = !alternate
    i = i - 1
  }
  sum % 10 == 0
}

///|
fn scan_bank_cards(chars : Array[Char], preview : Bool) -> Array[Finding] {
  let out : Array[Finding] = []
  let mut i = 0
  while i < chars.length() {
    if !is_ascii_digit(chars[i]) {
      i = i + 1
      continue
    }
    let start = i
    while i < chars.length() && is_ascii_digit(chars[i]) {
      i = i + 1
    }
    let length = i - start
    if length >= 13 && length <= 19 && luhn_valid(chars, start, i) {
      out.push(
        Finding::new(
          "bank-card",
          BankCard,
          Critical,
          start,
          i,
          94,
          safe_preview(chars, start, i, preview),
          "Payment card number passing Luhn checksum",
        ),
      )
    }
  }
  out
}

///|
fn parse_octet(chars : Array[Char], start : Int, end : Int) -> Int {
  if end <= start || end - start > 3 {
    return -1
  }
  let mut value = 0
  for i = start; i < end; i = i + 1 {
    if !is_ascii_digit(chars[i]) {
      return -1
    }
    value = value * 10 + chars[i].to_int() - '0'.to_int()
  }
  if value > 255 {
    -1
  } else {
    value
  }
}

///|
fn scan_ipv4(chars : Array[Char], preview : Bool) -> Array[Finding] {
  let out : Array[Finding] = []
  let mut i = 0
  while i < chars.length() {
    if !is_ascii_digit(chars[i]) {
      i = i + 1
      continue
    }
    let start = i
    if start > 0 &&
      (is_ascii_digit(chars[start - 1]) || chars[start - 1] == '.') {
      i = i + 1
      continue
    }
    let mut parts = 0
    let mut valid = true
    while parts < 4 && valid {
      let part_start = i
      while i < chars.length() && is_ascii_digit(chars[i]) {
        i = i + 1
      }
      if parse_octet(chars, part_start, i) < 0 {
        valid = false
      }
      parts = parts + 1
      if parts < 4 {
        if i >= chars.length() || chars[i] != '.' {
          valid = false
        } else {
          i = i + 1
        }
      }
    }
    if valid && parts == 4 && (i == chars.length() || !is_ascii_digit(chars[i])) {
      out.push(
        Finding::new(
          "ipv4",
          IPv4,
          Low,
          start,
          i,
          90,
          safe_preview(chars, start, i, preview),
          "IPv4 address",
        ),
      )
    } else {
      i = start + 1
    }
  }
  out
}

///|
fn scan_jwts(chars : Array[Char], preview : Bool) -> Array[Finding] {
  let out : Array[Finding] = []
  let header = "eyJ".to_array()
  let mut from = 0
  while from < chars.length() {
    let start = index_of_from(chars, header, from)
    if start < 0 {
      break
    }
    let mut end = start
    let mut dots = 0
    while end < chars.length() &&
          (is_base64ish(chars[end]) || chars[end] == '.') {
      if chars[end] == '.' {
        dots = dots + 1
      }
      end = end + 1
    }
    if dots == 2 && end - start >= 30 {
      out.push(
        Finding::new(
          "jwt",
          Jwt,
          Critical,
          start,
          end,
          97,
          safe_preview(chars, start, end, preview),
          "JSON Web Token",
        ),
      )
    }
    from = start + 3
  }
  out
}

///|
fn scan_structured(chars : Array[Char], preview : Bool) -> Array[Finding] {
  let out : Array[Finding] = []
  for finding in scan_emails(chars, preview) {
    out.push(finding)
  }
  for finding in scan_phone_numbers(chars, preview) {
    out.push(finding)
  }
  for finding in scan_identities(chars, preview) {
    out.push(finding)
  }
  for finding in scan_bank_cards(chars, preview) {
    out.push(finding)
  }
  for finding in scan_ipv4(chars, preview) {
    out.push(finding)
  }
  for finding in scan_jwts(chars, preview) {
    out.push(finding)
  }
  out
}