///|
fn alphabet_class_count(chars : Array[Char], start : Int, end : Int) -> Int {
  let mut lower = false
  let mut upper = false
  let mut digit = false
  let mut symbol = false
  for i = start; i < end; i = i + 1 {
    let c = chars[i]
    if c >= 'a' && c <= 'z' {
      lower = true
    } else if c >= 'A' && c <= 'Z' {
      upper = true
    } else if is_ascii_digit(c) {
      digit = true
    } else {
      symbol = true
    }
  }
  let mut count = 0
  if lower {
    count = count + 1
  }
  if upper {
    count = count + 1
  }
  if digit {
    count = count + 1
  }
  if symbol {
    count = count + 1
  }
  count
}

///|
fn unique_ascii_count(chars : Array[Char], start : Int, end : Int) -> Int {
  let seen = Array::make(128, false)
  let mut count = 0
  for i = start; i < end; i = i + 1 {
    let code = chars[i].to_int()
    if code >= 0 && code < 128 && !seen[code] {
      seen[code] = true
      count = count + 1
    }
  }
  count
}

///|
fn entropy_score_milli(chars : Array[Char], start : Int, end : Int) -> Int {
  let length = end - start
  if length <= 0 {
    return 0
  }
  let unique = unique_ascii_count(chars, start, end)
  let classes = alphabet_class_count(chars, start, end)
  let uniqueness = unique * 1000 / length
  let length_bonus = if length >= 48 {
    850
  } else if length >= 32 {
    650
  } else if length >= 24 {
    450
  } else {
    200
  }
  uniqueness * 3 + classes * 240 + length_bonus
}

///|
fn entropy_token_char(c : Char) -> Bool {
  is_ascii_alnum(c) ||
  c == '+' ||
  c == '/' ||
  c == '_' ||
  c == '-' ||
  c == '.' ||
  c == '~'
}

///|
fn obvious_natural_word(chars : Array[Char], start : Int, end : Int) -> Bool {
  let mut only_letters = true
  let mut vowels = 0
  for i = start; i < end; i = i + 1 {
    let c = chars[i]
    if !is_ascii_letter(c) {
      only_letters = false
    }
    if c == 'a' ||
      c == 'e' ||
      c == 'i' ||
      c == 'o' ||
      c == 'u' ||
      c == 'A' ||
      c == 'E' ||
      c == 'I' ||
      c == 'O' ||
      c == 'U' {
      vowels = vowels + 1
    }
  }
  only_letters && vowels * 5 >= end - start
}

///|
fn scan_entropy(chars : Array[Char], options : ScanOptions) -> Array[Finding] {
  let out : Array[Finding] = []
  let mut i = 0
  while i < chars.length() {
    if !entropy_token_char(chars[i]) {
      i = i + 1
      continue
    }
    let start = i
    while i < chars.length() && entropy_token_char(chars[i]) {
      i = i + 1
    }
    let length = i - start
    if length >= options.minimum_entropy_length &&
      !obvious_natural_word(chars, start, i) {
      let score = entropy_score_milli(chars, start, i)
      if score >= options.entropy_threshold_milli {
        let confidence = if score >= 5000 {
          95
        } else if score >= 4400 {
          86
        } else {
          75
        }
        out.push(
          Finding::new(
            "high-entropy",
            HighEntropy,
            High,
            start,
            i,
            confidence,
            safe_preview(chars, start, i, options.include_preview),
            "High-entropy token candidate",
          ),
        )
      }
    }
  }
  out
}