package uno

import "math/rand/v2"

// Calling UNO, and catching the seat that didn't.
//
// This was the one rule on the box the table wasn't playing. A hand going down to
// one card used to emit the "uno" event by itself, which made the call a thing
// that *happened to you* rather than a thing you did — and a rule nobody can fail
// is not a rule, it's an announcement.
//
// So now: play your second-to-last card and you owe the table a word. Say it
// (Move.Uno) and you're safe. Stay quiet and the bots get one look at you, right
// then, before any of them plays — because a bot that has moved on is a bot that
// has stopped watching your hand. Miss it and you take two.
//
// It runs the other way too, and that half is the fun one. A bot forgets often
// enough to be worth watching for, and when it does it says *nothing* — there is
// no event, no badge, no tell on the felt except the count beside its fan reading
// "1 card" with no UNO on it. Catch it (MoveCatch) and it takes two. Call a seat
// that had nothing to hide and you take two yourself, which is what stops the
// catch button from being a thing you simply mash every turn.
//
// The whole rule turns on one asymmetry, and it's deliberate: the bots get a
// *roll* to notice you, and you get to actually look. Attention is the edge here,
// and it's the player's to take.

const (
	// CatchPenalty is what silence costs, both ways. Two, as printed on the box.
	CatchPenalty = 2

	// botForget is how often a bot goes down to one card without saying so. High
	// enough that watching the counts pays — at a full table you'll get a catch to
	// make every few games — and low enough that a quiet seat is still a thing you
	// have to notice rather than assume.
	botForget = 0.30

	// botAlert is the chance a *single* bot notices that you didn't call. They each
	// get a look, so forgetting is punished about 75% of the time heads-up and 98%
	// at a full table: the more seats there are watching you, the less you get away
	// with, which is the right shape for it.
	botAlert = 0.75
)

// botForgets rolls for whether a bot muffs its call.
func botForgets(rng *rand.Rand) bool { return rng.Float64() < botForget }

// declare records whether a seat that is now on one card said so, and — if it did
// — announces it. A seat on any other number of cards owes nothing and this does
// nothing, which is what makes it safe to call after every play.
func (s *State) declare(seat int, called bool, evs *[]Event) {
	if !s.playing() || len(s.Hands[seat]) != 1 {
		return
	}
	s.ensureCalled()
	s.Called[seat] = called
	if called {
		*evs = append(*evs, Event{Kind: EvUno, Seat: seat, Left: 1})
	}
}

// botsCatch is the window. The seat that just played is holding one card and
// didn't say the word — so every bot still in the hand gets one look, in seat
// order, and the first to see it takes them for two.
//
// It runs before runBots on purpose. The catch has to land while the table is
// still looking at the card that was played, not three turns later. Only the bots
// catch on a roll here; a human polices the table with the catch button, which is
// the asymmetry the whole rule turns on — attention is the player's edge.
func (s *State) botsCatch(actor int, evs *[]Event, rng *rand.Rand) {
	if !s.playing() || len(s.Hands[actor]) != 1 || s.called(actor) {
		return
	}
	for _, seat := range s.alive() {
		if seat == actor || !s.Seats[seat].Bot {
			continue // a human catches with the button, not on a roll
		}
		if rng.Float64() >= botAlert {
			continue // this one wasn't looking
		}
		s.penalise(actor, seat, EvCaught, evs, rng)
		return // caught once is caught. They don't queue up to take turns at you
	}
}

// seatCatches calls out a seat the caller thinks is holding one card in silence.
//
// It is not a turn: right or wrong, the turn stays where it was. What it costs is
// the risk — a seat that did call, or isn't on one card at all, is a seat the
// caller has accused of nothing, and that is two cards to them.
func (s *State) seatCatches(caller int, m Move, rng *rand.Rand) ([]Event, error) {
	seat := m.Seat
	if seat == caller || seat < 0 || seat >= len(s.Hands) || !s.live(seat) {
		return nil, ErrNoCatch
	}
	var evs []Event
	if len(s.Hands[seat]) == 1 && !s.called(seat) {
		s.penalise(seat, caller, EvCaught, &evs, rng) // got them
	} else {
		s.penalise(caller, seat, EvMiscall, &evs, rng) // they were clean, and you weren't looking
	}
	return evs, nil
}

