package main

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/fatih/color"
	termbox "github.com/nsf/termbox-go"

	"../../"
)

var regs = map[string]int{
	"a": 0,
	"b": 0,
	"c": 0,
	"d": 0,
}

var instructions, lastInst []string
var curr, cursor int
var done, debug, pause, step, skip bool
var breakpoints []int
var eventChan chan termbox.Event
var outBuff string

// Available arguments:
// -d		Debug Mode
// -p		Pause on start
// -25  Run day 25 simulation
func main() {
	var inpFn string
	if inpFn = aoc.GetArgNumber(1); inpFn == "" {
		done = true
	}
	if inpFn != "" {
		instructions = aoc.FileToStringSlice(inpFn)
	}

	if aoc.ArgIsSet("-d") {
		debug = true
	}
	if aoc.ArgIsSet("-p") {
		pause = true
	}
	if aoc.ArgIsSet("-25") {
		// If running the day 25 simulation, ignore debug and pause flags
		fmt.Println("Running Day 25 simulation, disabling debug & pause")
		debug = false
		pause = false
	}
	if debug {
		err := termbox.Init()
		//tWidth, tHeight := termbox.Size()
		if err != nil {
			fmt.Println("Error initializing termbox")
			os.Exit(1)
		}

		defer termbox.Close()
		eventChan = make(chan termbox.Event)
		go readUserInput(eventChan)
		go sendNoneEvent(eventChan)
	}
	if aoc.ArgIsSet("-25") {
		var day25Solved bool
		regAStart := regs["a"]
		regBStart := regs["b"]
		regCStart := regs["c"]
		regDStart := regs["d"]
		for !day25Solved {
			regs["a"] = regAStart
			regs["b"] = regBStart
			regs["c"] = regCStart
			regs["d"] = regDStart
			ProcInstructions()
			day25Solved = true
			fmt.Println(regAStart, ":", outBuff)
			if outBuff != "0101010101" {
				day25Solved = false
				regAStart++
			}
		}
	} else {
		ProcInstructions()
		PrintState()
	}
	//fmt.Println("Press any key to exit")
	//termbox.PollEvent()
}

func ProcInstructions() {
	locRegs := make(map[string]int)
	for k, v := range regs {
		locRegs[k] = v
	}
	curr = 0
	outBuff = ""
	var ins []string
	for curr < len(instructions) || done {
		isBreak := isBreakpoint(curr)
		if debug {
			lastInst = ins
		}
		ins = strings.Fields(instructions[curr])
		if len(ins) == 0 {
			break
		}
		if isMultOp(curr) {
			curr = doMultOp(curr)
			skip = true
		}
		if debug {
			if isBreak {
				pause = true
				step = false
			}
			for pause && !step && !done {
				// print state and wait for user to step
				PrintState()
				PrintInstructionState()
				PrintDebugHelp()
				ev := <-eventChan
				if ev.Type == termbox.EventKey {
					switch {
					case ev.Key == termbox.KeySpace:
						pause = !pause
					case ev.Ch == 'q':
						done = true
					case ev.Ch == 'b':
						toggleBreakpoint(cursor)
					case ev.Ch == 's':
						step = true
					case ev.Ch == 'u':
						updateRegister()
					case ev.Key == termbox.KeyArrowUp:
						if cursor > 0 {
							cursor--
						}
					case ev.Key == termbox.KeyArrowDown:
						if cursor < len(instructions)-1 {
							cursor++
						}
					}
				}
			}
			step = false
		}
		if done {
			// User hit 'q'
			break
		}
		if skip {
			skip = false
			continue
		}
		curr++
		switch ins[0] {
		case "out":
			v, ok := regs[ins[1]]
			if !ok {
				outBuff += ins[1]
			} else {
				outBuff += aoc.Itoa(v)
			}
			if aoc.ArgIsSet("-25") && len(outBuff) == 10 {
				// This should be long enough for our day 25 answer
				return
			}
			// If we're not debugging, just print it and reset the buffer
			if !debug && !aoc.ArgIsSet("-25") {
				fmt.Print(outBuff)
				outBuff = ""
			}
		case "jnz":
			// If we have a jnz c -2 it could be moving all of c into another register
			v, ok := regs[ins[1]]
			if !ok {
				v = aoc.Atoi(ins[1])
			}
			var p int
			if p, ok = regs[ins[2]]; !ok {
				p = aoc.Atoi(ins[2])
			}
			if v != 0 {
				// Subtract 1 from the jump because we incremented already
				curr += p - 1
			}
		case "mlt":
			// a three arg instruction: mlt a b c
			// take a * b, store in c
			var ok bool
			src1 := ins[1]
			src2 := ins[2]
			dst := ins[3]
			if _, ok := regs[dst]; !ok {
				// invalid destination
				continue
			}
			var src1I, src2I int
			if src1I, ok = regs[src1]; !ok {
				src1I = aoc.Atoi(src1)
			}
			if src2I, ok = regs[src2]; !ok {
				src2I = aoc.Atoi(src2)
			}
			regs[dst] = src1I * src2I
		case "cpy":
			src := ins[1]
			dst := ins[2]
			// check if dst is a register
			if _, ok := regs[dst]; !ok {
				// Nope, move along
				continue
			}
			// check if the src is a register
			if v, ok := regs[src]; ok {
				regs[dst] = v
			} else {
				// It's not, must be an int
				regs[dst] = aoc.Atoi(src)
			}
		case "inc":
			if _, ok := regs[ins[1]]; !ok {
				continue
			}
			regs[ins[1]] = regs[ins[1]] + 1
		case "dec":
			if _, ok := regs[ins[1]]; !ok {
				continue
			}
			regs[ins[1]] = regs[ins[1]] - 1
		case "tgl": // tgl alters an instruction
			src := ins[1]
			var srcI int
			if v, ok := regs[src]; ok {
				srcI = v
			} else {
				srcI = aoc.Atoi(src)
			}
			srcI = curr + srcI
			if srcI < 0 || srcI > len(instructions)-1 {
				// Invalid instruction
				continue
			}
			altPts := strings.Fields(instructions[srcI-1])
			if len(altPts) == 2 { // one argument
				if altPts[0] == "inc" {
					altPts[0] = "dec"
				} else {
					altPts[0] = "inc"
				}
			} else { // two arguments
				if altPts[0] == "jnz" {
					altPts[0] = "cpy"
				} else {
					altPts[0] = "jnz"
				}
			}
			instructions[srcI-1] = strings.Join(altPts, " ")
		}
		if debug {
			PrintState()
		}
	}
}

