package day21

import (
	"adventofcode2024/utils"
	"adventofcode2024/utils/dijkstra"
	"adventofcode2024/utils/memo"
	"fmt"
	"math"
	"strings"
)

type Pair struct {
	s dijkstra.Point
	d dijkstra.Point
}

type Path struct {
	path     []dijkstra.Point
	path_str string
	cost     int
}

type State struct {
	code         string
	depth        int
	isNumericPad bool
}

var numericPad = map[dijkstra.Point]string{
	{X: 0, Y: 0}: "7", {X: 1, Y: 0}: "8", {X: 2, Y: 0}: "9",
	{X: 0, Y: 1}: "4", {X: 1, Y: 1}: "5", {X: 2, Y: 1}: "6",
	{X: 0, Y: 2}: "1", {X: 1, Y: 2}: "2", {X: 2, Y: 2}: "3",
	{X: 1, Y: 3}: "0", {X: 2, Y: 3}: "A",
}

var directionPad = map[dijkstra.Point]string{
	{X: 1, Y: 0}: "^", {X: 2, Y: 0}: "A",
	{X: 0, Y: 1}: "<", {X: 1, Y: 1}: "v", {X: 2, Y: 1}: ">",
}

var directions = []dijkstra.Point{{X: 0, Y: 1}, {X: 0, Y: -1}, {X: 1, Y: 0}, {X: -1, Y: 0}}

var lookup_np = get_paths(numericPad)
var lookup_dp = get_paths(directionPad)

var m *memo.Memo

func init() {
	m = memo.New(get_shortest_path)
}

func Part1(input string) int {
	codes := strings.Split(input, "\n")
	cost := 0
	for _, code := range codes {
		initialState := State{
			code:         code,
			depth:        2,
			isNumericPad: true,
		}
		result := m.Get(initialState).(int)
		cost += result * utils.MustAtoi(code[:len(code)-1])
	}
	return cost
}

func Part2(input string) int {
	codes := strings.Split(input, "\n")
	cost := 0
	for _, code := range codes {
		initialState := State{
			code:         code,
			depth:        25,
			isNumericPad: true,
		}
		result := m.Get(initialState).(int)
		cost += result * utils.MustAtoi(code[:len(code)-1])
	}
	return cost
}

func get_paths(in map[dijkstra.Point]string) map[Pair][]Path {
	lookup := make(map[Pair][]Path)
	data := dijkstra.InputGraph{}
	for p := range in {
		for _, dir := range directions {
			np := dijkstra.Point{X: p.X + dir.X, Y: p.Y + dir.Y}
			if in[np] != "" {
				data.Graph = append(data.Graph, dijkstra.InputData{
					Source:      p,
					Destination: np,
					Weight:      1,
				})
			}
		}
	}
	graph := dijkstra.CreateGraph(data)
	for s := range in {
		for d := range in {
			paths, cost := dijkstra.GetAllShortestPaths(s, d, graph)

			for _, path := range paths {
				path_str := ""
				for x := 1; x < len(path); x++ {
					f := path[x-1]
					t := path[x]

					switch {
					case f.X == t.X && f.Y < t.Y:
						// Moving verticallY down
						path_str += "v"
					case f.X == t.X && f.Y > t.Y:
						// Moving verticallY up
						path_str += "^"
					case f.Y == t.Y && f.X < t.X:
						// Moving horizontallY right
						path_str += ">"
					case f.Y == t.Y && f.X > t.X:
						// Moving horizontally left
						path_str += "<"
					default:
						// No matching case or invalid input
						fmt.Println("Invalid direction")
					}

				}
				lookup[Pair{s, d}] = append(lookup[Pair{s, d}], Path{path, path_str, cost})
			}

		}
	}
	return lookup
}

func find_point(in string, lookup map[dijkstra.Point]string) dijkstra.Point {
	for k, v := range lookup {
		if v == in {
			return k
		}
	}
	return dijkstra.Point{}
}

func get_shortest_path(key interface{}) interface{} {
	state := key.(State)

	if state.depth < 0 {
		return len(state.code)
	}

	// (Optional) If you need to modify the state, work on a copy.
	newState := state
	newState.code = "A" + state.code

	cost := 0
	for x := 0; x<len(newState.code)-1; x++ {
		pps := shortest_paths(string(newState.code[x]), string(newState.code[x+1]), newState.isNumericPad)
		min := math.MaxInt // Start with a very high number not just 65535
		for _, pp := range pps {
			nextState := State{
				code:         pp.path_str + "A",
				depth:        newState.depth - 1,
				isNumericPad: false,
			}
			mm := m.Get(nextState).(int)
			if mm < min {
				min = mm
			}
		}
		cost += min
	}
	return cost
}

func shortest_paths(s, d string, isNumericPad bool) []Path {
	if isNumericPad {
		return lookup_np[Pair{s: find_point(s, numericPad), d: find_point(d, numericPad)}]
	} else {
		return lookup_dp[Pair{s: find_point(s, directionPad), d: find_point(d, directionPad)}]
	}
}