// penalise makes a seat take the price of the call: cards off the deck, quietly —
// no draw event, because what the table is being told about is the catch, and a
// draw event alongside it would animate the same two cards twice.
//
// In No Mercy those two cards can be the two that bury them, which is a fine way
// to go: caught on one card, dead on twenty-five.
func (s *State) penalise(victim, by int, kind string, evs *[]Event, rng *rand.Rand) {
	got := s.drawCards(victim, CatchPenalty, evs, rng)
	if len(got) == 0 {
		return // the table has nothing left to punish anybody with
	}
	s.ensureCalled()
	s.Called[victim] = false
	*evs = append(*evs, s.mine(Event{
		Kind: kind, Seat: victim, By: by, N: len(got), Left: len(s.Hands[victim]),
	}))
	s.mercy(victim, evs, rng)
}

// UnoAt is which cards in your hand would leave you holding exactly one, if you
// played them. It is the table's cue to ask you for the call, and it comes from
// here rather than from the browser counting your cards because No Mercy's
// "discard all" doesn't take one card out of your hand — it takes every card of
// its colour, and the browser guessing at that is the browser getting it wrong.
//
// It answers for every card, legal or not. The table only ever asks about a card
// you were allowed to play anyway.
func (s State) UnoAt(seat int) []int {
	if !s.playing() || s.Turn != seat {
		return nil
	}
	hand := s.Hands[seat]
	var out []int
	for i, c := range hand {
		left := len(hand) - 1
		if c.Value == DiscardAll {
			for j, other := range hand {
				if j != i && other.Color == c.Color && !other.IsWild() {
					left--
				}
			}
		}
		if left == 1 {
			out = append(out, i)
		}
	}
	return out
}

// Catchable is which seats are, right now, sitting on one card they never
// announced. It is what the browser puts a button on.
//
// This leaks nothing. The card counts are already on the felt and the UNO badge
// already isn't — this is the same two facts, subtracted, and doing that
// subtraction on the server is only so that the rule for what counts as catchable
// lives in one place instead of two.
func (s State) Catchable(viewer int) []int {
	if !s.playing() || s.Turn != viewer {
		return nil // you can only catch a seat on your own turn
	}
	var out []int
	for _, seat := range s.alive() {
		if seat != viewer && len(s.Hands[seat]) == 1 && !s.called(seat) {
			out = append(out, seat)
		}
	}
	return out
}

// called reports whether a seat holding one card announced it. Callers outside
// the package read the Called slice, which is on the state they already hold.
func (s State) called(seat int) bool {
	return seat < len(s.Called) && s.Called[seat]
}

// ensureCalled grows the slice to fit the table. A game dealt before this rule
// existed has no Called at all, and the seats in it are all — correctly —
// uncalled: nobody in that game ever said the word, because there was no way to.
func (s *State) ensureCalled() {
	for len(s.Called) < len(s.Hands) {
		s.Called = append(s.Called, false)
	}
}

// tidyCalls forgets the calls that no longer mean anything. A seat's call is only
// ever about the one card it is holding — draw into two and the word you said is
// spent, and saying it again is a new thing you have to do.
//
// Without this, a seat that called on one card, was made to draw, and worked its
// way back down to one would still be wearing the old call, and could never be
// caught again for the rest of the game.
func (s *State) tidyCalls() {
	s.ensureCalled()
	for seat := range s.Called {
		if len(s.Hands[seat]) != 1 {
			s.Called[seat] = false
		}
	}
}
