package cryptext

import (
	"crypto/rand"
	"io"
	"math/big"
	mathrand "math/rand"
	"strings"
)

const (
	ppStartChar            = "BCDFGHJKLMNPQRSTVWXZ"
	ppEndChar              = "ABDEFIKMNORSTUXYZ"
	ppVowel                = "AEIOUY"
	ppConsonant            = "BCDFGHJKLMNPQRSTVWXZ"
	ppSegmentLenMin        = 3
	ppSegmentLenMax        = 7
	ppMaxRepeatedVowel     = 2
	ppMaxRepeatedConsonant = 2
)

var ppContinuation = map[uint8]string{
	'A': "BCDFGHJKLMNPRSTVWXYZ",
	'B': "ADFIKLMNORSTUY",
	'C': "AEIKOUY",
	'D': "AEILORSUYZ",
	'E': "BCDFGHJKLMNPRSTVWXYZ",
	'F': "ADEGIKLOPRTUY",
	'G': "ABDEFHILMNORSTUY",
	'H': "AEIOUY",
	'I': "BCDFGHJKLMNPRSTVWXZ",
	'J': "AEIOUY",
	'K': "ADEFHILMNORSTUY",
	'L': "ADEFGIJKMNOPSTUVWYZ",
	'M': "ABEFIKOPSTUY",
	'N': "ABEFIKOPSTUY",
	'O': "BCDFGHJKLMNPRSTVWXYZ",
	'P': "AEFIJLORSTUY",
	'Q': "AEIOUY",
	'R': "ADEFGHIJKLMNOPSTUVYZ",
	'S': "ACDEIKLOPTUYZ",
	'T': "AEHIJOPRSUWY",
	'U': "BCDFGHJKLMNPRSTVWXZ",
	'V': "AEIOUY",
	'W': "AEIOUY",
	'X': "AEIOUY",
	'Y': "ABCDFGHKLMNPRSTVXZ",
	'Z': "AEILOTUY",
}

var ppLog2Map = map[int]float64{
	1:  0.00000000,
	2:  1.00000000,
	3:  1.58496250,
	4:  2.00000000,
	5:  2.32192809,
	6:  2.58496250,
	7:  2.80735492,
	8:  3.00000000,
	9:  3.16992500,
	10: 3.32192809,
	11: 3.45943162,
	12: 3.58496250,
	13: 3.70043972,
	14: 3.80735492,
	15: 3.90689060,
	16: 4.00000000,
	17: 4.08746284,
	18: 4.16992500,
	19: 4.24792751,
	20: 4.32192809,
	21: 4.39231742,
	22: 4.45943162,
	23: 4.52356196,
	24: 4.58496250,
	25: 4.64385619,
	26: 4.70043972,
	27: 4.75488750,
	28: 4.80735492,
	29: 4.85798100,
	30: 4.90689060,
	31: 4.95419631,
	32: 5.00000000,
}

var (
	ppVowelMap     = ppMakeSet(ppVowel)
	ppConsonantMap = ppMakeSet(ppConsonant)
	ppEndCharMap   = ppMakeSet(ppEndChar)
)

func ppMakeSet(v string) map[uint8]bool {
	mp := make(map[uint8]bool, len(v))
	for _, chr := range v {
		mp[uint8(chr)] = true
	}
	return mp
}

func ppRandInt(rng io.Reader, max int) int {
	v, err := rand.Int(rng, big.NewInt(int64(max)))
	if err != nil {
		panic(err)
	}
	return int(v.Int64())
}

func ppRand(rng io.Reader, chars string, entropy *float64) uint8 {
	chr := chars[ppRandInt(rng, len(chars))]

	*entropy = *entropy + ppLog2Map[len(chars)]

	return chr
}

func ppCharType(chr uint8) (bool, bool) {
	_, ok1 := ppVowelMap[chr]
	_, ok2 := ppConsonantMap[chr]

	return ok1, ok2
}

