package eu.jonahbauer.wizard.client.libgdx.state;

import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.client.libgdx.actions.overlay.InteractionOverlay;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.client.libgdx.util.Pair;
import eu.jonahbauer.wizard.common.messages.client.InteractionMessage;
import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.messages.observer.*;
import eu.jonahbauer.wizard.common.messages.player.*;
import eu.jonahbauer.wizard.common.messages.server.AckMessage;
import eu.jonahbauer.wizard.common.messages.server.GameMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.common.model.Card;
import eu.jonahbauer.wizard.common.model.Configuration;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;

import java.lang.ref.WeakReference;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.*;

@Log4j2
@Getter
@RequiredArgsConstructor
public final class Game extends BaseState {
    private final UUID self;
    private final UUID session;
    private final String sessionName;
    private final Configuration configuration;

    private List<ObserverMessage> pendingMessages;

    private final LinkedHashMap<UUID, String> players;
    private final Map<Integer, @Unmodifiable Map<UUID, Integer>> scores = new HashMap<>();
    private final Map<Integer, Map<UUID, Integer>> predictions = new HashMap<>();

    private int round = -1;
    private final Map<UUID, List<Card>> hands = new HashMap<>();
    private final Map<UUID, List<List<Card>>> tricks = new HashMap<>();

    private int trick = -1;
    private final List<Pair<UUID, Card>> stack = new ArrayList<>();
    private Interaction currentInteraction;
    private int pendingClearActivePlayer = 0;

    private Card trumpCard;
    private Card.Suit trumpSuit;

    private @Nullable GameScreen gameScreen;
    private final AtomicBoolean sending = new AtomicBoolean(false);

    private boolean juggling;
    private Card juggleCard;
    private boolean werewolf;

    private int finishing = 0;

    public void init(List<ObserverMessage> messages) {
        pendingMessages = messages;
    }

    @Override
    public Optional<ClientState> onEnter(Client client) {
        var out = handlePendingMessages(client);
        if (out.isPresent()) return out;

        gameScreen = new GameScreen(client.getGame(), self, players);
        client.getGame().setScreen(gameScreen);
        updateScreen();
        return super.onEnter(client);
    }

    @Override
    public Optional<ClientState> onErrorDismissed(Client client) {
        return Optional.of(new Menu());
    }

    //<editor-fold desc="onMessage">
    @Override
    public Optional<ClientState> onMessage(Client client, ServerMessage message) {
        try {
            if (message instanceof GameMessage game) {
                var observerMessage = game.getObserverMessage();
                return onMessage(client, observerMessage);
            } else if (message instanceof NackMessage nack) {
                return onNackMessage(client, nack);
            } else if (message instanceof AckMessage) {
                onAckMessage();
                return Optional.empty();
            } else {
                return unexpectedMessage(client, message);
            }
        } finally {
            executeDelayedFinishInteraction();
        }
    }

    private Optional<ClientState> onMessage(Client client, ObserverMessage message) {
        if (finishing != 0) return onMessageWhileFinishing(client, message);

        if (message instanceof StateMessage state) {
            switch (state.getState()) {
                case "starting_round" -> {
                    return onStartRound(client);
                }
                case "dealing" -> onDealing();
                case "starting_trick" -> onStartTrick();
                case "juggling" -> onJuggle();
                case "finishing_round" -> onFinishingRound();
                case "finished" -> {
                    return onFinished();
                }
                case "error" -> {
                    return onError(client);
                }
            }
        } else if (message instanceof HandMessage hand) {
            onHandMessage(hand.getPlayer(), hand.getHand());
        } else if (message instanceof PredictionMessage prediction) {
            onPredictionMessage(prediction.getPlayer(), prediction.getPrediction());
        } else if (message instanceof TrumpMessage trump) {
            onTrumpMessage(trump.getCard(), trump.getSuit());
        } else if (message instanceof TrickMessage trick) {
            onTrickMessage(trick.getPlayer(), trick.getCards());
        } else if (message instanceof CardMessage card) {
            onCardMessage(card.getPlayer(), card.getCard());
        } else if (message instanceof ScoreMessage score) {
            onScoreMessage(score.getPoints(), true);
        } else if (message instanceof UserInputMessage input) {
            onUserInputMessage(input.getPlayer(), input.getAction(), input.getTimeout());
        } else if (message instanceof TimeoutMessage) {
            onTimeoutMessage();
        } else {
            return unexpectedMessage(client, new GameMessage(message));
        }
        return Optional.empty();
    }

