package main

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

func main() {
	fmt.Println(part1(readData("example.txt")))
	fmt.Println(part1(readData("data.txt")))
	fmt.Println(part2(readData("example.txt")))
	fmt.Println(part2(readData("data.txt")))
}

func part1(tiles [][]byte, x, y int) (visited int) {
	directions := [4][2]int{
		{0, -1},
		{1, 0},
		{0, 1},
		{-1, 0},
	}

	dir := 0
	dx := directions[dir][0]
	dy := directions[dir][1]

	for {
		if tiles[y][x] != 'X' {
			tiles[y][x] = 'X'
			visited++
		}

		if y+dy < 0 || y+dy >= len(tiles) {
			return
		}

		if x+dx < 0 || x+dx >= len(tiles[y+dy]) {
			return
		}

		if tiles[y+dy][x+dx] == '#' {
			dir = (dir + 1) % 4
			dx = directions[dir][0]
			dy = directions[dir][1]
		} else {
			x += dx
			y += dy
		}
	}
}

type State struct {
	x, y int
	dir  int
}

func (state *State) GetDirection() (dx, dy int) {
	directions := [4][2]int{
		{0, -1},
		{1, 0},
		{0, 1},
		{-1, 0},
	}
	dx = directions[state.dir][0]
	dy = directions[state.dir][1]
	return
}

func (state *State) GetNextPosition() (x, y int) {
	dx, dy := state.GetDirection()
	x = state.x + dx
	y = state.y + dy
	return
}

func (state *State) IsAboutToExit(tiles [][]byte) bool {
	x, y := state.GetNextPosition()
	if y < 0 || y >= len(tiles) {
		return true
	}
	if x < 0 || x >= len(tiles[y]) {
		return true
	}
	return false
}

func (state *State) HasObstacleInFront(tiles [][]byte) bool {
	x, y := state.GetNextPosition()
	return tiles[y][x] == '#'
}

func (state *State) Clone() State {
	return State{
		state.x, state.y, state.dir,
	}
}

func (state *State) Rotate() {
	state.dir = (state.dir + 1) % 4
}

func copyVisited(visited [][]byte) (visitedCopy [][]byte) {
	visitedCopy = make([][]byte, len(visited))
	for i, sublist := range visited {
		visitedCopy[i] = make([]byte, len(sublist))
		copy(visitedCopy[i], sublist)
	}
	return
}

func canReachExit(tiles [][]byte, visited [][]byte, state State) bool {
	for !state.IsAboutToExit(tiles) {
		visitedBit := byte(1) << state.dir
		if visited[state.y][state.x]&visitedBit != 0 {
			return false
		}
		visited[state.y][state.x] |= visitedBit

		if state.HasObstacleInFront(tiles) {
			state.Rotate()
			continue
		}

		nextX, nextY := state.GetNextPosition()
		state.x = nextX
		state.y = nextY
	}
	return true
}

func part2(tiles [][]byte, startX, startY int) (positionsLooping int) {
	state := State{startX, startY, 0}

	visited := make([][]byte, len(tiles))
	for i := range visited {
		visited[i] = make([]byte, len(tiles[i]))
	}

	for !state.IsAboutToExit(tiles) {
		visitedBit := byte(1) << state.dir
		if visited[state.y][state.x]&visitedBit != 0 {
			panic("Already visited???")
		}
		visited[state.y][state.x] |= visitedBit

		if state.HasObstacleInFront(tiles) {
			state.Rotate()
			continue
		}

		nextX, nextY := state.GetNextPosition()
		if visited[nextY][nextX] == 0 {
			tiles[nextY][nextX] = '#'

			stateCopy := state.Clone()
			stateCopy.Rotate()
			if !canReachExit(tiles, copyVisited(visited), stateCopy) {
				positionsLooping++
			}
			tiles[nextY][nextX] = '.'
		}

		state.x = nextX
		state.y = nextY
	}

	return
}

func readData(fileName string) (tiles [][]byte, startX, startY int) {
	fp, err := os.Open(fileName)
	if err != nil {
		panic(err)
	}

	scanner := bufio.NewScanner(fp)

	y := 0
	for scanner.Scan() {
		line := []byte(strings.TrimSpace(scanner.Text()))

		x := slices.Index(line, '^')
		if x != -1 {
			startX = x
			startY = y
		}

		tiles = append(tiles, line)
		y += 1
	}

	return
}