package com.jsjdesigns.poker.core

/**
 * Five-to-seven card hand evaluation.
 *
 * [evaluate] returns a packed Int where a numerically larger value is a strictly
 * better hand, so comparing hands is a plain `>`. Layout is
 * `category(4 bits) | t1 | t2 | t3 | t4 | t5` with each tiebreak nibble holding a
 * rank in 2..14.
 *
 * This deliberately avoids the "try all 21 five-card subsets" approach: equity
 * simulation calls this millions of times, so it buckets ranks and suits in a
 * single pass instead.
 */
object HandEvaluator {

    const val HIGH_CARD = 0
    const val PAIR = 1
    const val TWO_PAIR = 2
    const val TRIPS = 3
    const val STRAIGHT = 4
    const val FLUSH = 5
    const val FULL_HOUSE = 6
    const val QUADS = 7
    const val STRAIGHT_FLUSH = 8

    val CATEGORY_NAMES = arrayOf(
        "High Card", "Pair", "Two Pair", "Three of a Kind", "Straight",
        "Flush", "Full House", "Four of a Kind", "Straight Flush",
    )

    fun categoryOf(score: Int): Int = score ushr 20

    fun describe(score: Int): String = CATEGORY_NAMES[categoryOf(score)]

    /** Evaluates 5, 6 or 7 cards given as deck indices in 0..51. */
    fun evaluate(cards: IntArray, count: Int = cards.size): Int {
        val rankCount = IntArray(15)
        val suitCount = IntArray(4)
        val suitMask = IntArray(4)
        var rankMask = 0

        for (i in 0 until count) {
            val c = cards[i]
            val r = 2 + c / 4
            val s = c % 4
            rankCount[r]++
            suitCount[s]++
            suitMask[s] = suitMask[s] or (1 shl r)
            rankMask = rankMask or (1 shl r)
        }

        // Flushes dominate everything below a full house, so resolve them first.
        var flushSuit = -1
        for (s in 0..3) if (suitCount[s] >= 5) flushSuit = s

        if (flushSuit >= 0) {
            val fm = suitMask[flushSuit]
            val sfHigh = straightHigh(withWheel(fm))
            if (sfHigh > 0) return pack(STRAIGHT_FLUSH, sfHigh)
            return packTop5(FLUSH, fm)
        }

        // Quads and full houses outrank a straight, so count-based hands come next.
        var quad = 0
        var tripsHigh = 0
        var tripsLow = 0
        var pairHigh = 0
        var pairLow = 0
        for (r in 14 downTo 2) {
            when (rankCount[r]) {
                4 -> if (quad == 0) quad = r
                3 -> if (tripsHigh == 0) tripsHigh = r else if (tripsLow == 0) tripsLow = r
                2 -> if (pairHigh == 0) pairHigh = r else if (pairLow == 0) pairLow = r
            }
        }

        if (quad != 0) {
            val kicker = highestExcluding(rankMask, quad)
            return pack(QUADS, quad, kicker)
        }

        if (tripsHigh != 0 && (tripsLow != 0 || pairHigh != 0)) {
            // A second set plays as the pair when it beats the best actual pair.
            val pair = if (tripsLow > pairHigh) tripsLow else pairHigh
            return pack(FULL_HOUSE, tripsHigh, pair)
        }

        val straight = straightHigh(withWheel(rankMask))
        if (straight > 0) return pack(STRAIGHT, straight)

        if (tripsHigh != 0) {
            val k1 = highestExcluding(rankMask, tripsHigh)
            val k2 = highestExcluding(rankMask, tripsHigh, k1)
            return pack(TRIPS, tripsHigh, k1, k2)
        }

        if (pairHigh != 0 && pairLow != 0) {
            val kicker = highestExcluding(rankMask, pairHigh, pairLow)
            return pack(TWO_PAIR, pairHigh, pairLow, kicker)
        }

        if (pairHigh != 0) {
            val k1 = highestExcluding(rankMask, pairHigh)
            val k2 = highestExcluding(rankMask, pairHigh, k1)
            val k3 = highestExcluding(rankMask, pairHigh, k1, k2)
            return pack(PAIR, pairHigh, k1, k2, k3)
        }

        return packTop5(HIGH_CARD, rankMask)
    }

    /** Mirrors the ace into the low slot so A-2-3-4-5 registers as a straight. */
    private fun withWheel(mask: Int): Int =
        if (mask and (1 shl 14) != 0) mask or (1 shl 1) else mask

    /** Highest top-card of any five-in-a-row present in [mask], or 0. */
    private fun straightHigh(mask: Int): Int {
        for (high in 14 downTo 5) {
            val need = 0b11111 shl (high - 4)
            if (mask and need == need) return high
        }
        return 0
    }

    private fun highestExcluding(mask: Int, vararg exclude: Int): Int {
        var m = mask
        for (e in exclude) m = m and (1 shl e).inv()
        for (r in 14 downTo 2) if (m and (1 shl r) != 0) return r
        return 0
    }

    private fun pack(category: Int, vararg tiebreaks: Int): Int {
        var s = category
        for (i in 0 until 5) {
            s = (s shl 4) or (if (i < tiebreaks.size) tiebreaks[i] else 0)
        }
        return s
    }

    private fun packTop5(category: Int, mask: Int): Int {
        var s = category
        var taken = 0
        for (r in 14 downTo 2) {
            if (taken == 5) break
            if (mask and (1 shl r) != 0) {
                s = (s shl 4) or r
                taken++
            }
        }
        while (taken < 5) {
            s = s shl 4
            taken++
        }
        return s
    }
}
