package main

import (
	"fmt"
	"math"
	"strings"

	"github.com/alexchao26/advent-of-code-go/util"
)

func main() {
	input := util.ReadFile("../input.txt")
	lines := strings.Split(string(input), "\n")
	grid := make([][]string, len(lines))
	for i, v := range lines {
		grid[i] = strings.Split(v, "")

		// * Uncomment to print the input
		// fmt.Println(grid[i])
	}

	dijkstra := MakeDijkstraGrid(grid)
	// fmt.Println("initial dijkstra queue", dijkstra.queue)

	for !dijkstra.handleFrontOfQueue() {
		// * watch the queue grow and shrink
		// fmt.Println("  Queue", dijkstra.queue)
	}
	// fmt.Println("Final Queue", dijkstra.queue)

	fmt.Println("Distance to ZZ portal", dijkstra.grid[dijkstra.finishCoordinates[0]][dijkstra.finishCoordinates[1]].distance)
}

// Dijkstra struct stores the 2D grid of nodes and a queue of next points to check
// and a portal map to add jumps to the queue
type Dijkstra struct {
	grid              [][]*Node
	queue             [][2]int
	portalMap         map[string][2]int // map <portalName><originRow><originCol> to [2]int{<destinationRow><destinationCol>}
	startCoordinates  [2]int
	finishCoordinates [2]int
}

// Node data type is custom built for this algo, i.e. also stores if this is a portal cell
type Node struct {
	value           string
	distance        int
	portalName      string // <portalCharacters><row><col>, will be used to jump to other indexes
	jumpCoordinates [2]int // coordinates of its paired portal (if applicable)
}

// MakeDijkstraGrid does just that
func MakeDijkstraGrid(inputGrid [][]string) *Dijkstra {
	dijkstra := Dijkstra{}
	portalMapHelper := make(map[string][2]int)

	grid := make([][]*Node, len(inputGrid)-4)
	// iterate starting at 2,2 to skip the top and left and end len-2 to skip bottom & right
	for row := 2; row < len(inputGrid)-2; row++ {
		grid[row-2] = make([]*Node, len(inputGrid[0])-4)
		for col := 2; col < len(inputGrid[0])-2; col++ {
			// make a node for each cell
			switch value := inputGrid[row][col]; value {
			case "#": // wall
				grid[row-2][col-2] = &Node{"#", math.MaxInt32, "", [2]int{0, 0}}
				// if this is a hallway node, use a helper function to determine if there this is a portal
			case ".": // hallway
				hallwayNode := &Node{
					value:           ".",
					distance:        math.MaxInt32,
					portalName:      "",
					jumpCoordinates: [2]int{0, 0},
				}
				portalName := getPortalName(inputGrid, row, col)
				if len(portalName) != 0 {
					// assign portal name for this node
					hallwayNode.portalName = portalName

					// generatine the portal maps for each node is a pain...
					// if this is portal's pair hasn't been found yet (i.e. equal to zero value of [2]int), add it to a map
					if pairedPortal := portalMapHelper[portalName]; pairedPortal == [2]int{0, 0} {
						portalMapHelper[portalName] = [2]int{row - 2, col - 2}
					} else {
						// else it has been found, set the jumpCoordinates on this node to pair's coords
						hallwayNode.jumpCoordinates = pairedPortal
						// set its pair's jumpCoordinates to this node's coords
						grid[pairedPortal[0]][pairedPortal[1]].jumpCoordinates = [2]int{row - 2, col - 2}
					}
				}
				grid[row-2][col-2] = hallwayNode
				// if it is AA, update the distance of this node to zero, initialize queue
				if portalName == "AA" {
					// !! unused
					dijkstra.startCoordinates = [2]int{row - 2, col - 2}

					hallwayNode.distance = 0
					dijkstra.queue = [][2]int{
						[2]int{row - 2, col - 2},
					}
				}
				// if end portal, set finish coordinates
				if portalName == "ZZ" {
					dijkstra.finishCoordinates = [2]int{row - 2, col - 2}
				}
			}
		}
	}

	// set grid field
	dijkstra.grid = grid

	return &dijkstra
}

// helper function to run in 4 directions and see if any of them are a capital letter
// if that's true, then grab the portal name in that direction and return it (two char string)
func getPortalName(grid [][]string, row, col int) string {
	// NOTE I'm hard coding directions
	leftTwo := grid[row][col-2] + grid[row][col-1]
	rightTwo := grid[row][col+1] + grid[row][col+2]
	upTwo := grid[row-2][col] + grid[row-1][col]
	downTwo := grid[row+1][col] + grid[row+2][col]

	isPortalString := func(str string) bool {
		ascii1 := str[0] - 'A'
		ascii2 := str[1] - 'A'

		if ascii1 >= 0 && ascii1 < 26 && ascii2 >= 0 && ascii2 < 26 {
			return true
		}
		return false
	}

	// if both characters are capital letters
	switch {
	case isPortalString(leftTwo):
		return leftTwo
	case isPortalString(rightTwo):
		return rightTwo
	case isPortalString(upTwo):
		return upTwo
	case isPortalString(downTwo):
		return downTwo
	}

	return ""
}

// returns true if the queue is empty OR the ZZ portal has been reached
func (dijkstra *Dijkstra) handleFrontOfQueue() (done bool) {
	dRow := [4]int{0, 0, -1, 1}
	dCol := [4]int{-1, 1, 0, 0}

	row, col := dijkstra.queue[0][0], dijkstra.queue[0][1]
	currentNode := dijkstra.grid[row][col]

	if currentNode.portalName == "ZZ" {
		return true
	}

	for i := 0; i < 4; i++ {
		nextRow, nextCol := row+dRow[i], col+dCol[i]
		isInbounds := nextRow >= 0 && nextRow < len(dijkstra.grid) && nextCol >= 0 && nextCol < len(dijkstra.grid[0])
		if isInbounds {
			// if the nextNode is a hallway & has not been traveled to yet
			if nextNode := dijkstra.grid[nextRow][nextCol]; nextNode != nil && nextNode.value == "." && nextNode.distance == math.MaxInt32 {
				// update the distance of the nextNode
				nextNode.distance = currentNode.distance + 1
				// add its coordinates to the queue
				dijkstra.queue = append(dijkstra.queue, [2]int{nextRow, nextCol})
			}
		}
	}

	// check if a portal jump is possible!
	if currentNode.portalName != "" {
		// find coordinates to jump to and the node itself
		jumpRow := currentNode.jumpCoordinates[0]
		jumpCol := currentNode.jumpCoordinates[1]
		jumpNode := dijkstra.grid[jumpRow][jumpCol]

		// update distance
		jumpNode.distance = currentNode.distance + 1
		// add to queue
		dijkstra.queue = append(dijkstra.queue, currentNode.jumpCoordinates)
	}

	// dequeue, return true if queue is now empty
	dijkstra.queue = dijkstra.queue[1:]
	if len(dijkstra.queue) == 0 {
		return true
	}
	return false
}