    private Optional<ClientState> onMessageWhileFinishing(Client client, ObserverMessage message) {
        if (finishing == 1) { // last "finishing_round" has been received
            if (message instanceof ScoreMessage score) {
                onScoreMessage(score.getPoints(), false);
                return Optional.empty();
            } else if (message instanceof StateMessage state && "finishing".equals(state.getState())) {
                finishing++;
                return Optional.empty();
            }
        } else if (finishing == 2) { // "finishing" has been received
            if (message instanceof ScoreMessage) {
                return Optional.empty();
            } else if (message instanceof StateMessage state && "finished".equals(state.getState())) {
                onFinished();
                finishing++;
                return returnToSession();
            }
        }

        return unexpectedMessage(client, new GameMessage(message));
    }

    private Optional<ClientState> onStartRound(Client client) {
        if (isLastRound()) {
            log.fatal("Cannot start round {} with {} players", round + 1, players.size());
            return unexpectedMessage(client, new GameMessage(new StateMessage("starting_round")));
        }
        log.info("Round {} is starting...", round + 1);

        round ++;
        tricks.clear();
        trumpSuit = null;
        trumpCard = null;
        stack.clear();
        trick = -1;

        if (gameScreen != null) gameScreen.startRound(round);
        return Optional.empty();
    }

    private void onDealing() {
        if (gameScreen != null) gameScreen.deal();
    }

    private void onStartTrick() {
        log.info("Trick {} is starting...", trick + 1);
        trick ++;
        stack.clear();
        finishInteraction();
        if (gameScreen != null) gameScreen.startTrick();
    }

    private void onJuggle() {
        juggling = true;
        juggleCard = null;
    }

    private void onFinishingRound() {
        if (isLastRound()) finishing = 1; // start finish procedure
    }

    private Optional<ClientState> onFinished() {
        log.info("The game has finished.");
        if (gameScreen != null) {
            gameScreen.showScoreOverlay(true);
        }
        return returnToSession();
    }

    private Optional<ClientState> onError(Client client) {
        log.error("The game has finished with an error.");
        showErrorScreen(client, "The game has finished with an error.");
        return returnToSession();
    }

    private void onHandMessage(@NotNull UUID player, @Unmodifiable @NotNull List<@NotNull Card> hand) {
        checkPlayer(player);
        log.info("{} hand cards are: {}", nameOf(player, true, true), hand);

        if (juggling) checkActivePlayer(player, JUGGLE_CARD);
        finishInteraction();

        hands.put(player, new ArrayList<>(hand));
        if (gameScreen != null) {
            gameScreen.setSelectedCard(null);
            gameScreen.setHand(player, hand, juggling);
        }
        juggling = false;
    }

    private void onPredictionMessage(@NotNull UUID player, @Range(from = 0, to = Integer.MAX_VALUE) int prediction) {
        checkPlayer(player);
        checkActivePlayer(player, MAKE_PREDICTION, CHANGE_PREDICTION);
        log.info("{} predicted: {}", nameOf(player, true, false), prediction);

        boolean changed = currentInteraction != null && currentInteraction.action() == CHANGE_PREDICTION;
        finishInteraction();

        predictions.computeIfAbsent(round, r -> new HashMap<>()).put(player, prediction);
        if (gameScreen != null) gameScreen.addPrediction(round, player, prediction, changed);
    }

