package org.example.e3fxgaming.adventOfCode.aoc2023.day05

import org.example.e3fxgaming.adventOfCode.utility.Day
import org.example.e3fxgaming.adventOfCode.utility.InputParser
import org.example.e3fxgaming.adventOfCode.utility.MultiLineInputParser
import java.util.PriorityQueue

class Day05(input: String) : Day<Day05.Almanac, Day05.Almanac> {
    data class Almanac(
        val seeds: List<Long>,
        val resourceMaps: List<List<Pair<LongRange, LongRange>>>
    )

    override val partOneParser: InputParser<Almanac> = object : MultiLineInputParser<Almanac>(input) {
        override fun parseFullInput(fullInput: String): List<Almanac> {
            val components = fullInput.split(System.lineSeparator().repeat(2))
            val seeds = components.first().substringAfter("seeds: ").split(' ').map(String::toLong)

            val resourceMaps = components.drop(1).map { componentDesc ->
                val componentLines = componentDesc.split('\n')
                componentLines.drop(1).map { tripletLine ->
                    tripletLine.split(' ').map(String::toLong).let { (destRangeStart, sourceRangeStart, size) ->
                        sourceRangeStart..<(sourceRangeStart + size) to destRangeStart..<(destRangeStart + size)
                    }
                }.sortedBy { it.first.first }
            }

            return listOf(Almanac(seeds, resourceMaps))
        }

    }

    override val partTwoParser: InputParser<Almanac> = partOneParser

    override fun solveFirst(given: List<Almanac>): String =
        solve(given.first(), given.first().seeds.map { it..it })

    private fun List<LongRange>.sortedByStartAndEnd() = sortedWith(
        compareBy(LongRange::first)
            .thenComparing(LongRange::last)
    )

    private fun solve(almanac: Almanac, sections: List<LongRange>): String {
        var currentSections = sections.sortedByStartAndEnd()

        almanac.resourceMaps.forEach { conversionMap ->
            //map to next resource
            currentSections = buildList {
                val toTransform: PriorityQueue<LongRange> = PriorityQueue(compareBy { it.first })
                toTransform.addAll(currentSections)

                for ((sourceRange, targetRange) in conversionMap) {
                    while (toTransform.isNotEmpty() && toTransform.peek().first <= sourceRange.last) {
                        val currentToTransform = toTransform.remove()
                        //currentToTransform part before sourceRange
                        if (currentToTransform.first < sourceRange.first) {
                            val rangeStart = currentToTransform.first
                            val elementsInCurrentToTransform = currentToTransform.last - currentToTransform.first + 1

                            val rangeEnd = (rangeStart + elementsInCurrentToTransform - 1)
                                .coerceAtMost(sourceRange.first - 1)

                            add(rangeStart..rangeEnd)
                        }
                        //currentToTransform part in sourceRange
                        if (currentToTransform.last > sourceRange.first) {
                            val subRangeStart = currentToTransform.first.coerceAtLeast(sourceRange.first)
                            val subRangeEnd = currentToTransform.last.coerceAtMost(sourceRange.last)

                            val sourceTargetOffset = targetRange.first - sourceRange.first
                            val translatedStart = subRangeStart + sourceTargetOffset
                            val translatedEnd = subRangeEnd + sourceTargetOffset

                            add(translatedStart..translatedEnd)
                        }
                        //currentToTransform part after sourceRange
                        if (currentToTransform.last > sourceRange.last) {
                            val subRangeStart = sourceRange.last + 1
                            val subRangeEnd = currentToTransform.last
                            toTransform.add(subRangeStart..subRangeEnd)
                        }

                    }
                    if (toTransform.isEmpty()) break
                }

                while (toTransform.isNotEmpty()) {
                    add(toTransform.remove())
                }
            }

            //condense current ranges
            currentSections = buildList {
                currentSections.sortedByStartAndEnd().forEach {
                    when {
                        isNotEmpty() && (it.first in last() || it.first == last().last + 1) -> {
                            val previous = removeLast()
                            add(previous.first..(previous.last.coerceAtLeast(it.last)))
                        }

                        else -> add(it)
                    }
                }
            }
        }

        return currentSections.first().first.toString()

    }

    override fun solveSecond(given: List<Almanac>): String =
        solve(given.first(), given.first().seeds.chunked(2).map { it[0]..<(it[0] + it[1]) })
}

fun main() = Day05(
    Day05::class.java.getResource("/2023/day05/realInput1.txt")!!.readText()
).runBoth()
package org.example.e3fxgaming.adventOfCode.aoc2023.day05

