package eu.jonahbauer.wizard.core.machine;

import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage;
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.common.messages.player.*;
import eu.jonahbauer.wizard.common.model.Card;
import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.messages.Observer;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.junit.jupiter.api.Assertions;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static org.junit.jupiter.api.Assertions.*;

/**
 * A queue of {@link PlayerMessage}s that will automatically be sent to a {@link Game} when
 * {@linkplain UserInputMessage input is required}. The {@code PlayerMessage}s are ordered and it is asserted that
 * the incoming {@code UserInputMessage}s match the given {@code PlayerMessage}s.
 */
public class MessageQueue implements Observer {
    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    private final LinkedList<QueuedMessage> messages = new LinkedList<>();
    @Setter
    private Game game;

    private BulkQueuedMessage bulk;

    public MessageQueue add(UUID player, UserInputMessage.Action action, PlayerMessage message) {
        if (bulk != null) {
            bulk.getMessages().add(new SingleQueuedMessage(player, action, message));
        } else {
            messages.add(new SingleQueuedMessage(player, action, message));
        }
        return this;
    }

    public MessageQueue addCard(UUID player, Card card) {
        return add(player, UserInputMessage.Action.PLAY_CARD, new PlayCardMessage(card));
    }

    public MessageQueue addJuggle(UUID player, Card card) {
        return add(player, UserInputMessage.Action.JUGGLE_CARD, new JuggleMessage(card));
    }

    public MessageQueue addPrediction(UUID player, int prediction) {
        return add(player, UserInputMessage.Action.MAKE_PREDICTION, new PredictMessage(prediction));
    }

    public MessageQueue addChangePrediction(UUID player, int prediction) {
        return add(player, UserInputMessage.Action.CHANGE_PREDICTION, new PredictMessage(prediction));
    }

    public MessageQueue addPickTrump(UUID player, Card.Suit trumpSuit) {
        return add(player, UserInputMessage.Action.PICK_TRUMP, new PickTrumpMessage(trumpSuit));
    }

    public MessageQueue addCards(List<UUID> players, Map<UUID, List<Card>> cards, int startPlayer) {
        Map<UUID, Integer> nextCards = new HashMap<>();
        int size = players.size();
        boolean changed = true;
        while (changed) {
            changed = false;
            for (int i = 0; i < size; i++) {
                var player = players.get((i + startPlayer) % size);
                var hand = cards.get(player);

                int next = nextCards.getOrDefault(player, 0);

                if (next < hand.size()) {
                    nextCards.merge(player, 1, Integer::sum);
                    var card = hand.get(next);
                    addCard(player, card);
                    changed = true;
                }
            }
        }
        return this;
    }

    public MessageQueue assertThrows(Class<? extends Exception> exception) {
        var message = messages.getLast();
        if (message instanceof SingleQueuedMessage singleMessage) {
            singleMessage.setException(exception);
        } else if (message instanceof BulkQueuedMessage bulkMessage) {
            bulkMessage.getMessages().get(bulkMessage.getMessages().size() - 1).setException(exception);
        }
        return this;
    }

    public MessageQueue begin() {
        if (bulk != null) {
            end();
        }
        bulk = new BulkQueuedMessage();
        messages.add(bulk);
        return this;
    }

    public MessageQueue end() {
        if (bulk == null) throw new IllegalStateException();
        bulk = null;
        return this;
    }

    public MessageQueue sync(UUID... players) {
        var bulk = new BulkQueuedMessage();
        Arrays.stream(players)
              .map(player -> new SingleQueuedMessage(player, UserInputMessage.Action.SYNC, new ContinueMessage()))
              .forEach(bulk.getMessages()::add);
        messages.add(bulk);
        return this;
    }

    public void doNotify(ObserverMessage om) {
        try {
            System.out.println(om);
            if (om instanceof UserInputMessage message) {
                UUID player = message.getPlayer();
                assertFalse(messages.isEmpty(), "User input is required but none is provided.");

                var queued = messages.poll();

                List<SingleQueuedMessage> list;
                if (queued instanceof SingleQueuedMessage s) {
                    list = List.of(s);
                } else if (queued instanceof BulkQueuedMessage b) {
                    list = b.getMessages();
                } else {
                    throw new AssertionError();
                }

                for (SingleQueuedMessage queuedSingle : list) {
                    var queuedPlayer = queuedSingle.getPlayer();
                    var queuedAction = queuedSingle.getAction();
                    var queuedMessage = queuedSingle.getMessage();
                    var exception = queuedSingle.getException();

                    if (exception == null) {
                        if (player != null) assertEquals(queuedPlayer, player);
                        assertEquals(queuedAction, message.getAction());
                    }

                    System.out.println(queuedPlayer + ": " + queuedMessage);
                    if (exception != null) {
                        Assertions.assertThrows(
                                exception,
                                () -> game.onMessage(queuedPlayer, queuedMessage),
                                "Excepted exception for message " + queuedMessage + " from player " + queuedPlayer + "."
                        );
                    } else {
                        Assertions.assertDoesNotThrow(() -> game.onMessage(queuedPlayer, queuedMessage));
                    }
                }
            }
        } catch (Throwable t) {
            game.transition(new GameState(GameData.EMPTY) {
                @Override
                public Optional<GameState> onEnter(Game context) {
                    throw t;
                }
            });
        }
    }

    @Override
    public void notify(ObserverMessage message) {
        executor.execute(() -> doNotify(message));
    }

    @Override
    public void notify(UUID player, ObserverMessage message) {

    }

    private interface QueuedMessage {}

    @Getter
    @Setter
    @RequiredArgsConstructor
    private static class SingleQueuedMessage implements QueuedMessage {
        private final UUID player;
        private final UserInputMessage.Action action;
        private final PlayerMessage message;
        private Class<? extends Exception> exception;
    }

    @Getter
    private static class BulkQueuedMessage implements QueuedMessage {
        private final List<SingleQueuedMessage> messages = new ArrayList<>();
    }
}
