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

import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.GameState;
import eu.jonahbauer.wizard.core.machine.states.trick.StartingTrick;
import eu.jonahbauer.wizard.common.messages.observer.PredictionMessage;
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.common.messages.player.PredictMessage;
import lombok.Getter;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

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

@Getter
public final class Predicting extends RoundState {
    public Predicting(GameData data) {
        super(data.requireEach(PLAYERS, HANDS).require(PREDICTIONS, TRUMP_SUIT, CURRENT_PLAYER));
    }

    @Override
    public Optional<GameState> onEnter(Game game) {
        game.notify(new UserInputMessage(get(CURRENT_PLAYER), MAKE_PREDICTION, getTimeout(game, true)));
        return timeout(game);
    }

    @Override
    public Optional<GameState> onTimeout(Game game) {
        try {
            checkPrediction(game, 0);
            return transition(game, 0);
        } catch (IllegalArgumentException e) {
            try {
                checkPrediction(game, 1);
                return transition(game, 1);
            } catch (IllegalArgumentException e2) {
                throw new AssertionError(e2);
            }
        }
    }

    @Override
    public Optional<GameState> onMessage(Game game, UUID player, PlayerMessage message) {
        if (get(CURRENT_PLAYER).equals(player) && message instanceof PredictMessage predictMessage) {
            checkPrediction(game, predictMessage.getPrediction());
            return transition(game, predictMessage.getPrediction());
        } else {
            return super.onMessage(game, player, message);
        }
    }

    private void checkPrediction(Game game, int prediction) {
        int round = get(ROUND);
        if (prediction < 0 || prediction > round + 1) {
            throw new IllegalArgumentException("Prediction must be between 0 and " + (round + 1) + ".");
        }

        if (!game.getConfig().allowExactPredictions() && isLastPlayer()) {
            int sum = get(PREDICTIONS).values().stream().mapToInt(i -> i).sum();
            if (sum + prediction == round + 1) {
                throw new IllegalArgumentException("Predictions must not add up.");
            }
        }
    }

    private Optional<GameState> transition(Game game, int prediction) {
        game.notify(new PredictionMessage(get(CURRENT_PLAYER), prediction));

        // add prediction
        var predictions = new HashMap<>(get(PREDICTIONS));
        predictions.put(get(CURRENT_PLAYER), prediction);

        GameData data = getData().with(
                PREDICTIONS, Map.copyOf(predictions),
                CURRENT_PLAYER, getNextPlayer()
        );

        if (isLastPlayer()) {
            return transition(new StartingTrick(data));
        } else {
            return transition(new Predicting(data));
        }
    }

    private boolean isLastPlayer() {
        return getDealer().equals(get(CURRENT_PLAYER));
    }
}
