package org.ggp.base.apps.benchmark;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.ggp.base.player.GamePlayer;
import org.ggp.base.player.gamer.Gamer;
import org.ggp.base.player.gamer.statemachine.random.RandomGamer;
import org.ggp.base.server.GameServer;
import org.ggp.base.server.event.ServerNewMovesEvent;
import org.ggp.base.util.game.Game;
import org.ggp.base.util.game.GameRepository;
import org.ggp.base.util.gdl.grammar.Gdl;
import org.ggp.base.util.gdl.grammar.GdlPool;
import org.ggp.base.util.gdl.grammar.GdlRelation;
import org.ggp.base.util.gdl.grammar.GdlRule;
import org.ggp.base.util.match.Match;
import org.ggp.base.util.observer.Event;
import org.ggp.base.util.observer.Observer;
import org.ggp.base.util.statemachine.Move;
import org.ggp.base.util.statemachine.Role;
import org.ggp.base.util.symbol.factory.SymbolFactory;
import org.ggp.base.util.symbol.factory.exceptions.SymbolFormatException;
import org.ggp.base.util.symbol.grammar.Symbol;
import org.ggp.base.util.symbol.grammar.SymbolList;

/**
 * PlayerTester is a benchmarking tool to evaluate the skill of a player.
 * It only requires one player to be running and uses only minimal extra
 * memory, so it can run on the same machine as the player. It run a set
 * of test cases, and computes a benchmark score for the player based on
 * how many of the tests the player passes.
 *
 * Tests consist of a game, a state in that game, a role that the player
 * will act as, and a set of "good" move choices. The player passes the
 * test if, when put into a match of the given game in the given state as
 * the given role, the player chooses one of the "good" move choices in a
 * reasonable amount of time.
 *
 * Mechanically, this is done by constructing an artificial game in which
 * the original game is modified to start in the chosen state, by adjusting
 * the set of "init" propositions. The player is put into a match playing
 * the game, with a suitable start clock, and runs through exactly one move.
 * Then the move is checked against the list of "good" moves, a "pass/fail"
 * verdict is issued, and the match is aborted. Repeat for all tests.
 *
 * The benchmark score is deterministic if the player is deterministic.
 *
 * @author Sam Schreiber
 */
public class PlayerTester {
    /**
     * TestCase is a test case for the PlayerTester.
     *
     * Each test case contains a game, a game state, a role, and a set of "good"
     * moves for that role to take in that game state when playing that game. The
     * PlayerTester will simulate this with an actual player, and determine if the
     * move the player chooses is one of the "good" moves.
     */
    public static class TestCase {
        public final String suiteName;
        public final String gameKey;
        public final int nRole;
        public final int nStartClock;
        public final int nPlayClock;
        public final String theState;
        public final String[] goodMovesArr;

        public TestCase(String suiteName, String gameKey, int nRole, int nStartClock, int nPlayClock, String theState, String[] goodMovesArr) {
            this.suiteName = suiteName;
            this.gameKey = gameKey;
            this.nRole = nRole;
            this.nStartClock = nStartClock;
            this.nPlayClock = nPlayClock;
            this.theState = theState;
            this.goodMovesArr = goodMovesArr;
        }
    }

    /**
     * getMediasResGame takes a game and a desired initial state, and rewrites
     * the game rules so that the game begins in that initial state. To do this
     * it removes all of the "init" rules and substitutes in its own set.
     * @throws SymbolFormatException
     */
    static Game getMediasResGame(String gameKey, String theState) throws SymbolFormatException {
        StringBuilder newRulesheet = new StringBuilder();
        List<Gdl> theRules = GameRepository.getDefaultRepository().getGame(gameKey).getRules();
        for (Gdl gdl : theRules) {
            if (gdl instanceof GdlRule) {
                // Capture and drop init statements of the form "( <= ( init ( pool ?piece ) ) ( piece ?piece ) )"
                GdlRule rule = (GdlRule)gdl;
                if (rule.getHead().getName().equals(GdlPool.INIT)) {
                    ;
                } else {
                    newRulesheet.append(gdl.toString() + "\n");
                }
            } else if (gdl instanceof GdlRelation) {
                // Capture and drop init statements of the form "( init ( cell 4 4 empty ) )"
                GdlRelation rel = (GdlRelation)gdl;
                if (rel.getName().equals(GdlPool.INIT)) {
                    ;
                } else {
                    newRulesheet.append(gdl.toString() + "\n");
                }
            } else {
                newRulesheet.append(gdl.toString() + "\n");
            }
        }

        // Once all of the existing init lines have been removed, add new ones.
        SymbolList newInitialState = (SymbolList)SymbolFactory.create(theState);
        for (int i = 0; i < newInitialState.size(); i++) {
            Symbol anInit = newInitialState.get(i);
            newRulesheet.append("( INIT " + anInit + " )\n");
        }

        return Game.createEphemeralGame("( " + newRulesheet.toString() + " )");
    }

