package uno

import "math/rand/v2"

// The bots.
//
// Lifted from the ones gogobee's UNO plays in Matrix, which are genuinely decent
// company — they hold their wild draw fours back until you're close to going
// out, they follow the colour in play when they can, and they get out of the way
// of their own hand. Two things changed on the way over:
//
// The RNG is threaded. gogobee's bots reach for the package global, which is why
// its own tests can only assert that a bot played *something* legal. These take
// the game's generator, so a bot's choice is part of what a seed replays.
//
// They are not the same bot at every table. A single deterministic policy is a
// puzzle: play round it once and it never surprises you again. So the bot takes
// the best card it sees most of the time, and now and then takes the second best
// — enough that you cannot count what it is holding by what it plays.

// botSlip is how often a bot takes its second choice instead of its first. Low
// enough that it still plays well, high enough that it isn't a lookup table.
const botSlip = 6 // one turn in six

// botPick chooses a card to play, or reports -1 when the bot has nothing legal.
func botPick(hand []Card, top Card, topColor Color, minOpponent int, rng *rand.Rand) (Card, int) {
	var playable []int
	for i, c := range hand {
		if c.CanPlayOn(top, topColor) {
			playable = append(playable, i)
		}
	}
	if len(playable) == 0 {
		return Card{}, -1
	}

	order := botRank(hand, topColor, playable, minOpponent)
	pick := order[0]
	if len(order) > 1 && rng.IntN(botSlip) == 0 {
		pick = order[1]
	}
	return hand[pick], pick
}

// botRank sorts the playable cards best-first, by the bot's own priorities.
//
// The shape of it: hurt the leader if there is one, otherwise get rid of
// something useful and keep the wilds back. A wild draw four spent early is a
// wild draw four you don't have when somebody is sitting on one card.
func botRank(hand []Card, topColor Color, playable []int, minOpponent int) []int {
	var wd4, wilds, actions, numbers []int
	for _, i := range playable {
		switch c := hand[i]; {
		case c.Value == WildDrawFour:
			wd4 = append(wd4, i)
		case c.Value == WildCard:
			wilds = append(wilds, i)
		case c.Value.Action():
			actions = append(actions, i)
		default:
			numbers = append(numbers, i)
		}
	}

	// Following the colour in play is worth more than not, because it keeps the
	// bot's hand flexible — so within each group, the cards already in colour go
	// first.
	inColorFirst := func(idx []int) []int {
		var same, other []int
		for _, i := range idx {
			if hand[i].Color == topColor {
				same = append(same, i)
			} else {
				other = append(other, i)
			}
		}
		return append(same, other...)
	}

	var out []int
	if minOpponent >= 0 && minOpponent <= 2 {
		// Somebody is about to go out. This is what the +4 was being saved for.
		out = append(out, wd4...)
		out = append(out, inColorFirst(actions)...)
		out = append(out, wilds...)
		out = append(out, inColorFirst(numbers)...)
		return out
	}
	out = append(out, inColorFirst(actions)...)
	out = append(out, inColorFirst(numbers)...)
	out = append(out, wilds...)
	out = append(out, wd4...)
	return out
}

// botStack answers a stack, or reports -1 when the bot has nothing to answer it
// with and has to eat the lot.
//
// It plays the *smallest* draw card it holds. The bill is passed on either way —
// what it is passing on is the stack plus whatever it added — so the cheap card
// does the same job as the expensive one, and keeps the +10 in hand for a turn
// when the bot is the one choosing to hurt somebody rather than the one dodging.
//
// The slip is here too: one time in six it reaches for the second-smallest, so a
// player can't read the stack it just passed as a complete inventory of what the
// bot doesn't have.
func botStack(hand []Card, topColor Color, rng *rand.Rand) (Card, int) {
	var can []int
	for i, c := range hand {
		if c.CanStackOn(topColor) {
			can = append(can, i)
		}
	}
	if len(can) == 0 {
		return Card{}, -1
	}
	// Smallest draw first. A stable insertion sort: there are never many.
	for i := 1; i < len(can); i++ {
		for j := i; j > 0 && hand[can[j]].Value.Draw() < hand[can[j-1]].Value.Draw(); j-- {
			can[j], can[j-1] = can[j-1], can[j]
		}
	}
	pick := can[0]
	if len(can) > 1 && rng.IntN(botSlip) == 0 {
		pick = can[1]
	}
	return hand[pick], pick
}

// botRouletteColor names the colour for a roulette: whichever the bot holds
// *least* of. The victim flips until that colour turns up, so the rarer the
// colour, the longer they flip and the more they keep. Naming the colour you're
// long in is naming the one that ends the flipping soonest, which is mercy — and
// this is not that game.
func botRouletteColor(hand []Card, rng *rand.Rand) Color {
	counts := [5]int{}
	for _, c := range hand {
		if c.Color.Playable() {
			counts[c.Color]++
		}
	}
	best, bestN := Wild, 1<<30
	for col := Red; col <= Green; col++ {
		if counts[col] < bestN {
			best, bestN = col, counts[col]
		}
	}
	if best == Wild {
		return Red + Color(rng.IntN(4))
	}
	return best
}

// botColor names a colour for a wild: whichever the bot holds most of, so the
// card it plays next is one it already has. A hand of nothing but wilds picks
// at random rather than always saying red, which would be a tell.
func botColor(hand []Card, rng *rand.Rand) Color {
	counts := [5]int{}
	for _, c := range hand {
		if c.Color.Playable() {
			counts[c.Color]++
		}
	}
	best, bestN := Wild, 0
	for col := Red; col <= Green; col++ {
		if counts[col] > bestN {
			best, bestN = col, counts[col]
		}
	}
	if bestN == 0 {
		return Red + Color(rng.IntN(4))
	}
	return best
}

// botPool are the regulars a bot seat is named from. Flavour, and load-bearing
// flavour: "Kiwi played a +4" is a table, "Bot 2 played a +4" is a test fixture.
// More names than a full table, so no two chairs ever share one.
var botPool = []string{
	"Kiwi", "Mochi", "Bramble", "Pixel", "Gus", "Nori", "Waffle", "Marzipan",
	"Tuck", "Bebop", "Olive", "Rascal", "Peaches", "Dot", "Sable", "Clementine",
}
