package day20

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

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)
	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 {
	const saves int = 100
	count := 0
	maze := Maze{}.Parse(input)
	//	fmt.Println(maze)
	data := inputGraph(*maze)
	graph := dijkstra.CreateGraph(data)
	path, dist := dijkstra.GetShortestPath(data.From, data.To, graph)
	cost := make(map[Vec]int)
	cheats := []Cheat{}

	for i, p := range path {
		cost[Vec{p.X, p.Y}] = i
	}
	cheat_data := cheatGraph(*maze)

	for _, s := range path[:len(path)-saves] {
		fmt.Println("Checking start:", s)
		for _, e := range path[saves:] {
			fmt.Println("\tChecking end", e)
			cheat_data_1 := cheatGraphAdd(maze, cheat_data, s)
			cheat_data_2 := cheatGraphAdd(maze, cheat_data_1, e)
			cheat_graph := dijkstra.CreateGraph(cheat_data_2)
			_, cheat_dist := dijkstra.GetShortestPath(dijkstra.Point{X: s.X, Y: s.Y}, dijkstra.Point{X: e.X, Y: e.Y}, cheat_graph)
			fmt.Println("\t\tcheat distance:", cheat_dist)
			if cheat_dist <= 20 {
				cheat := Cheat{start: Vec{s.X, s.Y}, end: Vec{e.X, e.Y}, cost: cost[Vec{s.X, s.Y}] + cheat_dist + dist - cost[Vec{e.X, e.Y}]}
				fmt.Println("\t\tSaving cheat:", cheat)
				cheats = append(cheats, cheat)
			}
		}
	}
	for _, cheat := range cheats {
		save := dist - cheat.cost
		//		fmt.Println("[", i, "]", "save", save)
		if save >= saves {
			count++
		}
	}
	return count
}

func abs(in int) int {
	if in < 0 {
		return in * -1
	}
	return in
}

func inputGraph(maze Maze) dijkstra.InputGraph {
	data := dijkstra.InputGraph{}
	data.From = dijkstra.Point{X: maze.start.x, Y: maze.start.y}
	data.To = dijkstra.Point{X: maze.end.x, Y: maze.end.y}
	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
				}
				data.Graph = append(data.Graph, dijkstra.InputData{
					Source:      dijkstra.Point{X: x, Y: y},
					Destination: dijkstra.Point{X: nx, Y: ny},
					Weight:      1,
				})
			}
		}
	}

	return data
}

func cheatGraph(maze Maze) dijkstra.InputGraph {
	data := dijkstra.InputGraph{}
	for y := 0; y < maze.height; y++ {
		for x := 0; x < maze.width; x++ {
			if maze.grid.Get(x, y) == CORRIDOR {
				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) == CORRIDOR {
					continue
				}
				data.Graph = append(data.Graph, dijkstra.InputData{
					Source:      dijkstra.Point{X: x, Y: y},
					Destination: dijkstra.Point{X: nx, Y: ny},
					Weight:      1,
				})
			}
		}
	}

	return data
}

func cheatGraphAdd(maze *Maze, in dijkstra.InputGraph, s dijkstra.Point) dijkstra.InputGraph {
	x := s.X
	y := s.Y
	out := dijkstra.InputGraph{}
	out.From = in.From
	out.To = in.To
	out.Graph = append([]dijkstra.InputData{}, in.Graph...)

	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) == CORRIDOR {
			continue
		}
		out.Graph = append(out.Graph, dijkstra.InputData{
			Source:      dijkstra.Point{X: x, Y: y},
			Destination: dijkstra.Point{X: nx, Y: ny},
			Weight:      1,
		})
	}
	return out
}

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
}
