package aoc2023

import chineseRemainder
import primeFactors
import println
import readInput
import sieveOfErastosthenes
import kotlin.math.abs
import kotlin.math.sign

/*
--- Day 24: Never Tell Me The Odds ---
https://adventofcode.com/2023/day/24
*/
fun main() {

    val inlineTestInput = """
19, 13, 30 @ -2, 1, -2
18, 19, 22 @ -1, -1, -2
20, 25, 34 @ -2, -2, -4
12, 31, 28 @ -1, -2, -1
20, 19, 15 @ 1, -5, -3
"""

    data class Hailstone(val px: Long, val py: Long, val pz: Long, val vx: Int, val vy: Int, val vz: Int)

    fun part1(input: List<String>, testx: LongRange, testy: LongRange): Int {
        val hailstones = input.map {
            val (x1, y1, z1, x2, y2, z2) = "(\\d+), (\\d+), (\\d+) @ ([-\\d]+), ([-\\d]+), ([-\\d]+)".toRegex()
                .matchEntire(it)?.destructured!!
            Hailstone(x1.toLong(), y1.toLong(), z1.toLong(), x2.toInt(), y2.toInt(), z2.toInt())
        }
        var count = 0
        for (h1 in hailstones.indices) {
            for (h2 in h1 + 1..hailstones.lastIndex) {
                val s1 = hailstones[h1]
                val s2 = hailstones[h2]

                val dx = (s2.px - s1.px).toDouble()
                val dy = (s2.py - s1.py).toDouble()

                val d = s1.vx.toDouble() * s2.vy.toDouble() -
                    s1.vy.toDouble() * s2.vx.toDouble()

                val t = dx * s2.vy.toDouble() - dy * s2.vx.toDouble()
                val u = dx * s1.vy.toDouble() - dy * s1.vx.toDouble()

                if (t * d.sign > 0 && u * d.sign > 0) {
                    val hx = (s1.px + (t * s1.vx.toDouble()) / d)
                    val hy = (s1.py + (t * s1.vy.toDouble()) / d)
                    if (hx.toLong() in testx && hy.toLong() in testy) count++
                }
            }
        }
        return count
    }

    fun part2(input: List<String>): Long {
        val hailstones = input.map {
            val (x1, y1, z1, x2, y2, z2) = "(\\d+), (\\d+), (\\d+) @ ([-\\d]+), ([-\\d]+), ([-\\d]+)".toRegex()
                .matchEntire(it)?.destructured!!
            Hailstone(x1.toLong(), y1.toLong(), z1.toLong(), x2.toInt(), y2.toInt(), z2.toInt())
        }//.sortedBy { abs(it.vx) + abs(it.vy) + abs(it.vz) }
        //.take(3)

        val h1 = hailstones[0]
        val h2 = hailstones[1]
        val h3 = hailstones[2]

        // rx = h1.px + t1*(h1.vx - rvx) = h2.px + t2*(h2.vx - rvx) = h3.px + t3*(h3.vx - rvx)
        // ry = h1.py + t1*(h1.vy - rvy) = h2.py + t2*(h2.vy - rvy) = h3.py + t3*(h3.vy - rvy)
        // rz = h1.pz + t1*(h1.vz - rvz) = h2.pz + t2*(h2.vz - rvz) = h3.pz + t3*(h3.vz - rvz)

        // [1] h1.px - h2.px = t2*(h2.vx - rvx) - t1*(h1.vx - rvx)
        // [2] h1.px - h3.px = t3*(h3.vx - rvx) - t1*(h1.vx - rvx)
        // [3] h2.px - h3.px = t3*(h3.vx - rvx) - t2*(h2.vx - rvx)

        // [4] h1.py - h2.py = t2*(h2.vy - rvy) - t1*(h1.vy - rvy)
        // [5] h1.py - h3.py = t3*(h3.vy - rvy) - t1*(h1.vy - rvy)
        // [6] h2.py - h3.py = t3*(h3.vy - rvy) - t2*(h2.vy - rvy)

        // [7] h1.pz - h2.pz = t2*(h2.vz - rvz) - t1*(h1.vz - rvz)
        // [8] h1.pz - h3.pz = t3*(h3.vz - rvz) - t1*(h1.vz - rvz)
        // [9] h2.pz - h3.pz = t3*(h3.vz - rvz) - t2*(h2.vz - rvz)

        val sieve = sieveOfErastosthenes(200)
        for (rvx in -1000..1000) {
            val speedMods = hailstones.mapNotNull { if (it.vx - rvx != 0) abs(it.vx - rvx).toLong() to (it.px % abs(it.vx - rvx)) else null }

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

            val rx = reducedPairs.chineseRemainder()
            //val rx = 24

            val bang1Time = (h1.px - rx) / (rvx - h1.vx)
            val bang2Time = (h2.px - rx) / (rvx - h2.vx)
            val h1px = h1.px + h1.vx * bang1Time
            val h1py = h1.py + h1.vy * bang1Time
            val h1pz = h1.pz + h1.vz * bang1Time
            val h2px = h2.px + h2.vx * bang2Time
            val h2py = h2.py + h2.vy * bang2Time
            val h2pz = h2.pz + h2.vz * bang2Time
            val rvy = (h2py - h1py) / (bang2Time - bang1Time)
            val rvz = (h2pz - h1pz) / (bang2Time - bang1Time)
            val ry = h1py - bang1Time * rvy
            val rz = h1pz - bang1Time * rvz
            return rx + ry + rz
        }
        return 0L
    }

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

    val input = readInput("aoc2023/Day24")
    part1(input, 200000000000000L..400000000000000L, 200000000000000L..400000000000000L).println()
    part2(input).println()
}
