package main

import (
	"errors"
	"fmt"
	"math/rand"
	"os"
	"strings"
	"time"

	h "git.bullercodeworks.com/brian/adventofcode/helpers"
	"github.com/fatih/color"
	termbox "github.com/nsf/termbox-go"
)

func main() {
	var inpFn string
	var done bool
	if inpFn = h.GetArgNumber(1); inpFn != "" {
		done = true
	}

	err := termbox.Init()
	tWidth, tHeight := termbox.Size()
	if err != nil {
		fmt.Println("Error initializing termbox")
		os.Exit(1)
	}
	defer termbox.Close()
	var menuPos int
	cursor := color.New(color.FgBlack).Add(color.BgWhite)
	for !done {
		fmt.Println(h.ClearScreen)
		title := color.New(color.FgBlack).Add(color.BgYellow)
		title.Println(CenterText("day 22", tWidth))
		if menuPos == 0 {
			cursor.Println(CenterText("Start Small Game", tWidth))
		} else {
			fmt.Println(CenterText("Start Small Game", tWidth))
		}
		if menuPos == 1 {
			cursor.Println(CenterText("Start Medium Game", tWidth))
		} else {
			fmt.Println(CenterText("Start Medium Game", tWidth))
		}
		if menuPos == 2 {
			cursor.Println(CenterText("Start Large Game", tWidth))
		} else {
			fmt.Println(CenterText("Start Large Game", tWidth))
		}
		if menuPos == 3 {
			cursor.Println(CenterText("Exit", tWidth))
		} else {
			fmt.Println(CenterText("Exit", tWidth))
		}
		fmt.Println()
		fmt.Println(CenterText("To run the AdventOfCode Puzzle, pass the input as an argument to the program:", tWidth))
		fmt.Println(CenterText("./day22 <filename>", tWidth))
		fmt.Println(CenterText("For the part 1 solution:", tWidth))
		fmt.Println(CenterText("./day22 <filename> -1", tWidth))
		ev := termbox.PollEvent()
		if ev.Type == termbox.EventKey {
			switch {
			case ev.Key == termbox.KeyArrowUp || ev.Ch == 'k':
				if menuPos > 0 {
					menuPos--
				}
			case ev.Key == termbox.KeyArrowDown || ev.Ch == 'j':
				if menuPos < 3 {
					menuPos++
				}
			case ev.Key == termbox.KeyEnter:
				done = true
			}
		}
	}

	// Default to small game
	sizeX, sizeY := 6, 6
	switch menuPos {
	case 1:
		// Generate a medium game
		sizeX, sizeY = 10, 10
	case 2:
		// Generate a large game
		sizeX, sizeY = 15, 15
	case 3:
		return
	}

	// Create the display
	d := CreateDisplay(tWidth, tHeight)
	if inpFn != "" {
		input := h.FileToStringSlice(inpFn)
		d.reset(CreateNodesFromInput(input))
	} else {
		d.reset(GenerateNodesForGrid(sizeX, sizeY))
		// Grid is generated, designate one node as start (set used = 0)
		stX, stY := rand.Intn(sizeX), rand.Intn(sizeY)
		d.nodes[iToLoc(stX, stY)].Used = 0
		d.cursX, d.cursY = stX, stY
		d.goalX, d.goalY = rand.Intn(sizeX), rand.Intn(sizeY)
		d.nodes[iToLoc(d.goalX, d.goalY)].Used = rand.Intn(75)
	}

	if h.ArgIsSet("-1") {
		var viablePairs []string
		for _, v1 := range d.nodes {
			for _, v2 := range d.nodes {
				if ok, _ := IsViablePair(v1, v2); ok {
					viablePairs = AddViablePair(v1, v2, viablePairs)
				}
			}
		}
		fmt.Println("Viable Pairs:", len(viablePairs))
		return
	}

	done = false

	for !done {
		fmt.Println(h.ClearScreen)
		d.PrintGrid()
		ev := termbox.PollEvent()
		if d.goalX == 0 && d.goalY == 0 {
			if ev.Type == termbox.EventKey {
				done = true
			}
		} else {
			if ev.Type == termbox.EventResize {
				d.screenWidth, d.screenHeight = termbox.Size()
			} else if ev.Type == termbox.EventKey {
				switch {
				case ev.Key == termbox.KeyArrowUp || ev.Ch == 'k':
					d.resetError()
					if d.cursorMode {
						d.moveCursorUp()
					} else {
						d.moveDataDown()
					}
				case ev.Key == termbox.KeyArrowRight || ev.Ch == 'l':
					d.resetError()
					if d.cursorMode {
						d.moveCursorRight()
					} else {
						d.moveDataLeft()
					}
				case ev.Key == termbox.KeyArrowDown || ev.Ch == 'j':
					d.resetError()
					if d.cursorMode {
						d.moveCursorDown()
					} else {
						d.moveDataUp()
					}
				case ev.Key == termbox.KeyArrowLeft || ev.Ch == 'h':
					d.resetError()
					if d.cursorMode {
						d.moveCursorLeft()
					} else {
						d.moveDataRight()
					}
				case ev.Ch == 'q':
					done = true
				case ev.Key == termbox.KeySpace:
					d.switchMode()
				}
			}
		}
	}
}

