package aoc2023

import println
import readInput

/*
--- Day 22: Sand Slabs ---
https://adventofcode.com/2023/day/22
*/
fun main() {

    val inlineTestInput = """
1,0,1~1,2,1
0,0,2~2,0,2
0,2,3~2,2,3
0,0,4~0,2,4
2,0,5~2,2,5
0,1,6~2,1,6
1,1,8~1,1,9
"""

    data class Brick(
        val id: Int,
        val x1: Int,
        val y1: Int,
        val z1: Int,
        val x2: Int,
        val y2: Int,
        val z2: Int,
        val onTopBricks: MutableSet<Brick> = HashSet(),
        val uponBricks: MutableSet<Brick> = HashSet()
    ) {
        fun h() = z2 - z1 + 1
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as Brick

            return id == other.id
        }

        override fun hashCode(): Int {
            return id
        }

        override fun toString(): String {
            return "Brick(id=$id, uponBricks=$uponBricks)"
        }
    }

    fun parts(input: List<String>, width: Int, height: Int): Pair<Int, Int> {
        val bricks = input.mapIndexed { id, it ->
            val (x1, y1, z1, x2, y2, z2) = "(\\d+),(\\d+),(\\d+)~(\\d+),(\\d+),(\\d+)".toRegex()
                .matchEntire(it)?.destructured!!
            Brick(id, x1.toInt(), y1.toInt(), z1.toInt(), x2.toInt(), y2.toInt(), z2.toInt())
        }
            .sortedBy { it.z2 }
        val brickTopZ = IntArray(width * height)
        val brickStacks = Array<MutableList<Brick>>(width * height) { ArrayList() }
        for (b in bricks) {
            var flatTopZ = 0
            for (y in b.y1..b.y2) {
                for (x in b.x1..b.x2) {
                    flatTopZ = flatTopZ.coerceAtLeast(brickTopZ[y * width + x])
                }
            }
            for (y in b.y1..b.y2) {
                for (x in b.x1..b.x2) {
                    val topBrick = brickStacks[y * width + x].lastOrNull()
                    if (topBrick != null) {
                        val topZ = brickTopZ[y * width + x]
                        if (topZ == flatTopZ) {
                            b.uponBricks.add(topBrick)
                            topBrick.onTopBricks.add(b)
                        }
                    }
                    brickStacks[y * width + x].add(b)
                    brickTopZ[y * width + x] = flatTopZ + b.h()
                }
            }
        }
        val disBricks = bricks.filterNot { it.onTopBricks.all { it.uponBricks.size != 1 } }
        val safeBricks = bricks.size - disBricks.size
        var fallingSum = 0
        for (brick in disBricks) {
            val fallingSet = HashSet<Brick>()
            val removeQueue = ArrayDeque<Brick>()
            fallingSet.add(brick)
            removeQueue.add(brick)
            while (removeQueue.isNotEmpty()) {
                val falling = removeQueue.removeFirst()
                fallingSet.add(falling)
                removeQueue.addAll(falling.onTopBricks.filter { fallingSet.containsAll(it.uponBricks) })
            }
            fallingSum += fallingSet.size - 1
        }
        return safeBricks to fallingSum
    }

    // test if implementation meets criteria from the description, like:
    val testInput = inlineTestInput.trim().reader().readLines()
    //val testInput = readInput("aoc2023/Day22_test")
    val (testInputPart1Result, testInputPart2Result) = parts(testInput, 3, 3)
    println("Part 1 Test: $testInputPart1Result")
    println("Part 2 Test: $testInputPart2Result")
    check(testInputPart1Result == 5)
    check(testInputPart2Result == 7)

    val input = readInput("aoc2023/Day22")
    parts(input, 10, 10).println()
}