func ppCharsetRemove(cs string, set map[uint8]bool, allowEmpty bool) string {
	result := ""
	for _, chr := range cs {
		if _, ok := set[uint8(chr)]; !ok {
			result += string(chr)
		}
	}
	if result == "" && !allowEmpty {
		return cs
	}
	return result
}

func ppCharsetFilter(cs string, set map[uint8]bool, allowEmpty bool) string {
	result := ""
	for _, chr := range cs {
		if _, ok := set[uint8(chr)]; ok {
			result += string(chr)
		}
	}
	if result == "" && !allowEmpty {
		return cs
	}
	return result
}

func PronouncablePasswordExt(rng io.Reader, pwlen int) (string, float64) {

	// kinda pseudo markov-chain - with a few extra rules and no weights...

	if pwlen <= 0 {
		return "", 0
	}

	vowelCount := 0
	consoCount := 0
	entropy := float64(0)

	startChar := ppRand(rng, ppStartChar, &entropy)

	result := string(startChar)
	currentChar := startChar

	isVowel, isConsonant := ppCharType(currentChar)
	if isVowel {
		vowelCount = 1
	}
	if isConsonant {
		consoCount = ppMaxRepeatedConsonant
	}

	segmentLen := 1

	segmentLenTarget := ppSegmentLenMin + ppRandInt(rng, ppSegmentLenMax-ppSegmentLenMin)

	for len(result) < pwlen {

		charset := ppContinuation[currentChar]
		if vowelCount >= ppMaxRepeatedVowel {
			charset = ppCharsetRemove(charset, ppVowelMap, false)
		}
		if consoCount >= ppMaxRepeatedConsonant {
			charset = ppCharsetRemove(charset, ppConsonantMap, false)
		}

		lastOfSegment := false
		newSegment := false

		if len(result)+1 == pwlen {
			// last of result
			charset = ppCharsetFilter(charset, ppEndCharMap, false)
		} else if segmentLen+1 == segmentLenTarget {
			// last of segment
			charsetNew := ppCharsetFilter(charset, ppEndCharMap, true)
			if charsetNew != "" {
				charset = charsetNew
				lastOfSegment = true
			}
		} else if segmentLen >= segmentLenTarget {
			// (perhaps) start of new segment
			if _, ok := ppEndCharMap[currentChar]; ok {
				charset = ppStartChar
				newSegment = true
			} else {
				// continue segment for one more char to (hopefully) find an end-char
				charsetNew := ppCharsetFilter(charset, ppEndCharMap, true)
				if charsetNew != "" {
					charset = charsetNew
					lastOfSegment = true
				}
			}
		} else {
			// normal continuation
		}

		newChar := ppRand(rng, charset, &entropy)
		if lastOfSegment {
			currentChar = newChar
			segmentLen++
			result += strings.ToLower(string(newChar))
		} else if newSegment {
			currentChar = newChar
			segmentLen = 1
			result += strings.ToUpper(string(newChar))
			segmentLenTarget = ppSegmentLenMin + ppRandInt(rng, ppSegmentLenMax-ppSegmentLenMin)
			vowelCount = 0
			consoCount = 0
		} else {
			currentChar = newChar
			segmentLen++
			result += strings.ToLower(string(newChar))
		}

		isVowel, isConsonant := ppCharType(currentChar)
		if isVowel {
			vowelCount++
			consoCount = 0
		}
		if isConsonant {
			vowelCount = 0
			if newSegment {
				consoCount = ppMaxRepeatedConsonant
			} else {
				consoCount++
			}
		}
	}

	return result, entropy
}

func PronouncablePassword(len int) string {
	v, _ := PronouncablePasswordExt(rand.Reader, len)
	return v
}

func PronouncablePasswordSeeded(seed int64, len int) string {

	v, _ := PronouncablePasswordExt(mathrand.New(mathrand.NewSource(seed)), len)
	return v
}