package door

import (
	"log"
	"math/rand"
	"time"
	"unicode"
)

/*
No More Secrets - from "Sneakers"
https://github.com/bartobri/no-more-secrets

*/

type NoMoreSecretsConfig struct {
	Jumble_Sec        int    // in sec
	Jumble_Loop_Speed int    // in ms
	Reveal_Loop_Speed int    // in ms
	Max_Time          int    // Max value before reveal (per character)
	Color             string // Color to use before reveal
}

// The default configuration for NoMoreSecrets
var NoMoreSecretsDefault NoMoreSecretsConfig

// Initialize the Defaults
func init() {
	NoMoreSecretsDefault = NoMoreSecretsConfig{
		Jumble_Sec:        2,
		Jumble_Loop_Speed: 35,
		Reveal_Loop_Speed: 50,
		Max_Time:          5000,
		Color:             ColorText("CYAN ON BLACK"),
	}
}

// Get random character code for jumble
// We use chr 0x21 - 0xdf (excluding 0x7f)
func getRandom() byte {
	// 0x7f = backspace / rubout
	var rb byte = 0x7f
	for rb == 0x7f {
		rb = byte(rand.Intn(0xdf-0x21) + 0x21)
	}
	return rb
}

/*
NoMoreSecrets - render output as random, then fade in the original text.

Example:

func About_Example_NoMoreSecrets(d *door.Door) {
	W := 60
	center_x := (door.Width - W) / 2
	center_y := (door.Height - 16) / 2
	about := door.Panel{X: center_x,
		Y:           center_y,
		Width:       W,
		Style:       door.SINGLE_DOUBLE,
		BorderColor: door.ColorText("BOLD YELLOW ON BLUE"),
	}
	about.Lines = append(about.Lines, door.Line{Text: fmt.Sprintf("%*s", -W, "About This Door"),
		DefaultColor: door.ColorText("BOLD CYAN ON BLUE")})
	about.Lines = append(about.Lines, about.Spacer())

	about.Lines = append(about.Lines, door.Line{Text: fmt.Sprintf("%*s", -W, "Test Door written in go, using go door.")})
	var copyright string = "(C) 2022 Bugz, Red Green Software"

	if door.Unicode {
		copyright = strings.Replace(copyright, "(C)", "\u00a9", -1)
	}

	about.Lines = append(about.Lines,
		door.Line{Text: fmt.Sprintf("%*s", -W, copyright),
			DefaultColor: door.ColorText("BOLD WHITE ON BLUE")})
	for _, text := range []string{"",
		"This door was written by Bugz.",
		"",
		"It is written in Go, understands CP437 and unicode, adapts",
		"to screen sizes, uses door32.sys, supports TheDraw Fonts,",
		"and runs on Linux and Windows."} {
		about.Lines = append(about.Lines, door.Line{Text: fmt.Sprintf("%*s", -W, text)})
	}

	better := door.NoMoreSecretsDefault
	better.Color = door.ColorText("BOLD CYAN ON BLUE")

	door.NoMoreSecrets(about.Output(), d, &better)
}
*/
func NoMoreSecrets(output string, Door *Door, config *NoMoreSecretsConfig) {
	// Find ANSI codes, strip color (We'll handle color)

	rand.Seed(time.Now().UnixNano())

	if Unicode {
		var original []rune = []rune(output) // original / master
		var work []rune = make([]rune, 0)    // work copy we modify
		var workpos int = 0                  // where we are in the work copy
		var charpos []int                    // where characters are we can change
		var chartime []int                   // character time / reveal timeout
		var revealpos map[int]int            // workpos to original position
		var colormap map[int]string          // index to color end position (workpos)
		var coloridx []int                   // index of positions (map isn't ordered)
		var lastcolor []int                  // LastColor tracking for keeping colors proper
		var currentANSI string               // ANSI Escape code we've extracted
		var ANSIchar rune                    // Last character of the ANSI Escape code

		colormap = make(map[int]string)
		revealpos = make(map[int]int)
		coloridx = make([]int, 0)

		// default color = "reset"
		lastcolor = make([]int, 1)
		lastcolor[0] = 0

		// Not using range, I want to be able to look ahead and modify
		// x.

		for x := 0; x < len(original); x++ {
			var char rune = original[x]
			if char == '\x1b' {
				// ANSI Code
				currentANSI = "\x1b["

				if original[x+1] != '[' {
					log.Println("NoMoreSecrets: Found \\x1b not followed by [!")
				}
				x += 2
				for {
					currentANSI += string(original[x])
					if unicode.IsLetter(original[x]) {
						ANSIchar = original[x]
						break
					}
					x++
				}

				// log.Printf("curentANSI: END @ %d [%#v]\n", x, currentANSI[1:])

				// Is this a color code?
				if ANSIchar == 'm' {
					// Yes, don't store in work.  Process code.

					Door.UpdateLastColor(currentANSI, &lastcolor)
					colormap[workpos] = Color(lastcolor...)
					coloridx = append(coloridx, workpos)
					// log.Printf("Added %d with %s\n", workpos, colormap[workpos][1:])
				} else {
					// Not a color code.  Add to work.
					var ANSIrunes []rune = []rune(currentANSI)
					work = append(work, ANSIrunes...)
					workpos += len(ANSIrunes)
				}
				currentANSI = ""
			} else {
				// Not escape, so what is it?
				if unicode.IsPrint(char) {

					if char == ' ' {
						work = append(work, char)
						chartime = append(chartime, 0)
					} else {
						work = append(work, char)
						chartime = append(chartime, rand.Intn(config.Max_Time+1))
					}

					charpos = append(charpos, workpos)
					revealpos[workpos] = x
					workpos++
				} else {
					// control code, CR NL.
					work = append(work, char)
					workpos++
				}
			}
		}

		// jumble loop
		var renderF func() string = func() string {
			var result string
			var lastcolor string
			var pos int = 0

			for idx, char := range work {
				var found bool
				_, found = revealpos[idx]
				if found {
					for charpos[pos] != idx {
						pos++
					}
					// This is a character
					if chartime[pos] != 0 && char != ' ' {
						if lastcolor != config.Color {
							result += config.Color
							lastcolor = config.Color
						}
					} else {
						// look up the color in the colormap
						var best string

						// use the coloridx, lookup in colormap
						for _, cpos := range coloridx {
							if cpos > idx {
								break
							}
							best = colormap[cpos]
						}

						if lastcolor != best {
							result += best
							lastcolor = best
						}
					}
				}
				result += string(char)
			}
			return result
		}

		for i := 0; i < (config.Jumble_Sec*1000)/config.Jumble_Loop_Speed; i++ {
			for _, pos := range charpos {
				if work[pos] != ' ' {
					// Safe way to handle bytes to unicode

					var rb byte = getRandom()
					var safe []byte = []byte{rb}
					var rndchar string = CP437_to_Unicode(string(safe))
					work[pos] = []rune(rndchar)[0]
				}
			}
			Door.Write(renderF())
			if Door.Disconnect() {
				return
			}
			time.Sleep(time.Millisecond * time.Duration(config.Jumble_Loop_Speed))
		}

		for {
			var revealed bool = true
			for idx, pos := range charpos {
				if work[pos] != ' ' {
					if chartime[idx] > 0 {
						if chartime[idx] < 500 {
							if rand.Intn(3) == 0 {
								var safe []byte = []byte{getRandom()}
								var rndchar string = CP437_to_Unicode(string(safe))
								work[pos] = []rune(rndchar)[0]
							}
						} else {
							if rand.Intn(10) == 0 {
								var safe []byte = []byte{getRandom()}
								var rndchar string = CP437_to_Unicode(string(safe))
								work[pos] = []rune(rndchar)[0]
							}
						}

						if chartime[idx] < config.Reveal_Loop_Speed {
							chartime[idx] = 0
						} else {
							chartime[idx] -= config.Reveal_Loop_Speed
						}
						revealed = false
					} else {
						work[pos] = original[revealpos[pos]]
					}
				}
			}

			Door.Write(renderF())
			if Door.Disconnect() {
				return
			}
			time.Sleep(time.Millisecond * time.Duration(config.Reveal_Loop_Speed))
			if revealed {
				break
			}
		}

	} else {
		// CP437
		var original []byte = []byte(output) // original / master
		var work []byte                      // work copy we modify
		var workpos int = 0                  // where are we in the work copy
		var charpos []int                    // where characters are we can change
		var chartime []int                   // character time / reveal timeout
		var revealpos map[int]int            // workpos to original position
		var colormap map[int]string          // index to color end position (workpos)
		var coloridx []int                   // index of positions (map isn't ordered)
		var lastcolor []int                  // LastColor tracking for keeping color proper
		var currentANSI string               // ANSI Escape code we've extracted
		var ANSIchar byte                    // Last character of the ANSI Escape code

		work = make([]byte, 0)
		colormap = make(map[int]string)
		revealpos = make(map[int]int)
		coloridx = make([]int, 0)

		// default color = "reset"
		lastcolor = make([]int, 1)
		lastcolor[0] = 0

		// Not using range, I want to be able to look ahead and modify
		// x.

		for x := 0; x < len(original); x++ {
			var char byte = original[x]
			if char == '\x1b' {
				// ANSI Code
				currentANSI = "\x1b["

				if original[x+1] != '[' {
					log.Println("NoMoreSecrets: Found \\x1b not followed by [!")
				}
				x += 2
				for {
					currentANSI += string(original[x])
					if unicode.IsLetter(rune(original[x])) {
						ANSIchar = original[x]
						break
					}
					x++
				}

				// Is this a color code?
				if ANSIchar == 'm' {
					// Yes, don't store in work.  Process code.
					Door.UpdateLastColor(currentANSI, &lastcolor)
					colormap[workpos] = Color(lastcolor...)
					coloridx = append(coloridx, workpos)
				} else {
					// Not a color code.  Add to work.
					work = append(work, []byte(currentANSI)...)
					workpos += len(currentANSI)
				}
				currentANSI = ""
			} else {
				// Not escape, so what is it?
				if unicode.IsPrint(rune(char)) {

					if char == ' ' {
						work = append(work, char)
						chartime = append(chartime, 0)
					} else {
						work = append(work, char)
						chartime = append(chartime, rand.Intn(config.Max_Time+1))
					}

					charpos = append(charpos, workpos)
					revealpos[workpos] = x
					workpos++
				} else {
					// control code, CR NL.
					work = append(work, char)
					workpos++
				}
			}
		}

		// jumble loop
		var renderF func() string = func() string {
			var result []byte
			var lastcolor string
			var pos int = 0

			for idx, char := range work {
				_, found := revealpos[idx]
				if found {
					for charpos[pos] != idx {
						pos++
					}
					// This is a character

					if chartime[pos] != 0 && char != ' ' {
						if lastcolor != config.Color {
							result = append(result, []byte(config.Color)...)
							lastcolor = config.Color
						}
					} else {
						// look up the color in the colormap
						var best string

						for _, cpos := range coloridx {
							if cpos > idx {
								break
							}
							best = colormap[cpos]
						}

						if lastcolor != best {
							result = append(result, []byte(best)...)
							lastcolor = best
						}
					}
				}
				result = append(result, char)
			}
			return string(result)
		}

		for i := 0; i < (config.Jumble_Sec*1000)/config.Jumble_Loop_Speed; i++ {
			for _, pos := range charpos {
				if work[pos] != ' ' {
					work[pos] = getRandom()
				}
			}
			Door.Write(renderF())
			if Door.Disconnect() {
				return
			}
			time.Sleep(time.Millisecond * time.Duration(config.Jumble_Loop_Speed))
		}

		for {
			var revealed bool = true
			for idx, pos := range charpos {
				if work[pos] != ' ' {
					if chartime[idx] > 0 {
						if chartime[idx] < 500 {
							if rand.Intn(3) == 0 {
								work[pos] = getRandom()
							}
						} else {
							if rand.Intn(10) == 0 {
								work[pos] = getRandom()
							}
						}

						if chartime[idx] < config.Reveal_Loop_Speed {
							chartime[idx] = 0
						} else {
							chartime[idx] -= config.Reveal_Loop_Speed
						}
						revealed = false
					} else {
						work[pos] = original[revealpos[pos]]
					}
				}
			}

			Door.Write(renderF())
			if Door.Disconnect() {
				return
			}
			time.Sleep(time.Millisecond * time.Duration(config.Reveal_Loop_Speed))
			if revealed {
				break
			}
		}
	}
}