package main

import (
	"flag"
	"fmt"
	"math"
	"strings"

	"github.com/alexchao26/advent-of-code-go/algos"
	"github.com/alexchao26/advent-of-code-go/util"
)

func main() {
	var part int
	flag.IntVar(&part, "part", 1, "part 1 or 2")
	flag.Parse()
	fmt.Println("Running part", part)

	var ans int
	if part == 1 {
		ans = part1(util.ReadFile("./input.txt"))
	} else {
		ans = part2(util.ReadFile("./input.txt"))
	}
	fmt.Println("Output:", ans)
}

func part1(input string) int {
	tiles := parseTilesFromInput(input)
	edgeSize := int(math.Sqrt(float64(len(tiles))))

	assembledTiles := backtrackAssemble(tiles, nil, map[int]bool{})

	// return product of corners
	product := assembledTiles[0][0].id
	product *= assembledTiles[0][edgeSize-1].id
	product *= assembledTiles[edgeSize-1][0].id
	product *= assembledTiles[edgeSize-1][edgeSize-1].id
	return product
}

func part2(input string) int {
	tiles := parseTilesFromInput(input)
	edgeSize := int(math.Sqrt(float64(len(tiles))))

	assembledTiles := backtrackAssemble(tiles, nil, map[int]bool{})

	// remove ALL borders from all tiles
	for r := range assembledTiles {
		for c, cell := range assembledTiles[r] {
			assembledTiles[r][c].contents = removeBordersFromGrid(cell.contents)
		}
	}

	// generate image from assembledTiles...
	// assembledTiles is effectively a 2D grid where each cell is a 2D grid representing a tile
	// there has to be an easier way to "flatten" a 4D grid into a 2D grid...
	var image [][]string
	for bigRow := 0; bigRow < edgeSize; bigRow++ {
		for subRow := 0; subRow < len(assembledTiles[0][0].contents); subRow++ {
			image = append(image, []string{})
			for bigCol := 0; bigCol < edgeSize; bigCol++ {
				subLine := assembledTiles[bigRow][bigCol].contents[subRow]
				image[len(image)-1] = append(image[len(image)-1], subLine...)
			}
		}
	}

	// get the coordinates of all monsters by iterating over all possible
	// orientations of the image
	var monsterCoords [][2]int
	for _, opt := range algos.AllGridOrientations(image) {
		monsterCoords = findMonsterCoords(opt)
		// assuming there's only one orientation of image with valid monsters
		if len(monsterCoords) > 0 {
			image = opt
			break
		}
	}

	// modify all monster coordinates to "O" characters (anything but "#")
	for _, coord := range monsterCoords {
		image[coord[0]][coord[1]] = "O"
	}

	// count up remaining "#" cells
	var roughWatersCount int
	for _, row := range image {
		for _, cell := range row {
			if cell == "#" {
				roughWatersCount++
			}
		}
	}

	return roughWatersCount
}

type tile struct {
	contents [][]string
	id       int
}

func parseTilesFromInput(input string) []*tile {
	ans := []*tile{}
	for _, block := range strings.Split(input, "\n\n") {
		split := strings.Split(block, "\n")
		var tileID int
		_, err := fmt.Sscanf(split[0], "Tile %d:", &tileID)
		if err != nil {
			panic(err)
		}

		var contents [][]string
		for _, line := range split[1:] {
			contents = append(contents, strings.Split(line, ""))
		}
		ans = append(ans, &tile{id: tileID, contents: contents})
	}
	return ans
}

