package uno

import "math/rand/v2"

// No Mercy.
//
// A rules dial, not a fourth table. The table size is still the tier — a duel is
// a duel — and No Mercy is a switch you throw across all three of them. What it
// changes is the game they play:
//
//   - A 168-card deck, with faces the normal one doesn't print: a coloured +4, a
//     +6, a +10, a skip-everyone, a discard-all, a reverse-and-draw-four, and a
//     colour roulette.
//   - Draw cards stack. A +2 pointed at you can be answered with any draw card
//     you hold, and the bill goes to the next seat with the two added on. Whoever
//     runs out of draw cards eats the lot.
//   - You draw until you can play. There is no drawing one card and shrugging.
//   - And twenty-five cards in your hand kills you. That is the whole point of
//     the deck: it is built to bury somebody, and the mercy rule is what happens
//     when it does.
//
// Everything here is reached from uno.go behind `s.Tier.NoMercy`. A normal game
// never runs a line of it.

// MercyLimit is the hand that ends you. Reach it and you are out of the game —
// your cards go back in the deck and the table plays on without you.
const MercyLimit = 25

// NewNoMercyDeck builds the 168.
//
// Per colour: two of each number, three skips, two skip-everyones, four
// reverses, two +2s, two coloured +4s and three discard-alls — thirty-six cards,
// times four colours. Then the wilds: eight reverse-draw-fours, four +6s, four
// +10s and eight roulettes. Unshuffled, same as NewDeck, because New shuffles and
// a test wants the order it was built in.
func NewNoMercyDeck() []Card {
	d := make([]Card, 0, 168)
	for _, col := range []Color{Red, Blue, Yellow, Green} {
		for v := Zero; v <= Nine; v++ {
			d = append(d, Card{col, v}, Card{col, v})
		}
		for i := 0; i < 3; i++ {
			d = append(d, Card{col, Skip})
		}
		for i := 0; i < 2; i++ {
			d = append(d, Card{col, SkipAll})
		}
		for i := 0; i < 4; i++ {
			d = append(d, Card{col, Reverse})
		}
		for i := 0; i < 2; i++ {
			d = append(d, Card{col, DrawTwo})
		}
		for i := 0; i < 2; i++ {
			d = append(d, Card{col, DrawFour})
		}
		for i := 0; i < 3; i++ {
			d = append(d, Card{col, DiscardAll})
		}
	}
	for i := 0; i < 8; i++ {
		d = append(d, Card{Wild, WildRevFour})
	}
	for i := 0; i < 4; i++ {
		d = append(d, Card{Wild, WildDrawSix})
	}
	for i := 0; i < 4; i++ {
		d = append(d, Card{Wild, WildDrawTen})
	}
	for i := 0; i < 8; i++ {
		d = append(d, Card{Wild, WildRoulette})
	}
	return d
}

// CanStackOn reports whether a card can be thrown onto a stack that is already
// building. Any draw card answers any other — there is no escalation rule, so a
// +2 is a legal reply to a +10 — but a *coloured* draw card still has to follow
// the colour in play. The wild draws always go.
//
// This is why the pending count is not a cap: what you are matching is the fact
// of a draw card, not its size.
func (c Card) CanStackOn(topColor Color) bool {
	if c.Value.Draw() == 0 {
		return false
	}
	if c.IsWild() {
		return true
	}
	return c.Color == topColor
}

// canStack reports whether a seat holds anything at all it could answer with.
func (s State) canStack(seat int) bool {
	for _, c := range s.Hands[seat] {
		if c.CanStackOn(s.Color) {
			return true
		}
	}
	return false
}

// absorb is what happens when the stack stops with you: you take every card in
// it, and you lose your turn. The pending count is cleared *before* the cards
// land, because a mercy kill inside the draw ends the seat and there must be no
// bill left standing against a seat that is no longer at the table.
func (s *State) absorb(seat int, evs *[]Event, rng *rand.Rand) {
	n := s.Pending
	s.Pending = 0
	s.deal(seat, n, true, evs, rng)

	// The seat can die paying the bill, and a mercy kill can end the hand — the last
	// seat but one dying leaves somebody alone to take the pot. So the phase is only
	// reset if there is still a hand to have a phase.
	if s.mercy(seat, evs, rng) && !s.playing() {
		return
	}
	if !s.live(seat) {
		s.Phase = PhasePlay
		s.advance(1)
		return // it died, but the table plays on. Don't skip a seat that isn't there.
	}
	*evs = append(*evs, Event{Kind: EvSkip, Seat: seat, Left: len(s.Hands[seat])})
	s.Phase = PhasePlay
	s.advance(1) // the turn is on the seat that just paid, so it moves one on
}

