package aoc2023

import println
import readInput
import splitLongs

/*
--- Day 5: If You Give A Seed A Fertilizer ---
https://adventofcode.com/2023/day/5
*/
fun main() {

    val inlineTestInput = """
seeds: 79 14 55 13

seed-to-soil map:
50 98 2
52 50 48

soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15

fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4

water-to-light map:
88 18 7
18 25 70

light-to-temperature map:
45 77 23
81 45 19
68 64 13

temperature-to-humidity map:
0 69 1
1 0 69

humidity-to-location map:
60 56 37
56 93 4
50 98 2
52 50 48
"""


    fun part1(input: List<String>): Long {
        val typeMap = mapOf(
            "seed" to 0,
            "soil" to 1,
            "fertilizer" to 2,
            "water" to 3,
            "light" to 4,
            "temperature" to 5,
            "humidity" to 6
        )
        val mapOfMaps = HashMap<Int, MutableList<Pair<LongRange, Long>>>()
        var seeds = LongArray(0)
        var fromId = 0
        for (i in input) {
            if (i.contains(":")) {
                val (type, data) = i.split(":")
                when (type) {
                    "seeds" -> seeds = data.splitLongs().toLongArray()
                    else -> {
                        val from = type.split("-")[0]
                        fromId = typeMap[from]!!
                    }
                }
            } else if (i.isNotBlank()) {
                val (destRangeStart, sourceRangeStart, length) = i.splitLongs()
                val rangeLists = mapOfMaps.getOrPut(fromId) { ArrayList() }
                rangeLists.add(LongRange(sourceRangeStart, sourceRangeStart + length - 1) to destRangeStart)
            }
        }
        val results = seeds.map {
            var dest = it
            for (i in 0..6) {
                val rangeLists = mapOfMaps[i]!!
                for (r in rangeLists) {
                    if (dest in r.first) {
                        dest = r.second + (dest - r.first.start)
                        break
                    }
                }
            }
            dest
        }
        return results.min()
    }

    fun part2(input: List<String>): Long {
        val typeMap = mapOf(
            "seed" to 0,
            "soil" to 1,
            "fertilizer" to 2,
            "water" to 3,
            "light" to 4,
            "temperature" to 5,
            "humidity" to 6
        )
        val mapOfMaps = HashMap<Int, MutableList<Pair<LongRange, Long>>>()
        var seeds = ArrayDeque<LongRange>()
        var fromId = 0
        for (i in input) {
            if (i.contains(":")) {
                val (type, data) = i.split(":")
                when (type) {
                    "seeds" -> seeds = ArrayDeque(
                        data.splitLongs().chunked(2)
                            .map { LongRange(it[0], it[0] + it[1] - 1) })

                    else -> {
                        val from = type.split("-")[0]
                        fromId = typeMap[from]!!
                    }
                }
            } else if (i.isNotBlank()) {
                val (destRangeStart, sourceRangeStart, length) = i.splitLongs()
                val rangeLists = mapOfMaps.getOrPut(fromId) { ArrayList() }
                rangeLists.add(LongRange(sourceRangeStart, sourceRangeStart + length - 1) to destRangeStart)
            }
        }
        for (i in 0..6) {
            mapOfMaps[i]!!.sortBy { it.first.first }
        }
        var sourceRanges = ArrayDeque(seeds.sortedBy { it.first })
        for (i in 0..6) {
            val newRanges = ArrayDeque<LongRange>()
            val rangeLists = mapOfMaps[i]!!
            while (sourceRanges.isNotEmpty()) {
                val sourceRange = sourceRanges.removeFirst()
                var found = false
                for ((mapRange, destPos) in rangeLists) {
                    if (sourceRange.first in mapRange && sourceRange.last in mapRange) {
                        val dest = destPos + (sourceRange.first - mapRange.first)
                        val destLength = sourceRange.last + 1 - sourceRange.first
                        newRanges.add(LongRange(dest, dest + destLength - 1))
                        found = true
                        break
                    } else if (sourceRange.first in mapRange) {
                        val dest = destPos + (sourceRange.first - mapRange.first)
                        val destLength = mapRange.last + 1 - sourceRange.first
                        newRanges.add(LongRange(dest, dest + destLength - 1))
                        val newSource = mapRange.last + 1
                        val newSourceLength = sourceRange.last - mapRange.last
                        sourceRanges.addFirst(LongRange(newSource, newSource + newSourceLength - 1))
                        found = true
                        break
                    } else if (sourceRange.last in mapRange) {
                        val destLength = sourceRange.last + 1 - mapRange.first
                        val dest = destPos
                        newRanges.add(LongRange(dest, dest + destLength - 1))
                        val newSource = sourceRange.first
                        val newSourceLength = mapRange.first - sourceRange.first
                        sourceRanges.addFirst(LongRange(newSource, newSource + newSourceLength - 1))
                        found = true
                        break
                        // funnily, this is unused, but it should also be a possibility
//                    } else if (sourceRange.first < mapRange.first && sourceRange.last > mapRange.last) {
//                        val destLength = mapRange.last + 1 - mapRange.first
//                        val dest = destPos + mapRange.first - sourceRange.first
//                        newRanges.add(LongRange(dest, dest + destLength - 1))
//                        val newSource1 = mapRange.first
//                        val newSource1Length = mapRange.first - sourceRange.first - 1
//                        sourceRanges.addFirst(LongRange(newSource1, newSource1 + newSource1Length - 1))
//                        val newSource2 = mapRange.last + 1
//                        val newSource2Length = sourceRange.last - mapRange.last + 1
//                        sourceRanges.addFirst(LongRange(newSource2, newSource2 + newSource2Length - 1))
//                        found = true
//                        break
                    }
                    //if (mapRange.first > sourceRange.first) break
                }
                if (!found) {
                    newRanges.add(sourceRange)
                }
            }
            sourceRanges = ArrayDeque(newRanges.sortedBy { it.first }.distinct())
        }

        return sourceRanges.minOf { it.first }
    }

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

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