// Fancy State Printing
func PrintState() {
	fmt.Println(aoc.ClearScreen)
	PrintRegs()
}

func PrintRegs() {
	datLine := fmt.Sprint("\u2502  a:", regs["a"], "  b:", regs["b"], "  c:", regs["c"], "  d:", regs["d"], "  \u2502")
	fmt.Println("\u250C" + strings.Repeat("\u2500", len(datLine)-6) + "\u2510")
	fmt.Println(datLine)
	fmt.Println("\u2514" + strings.Repeat("\u2500", len(datLine)-6) + "\u2518")
}

func PrintInstructionState() {
	optim := color.New(color.FgGreen)
	for pi := range instructions {
		if pi == curr {
			fmt.Print(">")
		} else {
			fmt.Print(" ")
		}
		if isBreakpoint(pi) {
			fmt.Print("B")
		} else {
			fmt.Print(" ")
		}
		if cursor == pi {
			cursor := color.New(color.FgBlack).Add(color.BgWhite)
			if isMultOp(pi) {
				cursor.Println("+ " + instructions[pi] + " == optimized")
			} else if mo, _ := isInMultOp(pi); mo {
				cursor.Println("| " + instructions[pi])
			} else {
				cursor.Println(instructions[pi])
			}
		} else {
			if isMultOp(pi) {
				optim.Println("+ " + instructions[pi] + " == optimized")
			} else if mo, _ := isInMultOp(pi); mo {
				optim.Println("| " + instructions[pi])
			} else {
				fmt.Println(instructions[pi])
			}
		}
	}
}

func PrintDebugHelp() {
	fmt.Println("(s): step | (space): pause/unpause | (b): toggle breakpoint")
	fmt.Println("(u): update register to value | (q): quit")
}

func readUserInput(e chan termbox.Event) {
	for {
		e <- termbox.PollEvent()
	}
}

func sendNoneEvent(e chan termbox.Event) {
	for {
		time.Sleep(time.Second / 32)
		if !pause {
			e <- termbox.Event{Type: termbox.EventNone}
		}
	}
}

// isInMultOp returns true and the pc for the
// start of the multiplication operation if pos is
// located within it
func isInMultOp(pos int) (bool, int) {
	// We need to check instructions[pos +- 5] to see if any
	// are a multiplication op and return the pos for the (first) one
	// that is
	if isMultOp(pos) {
		return true, pos
	}
	for i := 1; i < 6; i++ {
		if isMultOp(pos - i) {
			return true, (pos - i)
		}
	}
	return false, pos
}

