package main

import (
	"bufio"
	"fmt"
	"os"
	"time"
)

const (
	DIR_N  = -1i
	DIR_NE = 1 - 1i
	DIR_E  = 1
	DIR_SE = 1 + 1i
	DIR_S  = 1i
	DIR_SW = -1 + 1i
	DIR_W  = -1
	DIR_NW = -1 - 1i

	CLEAR_SCREEN = "\033[H\033[2J"
)

var scan []byte
var next []byte
var prevScans [][]byte
var width, height int

func main() {
	stdinToByteSlice()
	part1()
	part2()
}

func part1() {
	printScan()
	for i := 0; i < 10; i++ {
		fmt.Print(CLEAR_SCREEN)
		scan = tickToNext()
		printScan()
		time.Sleep(time.Millisecond * 250)
	}
	var ttlOpen, ttlTrees, ttlLmbr int
	for i := range scan {
		switch getByte(getPosFromInt(i)) {
		case '.':
			ttlOpen++
		case '|':
			ttlTrees++
		case '#':
			ttlLmbr++
		}
	}
	_ = ttlOpen
	fmt.Println("= Part 1 =")
	fmt.Println(ttlTrees * ttlLmbr)
}

func part2() {
	var isDupe bool
	var i int
	target := 1000000000
	for i = 0; i < target; i++ {
		next := tickToNext()
		if isDupe, prevScans = checkDuplicateState(next, prevScans); isDupe {
			i++
			scan = next
			break
		}
		prevScans = append(prevScans, next)
		scan = next
	}
	// We need to find the state after `target` increments
	scan = prevScans[(target-i)%len(prevScans)]
	var ttlOpen, ttlTrees, ttlLmbr int
	for i := range scan {
		switch getByte(getPosFromInt(i)) {
		case '.':
			ttlOpen++
		case '|':
			ttlTrees++
		case '#':
			ttlLmbr++
		}
	}
	_ = ttlOpen
	fmt.Println("= Part 2 =")
	fmt.Println(ttlTrees * ttlLmbr)
}

// checkDuplicateState returns the slice of prev that has all remaining available states
func checkDuplicateState(s []byte, prev [][]byte) (bool, [][]byte) {
	for i, v := range prev {
		if areasAreEqual(s, v) {
			return true, prev[i:]
		}
	}
	return false, prev
}

func areasAreEqual(a1, a2 []byte) bool {
	for i := range a1 {
		if a1[i] != a2[i] {
			return false
		}
	}
	return true
}

func tickToNext() []byte {
	var ret []byte
	for i := 0; i < len(scan); i++ {
		c := getPosFromInt(i)
		_, sTree, sLmbr := getSurroundingCounts(c)
		b := getByte(c)
		switch b {
		case '.':
			if sTree >= 3 {
				ret = append(ret, '|')
			} else {
				ret = append(ret, '.')
			}
		case '|':
			if sLmbr >= 3 {
				ret = append(ret, '#')
			} else {
				ret = append(ret, '|')
			}
		case '#':
			if sLmbr > 0 && sTree > 0 {
				ret = append(ret, '#')
			} else {
				ret = append(ret, '.')
			}
		}
	}
	return ret
}

// getSurroundingCounts takes a pos and returns:
//	Number of open areas
//	Number of trees
//  Number of lumberyards
// Surrounding that spot
func getSurroundingCounts(c complex64) (int, int, int) {
	var sOpen, sTree, sLmbr int
	for _, v := range []complex64{DIR_NW, DIR_N, DIR_NE, DIR_W, DIR_E, DIR_SW, DIR_S, DIR_SE} {
		switch getByte(c + v) {
		case '.':
			sOpen++
		case '|':
			sTree++
		case '#':
			sLmbr++
		}
	}
	return sOpen, sTree, sLmbr
}

func printScan() {
	for i := 0; i < len(scan)/width; i++ {
		fmt.Println(string(scan[i*width : (i+1)*width]))
	}
}

// getByte pulls a byte from the given position in the scan
func getByte(pos complex64) byte {
	//idx := int(real(pos)) + int(imag(pos))*width
	if int(real(pos)) < 0 || int(imag(pos)) < 0 || int(real(pos)) >= width || int(imag(pos)) >= height {
		return 0
	}
	return scan[int(real(pos))+int(imag(pos))*width]
}

func getPosComplex(x, y int) complex64 {
	return complex(float32(x), float32(y))
}

func getPosFromInt(i int) complex64 {
	return complex(float32(i%width), float32(i/width))
}

func getIdxFromPos(pos complex64) int {
	return int(real(pos)) + int(imag(pos))*width
}

func getCoordString(p complex64) string {
	return fmt.Sprintf("(%d,%d [%d])", int(real(p)), int(imag(p)), getIdxFromPos(p))
}

func stdinToByteSlice() {
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		s := scanner.Bytes()
		if width == 0 {
			width = len(s)
		}
		scan = append(scan, s...)
		height++
	}
}
