package aoc2023

import CharGrid
import RelPos
import println
import readInput

/*
--- Day 23: A Long Walk ---
https://adventofcode.com/2023/day/23
*/
fun main() {

    val inlineTestInput = """
#.#####################
#.......#########...###
#######.#########.#.###
###.....#.>.>.###.#.###
###v#####.#v#.###.#.###
###.>...#.#.#.....#...#
###v###.#.#.#########.#
###...#.#.#.......#...#
#####.#.#.#######.#.###
#.....#.#.#.......#...#
#.#####.#.#.#########v#
#.#...#...#...###...>.#
#.#.#v#######v###.###v#
#...#.>.#...>.>.#.###.#
#####v#.#.###v#.#.###.#
#.....#...#...#.#.#...#
#.#########.###.#.#.###
#...###...#...#...#.###
###.###.#.###v#####v###
#...#...#.#.>.>.#.>.###
#.###.###.#.###.#.#v###
#.....###...###...#...#
#####################.#
"""

    data class Node(val pos: RelPos, val relLen: Int, val path: LinkedHashSet<RelPos>)

    fun part1(input: List<String>): Int {
        val grid = CharGrid(input, '#')
        val queue = ArrayDeque<Node>()
        queue.add(Node(RelPos(1, 0), 0, LinkedHashSet()))
        var maxLength = 0
        while (queue.isNotEmpty()) {
            val node = queue.removeFirst()
            var pos = node.pos
            var relLen = node.relLen
            val path = node.path
            path.add(pos)
            var lastPos = pos
            do {
                if (pos.dr == grid.height - 1) {
                    maxLength = maxLength.coerceAtLeast(relLen)
                    break
                }
                relLen++
                var nextPos = CharGrid.PLUS_POS
                    .filter { grid[pos.dc + it.dc, pos.dr + it.dr] == '.' && (pos.dc + it.dc != lastPos.dc || pos.dr + it.dr != lastPos.dr) }
                    .map { it.translate(pos) }.singleOrNull { !path.contains(it) }
                val branchPos = CharGrid.PLUS_POS
                    .filter {
                        val c = grid[pos.dc + it.dc, pos.dr + it.dr]
                        (c == '>' && it.dc > 0) || (c == 'v' && it.dr > 0)
                            || c == '?'
                    }
                    .map { it.translate(pos) }.filter { !path.contains(it) }

                if (nextPos == null && branchPos.size == 1) {
                    nextPos = branchPos.single()
                } else {
                    if (branchPos.isNotEmpty()) {
                        path.add(pos)
                        branchPos.forEach { queue.addLast(Node(it, relLen, LinkedHashSet(path))) }
                    }
                }
                if (nextPos != null) {
                    lastPos = pos
                    pos = nextPos
                }
            } while (nextPos != null)
        }

        return maxLength
    }

    data class GNode(val pos: RelPos, val edges: MutableSet<Pair<Int, GNode>> = HashSet()) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as GNode

            return pos == other.pos
        }

        override fun hashCode(): Int {
            return pos.hashCode()
        }

        override fun toString(): String {
            return "GNode(pos=$pos)"
        }
    }

    fun rec(endnode: GNode, node: GNode, len: Int, visited: Array<BooleanArray>): Int {
        visited[node.pos.dr][node.pos.dc] = true
        var maxLen = 0
        for (e in node.edges) {
            if (e.second == endnode) {
                visited[node.pos.dr][node.pos.dc] = false
                return len + e.first
            }
            if (!visited[e.second.pos.dr][e.second.pos.dc]) {
                val longestPath = rec(endnode, e.second, len + e.first, visited)
                maxLen = maxLen.coerceAtLeast(longestPath)
            }
        }
        visited[node.pos.dr][node.pos.dc] = false
        return maxLen
    }

    fun part2(input: List<String>): Int {
        val modinput = input.map { it.replace(">", ".").replace("v", ".") }.toList()

        val map = HashMap<RelPos, GNode>()
        val grid = CharGrid(modinput, '#')
        val queue = ArrayDeque<Pair<GNode, RelPos>>()
        val startNode = GNode(RelPos(1, 0))
        queue.add(startNode to RelPos(0, 1))
        map[startNode.pos] = startNode

        val endPos = RelPos(grid.width - 2, grid.height - 1)
        val endNode = GNode(endPos)
        map[endPos] = endNode
        val visited = HashSet<GNode>()
        visited.add(startNode)
        while (queue.isNotEmpty()) {
            val (node, dir) = queue.removeFirst()
            var pos = node.pos.translate(dir)
            var relLen = 0
            var lastPos = node.pos
            do {
                val nextDirs = CharGrid.PLUS_POS
                    .filter { grid[pos.dc + it.dc, pos.dr + it.dr] == '.' && (pos.dc + it.dc != lastPos.dc || pos.dr + it.dr != lastPos.dr) }

                relLen++
                if (nextDirs.size == 1) {
                    lastPos = pos
                    pos = pos.translate(nextDirs.single())
                    if (pos == endPos) {
                        node.edges.add(relLen + 1 to endNode)
                        endNode.edges.add(relLen + 1 to node)
                        break
                    }
                } else {
                    val branchNode = map.computeIfAbsent(pos) { GNode(it) }
                    branchNode.edges.add(relLen to node)
                    node.edges.add(relLen to branchNode)
                    if (!visited.contains(branchNode)) {
                        visited.add(branchNode)
                        nextDirs.forEach { queue.addLast(branchNode to it) }
                    }
                }
            } while (nextDirs.size == 1)
        }

        // optimize endnode path
        var cNode: GNode = endNode
        var pNode = cNode
        while (cNode.edges.size == 1) {
            pNode = cNode
            cNode = cNode.edges.single().second
        }
        cNode.edges.removeIf { it.second != pNode }

        return rec(endNode, startNode, 0, Array(grid.height) { BooleanArray(grid.width) })
    }

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

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