package day16

import (
	"adventofcode2024/utils/dijkstra"
	"adventofcode2024/utils/grid2d"
	"adventofcode2024/utils/inputs"
	"fmt"
	_ "strings"
)

type (
	Vec  struct{ x, y int }
	Dir  struct{ x, y int }
	Cell rune
	Move struct {
		p Vec
		d Dir
	}
	Maze struct {
		height, width int
		start         Vec
		end           Vec
		grid          *grid2d.Grid[Cell]
	}
)

const (
	WALL     Cell = '#'
	CORRIDOR Cell = '.'
	START    Cell = 'S'
	END      Cell = 'E'
	PATH     Cell = '*'
)

var (
	NORTH      = Dir{0, -1}
	EAST       = Dir{1, 0}
	SOUTH      = Dir{0, 1}
	WEST       = Dir{-1, 0}
	DIRECTIONS = []Dir{NORTH, EAST, SOUTH, WEST}
)

func Part1(input string) int {
	maze := Maze{}.Parse(input)
	fmt.Println(maze)
	data := inputGraph(*maze)
	graph := dijkstra.CreateGraph(data)
	data.From.Label = "EAST"
	path, dist := dijkstra.GetShortestPath(data.From, data.To, graph)
	for _, p := range path {
		maze.grid.Set(p.X, p.Y, PATH)
	}
	fmt.Println(maze)
	return dist
}
func Part2(input string) int {
	maze := Maze{}.Parse(input)
	fmt.Println(maze)
	data := inputGraph(*maze)
	graph := dijkstra.CreateGraph(data)
	data.From.Label = "START"
	data.To.Label = "NORTH"
	paths, _ := dijkstra.GetAllShortestPaths(data.From, data.To, graph)
	bestPaths := make(map[Vec]struct{})
	for _, path := range paths {
		for _, node := range path {
			bestPaths[Vec{x: node.X, y: node.Y}] = struct{}{}
		}
	}
	return len(bestPaths)
}

