package main

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

// SeatMap ...
type SeatMap []rune

func (s SeatMap) ray(stride int, x, y int, dir [2]int) ([2]int, bool) {
	h := len(s) / stride
	x += dir[0]
	y += dir[1]
	for y >= 0 && y < h && x >= 0 && x < stride {
		r := s[(y*stride)+x]
		if r == '#' {
			return [2]int{x, y}, true
		} else if r == 'L' {
			break
		}

		x += dir[0]
		y += dir[1]
	}
	return [2]int{-1, -1}, false
}

// StepAdjacent ...
func (s SeatMap) StepAdjacent(stride int) bool {
	oldSeatMap := make(SeatMap, len(s))
	copy(oldSeatMap, s)

	var dirty bool
	for y := 0; y < len(s)/stride; y++ {
		i := y * stride
		for x := 0; x < stride; x++ {
			j := i + x
			if oldSeatMap[j] == '.' {
				continue
			}

			var numTakenSeats int
			for v := y - 1; v < y+2; v++ {
				for u := x - 1; u < x+2; u++ {
					if u == x && v == y {
						continue
					}

					if u >= 0 && u < stride && v >= 0 && v < len(s)/stride {
						if oldSeatMap[(v*stride)+u] == '#' {
							numTakenSeats++
						}
					}
				}
			}

			if oldSeatMap[j] == 'L' && numTakenSeats == 0 {
				s[j] = '#'
				dirty = true
			} else if oldSeatMap[j] == '#' && numTakenSeats >= 4 {
				s[j] = 'L'
				dirty = true
			}
		}
	}

	return dirty
}

// StepRay ...
func (s SeatMap) StepRay(stride int) bool {
	rays := [][2]int{
		{-1, -1},
		{0, -1},
		{1, -1},
		{1, 0},
		{1, 1},
		{0, 1},
		{-1, 1},
		{-1, 0},
	}

	oldSeatMap := make(SeatMap, len(s))
	copy(oldSeatMap, s)

	var dirty bool
	for y := 0; y < len(s)/stride; y++ {
		i := y * stride
		for x := 0; x < stride; x++ {
			j := i + x
			if oldSeatMap[j] == '.' {
				continue
			}

			var numTakenSeats int
			for _, ray := range rays {
				if _, ok := oldSeatMap.ray(stride, x, y, ray); ok {
					numTakenSeats++
				}
			}

			if oldSeatMap[j] == 'L' && numTakenSeats == 0 {
				s[j] = '#'
				dirty = true
			} else if oldSeatMap[j] == '#' && numTakenSeats >= 5 {
				s[j] = 'L'
				dirty = true
			}
		}
	}

	return dirty
}

func (s SeatMap) String(stride int) string {
	var result strings.Builder
	result.Grow(len(s) * 2)

	fmt.Fprintf(&result, "╭")
	for x := 1; x < stride+3; x++ {
		fmt.Fprintf(&result, "─")
	}
	fmt.Fprintf(&result, "╮\n")

	for y := 0; y < len(s)/stride; y++ {
		fmt.Fprintf(&result, "│ ")

		i := y * stride
		for x := 0; x < stride; x++ {
			fmt.Fprintf(&result, "%c", s[i+x])
		}

		fmt.Fprintf(&result, " │\n")
	}

	fmt.Fprintf(&result, "╰")
	for x := 1; x < stride+3; x++ {
		fmt.Fprintf(&result, "─")
	}
	fmt.Fprintf(&result, "╯")

	return result.String()
}

// Task1 ...
func Task1(seatMap SeatMap, stride int, print bool) int {
	if print {
		fmt.Println(seatMap.String(stride))
	}
	for seatMap.StepAdjacent(stride) {
		if print {
			fmt.Println(seatMap.String(stride))
		}
	}

	var result int
	for _, r := range seatMap {
		if r == '#' {
			result++
		}
	}
	return result
}

// Task2 ...
func Task2(seatMap SeatMap, stride int, print bool) int {
	if print {
		fmt.Println(seatMap.String(stride))
	}
	for seatMap.StepRay(stride) {
		if print {
			fmt.Println(seatMap.String(stride))
		}
	}

	var result int
	for _, r := range seatMap {
		if r == '#' {
			result++
		}
	}
	return result
}

func main() {
	file, _ := os.Open("input.txt")
	scanner := bufio.NewScanner(file)

	var stride int
	input := make([]rune, 0, 100)
	for scanner.Scan() {
		line := scanner.Text()
		if stride == 0 {
			stride = len(line)
		}

		input = append(input, []rune(line)...)
	}
	file.Close()

	seatMap := make(SeatMap, len(input))
	copy(seatMap, input)
	result := Task1(seatMap, stride, false)
	fmt.Printf("Task 1: %d\n", result)

	copy(seatMap, input)
	result = Task2(seatMap, stride, false)
	fmt.Printf("Task 2: %d\n", result)
}