package org.example.e3fxgaming.adventOfCode.aoc2023.day07

import org.example.e3fxgaming.adventOfCode.utility.Day
import org.example.e3fxgaming.adventOfCode.utility.IndividualLineInputParser
import org.example.e3fxgaming.adventOfCode.utility.InputParser

class Day07(input: String) : Day<Pair<String, Int>, Pair<String, Int>> {
    private abstract class CardComparator : Comparator<String> {
        protected enum class HandType {
            HighCard, OnePair, TwoPair, ThreeOfAKind, FullHouse, FourOfAKind, FiveOfAKind
        }

        protected fun Map<Int, List<Char>>.handType(): HandType = when {
            5 in this -> HandType.FiveOfAKind
            4 in this -> HandType.FourOfAKind
            3 in this -> if (2 in this) HandType.FullHouse else HandType.ThreeOfAKind
            this[2]?.size == 2 -> HandType.TwoPair
            this[2]?.size == 1 && this[1]?.size == 3 -> HandType.OnePair
            this[1]?.size == 5 -> HandType.HighCard
            else -> throw IllegalStateException("HandType could not be determined: $this")
        }

        protected abstract val cardStrength: List<Char>

        protected fun countToCardsToResult(
            o1: String,
            o2: String,
            o1CountToCards: Map<Int, List<Char>>,
            o2CountToCards: Map<Int, List<Char>>
        ): Int {
            val o1HandType = o1CountToCards.handType()
            val o2HandType = o2CountToCards.handType()

            if (o1HandType != o2HandType) return o1HandType.ordinal - o2HandType.ordinal

            return o1.zip(o2).firstNotNullOf {
                val result = cardStrength.indexOf(it.first) - cardStrength.indexOf(it.second)
                if (result != 0) result else null
            }
        }
    }


    private class CardComparatorPart1 : CardComparator() {
        override val cardStrength = listOf('2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A')

        private fun String.countToCards() = groupingBy { it }.eachCount().asIterable().groupBy(
            { it.value },
            { it.key }
        )

        override fun compare(o1: String, o2: String): Int {
            val o1CountToCards = o1.countToCards()
            val o2CountToCards = o2.countToCards()

            return countToCardsToResult(o1, o2, o1CountToCards, o2CountToCards)
        }
    }

    private class CardComparatorPart2 : CardComparator() {
        override val cardStrength = listOf('J', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'Q', 'K', 'A')

        private fun Map<Char, Int>.improveWithJoker(): Map<Char, Int> {
            if ('J' !in this || size == 1) return this
            val withoutJoker = (this - 'J').toMutableMap()
            val jokerCount = this.getValue('J')

            val improveEntry = withoutJoker.entries.reduce { largestEntrySoFar, currentEntry ->
                if (largestEntrySoFar.value < currentEntry.value) {
                    currentEntry
                } else largestEntrySoFar
            }

            withoutJoker[improveEntry.key] = improveEntry.value + jokerCount
            return withoutJoker
        }

        override fun compare(o1: String, o2: String): Int {
            val o1CharCount = o1.groupingBy { it }.eachCount()
            val o2CharCount = o2.groupingBy { it }.eachCount()

            val o1Improved = o1CharCount.improveWithJoker()
            val o2Improved = o2CharCount.improveWithJoker()

            val o1CountToCards = o1Improved.asIterable().groupBy(
                { it.value },
                { it.key }
            )
            val o2CountToCards = o2Improved.asIterable().groupBy(
                { it.value },
                { it.key }
            )

            return countToCardsToResult(o1, o2, o1CountToCards, o2CountToCards)
        }
    }

    override val partOneParser: InputParser<Pair<String, Int>> = object : IndividualLineInputParser<Pair<String, Int>>(
        input
    ) {
        override fun parseLine(line: String): Pair<String, Int> =
            line.split(' ').let { it[0].trim() to it[1].toInt() }
    }

    override val partTwoParser: InputParser<Pair<String, Int>> = partOneParser

    override fun solveFirst(given: List<Pair<String, Int>>): String = solve(given, CardComparatorPart1())

    override fun solveSecond(given: List<Pair<String, Int>>): String = solve(given, CardComparatorPart2())

    private fun solve(given: List<Pair<String, Int>>, comparator: CardComparator): String {
        val sortedHands = given.sortedWith(java.util.Comparator.comparing({ it.first }, comparator))

        return sortedHands.foldIndexed(0L) { rankMinusOne, result, (_, bid) ->
            result + ((rankMinusOne + 1) * bid)
        }.toString()
    }
}

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