func backtrackAssemble(tiles []*tile, assembledTiles [][]*tile, usedIndices map[int]bool) [][]*tile {
	// pray it's a square...
	edgeSize := int(math.Sqrt(float64(len(tiles))))
	if assembledTiles == nil {
		assembledTiles = make([][]*tile, edgeSize)
		for i := 0; i < edgeSize; i++ {
			assembledTiles[i] = make([]*tile, edgeSize)
		}
	}

	// iterate through all cells, skipping cells that have already been set
	for row := 0; row < edgeSize; row++ {
		for col := 0; col < edgeSize; col++ {
			// skip cells that have already been assigned
			if assembledTiles[row][col] == nil {
				// iterate over all available tiles (skip ones that are tagged "used")
				for i, t := range tiles {
					if !usedIndices[i] {
						// iterate over the OPTIONS for a particular tile, i.e. all 8 images of it...
						for _, opt := range algos.AllGridOrientations(t.contents) {
							// check if setting this tile is okay with (if applicable) tiles above
							// and to the left
							if row != 0 { // check above
								currentTopRow := getRow(opt, true)
								bottomOfAbove := getRow(assembledTiles[row-1][col].contents, false)
								// if they don't match, continue onto next option b/c this one doesn't match
								if currentTopRow != bottomOfAbove {
									continue
								}
							}
							if col != 0 { // check left, same logic checking above
								currentLeftCol := getCol(opt, true)
								rightColOfLeft := getCol(assembledTiles[row][col-1].contents, false)
								if currentLeftCol != rightColOfLeft {
									continue
								}
							}
							// set tile, mark tile as used, recurse
							t.contents = opt // side effects apply b/c t is a pointer
							assembledTiles[row][col] = t
							// if non-nil response, relay the return value up the call stack
							usedIndices[i] = true
							recurseResult := backtrackAssemble(tiles, assembledTiles, usedIndices)
							if recurseResult != nil {
								return recurseResult
							}
							// backtrack if nil response from recursing
							assembledTiles[row][col] = nil
							usedIndices[i] = false
						}
					}
				}
				// Note: this is a key part of backtracking, to escape if there are
				// no valid options for to set for this row/col
				if assembledTiles[row][col] == nil {
					return nil
				}
			}
		}
	}

	// if entire loop finishes, that means every cell has been assigned
	// return the assembled tiles to collapse the call stack
	return assembledTiles
}

// helper functions to get a string (easily comparable) of a single side
func getCol(grid [][]string, firstCol bool) string {
	var str string
	for i := range grid {
		if firstCol {
			str += grid[i][0]
		} else {
			str += grid[i][len(grid[0])-1]
		}
	}
	return str
}

func getRow(grid [][]string, firstRow bool) string {
	var str string
	for i := range grid[0] {
		if firstRow {
			str += grid[0][i]
		} else {
			str += grid[len(grid)-1][i]
		}
	}
	return str
}

func removeBordersFromGrid(grid [][]string) [][]string {
	var result [][]string

	for i := 1; i < len(grid)-1; i++ {
		result = append(result, []string{})
		for j := 1; j < len(grid[0])-1; j++ {
			result[i-1] = append(result[i-1], grid[i][j])
		}
	}

	return result
}

// returns all coordinates that make up any valid monsters...
// valid monsters are the '#' like so... dots are for visual effect
//                 ..#.
// #....##....##....###
// .#..#..#..#..#..#...
var monster = `                  # 
#    ##    ##    ###
 #  #  #  #  #  #   `

func findMonsterCoords(image [][]string) [][2]int {
	var monsterOffsets [][2]int
	var monsterHeight, monsterLength int
	for r, line := range strings.Split(monster, "\n") {
		for c, char := range line {
			if char == '#' {
				monsterOffsets = append(monsterOffsets, [2]int{r, c})
			}
			monsterLength = c + 1
		}
		monsterHeight++
	}

	// determine the top left corners of a found monster
	var monsterStartingCoords [][2]int
	for r := 0; r < len(image)-monsterHeight+1; r++ {
		for c := 0; c < len(image[0])-monsterLength+1; c++ {
			monsterFound := true
			for _, diff := range monsterOffsets {
				rowToCheck := r + diff[0]
				colToCheck := c + diff[1]
				if image[rowToCheck][colToCheck] != "#" {
					monsterFound = false
				}
			}
			if monsterFound {
				monsterStartingCoords = append(monsterStartingCoords, [2]int{r, c})
			}
		}
	}

	// generate a list of all the coordinates that are monsters
	var monsterCoords [][2]int
	for _, startingCoord := range monsterStartingCoords {
		for _, diff := range monsterOffsets {
			monsterCoords = append(monsterCoords, [2]int{startingCoord[0] + diff[0], startingCoord[1] + diff[1]})
		}
	}

	return monsterCoords
}
