package main

import (
	"fmt"
	"log"
	"os"
	"regexp"

	"git.annabunch.es/annabunches/adventofcode/2020/lib/util"
)

const (
	MATCH_NONE = iota
	MATCH_TOP
	MATCH_BOTTOM
	MATCH_LEFT
	MATCH_RIGHT
)

type Tile struct {
	id       int
	data     []string
	rotation int
	flippedX bool
	flippedY bool
}

func NewTile() *Tile {
	return &Tile{
		data:     make([]string, 0),
		rotation: 0,
		flippedX: false,
		flippedY: false,
	}
}

func (t *Tile) print() {
	for _, line := range t.data {
		fmt.Println(line)
	}
	fmt.Println()
}

func (t *Tile) rotate() {
	newData := make([]string, len(t.data))
	for i := len(t.data) - 1; i >= 0; i-- {
		for j, char := range t.data[i] {
			newData[j] += string(char)
		}
	}
	t.data = newData

	t.rotation++
	if t.rotation > 3 {
		t.rotation = 0
	}
}

func (t *Tile) flipX() {
	newData := make([]string, len(t.data))

	for i := 0; i < len(newData); i++ {
		newData[i] = t.data[len(t.data)-1-i]
	}

	t.data = newData
	t.flippedX = !t.flippedX
}

func (t *Tile) flipY() {
	newData := make([]string, len(t.data))

	for i := 0; i < len(newData); i++ {
		for j := 0; j < len(t.data[i]); j++ {
			newData[i] += string(t.data[i][len(t.data)-1-j])
		}
	}

	t.data = newData
	t.flippedY = !t.flippedY
}

func (t *Tile) reset() {
	for t.rotation != 0 {
		t.rotate()
	}

	if t.flippedX {
		t.flipX()
	}
	if t.flippedY {
		t.flipY()
	}
}

func parseInput(input []string) map[int]*Tile {
	tileMap := make(map[int]*Tile)

	re := regexp.MustCompile("^Tile ([0-9]+):$")
	id := 0
	tile := NewTile()
	for _, line := range input {
		if re.MatchString(line) {
			id = util.MustAtoi(re.FindStringSubmatch(line)[1])
			tile.id = id
			continue
		}
		if line == "" {
			tileMap[id] = tile
			tile = NewTile()
			id = 0
			continue
		}
		tile.data = append(tile.data, line)
	}

	// if needed, add the last one (might be missing our final blank line)
	if id != 0 {
		tileMap[id] = tile
	}

	return tileMap
}

func matchTiles(oldTile, newTile *Tile) int {
	for i := 0; i < 4; i++ {
		check := subMatchTiles(oldTile, newTile)
		if check != MATCH_NONE {
			return check
		}
		newTile.rotate()
	}

	newTile.reset()
	newTile.flipX()
	for i := 0; i < 4; i++ {
		check := subMatchTiles(oldTile, newTile)
		if check != MATCH_NONE {
			return check
		}
		newTile.rotate()
	}

	newTile.reset()
	newTile.flipY()
	for i := 0; i < 4; i++ {
		check := subMatchTiles(oldTile, newTile)
		if check != MATCH_NONE {
			return check
		}
		newTile.rotate()
	}

	newTile.reset()
	newTile.flipX()
	newTile.flipY()
	for i := 0; i < 4; i++ {
		check := subMatchTiles(oldTile, newTile)
		if check != MATCH_NONE {
			return check
		}
		newTile.rotate()
	}

	return MATCH_NONE
}

func subMatchTiles(oldTile, newTile *Tile) int {
	// check top
	if oldTile.data[0] == newTile.data[0] {
		return MATCH_TOP
	}
	// check bottom
	if oldTile.data[len(oldTile.data)-1] == newTile.data[len(newTile.data)-1] {
		return MATCH_BOTTOM
	}
	// check left
	match := true
	for i := 0; i < len(oldTile.data); i++ {
		if oldTile.data[i][0] != newTile.data[i][0] {
			match = false
			break
		}
	}
	if match {
		return MATCH_LEFT
	}

	// check right
	match = true
	for i := 0; i < len(oldTile.data); i++ {
		if oldTile.data[i][len(oldTile.data)-1] != newTile.data[i][len(newTile.data)-1] {
			match = false
			break
		}
	}
	if match {
		return MATCH_RIGHT
	}

	return MATCH_NONE
}

func arrangeTiles(tiles map[int]*Tile) map[[2]int]*Tile {
	grid := make(map[[2]int]*Tile)
	looseTiles := make([]*Tile, 0)
	for _, v := range tiles {
		looseTiles = append(looseTiles, v)
	}

	// arbitrarily place a first tile
	grid[[2]int{0, 0}] = looseTiles[0]
	looseTiles = looseTiles[1:]

	for len(looseTiles) > 0 {
		for coord, tile := range grid {
			if _, ok := grid[[2]int{coord[0] + 1, coord[1]}]; ok {
				if _, ok := grid[[2]int{coord[0] - 1, coord[1]}]; ok {
					if _, ok := grid[[2]int{coord[0], coord[1] + 1}]; ok {
						if _, ok := grid[[2]int{coord[0], coord[1] - 1}]; ok {
							continue
						}
					}
				}
			}

			for i, loose := range looseTiles {
				matched := matchTiles(tile, loose)
				// On a match, flip appropriately and set coords
				// check for already present tiles as well - skip if that's the case
				var newCoords [2]int
				willFlipX := true
				switch matched {
				case MATCH_TOP:
					newCoords = [2]int{coord[0], coord[1] + 1}
				case MATCH_BOTTOM:
					newCoords = [2]int{coord[0], coord[1] - 1}
				case MATCH_LEFT:
					newCoords = [2]int{coord[0] - 1, coord[1]}
					willFlipX = false
				case MATCH_RIGHT:
					newCoords = [2]int{coord[0] + 1, coord[1]}
					willFlipX = false
				}

				if matched != MATCH_NONE {
					if _, ok := grid[newCoords]; ok {
						continue
					}

					if willFlipX {
						loose.flipX()
					} else {
						loose.flipY()
					}

					grid[newCoords] = loose

					if i == len(looseTiles)-1 {
						looseTiles = looseTiles[:i]
					} else {
						looseTiles = append(looseTiles[:i], looseTiles[i+1:]...)
					}
					break // to avoid sequencing issues
				}
			}
		}
	}

	return grid
}

