package main

/* OpenGL & Go Tutorial
 * https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-1-hello-opengl
 *
 * Vertex/Fragment Shader Explanation
 * https://www.quora.com/What-is-a-vertex-shader-and-what-is-a-fragment-shader/answer/Harold-Serrano?srid=aVb
 */

import (
	"bufio"
	"fmt"
	"log"
	"math/rand"
	"os"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/go-gl/gl/v2.1/gl"
	"github.com/go-gl/glfw/v3.1/glfw"
)

// gl_Position = vec4(vp, 1.0);
const (
	width              = 500
	height             = 500
	vertexShaderSource = `
		#version 410
		in vec3 vp;
		void main() {
			gl_Position = vec4(vp, 1.0);
		}
	` + "\x00"
	fragmentShaderSource = `
		#version 410
		out vec4 frag_colour;
		void main() {
			frag_colour = vec4(1.0, 1.0, 1.0, 1.0);
		}
	` + "\x00"

	rows = 128
	cols = 128
	fps  = 10

	threshold = 0.15
)

var (
	square = []float32{
		-0.5, 0.5, 0,
		-0.5, -0.5, 0,
		0.5, -0.5, 0,

		-0.5, 0.5, 0,
		0.5, 0.5, 0,
		0.5, -0.5, 0,
	}
)

var diskGrid map[string]bool
var groups map[string]int
var inp string

func init() {
	runtime.LockOSThread()

	inp = "vbqugkhl" // My puzzle input
	if len(os.Args) > 1 {
		inp = os.Args[1]
	}
}

func main() {
	diskGrid = make(map[string]bool)
	groups = make(map[string]int)

	in := inp
	window := initGlfw()
	defer glfw.Terminate()

	program := initOpenGL()

	log.Println("Building DiskGrid...")
	var grpCnt int
	for i := 0; i < 128; i++ {
		row := GetBinaryString(KnotHash(fmt.Sprintf("%s-%d", in, i)))
		for j := range row {
			diskGrid[cs(i, j)] = (row[j] == '1')
			if row[j] == '1' {
				groups[cs(i, j)] = grpCnt
				grpCnt++
			}
		}
	}

	cells := makeCells()
	for !window.ShouldClose() {
		t := time.Now()

		//ReduceGroups()

		for x := range cells {
			for _, c := range cells[x] {
				c.checkState(cells)
			}
		}

		draw(cells, window, program)

		time.Sleep(time.Second/time.Duration(fps) - time.Since(t))
	}
}

// initGlfw initializes glfw and returns a Window to use.
func initGlfw() *glfw.Window {
	if err := glfw.Init(); err != nil {
		panic(err)
	}

	glfw.WindowHint(glfw.Resizable, glfw.False)
	glfw.WindowHint(glfw.ContextVersionMajor, 4)
	glfw.WindowHint(glfw.ContextVersionMinor, 1)
	glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
	glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)

	window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
	if err != nil {
		panic(err)
	}
	window.MakeContextCurrent()

	return window
}

// initOpenGL initializes OpenGL and returns an initialized program.
func initOpenGL() uint32 {
	if err := gl.Init(); err != nil {
		panic(err)
	}
	version := gl.GoStr(gl.GetString(gl.VERSION))
	log.Println("OpenGL version", version)

	vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
	if err != nil {
		panic(err)
	}
	fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
	if err != nil {
		panic(err)
	}

	prog := gl.CreateProgram()
	gl.AttachShader(prog, vertexShader)
	gl.AttachShader(prog, fragmentShader)
	gl.LinkProgram(prog)
	return prog
}

func draw(cells [][]*cell, window *glfw.Window, program uint32) {
	gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
	gl.UseProgram(program)

	for x := range cells {
		for _, c := range cells[x] {
			c.draw()
		}
	}

	glfw.PollEvents()
	window.SwapBuffers()
}

