package aoc2023

import println
import readInput

/*
--- Day 7: Camel Cards ---
https://adventofcode.com/2023/day/7
*/
fun main() {

    val inlineTestInput = """
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
"""

    val cardTypes = listOf("A", "K", "Q", "J", "T", "9", "8", "7", "6", "5", "4", "3", "2").toTypedArray()
    val cardTypeString = "AKQJT98765432"
    val cardTypeString2 = "AKQT98765432J"
    val cardTypeStringRev = cardTypeString.reversed()
    val cardTypeString2Rev = cardTypeString2.reversed()

    val charCmp = compareBy<Char> { cardTypeStringRev.indexOf(it) }

    fun rank(s: String): Int {
        val ss = s.toCharArray().sortedWith(charCmp).toCharArray().concatToString()
        // five
        for (c in cardTypes) {
            if (ss == c.repeat(5)) {
                return 6
            }
        }
        // four + high card
        for (c in cardTypes) {
            if (ss.contains(c.repeat(4))) {
                return 5
            }
        }
        // full house / three of a kind
        for (c in cardTypes) {
            if (ss.contains(c.repeat(3))) {
                val highCards = ss.replace(c.repeat(3), "")
                for (c2 in cardTypes) {
                    if (highCards.contains(c2.repeat(2))) {
                        return 4
                    }
                }
                // three of a kind
                return 3
            }
        }
        // two pair / one pair
        for (c in cardTypes) {
            if (ss.contains(c.repeat(2))) {
                val highCards = ss.replace(c.repeat(2), "")
                for (c2 in cardTypes) {
                    if (highCards.contains(c2.repeat(2))) {
                        return 2
                    }
                }
                // one pair
                return 1
            }
        }
        return 0
    }

    fun rank2(s: String): Int {
        val jokers = s.count { it == 'J' }
        val ss = s.replace("J", "")
        val good = ss.groupBy { it }.map { it.value.size }.filter { it > 1 }.sortedDescending()
        if (jokers > 3) return 100 // five
        if (jokers == 3 && good.isNotEmpty()) return 100 // five
        if (jokers == 3) return 90 // four
        if (jokers == 2 && good.isEmpty()) return 70 // triple
        if (jokers == 2 && good[0] == 3) return 100 // five
        if (jokers == 2) return 90 // four
        if (jokers == 1 && good.isEmpty()) return 50 // pair
        if (jokers == 1 && good.size == 2) return 80 // full house
        if (jokers == 1 && good[0] == 4) return 100 // five
        if (jokers == 1 && good[0] == 3) return 90 // four
        if (jokers == 1 && good[0] == 2) return 70 // triple
        if (jokers == 0 && good.isEmpty()) return 0 // nothing
        if (jokers == 0 && good[0] == 4) return 90 // four
        if (jokers == 0 && good[0] == 3 && good.size == 2) return 80 // full house
        if (jokers == 0 && good[0] == 3) return 70 // triple
        if (jokers == 0 && good.size == 2) return 60 // two pairs
        return 50 // one pair
    }

    fun part1(input: List<String>): Int {
        val comparator = compareBy<Pair<String, Int>> { rank(it.first) }
        val secondComparator = compareBy<Pair<String, Int>> { it.first.mapIndexed { i, c -> cardTypeStringRev.indexOf(c) shl ((5 - i) * 4) }.sum() }
        return input.map { it -> it.split(" ") }
            .map { it[0] to it[1].toInt() }
            .sortedWith(comparator.then(secondComparator))
            .mapIndexed { index, p -> (index + 1) * p.second }
            .sum()
    }

    fun part2(input: List<String>): Int {
        val comparator = compareBy<Pair<String, Int>> { rank2(it.first) }
        val secondComparator = compareBy<Pair<String, Int>> { it.first.mapIndexed { i, c -> cardTypeString2Rev.indexOf(c) shl ((5 - i) * 4) }.sum() }
        return input.map { it -> it.split(" ") }
            .map { it[0] to it[1].toInt() }
            .sortedWith(comparator.then(secondComparator))
            .mapIndexed { index, p -> (index + 1) * p.second }
            .sum()
    }

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

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