// roulette is the colour roulette: the next seat turns cards over until the
// named colour comes up, and keeps every card it turned. Then it loses its turn.
//
// The deck can run out mid-flip (the discard is reshuffled back under as usual,
// and even that can be dry), so this is bounded by what there is to draw, not by
// the colour ever actually appearing. A wild is not a colour and never ends it.
func (s *State) roulette(victim int, color Color, evs *[]Event, rng *rand.Rand) {
	got := 0
	for {
		if len(s.Deck) == 0 && !s.reshuffle(evs, rng) {
			break
		}
		c, ok := s.pop()
		if !ok {
			break
		}
		s.Hands[victim] = append(s.Hands[victim], c)
		got++
		if c.Color == color {
			break
		}
		if len(s.Hands[victim]) >= MercyLimit {
			break // they are dead already; stop dealing cards to a corpse
		}
	}
	if got > 0 {
		e := Event{Kind: EvRoulette, Seat: victim, N: got, Color: color, Left: len(s.Hands[victim])}
		*evs = append(*evs, s.mine(e))
	}
	if s.mercy(victim, evs, rng) {
		return
	}
	*evs = append(*evs, Event{Kind: EvSkip, Seat: victim, Left: len(s.Hands[victim])})
	s.advance(2)
}

// discardAll dumps every remaining card of a colour out of a hand and buries it
// under the card that was just played. The pile keeps its top: the played card
// stays the card in play, and the rest go beneath it, where they are still in the
// game (a reshuffle brings them back) and still count in a census.
func (s *State) discardAll(seat int, color Color, evs *[]Event) int {
	hand := s.Hands[seat]
	kept := make([]Card, 0, len(hand))
	var dumped []Card
	for _, c := range hand {
		if c.Color == color && !c.IsWild() {
			dumped = append(dumped, c)
		} else {
			kept = append(kept, c)
		}
	}
	s.Hands[seat] = kept
	if len(dumped) > 0 {
		top := s.Discard[len(s.Discard)-1]
		s.Discard = append(s.Discard[:len(s.Discard)-1], dumped...)
		s.Discard = append(s.Discard, top)
		*evs = append(*evs, s.mine(Event{Kind: EvDiscardAll, Seat: seat, N: len(dumped),
			Color: color, Left: len(kept)}))
	}
	return len(dumped)
}

// mercy checks a seat against the limit and, if it has crossed it, takes it out
// of the hand: its cards go back into the deck and it plays no more this hand. It
// reports whether the seat died.
//
// A mercy kill is no longer game over for anyone — it is one seat out of the
// current hand, human or bot alike, and the hand plays on among the living. When
// it leaves exactly one seat alive, that seat has outlived the table and takes the
// pot; whoever it is, they win it the same way going out first would.
func (s *State) mercy(seat int, evs *[]Event, rng *rand.Rand) bool {
	if !s.Tier.NoMercy || !s.live(seat) || len(s.Hands[seat]) < MercyLimit {
		return false
	}
	n := len(s.Hands[seat])
	s.Deck = append(s.Deck, s.Hands[seat]...)
	rng.Shuffle(len(s.Deck), func(i, j int) { s.Deck[i], s.Deck[j] = s.Deck[j], s.Deck[i] })
	s.Hands[seat] = nil
	s.Out[seat] = true
	s.Pending = 0 // a dead seat pays no bill, and leaves none behind
	*evs = append(*evs, s.mine(Event{Kind: EvMercy, Seat: seat, N: n, Left: 0}))

	if alive := s.alive(); len(alive) == 1 {
		s.settle(alive[0], OutcomeWon, evs) // the last one standing takes the pot
	}
	return true
}

// Live reports whether a seat is still in the game. The felt needs it: a seat the
// mercy rule has buried holds no cards, and a seat holding no cards is otherwise
// indistinguishable from the one that just went out and won.
func (s State) Live(seat int) bool { return s.live(seat) }

// alive lists the seats still in the game.
func (s State) alive() []int {
	var out []int
	for i := range s.Hands {
		if s.live(i) {
			out = append(out, i)
		}
	}
	return out
}

// live reports whether a seat is still in the current hand. Out is empty between
// hands and in any game saved before No Mercy existed, so a missing entry is a
// living seat.
func (s State) live(seat int) bool {
	return seat >= len(s.Out) || !s.Out[seat]
}
