package eu.jonahbauer.wizard.core.machine.states.trick;

import eu.jonahbauer.wizard.common.model.Card;
import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.states.InvalidDataException;
import eu.jonahbauer.wizard.common.messages.observer.CardMessage;
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.core.model.card.GameCards;
import eu.jonahbauer.wizard.core.util.Pair;
import org.jetbrains.annotations.NotNull;

import java.util.*;

import static eu.jonahbauer.wizard.core.machine.states.GameData.*;
import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.PLAY_CARD;

public final class PlayingCard extends TrickState {

    public PlayingCard(GameData data) {
        super(data.require(STACK));
        checkData(data);
    }

    @Override
    public void onEnter(Game game) {
        game.notify(new UserInputMessage(get(CURRENT_PLAYER), PLAY_CARD, getTimeout(game, true)));
        timeout(game);
    }

    @Override
    public void onMessage(Game game, UUID player, PlayerMessage message) {
        if (get(CURRENT_PLAYER).equals(player) && message instanceof PlayCardMessage cardMessage) {
            transition(game, cardMessage.getCard());
        } else {
            super.onMessage(game, player, message);
        }
    }

    @Override
    public void onTimeout(Game game) {
        var hand = get(HANDS).get(get(CURRENT_PLAYER));
        var stack = get(STACK);

        var card = GameCards.values().stream()
                            .filter(c -> c.isPlayable(hand, stack))
                            .findAny()
                            .orElseThrow(() -> new AssertionError("Cannot play any card."));

        transition(game, card.getCard());
    }

    private void transition(Game game, @NotNull Card card) {
        var currentPlayer = get(CURRENT_PLAYER);

        // create mutable stack
        var stack = get(STACK);
        var mutableStack = new ArrayList<Pair<UUID, Card>>(stack.size() + 1);
        mutableStack.addAll(stack);

        // create mutable hand
        var hands = get(HANDS);
        var mutableHands = new HashMap<>(hands);
        var hand = hands.get(currentPlayer);
        var mutableHand = new ArrayList<>(hand);

        GameCards.get(card).play(currentPlayer, mutableHand, mutableStack);

        // when card was played successfully
        game.notify(new CardMessage(currentPlayer, card));

        // apply modifications
        mutableHands.put(currentPlayer, List.copyOf(mutableHand));
        GameData data = getData().with(
                STACK, List.copyOf(mutableStack),
                HANDS, Map.copyOf(mutableHands)
        );

        // check whether the trick is finished
        var summary = data.get(HANDS).values().stream()
                          .mapToInt(Collection::size)
                          .summaryStatistics();

        if (summary.getMax() == summary.getMin()) { // everybody has the same amount of cards
            transition(game, new FinishingTrick(data));
        } else {
            transition(game, new PlayingCard(data.with(CURRENT_PLAYER, getNextPlayer())));
        }

    }

    private static void checkData(GameData data) {
        int expectedSize = data.get(ROUND) - data.get(TRICK);
        for (List<Card> hand : data.get(HANDS).values()) {
            if (hand.size() != expectedSize && hand.size() != expectedSize + 1) {
                throw new InvalidDataException("Unexpected hand size: " + hand.size() + " (expected " + expectedSize + " or " + (expectedSize + 1) + ").");
            }
        }
        int size = data.get(HANDS).get(data.get(CURRENT_PLAYER)).size();
        if (size != expectedSize + 1) {
            throw new InvalidDataException("Unexpected current player's hand size: " + size + " (expected " + (expectedSize + 1) + ")");
        }
    }
}