    private void onTrumpMessage(@Nullable Card trumpCard, @Nullable Card.Suit trumpSuit) {
        if (trumpCard == null) {
            log.info("There is no trump in this round.");
        } else {
            log.info("The trump suit is {} ({}).", trumpSuit != null ? trumpSuit : "yet to be determined.", trumpCard);
        }
        var player = currentInteraction != null && currentInteraction.action() == PICK_TRUMP ? currentInteraction.player() : null;
        finishInteraction();

        this.trumpCard = trumpCard;
        this.trumpSuit = trumpSuit;
        if (trumpCard == Card.WEREWOLF && trumpSuit == null) {
            werewolf = true;
        } else {
            werewolf = false;
            if (gameScreen != null) gameScreen.showTrumpOverlay(player, trumpCard, trumpSuit);
        }
    }

    private void onTrickMessage(@NotNull UUID player, @Unmodifiable @NotNull List<@NotNull Card> cards) {
        checkPlayer(player);
        log.info("This trick {} goes to {}.", cards, nameOf(player));

        this.stack.clear();
        var playerTricks = this.tricks.computeIfAbsent(player, p -> new ArrayList<>());
        playerTricks.add(cards);

        if (gameScreen != null) gameScreen.finishTrick(player, playerTricks.size());
    }

    private void onCardMessage(@NotNull UUID player, @NotNull Card card) {
        checkPlayer(player);
        checkActivePlayer(player, PLAY_CARD);
        log.info("{} played {}.", nameOf(player, true, false), card);
        finishInteraction();

        this.stack.add(Pair.of(player, card));

        var handCard = switch (card) {
            case CHANGELING_JESTER, CHANGELING_WIZARD -> Card.CHANGELING;
            case JUGGLER_BLUE, JUGGLER_GREEN, JUGGLER_RED, JUGGLER_YELLOW -> Card.JUGGLER;
            case CLOUD_BLUE, CLOUD_GREEN, CLOUD_RED, CLOUD_YELLOW -> Card.CLOUD;
            default -> card;
        };

        var hand = this.hands.get(player);
        if (hand != null) {
            hand.remove(handCard);
        }

        if (gameScreen != null) gameScreen.playCard(player, handCard, card);
    }

    private void onScoreMessage(@Unmodifiable Map<@NotNull UUID, @NotNull Integer> points, boolean showOverlay) {
        log.info("The scores are as follows: " + points);
        scores.put(round, points);
        if (gameScreen != null) {
            gameScreen.addScores(round, points);
            if (showOverlay) gameScreen.showScoreOverlay(false);
        }
    }

    private void onUserInputMessage(@Nullable UUID player, @NotNull UserInputMessage.Action action, long timeout) {
        checkPlayer(player);
        log.info(
                "Waiting for input {} from {}. (times out at {})",
                action,
                nameOf(player),
                LocalDateTime.ofInstant(Instant.ofEpochMilli(timeout), ZoneId.systemDefault())
        );

        if (action == UserInputMessage.Action.SYNC) {
            if (gameScreen != null) gameScreen.sync();
        } else {
            currentInteraction = new Interaction(player, action, timeout);

            showCurrentInteraction();

            if (werewolf && action == PICK_TRUMP) {
                werewolf = false;
            }
        }
    }

    private void onTimeoutMessage() {
        log.info("The previous interaction timed out.");
        delayedFinishInteraction();
        if (gameScreen != null) gameScreen.timeout();
    }

