package main

import (
	"flag"
	"fmt"
	"math"
	"regexp"
	"strings"

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

func main() {
	var part int
	flag.IntVar(&part, "part", 1, "part 1 or 2")
	flag.Parse()
	fmt.Println("Running part", part)

	ans := cleaningRobot(util.ReadFile("./input.txt"), part)
	fmt.Println("Output:", ans)
}

func cleaningRobot(input string, part int) int {
	var grid [][]string
	for _, l := range strings.Split(input, "\n") {
		grid = append(grid, strings.Split(l, ""))
	}

	// bfs from every numbered cell to every other
	// generate a weighted graph
	var graph [][]int
	for r, row := range grid {
		for c, cell := range row {
			if regexp.MustCompile("[0-9]").MatchString(cell) {
				poi := cell
				distancesFromPOI := bfsGetEdgeWeights(grid, [2]int{r, c})
				// initialize graph size
				if graph == nil {
					for i := 0; i < len(distancesFromPOI); i++ {
						graph = append(graph, make([]int, len(distancesFromPOI)))
					}
				}
				graph[cast.ToInt(poi)] = distancesFromPOI
			}
		}
	}

	// then a recursive, backtracking dfs on that weighted graph to determine
	// the shortest total path
	returnToZero := part != 1
	return dfs(graph, 0, map[int]bool{0: true}, returnToZero)
}

type bfsNode struct {
	row, col int // 0,0 is top left
	distance int
}

// allows passing through points of interest
func bfsGetEdgeWeights(grid [][]string, start [2]int) []int {
	// points of interest to distance to reach them from the starting coord
	poiToDistance := map[string]int{
		grid[start[0]][start[1]]: 0,
	}
	// run until all nodes have been seen...
	queue := []bfsNode{
		{start[0], start[1], 0},
	}
	visited := map[[2]int]bool{}
	for len(queue) > 0 {
		front := queue[0]
		queue = queue[1:]

		if visited[[2]int{front.row, front.col}] {
			continue
		}
		visited[[2]int{front.row, front.col}] = true

		if regexp.MustCompile("[0-9]").MatchString(grid[front.row][front.col]) {
			poiToDistance[grid[front.row][front.col]] = front.distance
		}
		for _, d := range dirs {
			nextRow, nextCol := front.row+d[0], front.col+d[1]
			// don't need to check for going out of bounds because there are walls
			// surrounding everything
			if grid[nextRow][nextCol] != "#" {
				queue = append(queue, bfsNode{
					row:      nextRow,
					col:      nextCol,
					distance: front.distance + 1,
				})
			}
		}

	}

	distances := make([]int, len(poiToDistance))
	for numStr, dist := range poiToDistance {
		distances[cast.ToInt(numStr)] = dist
	}
	return distances
}

var dirs = [][2]int{
	{0, -1},
	{0, 1},
	{1, 0},
	{-1, 0},
}

func dfs(graph [][]int, entryIndex int, visited map[int]bool, returnToZero bool) (minWeightSum int) {
	// if all nodes have been visited, return zero for part 1, or the distance
	// from the entryIndex to the zero POI
	if len(graph) == len(visited) {
		if returnToZero {
			return graph[entryIndex][0]
		}
		return 0
	}

	// get the minimum distance from a recursive call
	minDistance := math.MaxInt32
	for i, val := range graph[entryIndex] {
		if !visited[i] {
			visited[i] = true

			dist := val + dfs(graph, i, visited, returnToZero)
			minDistance = mathy.MinInt(minDistance, dist)

			delete(visited, i)
		}
	}

	return minDistance
}