// isMultOpStart return whether pos is the start of
// what can be optimized as multiplication
/*
	   > cpy b c
	     inc a
	     dec c
	     jnz c -2
	     dec d
	     jnz d -5
		 == add (b * d) to a
				set c & d to 0
				curr += 5
*/
func isMultOp(pos int) bool {
	if pos < 0 || pos > len(instructions)-1 {
		return false
	}
	ins := strings.Fields(instructions[pos])
	if len(ins) < 3 {
		return false
	}
	if ins[0] == "cpy" {
		if len(instructions) >= pos+6 {
			ins1 := strings.Fields(instructions[pos+1])
			ins2 := strings.Fields(instructions[pos+2])
			ins3 := strings.Fields(instructions[pos+3])
			ins4 := strings.Fields(instructions[pos+4])
			ins5 := strings.Fields(instructions[pos+5])
			if ins1[0] == "inc" && ins2[0] == "dec" && ins3[0] == "jnz" && ins4[0] == "dec" && ins5[0] == "jnz" {
				allGood := true
				if allGood {
					// Do the multiplication
					// ins[1] * ins4[1]
					// add that value to ins1[1]
					// set ins[2] to 0
					// set ins4[1] to 0
					// Then add 5 to the pc
					src1 := ins[1]
					src2 := ins4[1]
					dst := ins1[1]
					if _, ok := regs[dst]; !ok {
						// Invalid destination register
						allGood = false
					}
					if _, ok := regs[src1]; !ok {
						// Invalid source register
						allGood = false
					}
					if _, ok := regs[src2]; !ok {
						allGood = false
					}
					return allGood
				}
			}
		}
	}
	return false
}

// doMultOp performs a multiplcation operation and returns the new pc
func doMultOp(pos int) int {
	isOp, pos := isInMultOp(pos)
	if !isOp {
		// No optimization to do
		return pos
	}
	ins := strings.Fields(instructions[pos])
	ins1 := strings.Fields(instructions[pos+1])
	ins4 := strings.Fields(instructions[pos+4])
	// Do the multiplication
	// ins[1] * ins4[1]
	// add that value to ins1[1]
	// set ins[2] to 0
	// set ins4[1] to 0
	// Then add 5 to the pc
	src1 := ins[1]
	src2 := ins4[1]
	dst := ins1[1]
	regs[dst] += (regs[src1] * regs[src2])
	regs[ins[2]] = 0
	regs[ins4[1]] = 0
	pos += 6
	return pos
}

func toggleBreakpoint(pos int) {
	_, pos = isInMultOp(pos)
	for i := 0; i < len(breakpoints); i++ {
		if breakpoints[i] == pos {
			breakpoints = append(breakpoints[:i], breakpoints[i+1:]...)
			return
		}
	}
	breakpoints = append(breakpoints, pos)
}

func isBreakpoint(pos int) bool {
	_, pos = isInMultOp(pos)
	for i := 0; i < len(breakpoints); i++ {
		if breakpoints[i] == pos {
			return true
		}
	}
	return false
}

func updateRegister() {
	var updReg string
	fmt.Println("Update which register? (a,b,c,d)")
	ev := <-eventChan
	if ev.Type == termbox.EventKey {
		switch {
		case ev.Ch == 'a':
			updReg = "a"
		case ev.Ch == 'b':
			updReg = "b"
		case ev.Ch == 'c':
			updReg = "c"
		case ev.Ch == 'd':
			updReg = "d"
		default:
			fmt.Println("Invalid register (" + string(ev.Ch) + ")")
			fmt.Println("Press any key to continue")
			ev = <-eventChan
			return
		}
	}

	fmt.Println("Enter new value (must be integer, end with enter)")
	var newVal string
	for ev.Key != termbox.KeyEnter {
		ev = <-eventChan
		if ev.Ch >= '0' && ev.Ch <= '9' {
			newVal += string(ev.Ch)
			fmt.Print(string(ev.Ch))
		}
	}
	fmt.Println("Setting Register (" + updReg + ") to " + newVal)
	var v int
	var e error
	if v, e = strconv.Atoi(newVal); e != nil {
		fmt.Println()
		fmt.Println("Error parsing integer")
		fmt.Println(e)
		fmt.Println("Press any key to continue")
		ev = <-eventChan
		return
	}
	regs[updReg] = v
}
