package aoc2024

import CharGrid
import RelPos
import println
import readInput
import splitInts
import java.util.*

/*
--- Day 18: RAM Run ---
https://adventofcode.com/2024/day/18
*/
fun main() {

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

    data class Node(val pos: RelPos, val relLen: Int)

    fun part1(input: List<String>, steps: Int, wh: Int): Int {
        val posList = input.map { it.splitInts(",").toIntArray() }
        val grid = CharGrid(wh, wh, '#', fillChar = '.')
        posList.take(steps).forEach { grid[it[0], it[1]] = '#' }
        val queue = PriorityQueue<Node>(Comparator.comparing { -it.pos.dc - it.pos.dr })
        queue.add(Node(RelPos(0, 0), 0))
        val bestCosts = HashMap<RelPos, Int>()
        var minCost = Int.MAX_VALUE
        while (queue.isNotEmpty()) {
            val node = queue.remove()
            val pos = node.pos
            val cost = node.relLen
            if (pos.dc == wh - 1 && pos.dr == wh - 1) {
                minCost = minCost.coerceAtMost(cost)
                continue
            }
            if ((bestCosts[pos] ?: Int.MAX_VALUE) < cost) {
                continue
            }
            bestCosts[pos] = cost
            queue.addAll(CharGrid.PLUS_POS
                .map { pos.translate(it) }
                .filter { grid[it] == '.' && (bestCosts[it] ?: Int.MAX_VALUE) > cost + 1 }
                .map { Node(it, cost + 1) })
        }
        return minCost
    }

    fun part2(input: List<String>, wh: Int): RelPos {
        val posList = input.map { it.splitInts(",").toIntArray() }
        var lowerBound = 0
        var higherBound = posList.size
        do {
            val grid = CharGrid(wh, wh, '#', fillChar = '.')
            val boulderPos = (lowerBound + higherBound) / 2
            posList.take(boulderPos).forEach { grid[it[0], it[1]] = '#' }

            var foundPath = false
            val queue = PriorityQueue<Node>(Comparator.comparing { -it.pos.dc - it.pos.dr })
            queue.add(Node(RelPos(0, 0), 0))
            val bestCosts = HashMap<RelPos, Int>()
            while (queue.isNotEmpty()) {
                val node = queue.remove()
                val pos = node.pos
                val cost = node.relLen
                if (pos.dc == wh - 1 && pos.dr == wh - 1) {
                    foundPath = true
                    break
                }
                if ((bestCosts[pos] ?: Int.MAX_VALUE) < cost) {
                    continue
                }
                bestCosts[pos] = cost
                queue.addAll(CharGrid.PLUS_POS
                    .map { pos.translate(it) }
                    .filter { grid[it] == '.' && (bestCosts[it] ?: Int.MAX_VALUE) > cost + 1 }
                    .map { Node(it, cost + 1) })
            }
            if (foundPath) {
                lowerBound = boulderPos + 1
            } else {
                higherBound = boulderPos
            }
        } while (lowerBound < higherBound)
        println("$lowerBound $higherBound")
        return RelPos(posList[lowerBound - 1][0], posList[lowerBound - 1][1])
    }

    // test if implementation meets criteria from the description, like:
    val testInput = inlineTestInput.trim().reader().readLines()
    //val testInput = readInput("aoc2024/Day18_test")
    val testInputPart1Result = part1(testInput, 12, 7)
    println("Part 1 Test: $testInputPart1Result")
    val testInputPart2Result = part2(testInput, 7)
    println("Part 2 Test: $testInputPart2Result")
    check(testInputPart1Result == 22)
    check(testInputPart2Result == RelPos(6, 1))

    val input = readInput("aoc2024/Day18")
    part1(input, 1024, 71).println()
    part2(input, 71).println()
}
