package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	{
		exampleFloor := readFloor("example_map.txt")
		exampleMoves, _ := os.ReadFile("example_moves.txt")
		fmt.Println(part1(exampleFloor, exampleMoves), 10092)
	}

	{
		dataFloor := readFloor("data_map.txt")
		dataMoves, _ := os.ReadFile("data_moves.txt")
		fmt.Println(part1(dataFloor, dataMoves), 1492518)
	}

	// {
	// 	var simpleFloor1 [][]byte
	// 	simpleFloor1 = append(simpleFloor1, []byte("##########"))
	// 	simpleFloor1 = append(simpleFloor1, []byte("#........#"))
	// 	simpleFloor1 = append(simpleFloor1, []byte("#........#"))
	// 	simpleFloor1 = append(simpleFloor1, []byte("#.....O..#"))
	// 	simpleFloor1 = append(simpleFloor1, []byte("#.....O..#"))
	// 	simpleFloor1 = append(simpleFloor1, []byte("#.....O..#"))
	// 	simpleFloor1 = append(simpleFloor1, []byte("#.....@..#"))
	// 	simpleFloor1 = append(simpleFloor1, []byte("#........#"))
	// 	simpleFloor1 = append(simpleFloor1, []byte("#........#"))
	// 	simpleFloor1 = append(simpleFloor1, []byte("##########"))

	// 	simpleFloor := makeWide(simpleFloor1)
	// 	simpleMoves := []byte("^^")
	// 	part2(simpleFloor, simpleMoves)
	// }

	{
		exampleFloor := makeWide(readFloor("example_map.txt"))
		exampleMoves, _ := os.ReadFile("example_moves.txt")
		fmt.Println(part2(exampleFloor, exampleMoves), 9021)
	}

	{
		dataFloor := makeWide(readFloor("data_map.txt"))
		dataMoves, _ := os.ReadFile("data_moves.txt")
		fmt.Println(part2(dataFloor, dataMoves))
	}
}

func part1(floor [][]byte, moves []byte) (result int) {
	botx, boty := findInitialPosition(floor)

	doMoves(moves, botx, boty, floor, doMovePart1)

	for y, line := range floor {
		for x, c := range line {
			if c == 'O' {
				result += y*100 + x
			}
		}
	}

	return
}

func doMovePart1(floor [][]byte, startx, starty int, dx, dy int) (endx, endy int) {
	x := startx
	y := starty
	moved := 0
	for {
		moved += 1
		x += dx
		y += dy

		if floor[y][x] == '#' {
			return startx, starty
		}

		if floor[y][x] == '.' {
			if moved > 1 {
				floor[y][x] = 'O'
			}
			floor[starty+dy][startx+dx] = '@'
			floor[starty][startx] = '.'
			return startx + dx, starty + dy
		}
	}
}

func doMoves(moves []byte, botx int, boty int, floor [][]byte, doMoveFn func([][]byte, int, int, int, int) (int, int)) {
	for _, move := range moves {
		switch move {
		case '^':
			botx, boty = doMoveFn(floor, botx, boty, 0, -1)
		case 'v':
			botx, boty = doMoveFn(floor, botx, boty, 0, 1)
		case '<':
			botx, boty = doMoveFn(floor, botx, boty, -1, 0)
		case '>':
			botx, boty = doMoveFn(floor, botx, boty, 1, 0)
		}
	}
}

func findInitialPosition(floor [][]byte) (int, int) {
	var botx, boty int
	for y, line := range floor {
		idx := strings.IndexByte(string(line), '@')
		if idx >= 0 {
			botx = idx
			boty = y
			break
		}
	}
	return botx, boty
}

type QueuedMove struct {
	x, y int
	c    byte
}

func part2(floor [][]byte, moves []byte) (result int) {
	var botx, boty int
	for y, line := range floor {
		idx := strings.IndexByte(string(line), '@')
		if idx >= 0 {
			botx = idx
			boty = y
			break
		}
	}

	for _, move := range moves {
		var queued []QueuedMove
		var dx, dy int
		switch move {
		case '^':
			dx, dy = 0, -1
		case 'v':
			dx, dy = 0, 1
		case '<':
			dx, dy = -1, 0
		case '>':
			dx, dy = 1, 0
		default:
			continue
		}

		if doMovePart2(floor, botx+dx, boty+dy, dx, dy, &queued) {
			hasMadeMove := make(map[QueuedMove]bool)
			for i := range queued {
				m := queued[len(queued)-1-i]
				_, alreadyMade := hasMadeMove[m]
				if !alreadyMade {
					floor[m.y][m.x] = m.c
					floor[m.y-dy][m.x-dx] = '.'
					hasMadeMove[m] = true
				}
			}

			floor[boty][botx] = '.'
			botx += dx
			boty += dy
			floor[boty][botx] = '@'
		}

		if !checkIntegrity(floor) {
			panic("no")
		}
	}

	for y, line := range floor {
		for x, c := range line {
			if c == '[' {
				result += y*100 + x
			}
		}
	}

	return
}

func printMap(floor [][]byte) {
	for _, line := range floor {
		fmt.Println(string(line))
	}
}

func checkIntegrity(floor [][]byte) bool {
	for _, line := range floor {
		for x, c := range line {
			if c == '[' && line[x+1] != ']' {
				return false
			}
		}
	}
	return true
}

func enqueueMove(x, y int, c byte, queued *[]QueuedMove) {
	*queued = append(*queued, QueuedMove{x, y, c})
}

func doMovePart2(floor [][]byte, x, y int, dx, dy int, queued *[]QueuedMove) bool {
	switch floor[y][x] {
	case '.':
		return true
	case '#':
		return false
	case '[':
		if dx == 0 {
			enqueueMove(x, y+dy, '[', queued)
			enqueueMove(x+1, y+dy, ']', queued)
			return doMovePart2(floor, x, y+dy, dx, dy, queued) && doMovePart2(floor, x+1, y+dy, dx, dy, queued)
		} else if dx > 0 {
			enqueueMove(x+1, y, '[', queued)
			enqueueMove(x+2, y, ']', queued)
			return doMovePart2(floor, x+2, y, dx, dy, queued)
		}
	case ']':
		if dx == 0 {
			enqueueMove(x-1, y+dy, '[', queued)
			enqueueMove(x, y+dy, ']', queued)
			return doMovePart2(floor, x-1, y+dy, dx, dy, queued) && doMovePart2(floor, x, y+dy, dx, dy, queued)
		} else if dx < 0 {
			enqueueMove(x-1, y, ']', queued)
			enqueueMove(x-2, y, '[', queued)
			return doMovePart2(floor, x-2, y, dx, dy, queued)
		}
	}
	return false
}

func makeWide(floor [][]byte) (wide [][]byte) {
	wide = make([][]byte, len(floor))
	for y, line := range floor {
		w := make([]byte, len(line)*2)
		for i, c := range line {
			switch c {
			case '#':
				w[i*2+0] = '#'
				w[i*2+1] = '#'
			case '.':
				w[i*2+0] = '.'
				w[i*2+1] = '.'
			case 'O':
				w[i*2+0] = '['
				w[i*2+1] = ']'
			case '@':
				w[i*2+0] = '@'
				w[i*2+1] = '.'
			}
		}
		wide[y] = w
	}
	return
}

func readFloor(fileName string) (floor [][]byte) {
	fp, _ := os.Open(fileName)
	scanner := bufio.NewScanner(fp)
	for scanner.Scan() {
		floor = append(floor, []byte(strings.TrimSpace(scanner.Text())))
	}
	return
}