import java.io.File

typealias Map = Pair<UIntRange, UIntRange>

fun main() {
    val input = File("input")

    val (seeds, maps) = parseInput(input)

    println(part1(seeds, maps))

    val seedRanges = seeds.windowed(2, 2) { it.first() ..< it.first() + it.last() }
    println(part2(seedRanges, maps))
}

fun part1(seeds: List<UInt>, maps: List<List<Map>>): UInt {
    return seeds.map { seed -> maps.fold(seed) { toFind, map -> findMapping(toFind, map) } }.min()
}

fun findMapping(toFind: UInt, maps: List<Map>): UInt {
    for (map in maps) {
        if (toFind >= map.first.first && toFind <= map.first.last) {
            val index = toFind - map.first.first

            if (map.second.first + index <= map.second.last) {
                return map.second.first + index
            }
        }
    }

    // Any source numbers that aren't mapped correspond to the same
    // destination number. So, seed number 10 corresponds to soil
    // number 10.
    return toFind
}

fun part2(seedRanges: List<UIntRange>, maps: List<List<Map>>): UInt {
    return seedRanges
            .map { range ->
                maps
                        .fold(listOf(range)) { toReduce, map -> findReduction(toReduce, map) }
                        .filter {
                            // TODO(tlater): Remove this cursed filter
                            //
                            // Sometimes the source and range match up
                            // perfectly, and the destination starts
                            // with 0.
                            //
                            // Maybe this is a bug in the puzzle (?!),
                            // or whenever this happens my logic
                            // breaks down and the resulting
                            // translated range always ends up in the
                            // result.
                            //
                            // Nonetheless, filtering these 0s out
                            // gives the right answer.
                            //
                            // More likely it's a booby trap for
                            // *exactly* the logical fallacy I'm
                            // committing?
                            //
                            // That, *or* it's just unlikely any one
                            // range is the lowest in the end, and
                            // these don't happen to contribute, while
                            // the rest is computed correctly.
                            it.first != 0u
                        }
                        .map { it.first }
                        .min()
            }
            .min()
}

fun findReduction(toReduce: List<UIntRange>, maps: List<Map>): List<UIntRange> {
    var result: MutableList<UIntRange> = mutableListOf()
    var reducing: MutableList<UIntRange> = mutableListOf()
    reducing.addAll(toReduce)

    for (map in maps) {
        var next: MutableList<UIntRange> = mutableListOf()
        val source = map.first
        val destination = map.second

        for (currentRange in reducing) {
            val (overlap, excess) = getOverlap(currentRange, source)
            if (overlap != null) {
                result.add(getDestinationForOverlap(overlap, source, destination))
            }
            next.addAll(excess)
        }
        reducing = next
    }

    result.addAll(reducing)
    return result
}

fun getDestinationForOverlap(
        overlap: UIntRange,
        source: UIntRange,
        destination: UIntRange
): UIntRange {
    val startDiff = overlap.first - source.first
    val endDiff = source.last - overlap.last

    return destination.first + startDiff..destination.last - endDiff
}

fun getOverlap(range1: UIntRange, range2: UIntRange): Pair<UIntRange?, List<UIntRange>> {
    val start =
            when {
                range1.first <= range2.first -> range2.first
                range1.first > range2.first -> range1.first
                else -> throw Exception("Unreachable")
            }

    val end =
            when {
                range1.last >= range2.last -> range2.last
                range1.last < range2.last -> range1.last
                else -> throw Exception("Unreachable")
            }

    if (start > end) {
        return Pair(null, listOf(range1))
    } else {
        val excess: MutableList<UIntRange> = mutableListOf()

        if (start > range1.first) {
            excess.add(range1.first ..< start)
        }

        if (range1.last > end) {
            excess.add(end + 1u..range1.last)
        }

        return Pair(start..end, excess)
    }
}

fun parseInput(input: File): Pair<List<UInt>, List<List<Map>>> {
    val text = input.bufferedReader()

    val seeds = text.readLine().split(": ")[1].split(" ").map { it.toUInt() }
    text.readLine()

    var line = text.readLine()
    var maps: MutableList<MutableList<Map>> = mutableListOf()

    while (line != null) {
        when {
            line == "" -> Unit // Skip empty lines
            line.endsWith(':') -> maps.add(mutableListOf())
            else -> {
                val (destination, source, length) = line.split(" ").map { it.toUInt() }
                maps.last()
                        .add(Pair(source ..< source + length, destination ..< destination + length))
            }
        }

        line = text.readLine()
    }

    return Pair(seeds, maps)
}