import Foundation
import PokerCore
import PokerAI

/// Measures each parameter's effect on its own.
///
/// "Expert" was authored from poker theory and then measured backwards, so rather than keep
/// guessing which knob was wrong, this changes exactly one at a time against an otherwise
/// identical opponent and reports what each is worth. Anything that measures negative is not a
/// skill; it is a leak with a flattering name.
func runAblation(hands: Int, seed: UInt64, personality: Personality, seats: Int) {
    let band = personality.skillRange
    let base = personality.parameters(skill: band.lowerBound)
    let top = personality.parameters(skill: band.upperBound)

    let knobs: [(String, (inout BotParameters) -> Void)] = [
        ("openRange", { $0.openRange = top.openRange }),
        ("threeBetRange", { $0.threeBetRange = top.threeBetRange }),
        ("callVsRaiseRange", { $0.callVsRaiseRange = top.callVsRaiseRange }),
        ("blindDefenseRange", { $0.blindDefenseRange = top.blindDefenseRange }),
        ("foldToThreeBet", { $0.foldToThreeBet = top.foldToThreeBet }),
        ("cbetFrequency", { $0.cbetFrequency = top.cbetFrequency }),
        ("bluffFrequency", { $0.bluffFrequency = top.bluffFrequency }),
        ("aggression", { $0.aggression = top.aggression }),
        ("callSlack", { $0.callSlack = top.callSlack }),
        ("valueThreshold", { $0.valueThreshold = top.valueThreshold }),
        ("raiseThreshold", { $0.raiseThreshold = top.raiseThreshold }),
        ("betSizing", { $0.betSizing = top.betSizing }),
        ("sizingNoise", { $0.sizingNoise = top.sizingNoise }),
        ("limpTendency", { $0.limpTendency = top.limpTendency }),
        ("pushFoldDepth", { $0.pushFoldDepth = top.pushFoldDepth }),
        ("adaptation", { $0.adaptation = top.adaptation }),
        ("readError", { $0.readError = top.readError }),
    ]

    print("\(personality.name): each parameter moved from its skill-\(band.lowerBound) value to its")
    print("skill-\(band.upperBound) value, one at a time, against a table of the unchanged version.")
    print("\(seats)-handed, \(hands * seats) hands each. Positive means the change earns money.")
    print("")

    var rows: [(String, Double)] = []
    for (name, apply) in knobs {
        var params = base
        apply(&params)
        guard !equal(params, base) else { continue }
        var hero = Bot(id: "hero", personality: personality, skill: band.lowerBound, seed: 11)
        hero.parameters = params
        let r = Shootout.ring(hero, versus: personality, villainSkill: band.lowerBound,
                              seats: seats, hands: hands, seed: seed)
        rows.append((name, r.bigBlindsPer100))
    }

    // Everything at once, for comparison with the sum of the parts.
    var full = Bot(id: "hero", personality: personality, skill: band.upperBound, seed: 11)
    full.parameters = top
    let all = Shootout.ring(full, versus: personality, villainSkill: band.lowerBound,
                            seats: seats, hands: hands, seed: seed)

    for (name, value) in rows.sorted(by: { $0.1 > $1.1 }) {
        let bar = String(repeating: value >= 0 ? "+" : "-", count: min(30, Int(abs(value))))
        print(String(format: "  %-20@ %+8.2f  %@", name as NSString, value, bar))
    }
    print("")
    print(String(format: "  %-20@ %+8.2f", "all together" as NSString, all.bigBlindsPer100))
}

private func equal(_ a: BotParameters, _ b: BotParameters) -> Bool {
    a.openRange == b.openRange && a.threeBetRange == b.threeBetRange
        && a.callVsRaiseRange == b.callVsRaiseRange && a.blindDefenseRange == b.blindDefenseRange
        && a.foldToThreeBet == b.foldToThreeBet && a.cbetFrequency == b.cbetFrequency
        && a.bluffFrequency == b.bluffFrequency && a.aggression == b.aggression
        && a.callSlack == b.callSlack && a.valueThreshold == b.valueThreshold
        && a.raiseThreshold == b.raiseThreshold && a.betSizing == b.betSizing
        && a.sizingNoise == b.sizingNoise && a.limpTendency == b.limpTendency
        && a.pushFoldDepth == b.pushFoldDepth && a.adaptation == b.adaptation
        && a.readError == b.readError
}