func GenerateNodesForGrid(w, h int) (map[string]*Node, int, int) {
	nodes := make(map[string]*Node)
	rand.Seed(time.Now().UTC().UnixNano())
	// It doesn't play well if we have totals that are less than half(?) the highest
	for y := 0; y <= h; y++ {
		for x := 0; x <= w; x++ {
			ttl := 20 + rand.Intn(100-20)
			used := rand.Intn(ttl)
			n := CreateNode(x, y, ttl, used)
			nodes[iToLoc(x, y)] = n
		}
	}
	return nodes, w, h
}

// Returns the node map and the max X and max Y values
func CreateNodesFromInput(input []string) (map[string]*Node, int, int) {
	nodes := make(map[string]*Node)
	var maxX, maxY int
	for i := range input {
		if input[i] == "" {
			continue
		}
		if input[i][:9] == "/dev/grid" {
			var n *Node
			var err error
			if n, err = CreateNodeFromDfListing(input[i]); err != nil {
				fmt.Println(err)
				continue
			}
			nodes[h.Itoa(n.X)+";"+h.Itoa(n.Y)] = n
			if n.X > maxX {
				maxX = n.X
			}
			if n.Y > maxY {
				maxY = n.Y
			}
		}
	}
	return nodes, maxX, maxY
}

type Display struct {
	screenHeight, screenWidth int
	cursX, cursY              int
	goalX, goalY              int
	maxX, maxY                int
	errorX, errorY            int
	nodes                     map[string]*Node
	// Cursor Mode means the user can move the cursor around
	// Otherwise the screen locks on the node with 0 used
	cursorMode bool
	// moveCount tracks how many times we've moved data
	moveCount int

	message string
}

func CreateDisplay(h, w int) *Display {
	d := new(Display)
	d.screenHeight = h
	d.screenWidth = w
	d.errorX, d.errorY = -1, -1
	return d
}

func (d *Display) resetError() {
	d.errorX, d.errorY = -1, -1
}

func (d *Display) reset(n map[string]*Node, mX, mY int) {
	d.nodes = n
	d.maxX, d.maxY = mX, mY
	d.cursX, d.cursY = 0, 0
	d.goalX, d.goalY = d.maxX, 0
	d.errorX, d.errorY = -1, -1
	d.message = ""
	d.cursorMode = true
	d.switchMode()
}

func (d *Display) switchMode() {
	d.cursorMode = !d.cursorMode
	if !d.cursorMode {
		// Find the empty node and set the cursor to it
		for _, v := range d.nodes {
			if v.Used == 0 {
				d.cursX, d.cursY = v.X, v.Y
				break
			}
		}
	}
}

func (d *Display) moveCursorUp() {
	if d.cursY > 0 {
		d.cursY--
	}
}

func (d *Display) moveCursorRight() {
	if d.cursX < d.maxX {
		d.cursX++
	}
}

func (d *Display) moveCursorDown() {
	if d.cursY < d.maxY {
		d.cursY++
	}
}

func (d *Display) moveCursorLeft() {
	if d.cursX > 0 {
		d.cursX--
	}
}