    private Optional<ClientState> onNackMessage(Client client, @NotNull NackMessage nack) {
        sending.set(false);

        if (isActive() && currentInteraction.action() == JUGGLE_CARD && juggleCard != null) {
            juggleCard = null;
        }

        int code = nack.getCode();
        if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) {
            log.error(nack.getMessage());
            if (gameScreen != null) {
                gameScreen.addMessage(true, "game.message.literal", nack.getMessage());
                gameScreen.ready(false);
            }
            return Optional.empty();
        } else {
            return unexpectedMessage(client, nack);
        }
    }

    private void onAckMessage() {
        log.info("OK");
        sending.set(false);

        if (isActive() && currentInteraction.action() == JUGGLE_CARD && juggleCard != null) {
            if (gameScreen != null) gameScreen.setSelectedCard(juggleCard);
            juggleCard = null;
        }

        if (gameScreen != null) gameScreen.ready(true);
    }
    //</editor-fold>

    //<editor-fold desc="Screen Callbacks" defaultstate="collapsed">
    public Optional<ClientState> onCardClicked(Client client, @NotNull Card card) {
        assert gameScreen != null;
        if (isActive()) {
            if (currentInteraction.action() == PLAY_CARD) {
                if (card == Card.CLOUD || card == Card.JUGGLER || card == Card.CHANGELING) {
                    var oldOverlay = currentInteraction.overlay();
                    if (oldOverlay != null) oldOverlay.close();

                    currentInteraction.overlay(gameScreen.showSpecialCardOverlay(card, currentInteraction.timeout()));
                } else {
                    send(client, new PlayCardMessage(card));
                }
                return Optional.empty();
            } else if (currentInteraction.action() == JUGGLE_CARD) {
                if (send(client, new JuggleMessage(card))) {
                    juggleCard = card;
                }
                return Optional.empty();
            }
        }

        gameScreen.addMessage(true, "game.message.nack.not_allowed");
        return Optional.empty();
    }

    public Optional<ClientState> onSuitClicked(Client client, @NotNull Card.Suit suit) {
        assert gameScreen != null;
        if (isActive() && currentInteraction.action() == PICK_TRUMP) {
            send(client, new PickTrumpMessage(suit));
        } else {
            gameScreen.addMessage(true, "game.message.nack.not_allowed");
        }
        return Optional.empty();
    }

    public Optional<ClientState> onPredictionMade(Client client, int prediction) {
        assert gameScreen != null;
        if (isActive()) {
            if (currentInteraction.action() == MAKE_PREDICTION || currentInteraction.action() == CHANGE_PREDICTION) {
                send(client, new PredictMessage(prediction));
                return Optional.empty();
            }
        }

        gameScreen.addMessage(true, "game.message.nack.not_allowed");
        return Optional.empty();
    }

    public Optional<ClientState> sync(Client client) {
        assert gameScreen != null;
        send(client, new ContinueMessage());
        return Optional.empty();
    }
    //</editor-fold>

    public Optional<ClientState> returnToMenu() {
        return Optional.of(new Menu());
    }

    private Optional<ClientState> returnToSession() {
        return Optional.of(new Session(
                session, sessionName, configuration,
                players.entrySet().stream()
                       .map(entry -> new PlayerData(entry.getKey(), entry.getValue(), false))
                       .toList(),
                self,
                true
        ));
    }

    /**
     * Sends a message. Only one message may be sent at a time until without receiving an answer.
     * @return {@code true} iff the message was sent
     */
    private boolean send(Client client, PlayerMessage message) {
        if (message instanceof ContinueMessage || !sending.getAndSet(true)) {
            client.send(new InteractionMessage(message));
            return true;
        } else {
            if (gameScreen != null) gameScreen.addMessage(true, "game.message.nack.too_fast");
            return false;
        }
    }

    //<editor-fold desc="Interactions" defaultstate="collapsed">
    /**
     * Checks whether some action from the player is expected.
     */
    private boolean isActive() {
        return currentInteraction != null && (currentInteraction.player() == null || self.equals(currentInteraction.player()));
    }

    private void showCurrentInteraction() {
        if (gameScreen == null) return;

        var player = currentInteraction.player();
        var action = currentInteraction.action();
        var timeout = currentInteraction.timeout();

        gameScreen.setActivePlayer(player, action, timeout);

        if (player != null && werewolf && action == PICK_TRUMP) {
            gameScreen.swapTrumpCard(player);
        }

        if (isActive()) {
            switch (action) {
                case PICK_TRUMP -> currentInteraction.overlay(gameScreen.showPickTrumpOverlay(timeout, werewolf));
                case MAKE_PREDICTION -> currentInteraction.overlay(gameScreen.showMakePredictionOverlay(round, timeout));
                case CHANGE_PREDICTION -> currentInteraction.overlay(gameScreen.showChangePredictionOverlay(round, predictions.get(round).get(player), timeout ));
                case PLAY_CARD -> gameScreen.setPersistentMessage("game.message.play_card.self");
            }
        }
    }

    /**
     * Close the interaction overlay associated with the current interaction and reset the current interaction.
     */
    private void finishInteraction() {
        if (currentInteraction != null) {
            var overlay = currentInteraction.overlay();
            if (overlay != null) overlay.close();
        }

        currentInteraction = null;
        if (gameScreen != null) gameScreen.clearActivePlayer();
    }

    /**
     * UI-wise equivalent to {@link #finishInteraction()} but information about the interaction is kept until after the
     * next message.
     */
    private void delayedFinishInteraction() {
        pendingClearActivePlayer = 2;
        if (currentInteraction != null) {
            var overlay = currentInteraction.overlay();
            if (overlay != null) overlay.close();
        }
        if (gameScreen != null) gameScreen.clearActivePlayer();
    }

    private void executeDelayedFinishInteraction() {
        if (pendingClearActivePlayer > 0 && --pendingClearActivePlayer == 0) {
            finishInteraction();
        }
    }
    //</editor-fold>

    private Optional<ClientState> handlePendingMessages(Client client) {
        if (pendingMessages != null) {
            for (var message : pendingMessages) {
                var result = onMessage(client, message);
                if (result.isPresent()) {
                    return result;
                }
                executeDelayedFinishInteraction();
            }
            pendingMessages = null;
        }
        return Optional.empty();
    }

    private void updateScreen() {
        if (gameScreen == null) return;
        gameScreen.clear();

        gameScreen.setPredictions(predictions);
        gameScreen.setScores(scores);
        gameScreen.setTrump(trumpCard, trumpSuit);
        gameScreen.setStack(stack);

        var hand = hands.get(self);
        if (hand != null) {
            gameScreen.setHand(self, hand, false);
        }

        if (currentInteraction != null) {
            showCurrentInteraction();
        } else {
            gameScreen.clearActivePlayer();
        }
    }

    private boolean isLastRound() {
        return round + 1 >= 60 / getPlayers().size();
    }

    //<editor-fold desc="Logging" defaultState="collapsed">

    /**
     * Checks whether the given player is known and logs errors.
     */
    private void checkPlayer(UUID player) {
        if (player != null && !players.containsKey(player)) {
            log.error("Unknown player {}.", player);
        }
    }

    /**
     * Checks whether one of the given actions is currently expected from the given player and logs errors.
     */
    private void checkActivePlayer(UUID player, UserInputMessage.Action...actions) {
        if (currentInteraction != null && (currentInteraction.player() == null || currentInteraction.player().equals(player))) {
            for (var action : actions) {
                if (currentInteraction.action() == action) {
                    return;
                }
            }
        }

        log.warn("Received message does not match the previous user input message.");
    }

    /**
     * Returns the name of the given player.
     */
    private String nameOf(UUID player) {
        return nameOf(player, false, false);
    }

    /**
     * Returns the name of the given player, optionally with a capitalized first letter and/or a possessive suffix ("'s")
     */
    private String nameOf(UUID player, boolean capitalize, boolean possessive) {
        if (player == null) {
            return (capitalize ? "A" : "a") + "ll players" + (possessive ? "'" : "");
        } else if (self.equals(player)) {
            return (capitalize ? "Y" : "y") + "ou" + (possessive ? "r" : "");
        } else {
            return players.get(player) + (possessive ? "'s" : "");
        }
    }
    //</editor-fold>

    @Data
    @Accessors(fluent = true)
    public static final class Interaction {
        private final UUID player;
        private final UserInputMessage.Action action;
        private final long timeout;
        private WeakReference<InteractionOverlay> overlay;

        public void overlay(InteractionOverlay overlay) {
            this.overlay = new WeakReference<>(overlay);
        }

        public InteractionOverlay overlay() {
            if (overlay == null) return null;
            else return overlay.get();
        }
    }
}
