import Foundation

/// The strength of a five-card poker hand, packed into one comparable number:
/// category in the high bits, then five rank nibbles as tiebreakers.
public struct HandRank: Comparable, Hashable, Sendable, CustomStringConvertible {
    public let value: UInt32

    public enum Category: UInt32, Comparable, Sendable, CaseIterable {
        case highCard = 0, pair, twoPair, threeOfAKind, straight, flush, fullHouse, fourOfAKind, straightFlush

        public static func < (a: Category, b: Category) -> Bool { a.rawValue < b.rawValue }

        public var name: String {
            switch self {
            case .highCard: return "high card"
            case .pair: return "a pair"
            case .twoPair: return "two pair"
            case .threeOfAKind: return "three of a kind"
            case .straight: return "a straight"
            case .flush: return "a flush"
            case .fullHouse: return "a full house"
            case .fourOfAKind: return "four of a kind"
            case .straightFlush: return "a straight flush"
            }
        }
    }

    public init(value: UInt32) { self.value = value }

    init(_ category: Category, _ k1: UInt32 = 0, _ k2: UInt32 = 0, _ k3: UInt32 = 0, _ k4: UInt32 = 0, _ k5: UInt32 = 0) {
        value = (category.rawValue << 20) | (k1 << 16) | (k2 << 12) | (k3 << 8) | (k4 << 4) | k5
    }

    public var category: Category { Category(rawValue: value >> 20)! }

    /// The five tiebreaker ranks, most significant first. Zeros mean "unused".
    public var kickers: [UInt32] {
        [(value >> 16) & 0xF, (value >> 12) & 0xF, (value >> 8) & 0xF, (value >> 4) & 0xF, value & 0xF]
    }

    public static func < (a: HandRank, b: HandRank) -> Bool { a.value < b.value }

    public var description: String {
        let ranks = kickers.filter { $0 != 0 }.compactMap { Rank(rawValue: UInt8($0))?.letter }.map(String.init)
        return ranks.isEmpty ? category.name : "\(category.name) (\(ranks.joined(separator: "-")))"
    }
}

public enum HandEvaluator {

    /// Best five-card hand out of five, six or seven cards.
    public static func evaluate(_ cards: [Card]) -> HandRank {
        precondition(cards.count >= 5 && cards.count <= 7, "evaluate expects 5-7 cards")

        var rankCount = [UInt8](repeating: 0, count: 15)   // indexed by Rank.rawValue
        var suitCount = [UInt8](repeating: 0, count: 4)
        var suitMask = [UInt16](repeating: 0, count: 4)
        var rankMask: UInt16 = 0

        for card in cards {
            let r = card.rank
            let s = Int(card.suit.rawValue)
            rankCount[Int(r.rawValue)] += 1
            suitCount[s] += 1
            suitMask[s] |= r.bit
            rankMask |= r.bit
        }

        var best = HandRank(value: 0)

        // Flushes and straight flushes.
        if let flushSuit = (0..<4).first(where: { suitCount[$0] >= 5 }) {
            let mask = suitMask[flushSuit]
            if let high = straightHigh(mask) {
                return HandRank(.straightFlush, UInt32(high))   // nothing beats this
            }
            let top = topRanks(mask, 5)
            best = HandRank(.flush, top[0], top[1], top[2], top[3], top[4])
        }

        // Rank-count hands.
        var quad: UInt32 = 0
        var trips: [UInt32] = []
        var pairs: [UInt32] = []
        for r in stride(from: 14, through: 2, by: -1) {
            switch rankCount[r] {
            case 4: quad = UInt32(r)
            case 3: trips.append(UInt32(r))
            case 2: pairs.append(UInt32(r))
            default: break
            }
        }

        if quad != 0 {
            let kicker = topRanks(rankMask & ~Rank(rawValue: UInt8(quad))!.bit, 1)[0]
            return max(best, HandRank(.fourOfAKind, quad, kicker))
        }

        if let trip = trips.first {
            // A second set plays as the pair; otherwise the best actual pair does.
            let pairRank = trips.count > 1 ? trips[1] : pairs.first
            if let pairRank {
                return max(best, HandRank(.fullHouse, trip, pairRank))
            }
        }

        if best.category == .flush { return best }

        if let high = straightHigh(rankMask) {
            return HandRank(.straight, UInt32(high))
        }

        if let trip = trips.first {
            let rest = topRanks(rankMask & ~Rank(rawValue: UInt8(trip))!.bit, 2)
            return HandRank(.threeOfAKind, trip, rest[0], rest[1])
        }

        if pairs.count >= 2 {
            let used = Rank(rawValue: UInt8(pairs[0]))!.bit | Rank(rawValue: UInt8(pairs[1]))!.bit
            let kicker = topRanks(rankMask & ~used, 1)[0]
            return HandRank(.twoPair, pairs[0], pairs[1], kicker)
        }

        if let pair = pairs.first {
            let rest = topRanks(rankMask & ~Rank(rawValue: UInt8(pair))!.bit, 3)
            return HandRank(.pair, pair, rest[0], rest[1], rest[2])
        }

        let top = topRanks(rankMask, 5)
        return HandRank(.highCard, top[0], top[1], top[2], top[3], top[4])
    }

    /// Highest card of a five-in-a-row inside a 13-bit rank mask, or nil. Handles the wheel (5-4-3-2-A).
    public static func straightHigh(_ mask: UInt16) -> UInt8? {
        var high: UInt8 = 14
        while high >= 6 {
            let window: UInt16 = 0b11111 << UInt16(high - 6)
            if mask & window == window { return high }
            high -= 1
        }
        let wheel: UInt16 = 0b1_0000_0000_1111   // A,5,4,3,2
        return mask & wheel == wheel ? 5 : nil
    }

    /// The `count` highest ranks present in a mask, as rank raw values.
    public static func topRanks(_ mask: UInt16, _ count: Int) -> [UInt32] {
        var out: [UInt32] = []
        out.reserveCapacity(count)
        var r = 14
        while r >= 2 && out.count < count {
            if mask & Rank(rawValue: UInt8(r))!.bit != 0 { out.append(UInt32(r)) }
            r -= 1
        }
        while out.count < count { out.append(0) }
        return out
    }
}
