import utils from '../../scoring/utils'
import zxcvbnOptions from '../../Options'
import { LooseObject, MatchEstimated, MatchExtended } from '../../types'

interface EstimatePossiblePatternsOptions {
  token: string
  graph: string
  turns: number
}

const calcAverageDegree = (graph: LooseObject) => {
  let average = 0
  Object.keys(graph).forEach((key) => {
    const neighbors = graph[key]
    average += neighbors.filter((entry: string) => !!entry).length
  })
  average /= Object.entries(graph).length
  return average
}

const estimatePossiblePatterns = ({
  token,
  graph,
  turns,
}: EstimatePossiblePatternsOptions) => {
  const startingPosition = Object.keys(zxcvbnOptions.graphs[graph]).length
  const averageDegree = calcAverageDegree(zxcvbnOptions.graphs[graph])

  let guesses = 0
  const tokenLength = token.length
  // # estimate the number of possible patterns w/ tokenLength or less with turns or less.
  for (let i = 2; i <= tokenLength; i += 1) {
    const possibleTurns = Math.min(turns, i - 1)
    for (let j = 1; j <= possibleTurns; j += 1) {
      guesses += utils.nCk(i - 1, j - 1) * startingPosition * averageDegree ** j
    }
  }
  return guesses
}

export default ({
  graph,
  token,
  shiftedCount,
  turns,
}: MatchExtended | MatchEstimated) => {
  let guesses = estimatePossiblePatterns({ token, graph, turns })

  // add extra guesses for shifted keys. (% instead of 5, A instead of a.)
  // math is similar to extra guesses of l33t substitutions in dictionary matches.
  if (shiftedCount) {
    const unShiftedCount = token.length - shiftedCount
    if (shiftedCount === 0 || unShiftedCount === 0) {
      guesses *= 2
    } else {
      let shiftedVariations = 0
      for (let i = 1; i <= Math.min(shiftedCount, unShiftedCount); i += 1) {
        shiftedVariations += utils.nCk(shiftedCount + unShiftedCount, i)
      }
      guesses *= shiftedVariations
    }
  }
  return Math.round(guesses)
}