// makeVao initializes and returns a vertex array from the points provided.
func makeVao(points []float32) uint32 {
	var vbo uint32
	gl.GenBuffers(1, &vbo)
	gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
	gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)

	var vao uint32
	gl.GenVertexArrays(1, &vao)
	gl.BindVertexArray(vao)
	gl.EnableVertexAttribArray(0)
	gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
	gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)

	return vao
}

func compileShader(source string, shaderType uint32) (uint32, error) {
	shader := gl.CreateShader(shaderType)

	csources, free := gl.Strs(source)
	gl.ShaderSource(shader, 1, csources, nil)
	free()
	gl.CompileShader(shader)

	var status int32
	gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
	if status == gl.FALSE {
		var logLength int32
		gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)

		log := strings.Repeat("\x00", int(logLength+1))
		gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))

		return 0, fmt.Errorf("failed to compile %v: %v", source, log)
	}

	return shader, nil
}

func makeCells() [][]*cell {
	rand.Seed(time.Now().UnixNano())

	cells := make([][]*cell, rows, rows)
	for x := 0; x < rows; x++ {
		for y := 0; y < cols; y++ {
			var gr int
			var ok bool
			c := newCell(x, y)
			if gr, ok = groups[cs(x, y)]; ok {
				c.group = gr
			}

			c.alive = rand.Float64() < threshold
			c.aliveNext = c.alive

			cells[x] = append(cells[x], c)
		}
	}

	return cells
}

type cell struct {
	drawable uint32

	alive     bool
	aliveNext bool

	group int

	x int
	y int
}

func newCell(x, y int) *cell {
	points := make([]float32, len(square), len(square))
	copy(points, square)

	for i := 0; i < len(points); i++ {
		var position float32
		var size float32
		switch i % 3 {
		case 0:
			size = 1.0 / float32(cols)
			position = float32(x) * size
		case 1:
			size = 1.0 / float32(rows)
			position = float32(y) * size
		default:
			continue
		}

		if points[i] < 0 {
			points[i] = (position * 2) - 1
		} else {
			points[i] = ((position + size) * 2) - 1
		}
	}

	return &cell{
		drawable: makeVao(points),
		x:        x,
		y:        y,
		group:    0,
	}
}

func (c *cell) draw() {
	if !c.alive {
		return
	}

	gl.BindVertexArray(c.drawable)
	gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square)/3))
}

// checkState determines the state of the cell for the next tick of the game
func (c *cell) checkState(cells [][]*cell) {
	c.alive = c.aliveNext
	c.aliveNext = c.alive

	liveCount := c.liveNeighbors(cells)
	if c.alive {
		// 1. Any live cell with fewer than two live neighbours dies, as if caused by underpopulation
		if liveCount < 2 {
			c.alive = false
		}

		// 2. Any live cell with two or three live neighbors lives on to the next generation
		if liveCount == 2 || liveCount == 3 {
			c.aliveNext = true
		}

		// 3. Any live cell with more than three live neighbors dies, as if by overpopulation
		if liveCount > 3 {
			c.aliveNext = false
		}
	} else {
		// 4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction
		if liveCount == 3 {
			c.aliveNext = true
		}
	}
}

// liveNeighbors returns the number of live neighbors for a cell
func (c *cell) liveNeighbors(cells [][]*cell) int {
	var liveCount int
	add := func(x, y int) {
		// If we're at an edge, check the other side of the board.
		if x == len(cells) {
			x = 0
		} else if x == -1 {
			x = len(cells) - 1
		}
		if y == len(cells[x]) {
			y = 0
		} else if y == -1 {
			y = len(cells[x]) - 1
		}

		if cells[x][y].alive {
			liveCount++
		}
	}

	add(c.x-1, c.y)   // To the left
	add(c.x+1, c.y)   // To the right
	add(c.x, c.y+1)   // Up
	add(c.x, c.y-1)   // Down
	add(c.x-1, c.y+1) // Top-Left
	add(c.x+1, c.y+1) // Top-Right
	add(c.x-1, c.y-1) // Bottom-Left
	add(c.x+1, c.y-1) // Bottom-Right

	return liveCount
}