import org.example.e3fxgaming.adventOfCode.utility.Day
import org.example.e3fxgaming.adventOfCode.utility.InputParser
import org.example.e3fxgaming.adventOfCode.utility.MultiLineInputParser
import java.util.PriorityQueue

class Day05(input: String) : Day<Day05.Almanac, Day05.Almanac> {
    data class Almanac(
        val seeds: List<Long>,
        val resourceMaps: List<List<Pair<LongRange, LongRange>>>
    )

    override val partOneParser: InputParser<Almanac> = object : MultiLineInputParser<Almanac>(input) {
        override fun parseFullInput(fullInput: String): List<Almanac> {
            val components = fullInput.split(System.lineSeparator().repeat(2))
            val seeds = components.first().substringAfter("seeds: ").split(' ').map(String::toLong)

            val resourceMaps = components.drop(1).map { componentDesc ->
                val componentLines = componentDesc.split('\n')
                componentLines.drop(1).map { tripletLine ->
                    tripletLine.split(' ').map(String::toLong).let { (destRangeStart, sourceRangeStart, size) ->
                        sourceRangeStart..<(sourceRangeStart + size) to destRangeStart..<(destRangeStart + size)
                    }
                }.sortedBy { it.first.first }
            }

            return listOf(Almanac(seeds, resourceMaps))
        }

    }

    override val partTwoParser: InputParser<Almanac> = partOneParser

    override fun solveFirst(given: List<Almanac>): String =
        solve(given.first(), given.first().seeds.map { it..it })

    private fun List<LongRange>.sortedByStartAndEnd() = sortedWith(
        compareBy(LongRange::first)
            .thenComparing(LongRange::last)
    )

    private fun solve(almanac: Almanac, sections: List<LongRange>): String {
        var currentSections = sections.sortedByStartAndEnd()

        almanac.resourceMaps.forEach { conversionMap ->
            //map to next resource
            currentSections = buildList {
                val toTransform: PriorityQueue<LongRange> = PriorityQueue(compareBy { it.first })
                toTransform.addAll(currentSections)

                for ((sourceRange, targetRange) in conversionMap) {
                    while (toTransform.isNotEmpty() && toTransform.peek().first <= sourceRange.last) {
                        val currentToTransform = toTransform.remove()
                        //currentToTransform part before sourceRange
                        if (currentToTransform.first < sourceRange.first) {
                            val rangeStart = currentToTransform.first
                            val elementsInCurrentToTransform = currentToTransform.last - currentToTransform.first + 1

                            val rangeEnd = (rangeStart + elementsInCurrentToTransform - 1)
                                .coerceAtMost(sourceRange.first - 1)

                            add(rangeStart..rangeEnd)
                        }
                        //currentToTransform part in sourceRange
                        if (currentToTransform.last > sourceRange.first) {
                            val subRangeStart = currentToTransform.first.coerceAtLeast(sourceRange.first)
                            val subRangeEnd = currentToTransform.last.coerceAtMost(sourceRange.last)

                            val sourceTargetOffset = targetRange.first - sourceRange.first
                            val translatedStart = subRangeStart + sourceTargetOffset
                            val translatedEnd = subRangeEnd + sourceTargetOffset

                            add(translatedStart..translatedEnd)
                        }
                        //currentToTransform part after sourceRange
                        if (currentToTransform.last > sourceRange.last) {
                            val subRangeStart = sourceRange.last + 1
                            val subRangeEnd = currentToTransform.last
                            toTransform.add(subRangeStart..subRangeEnd)
                        }

                    }
                    if (toTransform.isEmpty()) break
                }

                while (toTransform.isNotEmpty()) {
                    add(toTransform.remove())
                }
            }

            //condense current ranges
            currentSections = buildList {
                currentSections.sortedByStartAndEnd().forEach {
                    when {
                        isNotEmpty() && (it.first in last() || it.first == last().last + 1) -> {
                            val previous = removeLast()
                            add(previous.first..(previous.last.coerceAtLeast(it.last)))
                        }

                        else -> add(it)
                    }
                }
            }
        }

        return currentSections.first().first.toString()

    }

    override fun solveSecond(given: List<Almanac>): String =
        solve(given.first(), given.first().seeds.chunked(2).map { it[0]..<(it[0] + it[1]) })
}

fun main() = Day05(
    Day05::class.java.getResource("/2023/day05/realInput1.txt")!!.readText()
).runBoth()