package aoc2025

import chineseRemainder
import gcdPositive
import primeFactors
import primeSequence
import println
import readInput
import sieveOfErastosthenes
import splitInts
import java.util.*

/*
--- Day 10: Factory ---
https://adventofcode.com/2025/day/10
*/
fun main() {

    val inlineTestInput = """
[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
[...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}
[.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}
"""

    data class Machine(val size: Int, val target: Int, val toggles: IntArray, val joltage: IntArray)

    fun parse(input: List<String>): ArrayList<Machine> {
        val machines = ArrayList<Machine>()
        for (i in input) {
            val stuff = i.split(" ")
            val machSize = stuff[0].length - 2
            val target = stuff[0].removeSurrounding("[", "]")
                .foldIndexed(0) { i, acc, ch -> acc + if (ch == '#') (1 shl i) else 0 }
            val toggles =
                stuff.drop(1).dropLast(1)
                    .map { it.removeSurrounding("(", ")").splitInts(",").fold(0) { acc, v -> acc + (1 shl v) } }
                    .toIntArray()
            val joltages = stuff.last().removeSurrounding("{", "}").splitInts(",").toIntArray()
            machines.add(Machine(machSize, target, toggles, joltages))
        }
        return machines
    }

    fun part1(input: List<String>): Int {
        val machines = parse(input)
        var sumButts = 0
        for (m in machines) {
            // look at which different possible sets of toggles need to be pressed to result in the target number
            // pressing an even time will cancel out the effect, so only look what happens if you press once
            var minPresses = Int.MAX_VALUE
            for (tm in 1 until (1 shl m.toggles.size)) {
                val odds = m.toggles.filterIndexed { i, v -> (1 shl i) and tm != 0 }
                val result = odds.fold(0) { acc, iv -> acc xor iv }
                if (result == m.target) {
                    minPresses = minPresses.coerceAtMost(odds.size)
                }
            }
            sumButts += minPresses
        }
        return sumButts
    }

    data class Toggle(val idx: Int, val v: Int, var min: Int = 0, var max: Int = Int.MAX_VALUE)

    fun applyJoltage(jolts: IntArray, toggle: Toggle, times: Int = 1): Boolean {
        var tt = toggle.v
        var jp = 0
        var valid = true
        while (tt != 0) {
            if (tt and 1 != 0) {
                jolts[jp] -= times
                if (jolts[jp] < 0) {
                    valid = false
                    break
                }
            }
            tt = tt shr 1
            jp++
        }
        return valid
    }

    fun findMaxButtonPresses(jolts: IntArray, toggle: Toggle): Int {
        var tt = toggle.v
        var jp = 0
        val maxPresses = Int.MAX_VALUE
        while (tt != 0) {
            if (tt and 1 != 0) {
                maxPresses.coerceAtMost(jolts[jp] / 2)
            }
            tt = tt shr 1
            jp++
        }
        return maxPresses
    }

    fun part2(input: List<String>): Int {
        val machines = parse(input)

        val sieve = sieveOfErastosthenes(10000)
        val primes = primeSequence(sieve).take(500).toList()
        var sumButts = 0
        for (m in machines) {
            println()
            val maxJoltage = m.joltage.max()
            val primefactors = primes.dropWhile { it <= maxJoltage }.take(m.size).map { it.toLong() }
            if (primefactors.size != m.size) throw IllegalStateException()
            val bigTarget = m.joltage.foldIndexed(0L) { index, acc, i -> acc.plus(i.toLong() * primefactors[index]) }
            val bigToggles = ArrayList<Long>()
            for (t in m.toggles) {
                var tt = t
                var bigToggle = 0L
                var idx = 0
                while (tt > 0) {
                    if (tt and 1 != 0) {
                        bigToggle += primefactors[idx]
                    }
                    idx += 1
                    tt = tt shr 1
                }
                bigToggles.add(bigToggle)
            }

            // j0 * bigtoggle[0] + j1 * bigtoggle[1] + ... = bigTarget
            // is a Linear Diophantine equation that can be solved with the extended Euclidean algorithm

            var gcd = gcdPositive(bigToggles[0], bigToggles[1])
            for (i in 2 until bigToggles.size) {
                gcd = gcdPositive(gcd, bigToggles[i])
            }
            if (bigTarget % gcd != 0L) throw IllegalStateException()

            val factorMap = HashMap<Long, Long>()
            var good = true
            val primeFactors = primeFactors(bigTarget, sieve)
            for (pf in primeFactors) {
                val sf = factorMap[pf]
                val pr = 1000 % pf
                if (sf != null && sf != pr) {
                    good = false
                    break
                }
                factorMap[pf] = pr
            }
            val reducedPairs = factorMap.map { it.value to it.key }.sortedBy { it.second }
            val rx = reducedPairs.chineseRemainder()

            val leastFingers = 0
            println(leastFingers)
            sumButts += leastFingers
        }
        return sumButts
    }

    fun part2old(input: List<String>): Int {
        val machines = parse(input)

        var sumButts = 0
        for (m in machines) {
            // generate toggles and calculate the global maximum of toggle presses for this toggle
            val toggles = m.toggles.mapIndexed { i, t ->
                Toggle(
                    i,
                    t,
                    max = IntRange(0, m.size).filter { b -> t and (1 shl b) != 0 }.minOf { m.joltage[it] })
            }
            // try to calculate a minimum number of toggle presses as the joltage needs to be reached exactly
            val subsets = Array(m.size) { toggles.filter { v -> (1 shl it) and v.v != 0 }.toTypedArray() }
            for (v in toggles) {
                var minT = 0
                for (b in 0 until m.size) {
                    var rj = m.joltage[b]
                    var found = false
                    for (s in subsets[b]) {
                        if (s === v) {
                            found = true
                        } else {
                            rj -= s.max
                            if (rj < 0) break
                        }
                    }
                    if (found) minT = minT.coerceAtLeast(rj)
                }
                if (minT > v.max) throw IllegalStateException()
                v.min = minT
            }

            var minPushes = Int.MAX_VALUE
            val jolts = m.joltage.copyOf()
            val pressCount = IntArray(m.toggles.size)

            // press all buttons regarding their minimal count (if any)
            for (t in toggles) {
                if (pressCount[t.idx] + t.min > t.max || !applyJoltage(jolts, t, times = t.min)) throw IllegalStateException()
                pressCount[t.idx] += t.min
            }
            // this is the starting point for the exhaustive search
            val pq = PriorityQueue(compareBy<Pair<IntArray, IntArray>> { it.second.sum() })
            pq.add(jolts to pressCount)
            while (pq.isNotEmpty()) {
                val (j, tc) = pq.poll()
                val pushes = tc.sum()
                if (pushes >= minPushes) break
                // if the joltage has counted down to zero, we're done
                if (j.sum() == 0) {
                    minPushes = pushes
                    break
                }
                for (t in toggles) {
                    if (tc[t.idx] + 2 <= t.max) {
                        val maxTimes = 1//findMaxButtonPresses(j, t)
                        if (maxTimes > 0) {
                            val nj = j.copyOf()
                            if (applyJoltage(nj, t, maxTimes)) {
                                val ntc = tc.copyOf()
                                ntc[t.idx] += maxTimes
                                pq.add(nj to ntc)
                            }
                        }
                    }
                }
            }
            println("$minPushes")
            sumButts += minPushes
        }

        return sumButts
    }

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

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