package aoc2023

import CharGrid
import println
import readInput

/*
--- Day 10: Pipe Maze ---
https://adventofcode.com/2023/day/10
*/
fun main() {

    val inlineTestInput = """
..F7.
.FJ|.
SJ.L7
|F--J
LJ...
"""

    val inlineTestInput2a = """
.F----7F7F7F7F-7....
.|F--7||||||||FJ....
.||.FJ||||||||L7....
FJL7L7LJLJ||LJ.L-7..
L--J.L7...LJS7F-7L7.
....F-J..F7FJ|L7L7L7
....L7.F7||L7|.L7L7|
.....|FJLJ|FJ|F7|.LJ
....FJL-7.||.||||...
....L---J.LJ.LJLJ...
"""

    val inlineTestInput2 = """
FF7FSF7F7F7F7F7F---7
L|LJ||||||||||||F--J
FL-7LJLJ||||||LJL-77
F--JF--7||LJLJ7F7FJ-
L---JF-JLJ.||-FJLJJ7
|F|F-JF---7F7-L7L|7|
|FFJF7L7F-JF7|JL---7
7-L-JL7||F7|L7F-7F7|
L.L7LFJ|||||FJL7||LJ
L7JLJL-JLJLJL--JLJ.L
"""

    val north = 1
    val south = 2
    val west = 4
    val east = 8
    val dirMask = north or south or west or east
    val start = 16

    val directions = mapOf(
        north to (0 to -1),
        south to (0 to 1),
        west to (-1 to 0),
        east to (1 to 0)
    )

    val mapping = mapOf(
        'S' to start,
        'F' to (south or east),
        '7' to (west or south),
        'J' to (north or west),
        'L' to (east or north),
        '-' to (west or east),
        '|' to (north or south),
        '.' to 0
    )

    fun swapDirections(code: Int) = ((code and (north or west)) shl 1) or ((code and (south or east)) shr 1)

    fun part1(input: List<String>): Int {
        val grid = CharGrid(input)
        val startPos = grid.findMatches { it == 'S' }.first()
        grid.apply { (mapping[it]!! + 32).toChar() }
        val currentDirs = directions.filter {
            (grid[startPos.first + it.value.first, startPos.second + it.value.second].code and swapDirections(it.key)) != 0
        }.keys.toTypedArray()
        val locations = arrayOf(startPos, startPos)
        var distance = 0
        var notHit = true
        do {
            distance++
            for (i in locations.indices) {
                val dir = currentDirs[i]
                val dirRel = directions[dir]!!
                val loc = locations[i]

                val newLoc = loc.first + dirRel.first to loc.second + dirRel.second
                if (grid[newLoc.first, newLoc.second] == 'Z') {
                    notHit = false
                    break
                }
                currentDirs[i] = (grid[newLoc.first, newLoc.second].code xor swapDirections(dir)) and dirMask
                locations[i] = newLoc
                grid[newLoc.first, newLoc.second] = 'Z'
            }
        } while (notHit)
        return distance
    }

    fun floodFill(grid: Array<IntArray>, c: Int, r: Int) {
        grid[r][c] = 2
        CharGrid.PLUS_POS.map { it.translate(c, r) }
//            .filter { it.dc in grid[0].indices && it.dr in grid.indices }
            .filter { grid[it.dr][it.dc] == 0 }.forEach { floodFill(grid, it.dc, it.dr) }
    }

    fun part2(input: List<String>): Int {
        val grid = CharGrid(input)
        val startPos = grid.findMatches { it == 'S' }.first()
        grid.apply { (mapping[it]!! + 32).toChar() }
        val currentDirs = directions.filter {
            (grid[startPos.first + it.value.first, startPos.second + it.value.second].code and swapDirections(it.key)) != 0
        }.keys.toTypedArray()

        val outline = Array(grid.height * 3) { IntArray(grid.width * 3) }
        val locations = arrayOf(startPos, startPos)
        var notHit = true
        do {
            for (i in locations.indices) {
                val dir = currentDirs[i]
                val dirRel = directions[dir]!!
                val loc = locations[i]

                outline[loc.second * 3 + 1][loc.first * 3 + 1] = 1
                outline[loc.second * 3 + 1 + dirRel.second][loc.first * 3 + 1 + dirRel.first] = 1
                outline[loc.second * 3 + 1 + dirRel.second * 2][loc.first * 3 + 1 + dirRel.first * 2] = 1

                val newLoc = loc.first + dirRel.first to loc.second + dirRel.second
                if (grid[newLoc.first, newLoc.second] == 'Z') {
                    notHit = false
                    break
                }
                val newDir = (grid[newLoc.first, newLoc.second].code xor swapDirections(dir)) and dirMask
                val newDirRel = directions[newDir]!!

                outline[newLoc.second * 3 + 1][newLoc.first * 3 + 1] = 1
                outline[newLoc.second * 3 + 1 + newDirRel.second][newLoc.first * 3 + 1 + newDirRel.first] = 1

                currentDirs[i] = newDir
                locations[i] = newLoc
                grid[newLoc.first, newLoc.second] = 'Z'
            }
        } while (notHit)

        // guess the inside
        val ipos = 1 to 1
        if (outline[startPos.second * 3 + 1 + ipos.second][startPos.first * 3 + 1 + ipos.first] != 0) {
            println("Bah!")
        }
        floodFill(outline, startPos.first * 3 + 1 + ipos.first, startPos.second * 3 + 1 + ipos.second)

        var insideCount = 0
        var outsideCount = 0
        grid.applyWithPos { grid, col, row ->
            if (grid[col, row] != 'Z') {
                if (outline[row * 3][col * 3] == 2) insideCount++ else outsideCount++
            }
            null
        }
        return insideCount
    }

    // test if implementation meets criteria from the description, like:
    val testInput = inlineTestInput.trim().reader().readLines()
    val testInput2 = inlineTestInput2.trim().reader().readLines()
    //val testInput = readInput("aoc2023/Day10_test")
    val testInputPart1Result = part1(testInput)
    println("Part 1 Test: $testInputPart1Result")
    //val testInputPart2Result = part2(testInput2)
    //println("Part 2 Test: $testInputPart2Result")
    check(testInputPart1Result == 8)
    //check(testInputPart2Result == 10)

    val input = readInput("aoc2023/Day10")
    part1(input).println()
    part2(input).println()
}