func (d *Display) moveDataUp() {
	if d.cursY >= d.maxY {
		d.message = "Invalid Request"
		return
	}
	src := iToLoc(d.cursX, d.cursY+1)
	dest := iToLoc(d.cursX, d.cursY)
	if ok, err := IsViablePair(d.nodes[src], d.nodes[dest]); ok {
		d.nodes[dest].Used = d.nodes[src].Used
		d.nodes[src].Used = 0
		d.cursY++
		d.message = "Data Moved"
		d.moveCount++
		if src == iToLoc(d.goalX, d.goalY) {
			d.goalY--
		}
	} else {
		d.errorX, d.errorY = d.cursX, d.cursY+1
		d.message = fmt.Sprintf(
			"(%d;%d)[%3d/%3d] (%d;%d)[%3d/%3d] %s",
			d.nodes[src].X, d.nodes[src].Y, d.nodes[src].Used, d.nodes[src].Size,
			d.nodes[dest].X, d.nodes[dest].Y, d.nodes[dest].Used, d.nodes[dest].Size,
			err.Error(),
		)
	}
}

func (d *Display) moveDataRight() {
	if d.cursX <= 0 {
		d.message = "Invalid Request"
		return
	}
	src := iToLoc(d.cursX-1, d.cursY)
	dest := iToLoc(d.cursX, d.cursY)
	if ok, err := IsViablePair(d.nodes[src], d.nodes[dest]); ok {
		d.nodes[dest].Used = d.nodes[src].Used
		d.nodes[src].Used = 0
		d.cursX--
		d.message = "Data Moved"
		d.moveCount++
		if src == iToLoc(d.goalX, d.goalY) {
			d.goalX++
		}
	} else {
		d.errorX, d.errorY = d.cursX-1, d.cursY
		d.message = fmt.Sprintf(
			"(%d;%d)[%3d/%3d] (%d;%d)[%3d/%3d] %s",
			d.nodes[src].X, d.nodes[src].Y, d.nodes[src].Used, d.nodes[src].Size,
			d.nodes[dest].X, d.nodes[dest].Y, d.nodes[dest].Used, d.nodes[dest].Size,
			err.Error(),
		)
	}
}

func (d *Display) moveDataDown() {
	if d.cursY <= 0 {
		d.message = "Invalid Request"
		return
	}
	src := iToLoc(d.cursX, d.cursY-1)
	dest := iToLoc(d.cursX, d.cursY)
	if ok, err := IsViablePair(d.nodes[src], d.nodes[dest]); ok {
		d.nodes[dest].Used = d.nodes[src].Used
		d.nodes[src].Used = 0
		d.cursY--
		d.message = "Data Moved"
		d.moveCount++
		if src == iToLoc(d.goalX, d.goalY) {
			d.goalY++
		}
	} else {
		d.errorX, d.errorY = d.cursX, d.cursY-1
		d.message = fmt.Sprintf(
			"(%d;%d)[%3d/%3d] (%d;%d)[%3d/%3d] %s",
			d.nodes[src].X, d.nodes[src].Y, d.nodes[src].Used, d.nodes[src].Size,
			d.nodes[dest].X, d.nodes[dest].Y, d.nodes[dest].Used, d.nodes[dest].Size,
			err.Error(),
		)
	}
}

func (d *Display) moveDataLeft() {
	if d.cursX >= d.maxX {
		d.message = "Invalid Request"
		return
	}
	src := iToLoc(d.cursX+1, d.cursY)
	dest := iToLoc(d.cursX, d.cursY)
	if ok, err := IsViablePair(d.nodes[src], d.nodes[dest]); ok {
		d.nodes[dest].Used = d.nodes[src].Used
		d.nodes[src].Used = 0
		d.cursX++
		d.message = "Data Moved"
		d.moveCount++
		if src == iToLoc(d.goalX, d.goalY) {
			d.goalX--
		}
	} else {
		d.errorX, d.errorY = d.cursX+1, d.cursY
		d.message = fmt.Sprintf(
			"(%d;%d)[%3d/%3d] (%d;%d)[%3d/%3d] %s",
			d.nodes[src].X, d.nodes[src].Y, d.nodes[src].Used, d.nodes[src].Size,
			d.nodes[dest].X, d.nodes[dest].Y, d.nodes[dest].Used, d.nodes[dest].Size,
			err.Error(),
		)
	}
}

