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

import eu.jonahbauer.wizard.common.messages.observer.PredictionMessage;
import eu.jonahbauer.wizard.common.messages.observer.TimeoutMessage;
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 eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.machine.states.GameData;
import eu.jonahbauer.wizard.core.machine.GameState;
import eu.jonahbauer.wizard.core.machine.states.InvalidDataException;

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

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

public final class ChangingPrediction extends RoundState {
    private transient final int oldPrediction;

    public ChangingPrediction(GameData data) {
        super(data.requireEach(PLAYERS, PREDICTIONS).require(TRICKS, SCORE, CLOUDED_PLAYER));
        checkData(data);
        oldPrediction = get(PREDICTIONS).get(get(CLOUDED_PLAYER));
    }

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

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

    @Override
    public Optional<GameState> onTimeout(Game game) {
        game.notify(new TimeoutMessage());
        return transition(game, oldPrediction + 1);
    }

    private void checkPrediction(int prediction) {
        if (prediction < 0) {
            throw new IllegalArgumentException("Prediction must be greater than or equal to 0.");
        } else if (Math.abs(prediction - oldPrediction) != 1) {
            throw new IllegalArgumentException("Prediction must differ from your old prediction by exactly one.");
        }
    }

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

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

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

        return transition(new FinishingRound(data));
    }

    private void checkData(GameData data) {
        if (data.get(CLOUDED_PLAYER) == null) {
            throw new InvalidDataException("Clouded player is null.");
        }
    }
}