func findCorners(grid map[[2]int]*Tile) []*Tile {
	corners := make([]*Tile, 0)

	// min and max coord values
	minX, minY, maxX, maxY := findLimits(grid)

	corners = append(corners, grid[[2]int{minX, minY}])
	corners = append(corners, grid[[2]int{maxX, minY}])
	corners = append(corners, grid[[2]int{minX, maxY}])
	corners = append(corners, grid[[2]int{maxX, maxY}])
	return corners
}

func findLimits(grid map[[2]int]*Tile) (int, int, int, int) {
	var minX, minY, maxX, maxY int
	for coord, _ := range grid {
		if coord[0] > maxX {
			maxX = coord[0]
		}
		if coord[0] < minX {
			minX = coord[0]
		}
		if coord[1] > maxY {
			maxY = coord[1]
		}
		if coord[1] < minY {
			minY = coord[1]
		}
	}
	return minX, minY, maxX, maxY
}

func stripBorder(tile *Tile) []string {
	ret := make([]string, 0)
	for i := 1; i < len(tile.data)-1; i++ {
		ret = append(ret, tile.data[i][1:len(tile.data)-1])
	}
	return ret
}

func combineTiles(grid map[[2]int]*Tile) *Tile {
	bigTile := NewTile()
	minX, minY, maxX, maxY := findLimits(grid)

	for j := maxY; j >= minY; j-- {
		row := make([]string, 8)
		for i := minX; i <= maxX; i++ {
			var tile *Tile
			tile, ok := grid[[2]int{i, j}]
			if !ok {
				log.Panicf("Couldn't find tile: %d, %d", i, j)
			}

			data := stripBorder(tile)
			for i, line := range data {
				row[i] = row[i] + line
			}
		}
		bigTile.data = append(bigTile.data, row...)
	}

	return bigTile
}

var monsterPattern = []string{
	"                  # ",
	"#    ##    ##    ###",
	" #  #  #  #  #  #   ",
}

func subFilterMonsters(image *Tile) int {
	numMonsters := 0
	for i := 0; i < len(image.data)-2; i++ {
		for j := 0; j < len(image.data[0])-19; j++ {
			if monsterCheck(image, i, j) {
				removeMonster(image, i, j)
				numMonsters++
			}
		}
	}
	return numMonsters
}

func removeMonster(image *Tile, i, j int) {
	for y := 0; y < 3; y++ {
		for x := 0; x < 20; x++ {
			if monsterPattern[y][x] == '#' {
				if j+x == len(image.data[i+y])-1 {
					image.data[i+y] = image.data[i+y][:j+x] + "O"
				} else {
					image.data[i+y] = image.data[i+y][:j+x] + "O" + image.data[i+y][j+x+1:]
				}
			}
		}
	}
}

func monsterCheck(image *Tile, i, j int) bool {
	data := image.data

	for y := 0; y < 3; y++ {
		for x := 0; x < 20; x++ {
			if data[i+y][j+x] == '.' && monsterPattern[y][x] == '#' {
				return false
			}
		}
	}

	return true
}

func filterMonsters(image *Tile) int {
	numMonsters := 0

	for i := 0; i < 4; i++ {
		numMonsters = subFilterMonsters(image)
		if numMonsters > 0 {
			return numMonsters
		}
		image.rotate()
	}

	image.reset()
	image.flipX()
	for i := 0; i < 4; i++ {
		numMonsters = subFilterMonsters(image)
		if numMonsters > 0 {
			return numMonsters
		}
	}

	image.reset()
	image.flipY()
	for i := 0; i < 4; i++ {
		numMonsters = subFilterMonsters(image)
		if numMonsters > 0 {
			return numMonsters
		}

	}

	image.reset()
	image.flipX()
	image.flipY()
	for i := 0; i < 4; i++ {
		numMonsters = subFilterMonsters(image)
		if numMonsters > 0 {
			return numMonsters
		}
	}

	return numMonsters
}

func main() {
	step := os.Args[1]
	values := util.InputParserStrings(os.Args[2])

	tileMap := parseInput(values)
	grid := arrangeTiles(tileMap)

	switch step {
	case "1":
		corners := findCorners(grid)
		product := 1
		for _, tile := range corners {
			product *= tile.id
		}
		fmt.Println(product)
	case "2":
		image := combineTiles(grid)
		monsters := filterMonsters(image)
		if monsters == 0 {
			log.Panicf("Found no monsters")
		}
		count := 0
		for _, line := range image.data {
			for _, char := range line {
				if char == '#' {
					count++
				}
			}
		}
		fmt.Println(count)
	}
}