package org.ggp.base.apps.server.scheduling;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.AbstractAction;
import javax.swing.JTabbedPane;

import org.ggp.base.apps.server.error.ErrorPanel;
import org.ggp.base.apps.server.history.HistoryPanel;
import org.ggp.base.apps.server.leaderboard.LeaderboardPanel;
import org.ggp.base.apps.server.states.StatesPanel;
import org.ggp.base.apps.server.visualization.VisualizationPanel;
import org.ggp.base.server.GameServer;
import org.ggp.base.server.event.ServerMatchUpdatedEvent;
import org.ggp.base.util.crypto.BaseCryptography.EncodedKeyPair;
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.presence.PlayerPresence;
import org.ggp.base.util.ui.CloseableTabs;

public final class Scheduler implements Observer
{
    public EncodedKeyPair signingKeys;

    private JTabbedPane matchesTabbedPane;
    private SchedulingPanel schedulingPanel;
    private LeaderboardPanel leaderboardPanel;

    private final List<PendingMatch> schedulingQueue;
    private final Set<String> activePlayers;

    private Map<String, WeakReference<GameServer>> gameServers;

    public Scheduler(JTabbedPane matchesTabbedPane, SchedulingPanel schedulingPanel, LeaderboardPanel leaderboardPanel) {
        this.schedulingPanel = schedulingPanel;
        this.leaderboardPanel = leaderboardPanel;
        this.matchesTabbedPane = matchesTabbedPane;
        schedulingQueue = new ArrayList<PendingMatch>();
        activePlayers = new HashSet<String>();
        gameServers = new HashMap<String, WeakReference<GameServer>>();
    }

    public void start() {
        new SchedulingThread().start();
    }

    public synchronized void addPendingMatch(PendingMatch spec) {
        if (spec.shouldQueue) {
            schedulingPanel.addPendingMatch(spec);
            schedulingQueue.add(spec);
        } else {
            doSchedule(spec);
        }
    }

    private synchronized boolean canSchedule(PendingMatch spec) {
        for (PlayerPresence player : spec.thePlayers) {
            if (!player.getStatus().toLowerCase().equals("available")) {
                return false;
            }
            if (activePlayers.contains(player.getName())) {
                return false;
            }
        }
        return true;
    }

    public synchronized void abortOngoingMatch(String matchID) {
        if (gameServers.containsKey(matchID)) {
            GameServer server = gameServers.get(matchID).get();
            if (server != null) {
                server.abort();
            }
        }
    }

    private synchronized void doSchedule(PendingMatch spec) {
        try {
            Match match = new Match(spec.matchID, spec.previewClock, spec.startClock, spec.playClock, spec.theGame, "");

            List<String> hosts = new ArrayList<String>(spec.thePlayers.size());
            List<Integer> ports = new ArrayList<Integer>(spec.thePlayers.size());
            List<String> playerNames = new ArrayList<String>(spec.thePlayers.size());
            for (PlayerPresence player : spec.thePlayers) {
                hosts.add(player.getHost());
                ports.add(player.getPort());
                playerNames.add(player.getName());
            }

            HistoryPanel historyPanel = new HistoryPanel();
            ErrorPanel errorPanel = new ErrorPanel();
            VisualizationPanel visualizationPanel = new VisualizationPanel(spec.theGame);
            StatesPanel statesPanel = new StatesPanel();

            if (spec.shouldDetail) {
                JTabbedPane tab = new JTabbedPane();
                tab.addTab("History", historyPanel);
                tab.addTab("Error", errorPanel);
                tab.addTab("Visualization", visualizationPanel);
                tab.addTab("States", statesPanel);
                CloseableTabs.addClosableTab(matchesTabbedPane, tab, spec.matchID, addTabCloseButton(tab));
            }

            match.setCryptographicKeys(signingKeys);
            match.setPlayerNamesFromHost(playerNames);
            if (spec.shouldScramble) {
                match.enableScrambling();
            }

            GameServer gameServer = new GameServer(match, hosts, ports);
            if (spec.shouldDetail) {
                gameServer.addObserver(errorPanel);
                gameServer.addObserver(historyPanel);
                gameServer.addObserver(visualizationPanel);
                gameServer.addObserver(statesPanel);
            }
            gameServer.addObserver(schedulingPanel);
            gameServer.addObserver(leaderboardPanel);
            gameServer.addObserver(this);
            gameServer.start();

            activePlayers.addAll(playerNames);

            if (spec.shouldSave) {
                File matchesDir = new File(System.getProperty("user.home"), "ggp-saved-matches");
                if (!matchesDir.exists()) {
                    matchesDir.mkdir();
                }
                File matchFile = new File(matchesDir, match.getMatchId() + ".json");
                gameServer.startSavingToFilename(matchFile.getAbsolutePath());
            }
            if (spec.shouldPublish) {
                if (!match.getGame().getRepositoryURL().contains("127.0.0.1")) {
                    gameServer.startPublishingToSpectatorServer("http://matches.ggp.org/");
                    gameServer.setForceUsingEntireClock();
                }
            }

            gameServers.put(spec.matchID, new WeakReference<GameServer>(gameServer));
            schedulingQueue.remove(spec);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void observe(Event genericEvent) {
        if (!(genericEvent instanceof ServerMatchUpdatedEvent)) return;
        ServerMatchUpdatedEvent event = (ServerMatchUpdatedEvent)genericEvent;
        Match match = event.getMatch();
        if (!match.isAborted() && !match.isCompleted()) return;
        activePlayers.removeAll(match.getPlayerNamesFromHost());
    }

    @SuppressWarnings("serial")
    private AbstractAction addTabCloseButton(final Component tabToClose) {
        return new AbstractAction("x") {
            @Override
            public void actionPerformed(ActionEvent evt) {
                for (int i = 0; i < matchesTabbedPane.getTabCount(); i++) {
                    if (tabToClose == matchesTabbedPane.getComponentAt(i)) {
                        matchesTabbedPane.remove(tabToClose);
                    }
                }
            }
        };
    }

    class SchedulingThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ie) {
                    ;
                }
                PendingMatch matchToSchedule = null;
                for (PendingMatch spec : schedulingQueue) {
                    if (canSchedule(spec)) {
                        matchToSchedule = spec;
                        break;
                    }
                }
                if (matchToSchedule != null) {
                    doSchedule(matchToSchedule);
                }
            }
        }
    }
}