func ReduceGroups() bool {
	var ret bool
	for x := 0; x < 128; x++ {
		for y := 0; y < 128; y++ {
			if oV, oOk := diskGrid[cs(x, y)]; oOk && oV {
				if dV, dOk := diskGrid[cs(x, y-1)]; dOk && dV && groups[cs(x, y-1)] != groups[cs(x, y)] {
					CombineBlockGroups(cs(x, y), cs(x, y-1))
					ret = true
				}
				if dV, dOk := diskGrid[cs(x-1, y)]; dOk && dV && groups[cs(x-1, y)] != groups[cs(x, y)] {
					CombineBlockGroups(cs(x, y), cs(x-1, y))
					ret = true
				}
				if dV, dOk := diskGrid[cs(x+1, y)]; dOk && dV && groups[cs(x+1, y)] != groups[cs(x, y)] {
					CombineBlockGroups(cs(x+1, y), cs(x, y))
					ret = true
				}
				if dV, dOk := diskGrid[cs(x, y+1)]; dOk && dV && groups[cs(x, y+1)] != groups[cs(x, y)] {
					CombineBlockGroups(cs(x, y+1), cs(x, y))
					ret = true
				}
			}
		}
	}
	return ret
}

func CombineBlockGroups(b1, b2 string) {
	if groups[b1] < groups[b2] {
		groups[b1] = groups[b2]
	} else {
		groups[b2] = groups[b1]
	}
}

// Get a map coordinate string for x, y
func cs(x, y int) string {
	return fmt.Sprint(x, "-", y)
}

// Get the x, y coordinate from a string
func sc(c string) (int, int) {
	pts := strings.Split(c, "-")
	return Atoi(pts[0]), Atoi(pts[1])
}

func KnotHash(in string) string {
	var idx, skip int
	var list []int
	for i := 0; i < 256; i++ {
		list = append(list, i)
	}
	inpBts := []byte(in)
	inpBts = append(inpBts, []byte{17, 31, 73, 47, 23}...)
	for j := 0; j < 64; j++ {
		for i := range inpBts {
			idx, skip, list = khRound(int(inpBts[i]), idx, skip, list)
		}
	}
	// Now calculate the dense hash
	var dense []byte
	for i := 0; i < len(list); i += 16 {
		dense = append(dense, xorList(list[i:i+16]))
	}
	return fmt.Sprintf("%x", dense)
}

func khRound(i, idx, skip int, list []int) (int, int, []int) {
	// if idx+i overflows, pull from the front
	var revList []int
	for j := idx; j < idx+i; j++ {
		revList = append([]int{list[j%256]}, revList...)
	}
	for j := 0; j < len(revList); j++ {
		list[(idx+j)%256] = revList[j]
	}

	idx += i + skip
	skip++
	return idx, skip, list
}

func xorList(in []int) byte {
	var ret byte
	for i := range in {
		ret ^= byte(in[i])
	}
	return ret
}

func GetBinaryString(in string) string {
	var bin string
	for i := range in {
		var v int
		if in[i] >= '0' && in[i] <= '9' {
			v = int(in[i] - '0')
		} else if in[i] >= 'a' && in[i] <= 'f' {
			v = int(in[i] - 'a' + 10)
		}
		nibble := fmt.Sprintf("%04s", strconv.FormatInt(int64(v), 2))
		bin += nibble
	}
	return bin
}

func StdinToString() string {
	var input string
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		input = scanner.Text()
	}
	return input
}

func Atoi(i string) int {
	var ret int
	var err error
	if ret, err = strconv.Atoi(i); err != nil {
		log.Fatal("Invalid Atoi")
	}
	return ret
}
