package aoc2024

import println
import readInput
import java.util.*
import kotlin.collections.ArrayDeque

/*
--- Day 24: Crossed Wires ---
https://adventofcode.com/2024/day/24
*/
fun main() {

    val inlineTestInput = """
x00: 1
x01: 1
x02: 1
y00: 0
y01: 1
y02: 0

x00 AND y00 -> z00
x01 XOR y01 -> z01
x02 OR y02 -> z02
"""

    val inlineTestInput2 = """
x00: 1
x01: 0
x02: 1
x03: 1
x04: 0
y00: 1
y01: 1
y02: 1
y03: 1
y04: 1

ntg XOR fgs -> mjb
y02 OR x01 -> tnw
kwq OR kpj -> z05
x00 OR x03 -> fst
tgd XOR rvg -> z01
vdt OR tnw -> bfw
bfw AND frj -> z10
ffh OR nrd -> bqk
y00 AND y03 -> djm
y03 OR y00 -> psh
bqk OR frj -> z08
tnw OR fst -> frj
gnj AND tgd -> z11
bfw XOR mjb -> z00
x03 OR x00 -> vdt
gnj AND wpb -> z02
x04 AND y00 -> kjc
djm OR pbm -> qhw
nrd AND vdt -> hwm
kjc AND fst -> rvg
y04 OR y02 -> fgs
y01 AND x02 -> pbm
ntg OR kjc -> kwq
psh XOR fgs -> tgd
qhw XOR tgd -> z09
pbm OR djm -> kpj
x03 XOR y03 -> ffh
x00 XOR y04 -> ntg
bfw OR bqk -> z06
nrd XOR fgs -> wpb
frj XOR qhw -> z04
bqk OR frj -> z07
y03 OR x01 -> nrd
hwm AND bqk -> z03
tgd XOR rvg -> z12
tnw OR pbm -> gnj
"""

    data class Node(val n1: String, val n2: String, val op: Int, var target: String, var val1: Boolean? = null, var val2: Boolean? = null) {
        fun output() = when (op) {
            0 -> val1!! || val2!!
            1 -> val1!! && val2!!
            2 -> val1!! != val2!!
            else -> false
        }

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as Node

            return target == other.target
        }

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

    val nodes = ArrayList<Node>()
    val nodesWaiting = HashMap<String, MutableList<Node>>()
    var satisfiedNodes = ArrayList<Node>()
    val zTargets = TreeSet<String>()
    val zState = HashMap<String, Boolean>()
    val targetParent = HashMap<String, Node>()
    val initialStates = HashMap<String, Boolean>()
    val zInvolvedNodes = HashMap<String, MutableSet<Node>>()
    val random = kotlin.random.Random(1337)

    fun readCircuit(input: List<String>) {
        nodes.clear()
        nodesWaiting.clear()
        satisfiedNodes.clear()
        zTargets.clear()
        zState.clear()
        targetParent.clear()
        initialStates.clear()
        var i = 0
        while (input[i++].isNotEmpty()) {
            val (n, st) = input[i - 1].split(": ")
            initialStates[n] = (st == "1")
        }
        while (i < input.size) {
            val (n1, op, n2, _, target) = input[i++].split(" ")
            val opCode = when (op) {
                "OR" -> 0
                "AND" -> 1
                "XOR" -> 2
                else -> throw IllegalStateException()
            }
            if (target.startsWith("z")) {
                zTargets.add(target)
            }
            val node = Node(n1, n2, opCode, target)
            nodes.add(node)
            targetParent[target] = node
        }
    }

    fun outputState(target: String, output: Boolean, newSatisfiedNodes: MutableCollection<Node>) {
        if (target.startsWith("z")) {
            zState[target] = output
        }
        val targetList = nodesWaiting[target] ?: return
        while (targetList.isNotEmpty()) {
            val tNode = targetList.removeFirst()
            if (tNode.n1 == target) {
                tNode.val1 = output
                if (tNode.val2 != null) {
                    newSatisfiedNodes.add(tNode)
                }
            } else {
                tNode.val2 = output
                if (tNode.val1 != null) {
                    newSatisfiedNodes.add(tNode)
                }
            }
        }
    }

    fun runCircuit(): Boolean {
        while (zState.size != zTargets.size) {
            if (satisfiedNodes.isEmpty()) {
                return false
            }
            val newSatisfiedNodes = ArrayList<Node>()
            for (node in satisfiedNodes) {
                val output = node.output()
                node.val1 = null
                node.val2 = null
                nodesWaiting.getOrPut(node.n1) { ArrayList() }.add(node)
                nodesWaiting.getOrPut(node.n2) { ArrayList() }.add(node)

                outputState(node.target, output, newSatisfiedNodes)
            }
            satisfiedNodes = newSatisfiedNodes
        }
        return true
    }

    fun reset() {
        satisfiedNodes.clear()
        nodesWaiting.clear()
        zState.clear()
        for (node in nodes) {
            node.val1 = null
            node.val2 = null
            nodesWaiting.getOrPut(node.n1) { ArrayDeque() }.add(node)
            nodesWaiting.getOrPut(node.n2) { ArrayDeque() }.add(node)
        }
    }

    fun setXY(n: String, value: Long) {
        var v = value
        for (i in 0..zTargets.size - 2) {
            outputState("%s%02d".format(n, i), (v and 1L) != 0L, satisfiedNodes)
            v = v shr 1
        }
    }

    fun readZ(): Long {
        var result = 0L
        for (i in zTargets.reversed()) {
            result *= 2
            if (zState[i] == true) result++
        }

        return result
    }

    fun calcInvolvedNodes() {
        zInvolvedNodes.clear()
        for (target in zTargets) {
            val set = HashSet<Node>()
            val queue = ArrayDeque<Node>()
            queue.add(targetParent[target]!!)
            while (queue.isNotEmpty()) {
                val node = queue.removeFirst()
                set.add(node)
                val t1 = targetParent[node.n1]
                val t2 = targetParent[node.n2]
                if (t1 != null && !set.contains(t1)) queue.add(t1)
                if (t2 != null && !set.contains(t2)) queue.add(t2)
            }
            zInvolvedNodes[target] = set
        }
    }

    fun swapOutputs(t1: String, t2: String) {
        val n1 = targetParent[t1]!!
        val n2 = targetParent[t2]!!
        n1.target = t2
        n2.target = t1
        targetParent[t1] = n2
        targetParent[t2] = n1
    }

    fun part1(input: List<String>): Long {
        readCircuit(input)
        reset()
        initialStates.forEach { outputState(it.key, it.value, satisfiedNodes) }
        runCircuit()

        return readZ()
    }

    fun checkIt(x: Long, y: Long, sum: Long, mask: Long = -1L): Boolean {
        reset()
        setXY("x", x)
        setXY("y", y)
        return runCircuit() && (readZ() and mask) == sum
    }

    fun countGoodBits(startbit: Int): Int {
        val inv = (1L shl (zTargets.size - 1)) - 1
        for (b in startbit..zTargets.size - 2) {
            val tv = 1L shl b
            val tvi = (1L shl (b + 1)) - 1L
            if (!(checkIt(tv, 0L, tv) &&
                    checkIt(tvi, 0L, tvi) &&
                    checkIt(0L, tv, tv) &&
                    checkIt(0L, tvi, tvi) &&
                    checkIt(tv, tv, 2 * tv) &&
                    checkIt(tvi, tvi, 2 * tvi) &&
                    checkIt(inv - tvi, 0L, 0L, tvi) &&
                    checkIt(0L, inv - tvi, 0L, tvi)
                    )
            ) {
                return b
            }
            for (i in 0..(1L shl ((b - 8).coerceIn(1..6)))) {
                val rx = random.nextLong() and tvi
                val ry = random.nextLong() and tvi
                if (!checkIt(rx, ry, rx + ry, mask = tvi * 2 + 1)) return b
            }
        }
        return zTargets.size
    }

    fun part2(input: List<String>): String {
        readCircuit(input)
        calcInvolvedNodes()

        val goodGates = HashSet<Node>()

        val swaps = ArrayList<Pair<String, String>>()

        for (b in 0..zTargets.size - 3) {
            val zb0 = zInvolvedNodes["z%02d".format(b)]!!
            val zb1 = zInvolvedNodes["z%02d".format(b + 1)]!!
            val zb2 = zInvolvedNodes["z%02d".format(b + 2)]!!

            val tv = 1L shl b
            val tvi = (1L shl (b + 1)) - 1L
            val good = checkIt(tv, 0L, tv) &&
                checkIt(tvi, 0L, tvi) &&
                checkIt(0L, tv, tv) &&
                checkIt(0L, tvi, tvi) &&
                checkIt(tv, tv, 2 * tv) &&
                checkIt(tvi, tvi, 2 * tvi)
            if (good) {
                goodGates.addAll(zb0)
                //goodGates.addAll(zb1)
            } else {
                println("Bit error at $b")
                val badList = zb0.union(zb1).union(zb2).minus(goodGates).map { it.target }.toList()
                var swapped = false
                out@ for (p1 in badList.indices) {
                    for (p2 in p1 + 1..badList.lastIndex) {
                        swapOutputs(badList[p1], badList[p2])
                        val goodBits = countGoodBits(b)
                        if (goodBits > b + 1) {
                            println("**** Successfully $goodBits swapped ${badList[p1]} with ${badList[p2]}")
                            swapped = true
                            swaps.add(badList[p1] to badList[p2])
                            break@out
                        } else {
                            swapOutputs(badList[p1], badList[p2])
                        }
                    }
                }
                if (!swapped) throw IllegalStateException("Sob!")
                calcInvolvedNodes()
            }
        }
        return swaps.flatMap { listOf(it.first, it.second) }.sorted().joinToString(",")
    }

    // test if implementation meets criteria from the description, like:
    val testInput = inlineTestInput.trim().reader().readLines()
    val testInput2 = inlineTestInput2.trim().reader().readLines()
    //val testInput = readInput("aoc2024/Day24_test")
    val testInputPart1Result = part1(testInput)
    println("Part 1 Test: $testInputPart1Result")
    val testInputPart1Result2 = part1(testInput2)
    println("Part 1 Test 2: $testInputPart1Result2")
//    val testInputPart2Result = part2(testInput2)
//    println("Part 2 Test: $testInputPart2Result")
    check(testInputPart1Result == 4L)
    check(testInputPart1Result2 == 2024L)
    //check(testInputPart2Result == "z00,z01,z02,z05")

    val input = readInput("aoc2024/Day24")
    part1(input).println()
    part2(input).println()
}