func inputGraph(maze Maze) dijkstra.InputGraph {
	data := dijkstra.InputGraph{}
	data.From = dijkstra.Point{X: maze.start.x, Y: maze.start.y, Label: "START"}
	data.To = dijkstra.Point{X: maze.end.x, Y: maze.end.y, Label: "END"}
	for y := 0; y < maze.height; y++ {
		for x := 0; x < maze.width; x++ {
			if maze.grid.Get(x, y) == WALL {
				continue
			}
			for _, dir := range DIRECTIONS {
				nx, ny := x+dir.x, y+dir.y
				if nx < 0 || ny < 0 || nx >= maze.width || ny >= maze.height {
					continue
				}
				if maze.grid.Get(nx, ny) == WALL {
					continue
				}
				for _, d := range getAllowedDirections(dir) {
					ss := dirToString(dir)
					ds := dirToString(d)
					data.Graph = append(data.Graph, dijkstra.InputData{
						Source:      dijkstra.Point{X: x, Y: y, Label: ss},
						Destination: dijkstra.Point{X: nx, Y: ny, Label: ds},
						Weight: func() int {
							if dir == d {
								return 1
							} else {
								return 1001
							}
						}(),
					})
				}
			}
		}
	}

	data.Graph = append(data.Graph, dijkstra.InputData{
		Source:      dijkstra.Point{X: maze.end.x, Y: maze.end.y, Label: "END"},
		Destination: dijkstra.Point{X: maze.end.x, Y: maze.end.y, Label: dirToString(EAST)},
		Weight:      0,
	})
	data.Graph = append(data.Graph, dijkstra.InputData{
		Source:      dijkstra.Point{X: maze.end.x, Y: maze.end.y, Label: "END"},
		Destination: dijkstra.Point{X: maze.end.x, Y: maze.end.y, Label: dirToString(WEST)},
		Weight:      0,
	})
	data.Graph = append(data.Graph, dijkstra.InputData{
		Source:      dijkstra.Point{X: maze.end.x, Y: maze.end.y, Label: "END"},
		Destination: dijkstra.Point{X: maze.end.x, Y: maze.end.y, Label: dirToString(NORTH)},
		Weight:      0,
	})
	data.Graph = append(data.Graph, dijkstra.InputData{
		Source:      dijkstra.Point{X: maze.end.x, Y: maze.end.y, Label: "END"},
		Destination: dijkstra.Point{X: maze.end.x, Y: maze.end.y, Label: dirToString(SOUTH)},
		Weight:      0,
	})

	data.Graph = append(data.Graph, dijkstra.InputData{
		Source:      dijkstra.Point{X: maze.start.x, Y: maze.start.y, Label: "START"},
		Destination: dijkstra.Point{X: maze.start.x, Y: maze.start.y, Label: dirToString(EAST)},
		Weight:      0,
	})
	data.Graph = append(data.Graph, dijkstra.InputData{
		Source:      dijkstra.Point{X: maze.start.x, Y: maze.start.y, Label: "START"},
		Destination: dijkstra.Point{X: maze.start.x, Y: maze.start.y, Label: dirToString(WEST)},
		Weight:      2000,
	})
	data.Graph = append(data.Graph, dijkstra.InputData{
		Source:      dijkstra.Point{X: maze.start.x, Y: maze.start.y, Label: "START"},
		Destination: dijkstra.Point{X: maze.start.x, Y: maze.start.y, Label: dirToString(NORTH)},
		Weight:      1000,
	})
	data.Graph = append(data.Graph, dijkstra.InputData{
		Source:      dijkstra.Point{X: maze.start.x, Y: maze.start.y, Label: "START"},
		Destination: dijkstra.Point{X: maze.start.x, Y: maze.start.y, Label: dirToString(SOUTH)},
		Weight:      1000,
	})

	data.Graph = append(data.Graph, dijkstra.InputData{
		Source:      dijkstra.Point{X: maze.start.x, Y: maze.start.y, Label: "EAST"},
		Destination: dijkstra.Point{X: maze.start.x, Y: maze.start.y, Label: "NORTH"},
		Weight:      1000,
	})

	return data
}

func dirToString(dir Dir) string {
	switch dir {
	case NORTH:
		return "NORTH"
	case EAST:
		return "EAST"
	case SOUTH:
		return "SOUTH"
	case WEST:
		return "WEST"
	default:
		return "UNKNOWN"
	}
}

func getAllowedDirections(direction Dir) []Dir {
	switch direction {
	case NORTH:
		return []Dir{NORTH, EAST, WEST}
	case SOUTH:
		return []Dir{SOUTH, EAST, WEST}
	case WEST:
		return []Dir{WEST, NORTH, SOUTH}
	case EAST:
		return []Dir{EAST, NORTH, SOUTH}
	}
	return []Dir{}
}

func (m Maze) Parse(input string) *Maze {
	m.grid = inputs.ToGrid2D(input, "\n", "", '?', func(c string) Cell { return Cell(c[0]) })
	m.height, m.width = m.grid.SizeY(), m.grid.SizeX()
	for y := 0; y < m.height; y++ {
		for x := 0; x < m.width; x++ {
			c := m.grid.Get(x, y)
			switch c {
			case WALL, CORRIDOR:
				m.grid.Set(x, y, c)
			case START:
				m.grid.Set(x, y, CORRIDOR)
				m.start = Vec{x, y}
			case END:
				m.grid.Set(x, y, CORRIDOR)
				m.end = Vec{x, y}
			}
		}
	}
	return &m
}

func (m *Maze) String() string {
	s := ""
	for y := 0; y < m.height; y++ {
		for x := 0; x < m.width; x++ {
			if y == m.start.y && x == m.start.x {
				s += string(START)
			} else if y == m.end.y && x == m.end.x {
				s += string(END)
			} else {
				s += string(m.grid.Get(x, y))
			}
		}
		s += "\n"
	}
	return s
}
