package org.ggp.base.apps.benchmark;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.ggp.base.util.Pair;
import org.ggp.base.util.game.Game;
import org.ggp.base.util.game.GameRepository;
import org.ggp.base.util.gdl.grammar.GdlSentence;
import org.ggp.base.util.statemachine.MachineState;
import org.ggp.base.util.statemachine.Move;
import org.ggp.base.util.statemachine.Role;
import org.ggp.base.util.statemachine.StateMachine;
import org.ggp.base.util.statemachine.exceptions.GoalDefinitionException;
import org.ggp.base.util.statemachine.exceptions.MoveDefinitionException;
import org.ggp.base.util.statemachine.exceptions.TransitionDefinitionException;
import org.ggp.base.util.statemachine.implementation.prover.ProverStateMachine;

/**
 * EndgameCaseGenerator uses a minimax solver to produce some test cases
 * for the PlayerTester. It runs a depth charge from the initial state of
 * a game down to a terminal state, and then backs off and runs minimax to
 * find the best moves in the resulting state. Since the state is near the
 * end of the game, the resulting minimax solve will usually finish quickly.
 * This produces a nice test case that can be used in the PlayerTester.
 *
 * Please note this will only produce a subset of the collection of tests that
 * are useful in PlayerTester: all of the tests that it produces are for states
 * that are near an ending to the game. Notably, it doesn't test critical moves
 * in the mid-game that heavily impact the outcome, but don't immediately lead
 * to a victory or loss for one player.
 *
 * @author Sam Schreiber
 */
public class EndgameCaseGenerator {
    private EndgameCaseGenerator() {
    }

    public static void main(String[] args) throws MoveDefinitionException, TransitionDefinitionException, GoalDefinitionException {
        generateTestCase("connectFour", 0, 5, 6, new ProverStateMachine());
    }

    public static void generateTestCase(String gameKey, int nRole, int nBackoff, int nMaxDepth, StateMachine theMachine) throws MoveDefinitionException, TransitionDefinitionException, GoalDefinitionException {
        // Load the game and create a state machine for it
        Game theGame = GameRepository.getDefaultRepository().getGame(gameKey);
        theMachine.initialize(theGame.getRules());
        Role ourRole = theMachine.getRoles().get(nRole);

        // Once the game is loaded, run depth charges until we find a suitable
        // endgame backoff state that can be used to produce a test case.
        while (true) {
            // Find a state that's nBackoff steps from a terminal state
            List<MachineState> theStates = new ArrayList<MachineState>();
            MachineState theChargeState = theMachine.getInitialState();
            while (!theMachine.isTerminal(theChargeState)) {
                theStates.add(theChargeState);
                theChargeState = theMachine.getRandomNextState(theChargeState);
            }
            MachineState theState = theStates.get(Math.max(theStates.size() - nBackoff, 0));

            // Solve the game from the backoff state. For moves that return
            // definite scores, track the best and worst scores. For moves that
            // don't return definite scores, track them separately.
            int bestScore = 0;
            int worstScore = 100;
            List<Pair<Move, Integer>> scoredMoves = new ArrayList<Pair<Move, Integer>>();
            Set<Move> unscoredMoves = new HashSet<Move>();
            for (Move ourMove : theMachine.getLegalMoves(theState, ourRole)) {
                Pair<Integer, Integer> theScore = minimax(theMachine, nRole, ourRole, theMachine.getRandomNextState(theState, ourRole, ourMove), nMaxDepth);
                if (theScore.left == theScore.right) {
                    bestScore = Math.max(bestScore, theScore.left);
                    worstScore = Math.min(worstScore, theScore.left);
                    scoredMoves.add(Pair.of(ourMove, theScore.left));
                } else {
                    unscoredMoves.add(ourMove);
                }
            }

            // If no moves had definite scores, it's a bad test case. Repeat!
            if (worstScore > bestScore) {
                System.out.println("Found a backoff state with no fully scored moves.");
                continue;
            }

            // If all scored moves are equally good, it's a bad test case. Repeat!
            if (bestScore == worstScore) {
                System.out.println("Found a backoff state in which all scored moves were equal at " + bestScore);
                continue;
            }

            // Select out the best moves for the "known good" answer. Also
            // include any unscored moves in this set, since they might be
            // good choices as well (we just don't know).
            Set<Move> bestMoves = new HashSet<Move>();
            for (Pair<Move, Integer> scoredMove : scoredMoves) {
                if (scoredMove.right == bestScore) {
                    bestMoves.add(scoredMove.left);
                }
            }
            StringBuilder goodMoveStrings = new StringBuilder();
            for (Move bestMove : bestMoves) {
                goodMoveStrings.append("\"" + bestMove + "\", ");
            }
            for (Move unscoredMove : unscoredMoves) {
                goodMoveStrings.append("\"" + unscoredMove + "\", ");
            }

            // Also represent the current state as a symbol list.
            StringBuilder theStateSymbols = new StringBuilder("( ");
            for (GdlSentence aSentence : theState.getContents()) {
                theStateSymbols.append(aSentence.getBody().get(0) + " ");
            }
            theStateSymbols.append(")");

            // And finally, output the code for the new test.
            System.out.println("new PlayerTester.TestCase(\"Endgame\", \"" + gameKey + "\", 0, 15, 5, \"" + theStateSymbols + "\", new String[] {" + goodMoveStrings + "}),");
            return;
        }
    }

    // This is a traditional minimax solver.
    private static Pair<Integer, Integer> minimax(StateMachine machine, int ourRoleIndex, Role ourRole, MachineState currentState, int depth) throws GoalDefinitionException, MoveDefinitionException, TransitionDefinitionException {
        // If we've hit our max depth, return immediately, sending back
        // no useful information about the final score.
        if (depth < 0) {
            return Pair.of(0, 100);
        }

        // Upon reaching a terminal state, we know its value immediately.
        if (machine.isTerminal(currentState)) {
            int theScore = machine.getGoal(currentState, ourRole);
            return Pair.of(theScore, theScore);
        }

        // Otherwise, perform recursive descent to compute the state's value.
        List<List<Move>> legalMoves = machine.getLegalJointMoves(currentState);
        Pair<Integer, Integer> overallScore = Pair.of(0, 0);
        for (Move ourMove : machine.getLegalMoves(currentState, ourRole)) {
            Pair<Integer, Integer> worstMove = Pair.of(100, 100);
            for (List<Move> jointMove : legalMoves) {
                if (jointMove.get(ourRoleIndex).equals(ourMove)) {
                    MachineState newState = machine.getNextState(currentState, jointMove);
                    Pair<Integer, Integer> score = minimax(machine, ourRoleIndex, ourRole, newState, depth - 1);
                    worstMove = Pair.of(Math.min(worstMove.left, score.left), Math.min(worstMove.right, score.right));
                    if(score.right == 0)
                        break;
                }
            }
            overallScore = Pair.of(Math.max(overallScore.left, worstMove.left), Math.max(overallScore.right, worstMove.right));
            if(overallScore.left == 100)
                break;
        }
        return overallScore;
    }
}