func (d *Display) PrintGrid() {
	cursor := color.New(color.FgBlack).Add(color.BgWhite)
	if !d.cursorMode {
		cursor = color.New(color.FgBlack).Add(color.BgYellow)
	}
	goal := color.New(color.FgGreen)
	error := color.New(color.FgRed)
	df := color.New(color.FgWhite)
	if d.goalX == 0 && d.goalY == 0 {
		df = color.New(color.FgYellow)
	}
	// Each cell takes up 9 width
	// We can only fit about (d.screenWidth / 9) cells wide
	//	and d.screenHeight tall
	//dispWidth := d.screenWidth / 9
	numCellsW := d.screenWidth / 3
	leftMost, rightMost := d.cursX-(numCellsW/2), d.cursX+(numCellsW/2)
	for leftMost < 0 {
		leftMost++
		rightMost++
	}
	if rightMost > d.maxX {
		rightMost = d.maxX
	}

	df.Print("    ")
	for j := leftMost; j <= rightMost; j++ {
		df.Printf("|%7d|", j)
	}
	df.Println()
	for i := 0; i <= d.maxY; i++ {
		df.Printf("|%2d|", i)
		for j := leftMost; j <= rightMost; j++ {
			n := d.nodes[iToLoc(j, i)]
			if j == d.cursX && i == d.cursY {
				cursor.Printf("[%3d/%3d]", n.Used, n.Size)
			} else if j == d.errorX && i == d.errorY {
				error.Printf("[%3d/%3d]", n.Used, n.Size)
			} else if j == d.goalX && i == d.goalY {
				goal.Printf("[%3d/%3d]", n.Used, n.Size)
			} else {
				if n != nil {
					df.Printf("[%3d/%3d]", n.Used, n.Size)
				} else {
					df.Print("[   /   ]")
				}
			}
		}
		df.Println()
	}
	df.Println(d.message)
	df.Println("Moves:", d.moveCount, "Goal Data at:", iToLoc(d.goalX, d.goalY))
	if d.cursorMode {
		cursor.Println("Cursor Mode")
	} else {
		cursor.Println("Data Move Mode")
	}
	df.Println("(q): quit, (space): switch mode")
}

func iToLoc(x, y int) string {
	return h.Itoa(x) + ";" + h.Itoa(y)
}

func AddViablePair(a, b *Node, list []string) []string {
	// Make sure this pair isn't already in the list
	for i := range list {
		if list[i] == a.GetLocString()+"=>"+b.GetLocString() || list[i] == b.GetLocString()+"=>"+a.GetLocString() {
			return list
		}
	}
	return append(list, a.GetLocString()+"=>"+b.GetLocString())
}

func IsViablePair(a, b *Node) (bool, error) {
	fmt.Println("  Initiating Viable Pair Check:", a.GetLocString(), b.GetLocString())
	if a == b {
		return false, errors.New("They're the same node")
	}
	//if a.Used == 0 {
	//	return false, errors.New(a.GetLocString() + " has no data")
	//}
	if a.Used > b.Size-b.Used {
		return false, errors.New(b.GetLocString() + " doesn't have enough space")
	}
	return true, nil
}

type Node struct {
	X, Y       int
	Size, Used int
}

func CreateNode(x, y, size, used int) *Node {
	n := new(Node)
	n.X, n.Y = x, y
	n.Size, n.Used = size, used
	return n
}

func CreateNodeFromDfListing(inp string) (*Node, error) {
	pts := strings.Fields(inp)
	if len(pts) != 5 {
		return nil, errors.New("Error Creating Node: Invalid DF Listing")
	}
	n := new(Node)
	parseLoc := strings.Split(pts[0], "-")
	if parseLoc[1][0] == 'x' {
		n.X = h.Atoi(parseLoc[1][1:])
	}
	if parseLoc[2][0] == 'y' {
		n.Y = h.Atoi(parseLoc[2][1:])
	}
	n.Size = h.Atoi(strings.TrimSuffix(pts[1], "T"))
	n.Used = h.Atoi(strings.TrimSuffix(pts[2], "T"))

	return n, nil
}

func (n *Node) GetLocString() string {
	return h.Itoa(n.X) + ";" + h.Itoa(n.Y)
}

func (n *Node) ToString() string {
	return fmt.Sprint("[", n.GetLocString(), "](S:", h.Itoa(n.Size), ";A:", h.Itoa(n.Size-n.Used), ";U:", h.Itoa(n.Used), ")")
}

func CenterText(txt string, width int) string {
	width -= len(txt)
	return strings.Repeat(" ", width/2) + txt + strings.Repeat(" ", width/2)
}
