package org.example.e3fxgaming.adventOfCode.aoc2023.day08

import org.example.e3fxgaming.adventOfCode.utility.Day
import org.example.e3fxgaming.adventOfCode.utility.InputParser
import org.example.e3fxgaming.adventOfCode.utility.Math
import org.example.e3fxgaming.adventOfCode.utility.MultiLineInputParser

class Day08(input: String) : Day<Day08.Maps, Day08.Maps> {
    data class Maps(
        val instructions: List<Char>,
        val map: Map<String, Pair<String, String>>
    )

    override val partOneParser: InputParser<Maps> = object : MultiLineInputParser<Maps>(input) {
        override fun parseFullInput(fullInput: String): List<Maps> {
            val lines = fullInput.split(System.lineSeparator())
            val instructions = lines.first().toList()

            val map = lines.drop(2).associate { mapLine ->
                val (from, toString) = mapLine.split(" = ")
                val (l, r) = toString.substring(1, toString.lastIndex).split(", ")

                from to (l to r)
            }

            return listOf(Maps(instructions, map))
        }
    }

    override val partTwoParser: InputParser<Maps> = partOneParser

    override fun solveFirst(given: List<Maps>): String {
        val onlyMaps = given.first()
        var currentPosition = "AAA"
        var steps = 0

        val instructionIterator = iterator {
            while (true) yieldAll(onlyMaps.instructions)
        }

        while (currentPosition != "ZZZ") {
            ++steps
            val currentOptions = onlyMaps.map.getValue(currentPosition)
            currentPosition = if (instructionIterator.next() == 'L') currentOptions.first else currentOptions.second
        }

        return steps.toString()
    }

    override fun solveSecond(given: List<Maps>): String {
        val onlyMaps = given.first()

        val startNodes = onlyMaps.map.keys.filter { it.last() == 'A' }

        val endPositionsAfterSteps = startNodes.map { startPosition ->
            val instructionIterator = iterator<IndexedValue<Char>> {
                while (true) yieldAll(onlyMaps.instructions.withIndex())
            }

            var nextInstruction = instructionIterator.next()

            var currentPosition = onlyMaps.map.getValue(startPosition).let {
                if (nextInstruction.value == 'L') it.first else it.second
            }
            val visited = mutableSetOf(startPosition + nextInstruction)
            nextInstruction = instructionIterator.next()
            var steps = 1
            buildList<Int> {
                while (currentPosition + nextInstruction.index !in visited) {
                    visited.add(currentPosition + nextInstruction.index)
                    if (currentPosition.endsWith('Z')) add(steps)
                    currentPosition = onlyMaps.map.getValue(currentPosition).let {
                        if (nextInstruction.value == 'L') it.first else it.second
                    }
                    nextInstruction = instructionIterator.next()
                    ++steps
                }
            }
        }

        val math = Math()
        return endPositionsAfterSteps.fold(1L) { acc, (a) ->
            math.lcm(acc, a.toLong())
        }.toString()
    }



}

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