package main

import (
	"fmt"
	"strings"

	h "git.bullercodeworks.com/brian/adventofcode/helpers"
)

var N h.Coordinate
var E h.Coordinate
var S h.Coordinate
var W h.Coordinate
var SeaMonster map[h.Coordinate]bool

func init() {
	N = h.Coordinate{X: 0, Y: -1}
	E = h.Coordinate{X: 1, Y: 0}
	S = h.Coordinate{X: 0, Y: 1}
	W = h.Coordinate{X: -1, Y: 0}
	SeaMonster = make(map[h.Coordinate]bool)
	monster := []string{
		"                  # ",
		"#    ##    ##    ###",
		" #  #  #  #  #  #   ",
	}
	for y := range monster {
		for x := range monster[y] {
			if monster[y][x] == '#' {
				SeaMonster[h.Coordinate{X: x, Y: y}] = true
			}
		}
	}
}

func main() {
	fmt.Println("# Day 20")
	fmt.Println()
	inp := h.StdinToStringSlice()
	part := h.OptArgNumber(1, "2")
	solve(inp, h.Atoi(part))
}

func solve(inp []string, part int) {
	tiles := buildTiles(inp)
	alignGrid(tiles)
	if part == 1 {
		var result uint64 = 1
		for k := range tiles {
			if len(tiles[k].Neighbors) == 2 {
				result *= uint64(k)
			}
		}
		fmt.Println("## Part 1\nAnswer:", result)
	} else {
		image := buildMapFromTiles(tiles)
		image = monsterSlayer(image)
		var waves int
		for y := range image {
			for x := range image[y] {
				if image[y][x] {
					waves++
				}
			}
		}
		fmt.Println("## Part2\nAnswer:", waves)
	}
}

func buildTiles(inp []string) map[int]*Tile {
	ret := make(map[int]*Tile)
	var tileNum int
	var tileLines []string
	for k := range inp {
		if inp[k] == "" {
			continue
		} else if strings.HasPrefix(inp[k], "Tile ") {
			if len(tileLines) != 0 {
				// Create the tile
				t := NewTile(tileNum, tileLines)
				ret[t.Num] = t
			}
			tileNum = h.Atoi(strings.TrimPrefix(strings.TrimSuffix(inp[k], ":"), "Tile "))
			tileLines = []string{}
		} else {
			tileLines = append(tileLines, inp[k])
		}
	}
	// Add the last tile
	if len(tileLines) != 0 {
		// Create the tile
		t := NewTile(tileNum, tileLines)
		ret[t.Num] = t
	}
	return ret
}

func alignGrid(tiles map[int]*Tile) {
	done := make(map[int]bool)
	for tile := range tiles {
		if len(tiles[tile].Neighbors) != 0 {
			done[tile] = true
		}
	}
	if len(done) == 0 {
		for tile := range tiles {
			done[tile] = true
			break
		}
	}

	match := true
	for match {
		match = false
		for tile := range done {
			for wrk := range tiles {
				if wrk == tile {
					continue
				}
				_, found := done[wrk]
				// Test if wrk can be a neighbor to tile
				if tiles[tile].IsNeighbor(tiles[wrk], !found) {
					match = true
					done[wrk] = true
				}
			}
		}
	}
}

func buildMapFromTiles(tiles map[int]*Tile) [][]bool {
	done := make(map[*Tile]bool)
	var wrk *Tile
	queue := make(map[*Tile]h.Coordinate)

	for t := range tiles {
		wrk = tiles[t]
		done[wrk] = true
		for n, p := range wrk.Neighbors {
			queue[p] = n
		}
		// TODO Analyze
		break
	}
	if wrk == nil {
		return nil
	}
	grid := make(map[h.Coordinate]*Tile)
	grid[h.Coordinate{}] = wrk
	var minX, minY, maxX, maxY int
	tileSize := len(wrk.Data) - 2
	for len(queue) > 0 {
		for p, dir := range queue {
			delete(queue, p)
			done[p] = true
			for n, np := range p.Neighbors {
				if _, ok := done[np]; !ok {
					queue[np] = dir.Relative(n)
				}
			}
			grid[dir] = p
			if dir.X < minX {
				minX = dir.X
			}
			if dir.X > maxX {
				maxX = dir.X
			}
			if dir.Y < minY {
				minY = dir.Y
			}
			if dir.Y > maxY {
				maxY = dir.Y
			}
		}
	}

	ret := make([][]bool, (maxY-minY+1)*tileSize)
	size := maxX - minX + 1
	// Build the map, trimming the borders
	for y := minY; y <= maxY; y++ {
		for x := minX; x <= maxX; x++ {
			tile := grid[h.Coordinate{X: x, Y: y}]
			for tileY, row := range tile.Data[1 : tileSize+1] {
				procY := (y-minY)*tileSize + tileY
				if ret[procY] == nil {
					ret[procY] = make([]bool, size*tileSize)
				}
				for tileX, t := range row[1 : len(row)-1] {
					procX := (x-minX)*tileSize + tileX
					ret[procY][procX] = t
				}
			}
		}
	}
	return ret
}

func monsterSlayer(inp [][]bool) [][]bool {
	var found bool
	for r := 0; r < 8; r++ {
		for y := range inp {
			for x := range inp[y] {
				match := true
				for m := range SeaMonster {
					if y+m.Y >= len(inp) {
						match = false
						break
					}
					if x+m.X >= len(inp[y+m.Y]) {
						match = false
						break
					}
					if !inp[y+m.Y][x+m.X] {
						match = false
						break
					}
				}
				if match {
					found = true
					// Remove the monster
					for m := range SeaMonster {
						inp[y+m.Y][x+m.X] = false
					}
				}
			}
		}
		if found {
			break
		}
		if r&1 == 0 {
			inp = FlipMap(inp)
		} else {
			inp = RotateMap(FlipMap(inp))
		}
	}
	return inp
}

func FlipMap(inp [][]bool) [][]bool {
	size := len(inp)
	newData := make([][]bool, size)
	for i := 0; i < size; i++ {
		newData[i] = make([]bool, size)
	}
	for y := range inp {
		for x := range inp[y] {
			newData[y][size-x-1] = inp[y][x]
		}
	}
	return newData
}

func RotateMap(inp [][]bool) [][]bool {
	size := len(inp)
	newData := make([][]bool, size)
	for y := 0; y < size; y++ {
		newData[y] = make([]bool, size)
	}
	for y := range inp {
		for x := range inp[y] {
			newData[size-x-1][y] = inp[y][x]
		}
	}
	return newData
}

func PrintMap(inp [][]bool) {
	for y := range inp {
		for x := range inp[y] {
			if inp[y][x] {
				fmt.Print("#")
			} else {
				fmt.Print(" ")
			}
		}
		fmt.Println()
	}
}

func PrintCoordMap(m map[h.Coordinate]bool) {
	minX := h.MAX_INT
	maxX := h.MIN_INT
	minY := h.MAX_INT
	maxY := h.MIN_INT

	for k := range m {
		if k.X < minX {
			minX = k.X
		}
		if k.X > maxX {
			maxX = k.X
		}
		if k.Y < minY {
			minY = k.Y
		}
		if k.Y > maxY {
			maxY = k.Y
		}
	}
	for y := minY; y <= maxY; y++ {
		for x := minX; x <= maxX; x++ {
			if v, ok := m[h.Coordinate{X: x, Y: y}]; ok {
				if v {
					fmt.Print("#")
				} else {
					fmt.Print(".")
				}
			} else {
				fmt.Print(".")
			}
		}
		fmt.Println()
	}
}
