 
   # utilities --------------------------------------------------------------------
 
-  cardinality: (password) ->
-    [lower, upper, digits, symbols, latin1_symbols, latin1_letters] = (
-      false for i in [0...6]
-    )
-    unicode_codepoints = []
-    for chr in password.split('')
-      ord = chr.charCodeAt(0)
-      if 0x30 <= ord <= 0x39
-        digits = true
-      else if 0x41 <= ord <= 0x5a
-        upper = true
-      else if 0x61 <= ord <= 0x7a
-        lower = true
-      else if ord <= 0x7f
-        symbols = true
-      else if 0x80 <= ord <= 0xBF
-        latin1_symbols = true
-      else if 0xC0 <= ord <= 0xFF
-        latin1_letters = true
-      else if ord > 0xFF
-        unicode_codepoints.push ord
-    c = 0
-    c += 10 if digits
-    c += 26 if upper
-    c += 26 if lower
-    c += 33 if symbols
-    c += 64 if latin1_symbols
-    c += 64 if latin1_letters
-    if unicode_codepoints.length
-      min_cp = max_cp = unicode_codepoints[0]
-      for cp in unicode_codepoints[1..]
-        min_cp = cp if cp < min_cp
-        max_cp = cp if cp > max_cp
-      # if the range between unicode codepoints is small,
-      # assume one extra alphabet is in use (eg cyrillic, korean) and add a ballpark +40
-      #
-      # if the range is large, be very conservative and add +100 instead of the range.
-      # (codepoint distance between chinese chars can be many thousand, for example,
-      # but that cardinality boost won't be justified if the characters are common.)
-      range = max_cp - min_cp + 1
-      range = 40 if range < 40
-      range = 100 if range > 100
-      c += range
-    c
-
 module.exports = scoring

  # search --- most guessable match sequence -------------------------------------
  # ------------------------------------------------------------------------------
  #
  # takes a list of overlapping matches, returns the non-overlapping sublist with
  # minimum guesses. O(nm) dp alg for length-n password with m candidate matches.
  #
  # the optimal "minimum guesses" sublist is here defined to be the sublist that
  # minimizes:
  #
  #    product(m.guesses for m in sequence) * factorial(sequence.length)
  #
  # the factorial term is the number of ways to order n patterns. it serves as a
  # light penalty for longer sequences.
  #
  # for example, consider a sequence that is date-repeat-dictionary.
  #  - an attacker would need to try other date-repeat-dictionary combinations,
  #    hence the product term.
  #  - an attacker would need to try repeat-date-dictionary, dictionary-repeat-date,
  #    ..., hence the factorial term.
  #  - an attacker would also likely try length-1 and length-2 sequences before
  #    length-3. those terms tend to be small in comparison and are excluded
  #    for simplicity.
  #
  # ------------------------------------------------------------------------------

  most_guessable_match_sequence: (password, matches) ->
    # at index k, the product of guesses of the optimal sequence covering password[0..k]
    optimal_product = []

    # at index k, the number of matches in the optimal sequence covering password[0..k].
    # used to calculate the factorial term.
    optimal_sequence_length = []

    # if the optimal sequence ends in a bruteforce match, this records those bruteforce
    # characters, otherwise ''
    ending_bruteforce_chars = ''

    # at index k, the final match (match.j == k) in said optimal sequence.
    backpointers = []

    make_bruteforce_match = (i, j) =>
      cardinality = @cardinality password[i..j]
      guesses = Math.pow cardinality, j - i + 1
      # return object:
      pattern: 'bruteforce'
      i: i
      j: j
      token: password[i..j]
      guesses: guesses
      guesses_log10: @log10(guesses)
      cardinality: cardinality
      length: j - i + 1

    for k in [0...password.length]
      # starting scenario to beat:
      # adding a brute-force character to the sequence with min guesses at k-1.
      ending_bruteforce_chars += password.charAt(k)

      l = ending_bruteforce_chars.length
      optimal_product[k] = Math.pow @cardinality(ending_bruteforce_chars), l
      optimal_sequence_length[k] = 1
      if k - l >= 0
        optimal_product[k] *= optimal_product[k - l]
        optimal_sequence_length[k] += optimal_sequence_length[k - l]

      backpointers[k] = make_bruteforce_match(k - l + 1, k)

      minimum_guesses = optimal_product[k] * @factorial optimal_sequence_length[k]
      for match in matches when match.j == k
        [i, j] = [match.i, match.j]
        candidate_product = @estimate_guesses match
        candidate_length = 1
        if i - 1 >= 0
          candidate_product *= optimal_product[i - 1]
          candidate_length += optimal_sequence_length[i - 1]
        candidate_guesses = candidate_product * @factorial candidate_length

        if candidate_guesses < minimum_guesses
          minimum_guesses = candidate_guesses
          ending_bruteforce_chars = ''
          optimal_product[k] = candidate_product
          optimal_sequence_length[k] = candidate_length
          backpointers[k] = match

    # walk backwards and decode the best sequence
    match_sequence = []
    k = password.length - 1
    while k >= 0
      match = backpointers[k]
      match_sequence.push match
      k = match.i - 1
    match_sequence.reverse()

    if password.length > 0
      sequence_length_multiplier = @factorial optimal_sequence_length[password.length - 1]
      guesses = optimal_product[password.length - 1] * sequence_length_multiplier
    else
      sequence_length_multiplier = 1
      guesses = 1

    # final result object
    password: password
    guesses: guesses
    guesses_log10: @log10 guesses
    sequence_product_log10: @log10(optimal_product[password.length - 1])
    sequence_length_multiplier_log10: @log10 sequence_length_multiplier
    sequence: match_sequence

  # ------------------------------------------------------------------------------

  estimate_guesses: (match) ->
    return match.guesses if match.guesses? # a match's guess estimate doesn't change. cache it.
    estimation_functions =
      bruteforce: @bruteforce_guesses
      dictionary: @dictionary_guesses
      spatial:    @spatial_guesses
      repeat:     @repeat_guesses
      sequence:   @sequence_guesses
      regex:      @regex_guesses
      date:       @date_guesses
    guesses = estimation_functions[match.pattern].call this, match
    if match.token.length == 1
      min_guesses = MIN_SUBMATCH_GUESSES_SINGLE_CHAR
    else
      min_guesses = MIN_SUBMATCH_GUESSES_MULTI_CHAR
    match.guesses = Math.max guesses, min_guesses
    match.guesses_log10 = @log10 match.guesses
    match.guesses

  bruteforce_guesses: (match) ->
    1 + Math.pow BRUTEFORCE_CARDINALITY, match.token.length

  repeat_guesses: (match) ->
    match.base_guesses * match.repeat_count
