package jess

import (
	"math"
	"strings"
)

var (
	// ASCII printable characters (character codes 32-127).
	passwordCharSets = []string{
		"abcdefghijklmnopqrstuvwxyz",
		"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
		"0123456789",
		"- .,_", // more common special characters, especially with passwords using words
		"!\"#$%&'()*+/:;<=>?@[\\]^`{|}~",
	}

	// extended ASCII codes (character code 128-255)
	// assume pool size of 32 (a quarter), as not all of them are common / easily accessible on every keyboard.
	passwordExtraPoolSize = 32

	createPasswordCallback func(signet *Signet, minSecurityLevel int) error
	getPasswordCallback    func(signet *Signet) error
)

// SetPasswordCallbacks sets callbacks that are used to let the user enter passwords.
func SetPasswordCallbacks(
	createPassword func(signet *Signet, minSecurityLevel int) error,
	getPassword func(signet *Signet) error,
) {
	if createPasswordCallback == nil {
		createPasswordCallback = createPassword
	}
	if getPasswordCallback == nil {
		getPasswordCallback = getPassword
	}
}

// CalculatePasswordSecurityLevel calculates the security level of the given password and iterations of the pbkdf algorithm.
func CalculatePasswordSecurityLevel(password string, iterations int) int {
	// TODO: this calculation is pretty conservative and errs on the safe side
	// maybe soften this up a litte, but couldn't find any scientific foundation for that

	charactersFound := 0
	distinctCharactersFound := 0
	characterPoolSize := 0

	// loop all character sets
	for _, charSet := range passwordCharSets {
		foundInCharSet := false

		// loop through every character in the character set
		for _, char := range charSet {
			// count occurrences in password
			cnt := countRuneInString(password, char)
			// disqualify if a single character is 1/4 of the password
			if cnt*4 >= len(password) {
				return -1
			}
			// we found something!
			if cnt > 0 {
				charactersFound += cnt
				distinctCharactersFound++
				foundInCharSet = true
			}
		}

		// if we found anything in this char set, add the it's length to the total pool
		if foundInCharSet {
			characterPoolSize += len(charSet)
		}
	}

	// disqualify if characters are repeated 4 or more times, on average
	if distinctCharactersFound*4 <= len(password) {
		return -1
	}

	// check if there are some extra characters
	if charactersFound < len(password) {
		// add the extra pool size
		characterPoolSize += passwordExtraPoolSize
	}

	possibleCombinationsWithPoolSize := math.Pow(float64(characterPoolSize), float64(len(password)))
	entropy := math.Log2(possibleCombinationsWithPoolSize)
	avgNumberOfGuesses := math.Pow(2, entropy-1)
	avgGuessingOperations := avgNumberOfGuesses * float64(iterations)
	securityLevel := math.Log2(avgGuessingOperations)

	return int(securityLevel) // always round down
}

func countRuneInString(s string, r rune) (n int) {
	for {
		i := strings.IndexRune(s, r)
		if i < 0 {
			return
		}
		n++
		s = s[i+1:]
	}
}