    public static boolean passesTest(String hostport, TestCase theCase) throws SymbolFormatException {
        final Game theGame = getMediasResGame(theCase.gameKey, theCase.theState);
        final Match theMatch = new Match("playerTester." + Match.getRandomString(5), -1, theCase.nStartClock, theCase.nPlayClock, theGame, "");

        // Set up fake players to pretend to play the game alongside the real player
        List<String> theHosts = new ArrayList<String>();
        List<Integer> thePorts = new ArrayList<Integer>();
        for (int i = 0; i < Role.computeRoles(theGame.getRules()).size(); i++) {
            theHosts.add("SamplePlayer" + i);
            thePorts.add(9147+i);
        }
        theHosts.set(theCase.nRole, hostport.split(":")[0]);
        thePorts.set(theCase.nRole, Integer.parseInt(hostport.split(":")[1]));

        // Set up a game server to play through the game, with all players playing randomly
        // except the real player that we're testing
        final GameServer theServer = new GameServer(theMatch, theHosts, thePorts);
        for (int i = 0; i < theHosts.size(); i++) {
            if (i != theCase.nRole) {
                theServer.makePlayerPlayRandomly(i);
            }
        }

        final int theRole = theCase.nRole;
        final Move[] theChosenMoveArr = new Move[1];
        theServer.addObserver(new Observer() {
            @Override
            public void observe(Event event) {
                if (event instanceof ServerNewMovesEvent) {
                    theChosenMoveArr[0] = ((ServerNewMovesEvent)event).getMoves().get(theRole);
                    theServer.abort();
                }
            }
        });

        theServer.start();
        try {
            theServer.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        final String theChosenMove = theChosenMoveArr[0].toString().toLowerCase();
        Set<String> goodMoves = new HashSet<String>(Arrays.asList(theCase.goodMovesArr));
        boolean passes = goodMoves.contains(theChosenMove);

        if (!passes) {
            System.out.println("Failure: player chose " + theChosenMove + "; acceptable moves are " + goodMoves);
        } else {
            System.out.println("Success: player chose " + theChosenMove + " which is an acceptable move.");
        }

        return passes;
    }

    public static Map<String, Double> getBenchmarkScores(String hostport) throws SymbolFormatException {
        Map<String, Integer> nTestsBySuite = new HashMap<String, Integer>();
        Map<String, Integer> nPassesBySuite = new HashMap<String, Integer>();

        for (TestCase aCase : PlayerTesterCases.TEST_CASES) {
            if (!nTestsBySuite.containsKey(aCase.suiteName)) nTestsBySuite.put(aCase.suiteName, 0);
            if (!nPassesBySuite.containsKey(aCase.suiteName)) nPassesBySuite.put(aCase.suiteName, 0);

            nTestsBySuite.put(aCase.suiteName, nTestsBySuite.get(aCase.suiteName) + 1);
            if (passesTest(hostport, aCase)) {
                nPassesBySuite.put(aCase.suiteName, nPassesBySuite.get(aCase.suiteName) + 1);
            }
        }

        Map<String, Double> benchmarkScores = new HashMap<String, Double>();
        for (String key : nTestsBySuite.keySet()) {
            benchmarkScores.put(key, Math.floor(100 * (double)nPassesBySuite.get(key) / (double)nTestsBySuite.get(key)));
        }

        return benchmarkScores;
    }

    public static Map<String, Double> getBenchmarkScores(Gamer aGamer) throws IOException, SymbolFormatException {
        GamePlayer player = new GamePlayer(3141, aGamer);
        player.start();
        Map<String, Double> theScores = getBenchmarkScores("127.0.0.1:3141");
        player.shutdown();
        return theScores;
    }

    public static void main(String[] args) throws InterruptedException, SymbolFormatException, IOException {
        System.out.println("Benchmark score for random player: " + getBenchmarkScores(new RandomGamer()));
    }
}