package day20

import (
	"container/heap"
	"fmt"
	"math"
	"strings"
)

var directions = [][]int{
	{0, 1},  // Right
	{1, 0},  // Down
	{0, -1}, // Left
	{-1, 0}, // Up
}

type State struct {
	row, col, dir, score int
	path                 []string
	//Would rather use tuple as key but cant get to work
	costMap map[[2]int]int
}

type PriorityQueue []State

func Part1(input string) int {
	maze := parseInput(input)
	_, path, costMap := solveMaze(maze)
	cheats := findCheats(path, costMap)
	return cheats
}

func Part2(input string) int {
	maze := parseInput(input)
	_, path, costMap := solveMaze(maze)
	cheats := findCheatsPart2(path, costMap)
	return cheats
}

func parseInput(input string) []string {
	parts := strings.Split(input, "\n")
	maze := []string{}
	maze = append(maze, parts...)

	return maze
}

func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
	return pq[i].score < pq[j].score
}
func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }

func (pq *PriorityQueue) Push(x interface{}) {
	*pq = append(*pq, x.(State))
}

func (pq *PriorityQueue) Pop() interface{} {
	old := *pq
	n := len(old)
	item := old[n-1]
	*pq = old[0 : n-1]
	return item
}

func solveMaze(maze []string) (int, []string, map[[2]int]int) {
	rows := len(maze)
	cols := len(maze[0])

	// Locate start and end points
	var startRow, startCol, endRow, endCol int
	for i := 0; i < rows; i++ {
		for j := 0; j < cols; j++ {
			if maze[i][j] == 'S' {
				startRow, startCol = i, j
			} else if maze[i][j] == 'E' {
				endRow, endCol = i, j
			}
		}
	}

	// Priority queue and visited set
	pq := &PriorityQueue{}
	heap.Init(pq)
	startPath := []string{fmt.Sprintf("(%d,%d)", startRow, startCol)}
	startCostMap := map[[2]int]int{
		{startRow, startCol}: 0,
	}
	heap.Push(pq, State{startRow, startCol, 0, 0, startPath, startCostMap})
	visited := make(map[[3]int]bool)

	for pq.Len() > 0 {
		cur := heap.Pop(pq).(State)

		// If reached the end point, return the score and path
		if cur.row == endRow && cur.col == endCol {
			return cur.score, cur.path, cur.costMap
		}

		// Mark as visited
		key := [3]int{cur.row, cur.col, cur.dir}
		if visited[key] {
			continue
		}
		visited[key] = true

		// Explore moves
		for i := 0; i < 4; i++ {
			// Compute new direction
			newDir := (cur.dir + i) % 4

			// Compute new position
			newRow := cur.row + directions[newDir][0]
			newCol := cur.col + directions[newDir][1]

			// Check bounds and wall
			if newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols && maze[newRow][newCol] != '#' {
				newCost := cur.score + 1
				if oldCost, exists := cur.costMap[[2]int{newRow, newCol}]; !exists || newCost < oldCost {
					// Update cost map
					newCostMap := make(map[[2]int]int)
					for k, v := range cur.costMap {
						newCostMap[k] = v
					}
					newCostMap[[2]int{newRow, newCol}] = newCost

					// Update path
					newPath := append([]string{}, cur.path...)
					newPath = append(newPath, fmt.Sprintf("(%d,%d)", newRow, newCol))

					heap.Push(pq, State{
						newRow,
						newCol,
						newDir,
						newCost,
						newPath,
						newCostMap,
					})
				}
			}
		}
	}

	return -1, nil, nil // No path found
}

func findCheats(path []string, costLookup map[[2]int]int) int {

	totalCheats := 0
	for index, position := range path {

		var row, col int
		//Should be a better way to do this
		fmt.Sscanf(position, "(%d,%d)", &row, &col)

		for _, dir := range directions {
			newRow := row + (dir[0] * 2)
			newCol := col + (dir[1] * 2)

			if cost, exists := costLookup[[2]int{newRow, newCol}]; exists {
				if cost > index && (cost-index-2) >= 100 {
					totalCheats++
				}
			}
		}
	}
	return totalCheats
}

func findCheatsPart2(path []string, costLookup map[[2]int]int) int {
	totalCheats := 0

	for index, position := range path {
		var row, col int
		//Should be a better way to do this
		fmt.Sscanf(position, "(%d,%d)", &row, &col)

		for x1 := row - 20; x1 <= row+20; x1++ {
			for y1 := col - 20 + int(math.Abs(float64(row-x1))); y1 <= col+20-int(math.Abs(float64(row-x1))); y1++ {
				if cost, exists := costLookup[[2]int{x1, y1}]; exists && cost != -1 {
					diff := cost - index - int(math.Abs(float64(row-x1))) - int(math.Abs(float64(col-y1)))
					if cost > index && diff >= 100 {
						totalCheats++
					}
				}
			}
		}
	}

	return totalCheats
}
