/**
 *  Copyright (C) 2002-2012   The FreeCol Team
 *
 *  This file is part of FreeCol.
 *
 *  FreeCol is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  FreeCol is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with FreeCol.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.sf.freecol.server;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageIO;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;

import net.sf.freecol.FreeCol;
import net.sf.freecol.common.FreeColException;
import net.sf.freecol.common.io.FreeColSavegameFile;
import net.sf.freecol.common.io.FreeColTcFile;
import net.sf.freecol.common.model.Building;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.ColonyTile;
import net.sf.freecol.common.model.Europe;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.FreeColObject;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.GameOptions;
import net.sf.freecol.common.model.HighScore;
import net.sf.freecol.common.model.IndianSettlement;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.Nation;
import net.sf.freecol.common.model.NationOptions;
import net.sf.freecol.common.model.NationOptions.Advantages;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Settlement;
import net.sf.freecol.common.model.Specification;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.networking.Connection;
import net.sf.freecol.common.networking.DOMMessage;
import net.sf.freecol.common.networking.NoRouteToServerException;
import net.sf.freecol.common.option.BooleanOption;
import net.sf.freecol.common.option.IntegerOption;
import net.sf.freecol.common.option.OptionGroup;
import net.sf.freecol.common.option.StringOption;
import net.sf.freecol.common.util.XMLStream;
import net.sf.freecol.server.ai.AIInGameInputHandler;
import net.sf.freecol.server.ai.AIMain;
import net.sf.freecol.server.ai.AIPlayer;
import net.sf.freecol.server.control.Controller;
import net.sf.freecol.server.control.InGameController;
import net.sf.freecol.server.control.InGameInputHandler;
import net.sf.freecol.server.control.PreGameController;
import net.sf.freecol.server.control.PreGameInputHandler;
import net.sf.freecol.server.control.UserConnectionHandler;
import net.sf.freecol.server.generator.MapGenerator;
import net.sf.freecol.server.generator.SimpleMapGenerator;
import net.sf.freecol.server.generator.TerrainGenerator;
import net.sf.freecol.server.model.ServerGame;
import net.sf.freecol.server.model.ServerModelObject;
import net.sf.freecol.server.model.ServerPlayer;
import net.sf.freecol.server.model.TransactionSession;
import net.sf.freecol.server.networking.DummyConnection;
import net.sf.freecol.server.networking.Server;

import org.w3c.dom.Element;


/**
 * The main control class for the FreeCol server. This class both starts and
 * keeps references to all of the server objects and the game model objects.
 * <br>
 * <br>
 * If you would like to start a new server you just create a new object of this
 * class.
 */
public final class FreeColServer {

    private static final Logger logger = Logger.getLogger(FreeColServer.class.getName());

    private static final int META_SERVER_UPDATE_INTERVAL = 60000;

    private static final int NUMBER_OF_HIGH_SCORES = 10;
    private static final String HIGH_SCORE_FILE = "HighScores.xml";

    /** Hex constant digits for get/restoreRandomState. */
    private static final String HEX_DIGITS = "0123456789ABCDEF";

    /**
     * The save game format used for saving games.
     *
     * Version 7-10 were used in 0.9.x.
     * Version 11 made a lot of changes and was introduced for the 0.10.0
     *     series.
     * Version 12 was introduced with HighSeas post-0.10.1.
     *
     * Please add to this comment if you increase the version.
     */
    public static final int SAVEGAME_VERSION = 12;

    /**
     * The oldest save game format that can still be loaded.
     * The promise is that FreeCol 0.n.* can load 0.(n-1).* games.
     *
     * TODO: revisit the numbering scheme and save compatibility promise
     * when 1.0 is released.
     */
    public static final int MINIMUM_SAVEGAME_VERSION = 7;

    /**
     * The specification to use when loading old format games where
     * a spec may not be readily available.
     */
    public static final String defaultSpec = "freecol";

    /** Constant for storing the state of the game. */
    public static enum GameState {STARTING_GAME, IN_GAME, ENDING_GAME}

    /** Stores the current state of the game. */
    private GameState gameState = GameState.STARTING_GAME;

    // Networking:
    private Server server;

    // Control:
    private final UserConnectionHandler userConnectionHandler;

    private final PreGameController preGameController;

    private final PreGameInputHandler preGameInputHandler;

    private final InGameInputHandler inGameInputHandler;

    private final InGameController inGameController;

    private ServerGame game;

    private AIMain aiMain;

    private MapGenerator mapGenerator;

    private boolean singleplayer;

    // The username of the player owning this server.
    private String owner;

    private boolean publicServer = false;

    private final int port;

    /** The name of this server. */
    private String name;

    /** The private provider for random numbers. */
    private Random random = null;

    /** Did the integrity check succeed */
    private boolean integrity = false;

    /** An active unit specified in a saved game. */
    private Unit activeUnit = null;

    /**
     * The high scores on this server.
     */
    private List<HighScore> highScores = null;

    public static final Comparator<HighScore> highScoreComparator
        = new Comparator<HighScore>() {
        public int compare(HighScore score1, HighScore score2) {
            return score2.getScore() - score1.getScore();
        }
    };


    /**
     * Starts a new server in a specified mode and with a specified port.
     *
     * @param publicServer This value should be set to <code>true</code> in
     *            order to appear on the meta server's listing.
     *
     * @param singleplayer Sets the game as singleplayer (if <i>true</i>) or
     *            multiplayer (if <i>false</i>).
     *
     * @param port The TCP port to use for the public socket. That is the port
     *            the clients will connect to.
     *
     * @param name The name of the server, or <code>null</code> if the default
     *            name should be used.
     *
     * @throws IOException if the public socket cannot be created (the exception
     *             will be logged by this class).
     *
     */
    public FreeColServer(Specification specification, boolean publicServer,
                         boolean singleplayer, int port, String name)
        throws IOException, NoRouteToServerException {
        this(specification, publicServer, singleplayer, port, name,
             Advantages.SELECTABLE);
    }

    public FreeColServer(Specification specification, boolean publicServer,
                         boolean singleplayer, int port, String name,
                         Advantages advantages)
        throws IOException, NoRouteToServerException {

        this.publicServer = publicServer;
        this.singleplayer = singleplayer;
        this.port = port;
        this.name = name;
        this.random = new Random(FreeCol.getFreeColSeed());

        userConnectionHandler = new UserConnectionHandler(this);
        preGameController = new PreGameController(this);
        preGameInputHandler = new PreGameInputHandler(this);
        inGameInputHandler = new InGameInputHandler(this);
        inGameController = new InGameController(this, random);

        game = new ServerGame(specification);
        game.setNationOptions(new NationOptions(specification, advantages));
        // @compat 0.9.x, 0.10.x
        fixGameOptions();
        // end compatibility code

        mapGenerator = new SimpleMapGenerator(random, specification);

        try {
            server = new Server(this, port);
            server.start();
        } catch (IOException e) {
            logger.warning("Exception while starting server: " + e);
            throw e;
        }
        updateMetaServer(true);
        startMetaServerUpdateThread();
    }

    /**
     * Starts a new server in a specified mode and with a specified port and
     * loads the game from the given file.
     *
     * @param savegame The file where the game data is located.
     *
     * @param port The TCP port to use for the public socket. That is the port
     *            the clients will connect to.
     *
     * @param name The name of the server, or <code>null</code> if the default
     *            name should be used.
     *
     * @exception IOException if the public socket cannot be created (the exception
     *             will be logged by this class).
     *
     * @exception FreeColException if the savegame could not be loaded.
     * @exception NoRouteToServerException if an error occurs
     */
    public FreeColServer(final FreeColSavegameFile savegame, int port, String name)
        throws IOException, FreeColException, NoRouteToServerException {
        this(savegame, port, name, null);
    }

    /**
     * Starts a new server in a specified mode and with a specified port and
     * loads the game from the given file.
     *
     * @param savegame The file where the game data is located.
     *
     * @param port The TCP port to use for the public socket. That is the port
     *            the clients will connect to.
     *
     * @param name The name of the server, or <code>null</code> if the default
     *            name should be used.
     *
     * @param specification a <code>Specification</code> value
     * @exception IOException if the public socket cannot be created (the exception
     *             will be logged by this class).
     *
     * @exception FreeColException if the savegame could not be loaded.
     * @exception NoRouteToServerException if an error occurs
     */
    public FreeColServer(final FreeColSavegameFile savegame, int port,
                         String name, Specification specification)
        throws IOException, FreeColException, NoRouteToServerException {
        this.port = port;
        this.name = name;
        //this.nationOptions = nationOptions;
        mapGenerator = null;
        userConnectionHandler = new UserConnectionHandler(this);
        preGameController = new PreGameController(this);
        preGameInputHandler = new PreGameInputHandler(this);
        inGameInputHandler = new InGameInputHandler(this);

        try {
            server = new Server(this, port);
            server.start();
        } catch (IOException e) {
            logger.warning("Exception while starting server: " + e);
            throw e;
        }
        try {
            loadGame(savegame, specification);
        } catch (FreeColException e) {
            server.shutdown();
            throw e;
        } catch (Exception e) {
            server.shutdown();
            FreeColException fe = new FreeColException("couldNotLoadGame");
            fe.initCause(e);
            throw fe;
        }
        if (random == null) {
            this.random = new Random(FreeCol.getFreeColSeed());
        }
        inGameController = new InGameController(this, random);
        mapGenerator = new SimpleMapGenerator(random, getSpecification());

        updateMetaServer(true);
        startMetaServerUpdateThread();
        TransactionSession.clearAll();
    }

    /**
     * Sets the mode of the game: singleplayer/multiplayer.
     *
     * @param singleplayer Sets the game as singleplayer (if <i>true</i>) or
     *            multiplayer (if <i>false</i>).
     */
    public void setSingleplayer(boolean singleplayer) {
        this.singleplayer = singleplayer;
    }

    /**
     * Checks if the user is playing in singleplayer mode.
     *
     * @return <i>true</i> if the user is playing in singleplayer mode,
     *         <i>false</i> otherwise.
     */
    public boolean isSingleplayer() {
        return singleplayer;
    }

    /**
     * Gets the specification from the game run by this server.
     *
     * @return The specification from the game.
     */
    public Specification getSpecification() {
        return game.getSpecification();
    }

    /**
     * Gets the <code>UserConnectionHandler</code>.
     *
     * @return The <code>UserConnectionHandler</code> that is beeing used when
     *         new client connect.
     */
    public UserConnectionHandler getUserConnectionHandler() {
        return userConnectionHandler;
    }

    /**
     * Gets the <code>Controller</code>.
     *
     * @return The <code>Controller</code>.
     */
    public Controller getController() {
        if (getGameState() == GameState.IN_GAME) {
            return inGameController;
        } else {
            return preGameController;
        }
    }

    /**
     * Gets the <code>PreGameInputHandler</code>.
     *
     * @return The <code>PreGameInputHandler</code>.
     */
    public PreGameInputHandler getPreGameInputHandler() {
        return preGameInputHandler;
    }

    /**
     * Gets the <code>InGameInputHandler</code>.
     *
     * @return The <code>InGameInputHandler</code>.
     */
    public InGameInputHandler getInGameInputHandler() {
        return inGameInputHandler;
    }

    /**
     * Gets the controller being used while the game is running.
     *
     * @return The controller from making a new turn etc.
     */
    public InGameController getInGameController() {
        return inGameController;
    }

    /**
     * Gets the <code>Game</code> that is being played.
     *
     * @return The <code>Game</code> which is the main class of the game-model
     *         being used in this game.
     */
    public ServerGame getGame() {
        return game;
    }

    /**
     * Sets the <code>Game</code> that is being played.
     *
     * @param game The new <code>Game</code>.
     */
    public void setGame(ServerGame game) {
        this.game = game;
    }

    /**
     * Sets the main AI-object.
     *
     * @param aiMain The main AI-object which is responsible for controlling,
     *            updating and saving the AI objects.
     */
    public void setAIMain(AIMain aiMain) {
        this.aiMain = aiMain;
    }

    /**
     * Gets the main AI-object.
     *
     * @return The main AI-object which is responsible for controlling, updating
     *         and saving the AI objects.
     */
    public AIMain getAIMain() {
        return aiMain;
    }

    /**
     * Gets the current state of the game.
     *
     * @return One of: {@link GameState#STARTING_GAME}, {@link GameState#IN_GAME} and
     *         {@link GameState#ENDING_GAME}.
     */
    public GameState getGameState() {
        return gameState;
    }

    /**
     * Sets the current state of the game.
     *
     * @param state The new state to be set. One of: {@link GameState#STARTING_GAME},
     *            {@link GameState#IN_GAME} and {@link GameState#ENDING_GAME}.
     */
    public void setGameState(GameState state) {
        gameState = state;
    }

    /**
     * Gets the network server responsible of handling the connections.
     *
     * @return The network server.
     */
    public Server getServer() {
        return server;
    }

    /**
     * Gets the integrity check result.
     *
     * @return The integrity check result.
     */
    public boolean getIntegrity() {
        return integrity;
    }

    /**
     * Gets the server random number generator.
     *
     * @return The server random number generator.
     */
    public Random getServerRandom() {
        return random;
    }

    /**
     * Sets the server random number generator.
     *
     * @param random The new random number generator.
     */
    public void setServerRandom(Random random) {
        this.random = random;
    }
        
    /**
     * Gets the <code>MapGenerator</code> this <code>FreeColServer</code> is
     * using when creating random maps.
     *
     * @return The <code>MapGenerator</code>.
     */
    public MapGenerator getMapGenerator() {
        return mapGenerator;
    }

    /**
     * Sets the <code>MapGenerator</code> this <code>FreeColServer</code> is
     * using when creating random maps.
     *
     * @param mapGenerator The <code>MapGenerator</code>.
     */
    public void setMapGenerator(MapGenerator mapGenerator) {
        this.mapGenerator = mapGenerator;
    }

    /**
     * Sends information about this server to the meta-server. The information
     * is only sent if <code>public == true</code>.
     */
    public void updateMetaServer() throws NoRouteToServerException {
        updateMetaServer(false);
    }

    /**
     * Returns the name of this server.
     *
     * @return The name.
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the name of this server.
     *
     * @param name The name.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Gets the owner of the <code>Game</code>.
     *
     * @return The owner of the game.
     */
    public String getOwner() {
        return owner;
    }

    /**
     * Sets the owner of the game.
     *
     * @param owner The new owner of the game.
     */
    public void setOwner(String owner) {
        this.owner = owner;
    }

    /**
     * Gets the active unit specified in a saved game, if any.
     *
     * @return The active unit.
     */
    public Unit getActiveUnit() {
        return activeUnit;
    }

    /**
     * Gets the active unit specified in a saved game, if any.
     *
     * @param unit The active unit to save.
     */
    public void setActiveUnit(Unit unit) {
        activeUnit = unit;
    }

    /**
     * Sets the public server.
     *
     * @param publicServer The new public server flag.
     */
    public void setPublicServer(boolean publicServer) {
        this.publicServer = publicServer;
    }

    /**
     * Starts the metaserver update thread if <code>publicServer == true</code>.
     *
     * This update is really a "Hi! I am still here!"-message, since an
     * additional update should be sent when a new player is added to/removed
     * from this server etc.
     */
    public void startMetaServerUpdateThread() {
        if (!publicServer) {
            return;
        }
        Timer t = new Timer(true);
        t.scheduleAtFixedRate(new TimerTask() {
                public void run() {
                    try {
                        updateMetaServer();
                    } catch (NoRouteToServerException e) {}
                }
            }, META_SERVER_UPDATE_INTERVAL, META_SERVER_UPDATE_INTERVAL);
    }

    /**
     * Sends information about this server to the meta-server. The information
     * is only sent if <code>public == true</code>.
     *
     * @param firstTime Should be set to <i>true></i> when calling this method
     *      for the first time.
     * @throws NoRouteToServerException if the meta-server cannot connect to
     *      this server.
     */
    public void updateMetaServer(boolean firstTime) throws NoRouteToServerException {
        if (!publicServer) {
            return;
        }
        Connection mc;
        try {
            mc = new Connection(FreeCol.META_SERVER_ADDRESS, FreeCol.META_SERVER_PORT, null, FreeCol.SERVER_THREAD);
        } catch (IOException e) {
            logger.warning("Could not connect to meta-server.");
            return;
        }
        try {
            Element element;
            if (firstTime) {
                element = DOMMessage.createNewRootElement("register");
            } else {
                element = DOMMessage.createNewRootElement("update");
            }
            // TODO: Add possibility of choosing a name:
            if (name != null) {
                element.setAttribute("name", name);
            } else {
                element.setAttribute("name", mc.getSocket().getLocalAddress().getHostAddress() + ":"
                                     + Integer.toString(port));
            }
            element.setAttribute("port", Integer.toString(port));
            element.setAttribute("slotsAvailable", Integer.toString(getSlotsAvailable()));
            element.setAttribute("currentlyPlaying", Integer.toString(getNumberOfLivingHumanPlayers()));
            element.setAttribute("isGameStarted", Boolean.toString(gameState != GameState.STARTING_GAME));
            element.setAttribute("version", FreeCol.getVersion());
            element.setAttribute("gameState", Integer.toString(getGameState().ordinal()));
            Element reply = mc.askDumping(element);
            if (reply != null && reply.getTagName().equals("noRouteToServer")) {
                throw new NoRouteToServerException();
            }
        } catch (IOException e) {
            logger.warning("Network error while communicating with the meta-server.");
            return;
        } finally {
            try {
                // mc.reallyClose();
                mc.close();
            } catch (IOException e) {
                logger.warning("Could not close connection to meta-server.");
                return;
            }
        }
    }

    /**
     * Removes this server from the metaserver's list. The information is only
     * sent if <code>public == true</code>.
     */
    public void removeFromMetaServer() {
        if (!publicServer) {
            return;
        }
        Connection mc;
        try {
            mc = new Connection(FreeCol.META_SERVER_ADDRESS, FreeCol.META_SERVER_PORT, null, FreeCol.SERVER_THREAD);
        } catch (IOException e) {
            logger.warning("Could not connect to meta-server.");
            return;
        }
        try {
            Element element = DOMMessage.createNewRootElement("remove");
            element.setAttribute("port", Integer.toString(port));
            mc.send(element);
        } catch (IOException e) {
            logger.warning("Network error while communicating with the meta-server.");
            return;
        } finally {
            try {
                // mc.reallyClose();
                mc.close();
            } catch (IOException e) {
                logger.warning("Could not close connection to meta-server.");
                return;
            }
        }
    }

    /**
     * Gets the number of player that may connect.
     *
     * @return The number of available slots for human players. This number also
     *         includes european players currently controlled by the AI.
     */
    public int getSlotsAvailable() {
        List<Player> players = game.getPlayers();
        int n = 0;
        for (int i = 0; i < players.size(); i++) {
            ServerPlayer p = (ServerPlayer) players.get(i);
            if (!p.isEuropean() || p.isREF()) {
                continue;
            }
            if (!(p.isDead() || p.isConnected() && !p.isAI())) {
                n++;
            }
        }
        return n;
    }

    /**
     * Gets the number of human players in this game that is still playing.
     *
     * @return The number.
     */
    public int getNumberOfLivingHumanPlayers() {
        List<Player> players = game.getPlayers();
        int n = 0;
        for (int i = 0; i < players.size(); i++) {
            if (!((ServerPlayer) players.get(i)).isAI() && !((ServerPlayer) players.get(i)).isDead()
                && ((ServerPlayer) players.get(i)).isConnected()) {
                n++;
            }
        }
        return n;
    }

    /**
     * Saves a game.
     *
     * @param file The file where the data will be written.
     * @param username The username of the player saving the game.
     * @throws IOException If a problem was encountered while trying to open,
     *             write or close the file.
     */
    public void saveGame(File file, String username, OptionGroup options) throws IOException {
        saveGame(file, username, options, null);
    }

    /**
     * Saves a game.
     *
     * @param file The file where the data will be written.
     * @param username The username of the player saving the game.
     * @param image an <code>Image</code> value
     * @exception IOException If a problem was encountered while trying to open,
     *             write or close the file.
     */
    public void saveGame(File file, String username, OptionGroup options, BufferedImage image)
        throws IOException {
        final ServerGame game = getGame();
        XMLOutputFactory xof = XMLOutputFactory.newInstance();
        JarOutputStream fos = null;
        try {
            XMLStreamWriter xsw;
            fos = new JarOutputStream(new FileOutputStream(file));

            if (image != null) {
                fos.putNextEntry(new JarEntry(FreeColSavegameFile.THUMBNAIL_FILE));
                ImageIO.write(image, "png", fos);
                fos.closeEntry();
            }

            if (options != null) {
                fos.putNextEntry(new JarEntry(FreeColSavegameFile.CLIENT_OPTIONS));
                options.save(fos);
                fos.closeEntry();
            }

            Properties properties = new Properties();
            properties.put("map.width", Integer.toString(game.getMap().getWidth()));
            properties.put("map.height", Integer.toString(game.getMap().getHeight()));
            fos.putNextEntry(new JarEntry(FreeColSavegameFile.SAVEGAME_PROPERTIES));
            properties.store(fos, null);
            fos.closeEntry();

            // save the actual game data
            fos.putNextEntry(new JarEntry(FreeColSavegameFile.SAVEGAME_FILE));
            xsw = xof.createXMLStreamWriter(fos, "UTF-8");

            xsw.writeStartDocument("UTF-8", "1.0");
            xsw.writeComment("Game version: "+FreeCol.getRevision());
            xsw.writeStartElement("savedGame");

            // Add the attributes:
            xsw.writeAttribute("owner", username);
            xsw.writeAttribute("publicServer", Boolean.toString(publicServer));
            xsw.writeAttribute("singleplayer", Boolean.toString(singleplayer));
            xsw.writeAttribute("version", Integer.toString(SAVEGAME_VERSION));
            xsw.writeAttribute("randomState", getRandomState(random));
            if (getActiveUnit() != null) {
                xsw.writeAttribute("activeUnit", getActiveUnit().getId());
            }
            // Add server side model information:
            xsw.writeStartElement("serverObjects");
            for (ServerModelObject smo : game.getServerModelObjects()) {
                xsw.writeStartElement(smo.getServerXMLElementTagName());
                xsw.writeAttribute(FreeColObject.ID_ATTRIBUTE,
                    ((FreeColGameObject) smo).getId());
                xsw.writeEndElement();
            }
            xsw.writeEndElement();
            // Add the game:
            game.toXML(xsw, null, true, true);
            // Add the AIObjects:
            if (aiMain != null) {
                aiMain.toXML(xsw);
            }
            xsw.writeEndElement();
            xsw.writeEndDocument();
            xsw.flush();
            xsw.close();
        } catch (XMLStreamException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            logger.warning(sw.toString());
            throw new IOException("XMLStreamException.");
        } catch (Exception e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            logger.warning(sw.toString());
            throw new IOException(e.toString());
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                // do nothing
            }
        }
    }

    /**
     * Creates a <code>XMLStream</code> for reading the given file.
     * Compression is automatically detected.
     *
     * @param fis The file to be read.
     * @return The <code>XMLStreamr</code>.
     * @exception IOException if thrown while loading the game or if a
     *                <code>XMLStreamException</code> have been thrown by the
     *                parser.
     */
    public static XMLStream createXMLStreamReader(FreeColSavegameFile fis) throws IOException {
        return new XMLStream(fis.getSavegameInputStream());
    }

    /**
     * Loads a game.
     *
     * @param fis The file where the game data is located.
     * @return The username of the player saving the game.
     * @throws IOException If a problem was encountered while trying to open,
     *             read or close the file.
     * @exception IOException if thrown while loading the game or if a
     *                <code>XMLStreamException</code> have been thrown by the
     *                parser.
     * @exception FreeColException if the savegame contains incompatible data.
     */
    public Game loadGame(final FreeColSavegameFile fis)
        throws IOException, FreeColException {
        return loadGame(fis, null);
    }

    /**
     * Reads just the game part from a save game.
     * This routine exists apart from loadGame so that the map generator
     * can load the predefined maps.
     *
     * @param fis The stream to read from.
     * @param specification The <code>Specification</code> to use in the game.
     * @param server Use this (optional) server to load into.
     * @return The game found in the stream.
     * @exception FreeColException if the format is incompatible.
     * @exception IOException if there is a problem reading the stream.
     */
    public static ServerGame readGame(final FreeColSavegameFile fis,
                                      Specification specification,
                                      FreeColServer server)
        throws IOException, FreeColException {
        final int savegameVersion = getSavegameVersion(fis);
        ArrayList<String> serverStrings = null;
        XMLStream xs = null;
        try {
            ServerGame game = null;
            String active = null;
            xs = createXMLStreamReader(fis);
            final XMLStreamReader xsr = xs.getXMLStreamReader();
            xsr.nextTag();

            logger.info("Found savegame version " + savegameVersion);

            if (server != null) {
                server.setSingleplayer(FreeColObject.getAttribute(xsr,
                        "singleplayer", true));
                server.setPublicServer(FreeColObject.getAttribute(xsr,
                        "publicServer", false));
                String randomState = xsr.getAttributeValue(null, "randomState");
                if (randomState != null && randomState.length() > 0) {
                    try {
                        server.setServerRandom(restoreRandomState(randomState));
                    } catch (IOException e) {
                        logger.log(Level.WARNING, "Failed to restore random state!", e);
                    }
                }
                server.setOwner(xsr.getAttributeValue(null, "owner"));
                active = xsr.getAttributeValue(null, "activeUnit");
            }

            while (xsr.nextTag() != XMLStreamConstants.END_ELEMENT) {
                if (xsr.getLocalName().equals("serverObjects")) {
                    serverStrings = new ArrayList<String>();
                    while (xsr.nextTag() != XMLStreamConstants.END_ELEMENT) {
                        serverStrings.add(xsr.getLocalName());
                        serverStrings.add(xsr.getAttributeValue(null,
                                FreeColObject.ID_ATTRIBUTE));
                        xsr.nextTag();
                    }
                    // @compat 0.9.x
                    if (savegameVersion < 11) {
                        v11FixServerObjects(serverStrings, fis);
                    }
                    // end compatibility code
                } else if (xsr.getLocalName().equals(Game.getXMLElementTagName())) {
                    // @compat 0.9.x
                    if (savegameVersion < 9 && specification == null) {
                        specification = new FreeColTcFile(defaultSpec)
                            .getSpecification();
                        logger.info("Reading old format game"
                            + " (version: " + savegameVersion
                            + "), using " + defaultSpec + " specification.");
                    }
                    // end compatibility code
                    // Read the game
                    game = new ServerGame(null, xsr, serverStrings,
                        specification);
                    game.setCurrentPlayer(null);
                    if (server != null) server.setGame(game);
                    // @compat 0.9.x
                    if (savegameVersion < 9
                        && game.getDifficultyLevel() == null) {
                        game.getSpecification().applyDifficultyLevel("model.difficulty.medium");
                        logger.info("Applying default difficulty of medium.");
                    }
                    // end compatibility code
                } else if (xsr.getLocalName().equals(AIMain.getXMLElementTagName())) {
                    if (server == null) break;
                    server.setAIMain(new AIMain(server, xsr));
                } else {
                    throw new XMLStreamException("Unknown tag: "
                        + xsr.getLocalName());
                }
            }

            if (server != null && active != null && game != null) {
                // Now units are all present, set active unit.
                FreeColGameObject fcgo = game.getFreeColGameObjectSafely(active);
                if (fcgo instanceof Unit) {
                    server.setActiveUnit((Unit) fcgo);
                }
            }
            return game;
        } catch (Exception e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            logger.warning(sw.toString());
            throw new IOException(e.toString());
        } finally {
            if (xs != null) xs.close();
        }
    }

    /**
     * Loads a game.
     *
     * @param fis The file where the game data is located.
     * @param specification a <code>Specification</code> value
     * @return The new game.
     * @exception FreeColException if the savegame contains incompatible data.
     * @exception IOException if there is problem reading a stream.
     */
    public Game loadGame(final FreeColSavegameFile fis,
                         Specification specification)
        throws FreeColException, IOException {

        ServerGame game = readGame(fis, specification, this);
        gameState = GameState.IN_GAME;
        integrity = game.checkIntegrity();

        int savegameVersion = getSavegameVersion(fis);
        // @compat 0.10.x
        if (savegameVersion < 12) {
            for (Player p : game.getPlayers()) {
                if(!p.isIndian() && p.getEurope() != null) {
                    p.initializeHighSeas();
                    
                    for (Unit u : p.getEurope().getUnitList()) {
                        // move units to high seas use setLocation()
                        // so that units are removed from Europe, and
                        // appear in correct panes in the EuropePanel
                        // do not set the UnitState, as this clears
                        // workLeft
                        if (u.getState() == Unit.UnitState.TO_EUROPE) {
                            logger.info("Found unit on way to europe: "
                                + u.toString());
                            u.setLocation(p.getHighSeas());
                            u.setDestination(p.getEurope());
                        } else if (u.getState() == Unit.UnitState.TO_AMERICA) {
                            logger.info("Found unit on way to new world: "
                                + u.toString());
                            u.setLocation(p.getHighSeas());
                            u.setDestination(getGame().getMap());
                        }
                    }
                }
            }

            for (Tile tile : game.getMap().getAllTiles()) {
                TerrainGenerator.encodeStyle(tile);
            }
        }
        // end compatibility code
        
        // @compat 0.9.x, 0.10.x
        fixGameOptions();
        // end compatibility code

        // @compat 0.9.x
        if (savegameVersion < 11) {
            for (Tile t : game.getMap().getAllTiles()) t.fixup09x();
        }
        // end compatibility code

        // AI initialization.
        AIMain aiMain = getAIMain();
        if (!aiMain.checkIntegrity()) {
            aiMain = new AIMain(this);
            logger.info("Replacing AIMain.");
        }
        game.setFreeColGameObjectListener(aiMain);

        Collections.sort(game.getPlayers(), Player.playerComparator);
        for (Player player : game.getPlayers()) {
            if (player.isAI()) {
                ServerPlayer serverPlayer = (ServerPlayer) player;
                DummyConnection theConnection
                    = new DummyConnection("Server-Server-" + player.getName(),
                        getInGameInputHandler());
                DummyConnection aiConnection
                    = new DummyConnection("Server-AI-" + player.getName(),
                        new AIInGameInputHandler(this, serverPlayer, aiMain));
                aiConnection.setOutgoingMessageHandler(theConnection);
                theConnection.setOutgoingMessageHandler(aiConnection);
                getServer().addDummyConnection(theConnection);
                serverPlayer.setConnection(theConnection);
                serverPlayer.setConnected(true);
            }
        }

        return game;
    }

    /**
     * Gets the save game version from a saved game.
     *
     * @param fis The saved game.
     * @return The saved game version.
     * @exception FreeColException if the version is incompatible.
     */
    private static int getSavegameVersion(final FreeColSavegameFile fis)
        throws FreeColException {
        XMLStream xs = null;
        try {
            xs = createXMLStreamReader(fis);
            final XMLStreamReader xsr = xs.getXMLStreamReader();
            xsr.nextTag();

            final String version = xsr.getAttributeValue(null, "version");
            int savegameVersion = 0;
            try {
                savegameVersion = Integer.parseInt(version);
            } catch (Exception e) {
                throw new FreeColException("incompatibleVersions");
            }
            if (savegameVersion < MINIMUM_SAVEGAME_VERSION) {
                throw new FreeColException("incompatibleVersions");
            }
            return savegameVersion;
        } catch (Exception e) {
            throw new FreeColException(e.toString());
        } finally {
            if (xs != null) xs.close();
        }
    }

    // @compat 0.9.x, 0.10.x
    private void fixGameOptions() {
        // Add a default value for options new to each version
        // that are not part of the difficulty settings.
        // Annotate with save format version where introduced
        Specification spec = game.getSpecification();

        // @compat 0.9.x
        // Introduced: SAVEGAME_VERSION == 11
        addIntegerOption("model.option.monarchSupport", "", 2);
        // Introduced: SAVEGAME_VERSION == 11
        addStringOption("model.option.buildOnNativeLand", "",
            "model.option.buildOnNativeLand.never");
        // Introduced: SAVEGAME_VERSION == 11
        if (!spec.hasOption("model.option.amphibiousMoves")) {
            addBooleanOption("model.option.amphibiousMoves",
                "gameOptions.map", false);
            spec.addModifier(new Modifier("model.modifier.amphibiousAttack",
                    Specification.AMPHIBIOUS_ATTACK_PENALTY_SOURCE,
                    -75.0f,
                    Modifier.Type.PERCENTAGE));
        }
        // Introduced: SAVEGAME_VERSION == 11
        addBooleanOption("model.option.settlementActionsContactChief",
            "gameOptions.map", false);

        // @compat 0.10.x
        // Introduced: SAVEGAME_VERSION == 12
        addBooleanOption("model.option.enhancedMissionaries",
            "gameOptions.map", false);
        // Introduced: SAVEGAME_VERSION == 12
        addBooleanOption("model.option.continueFoundingFatherRecruitment",
            "gameOptions.map", false);
        // Introduced: SAVEGAME_VERSION == 12
        addIntegerOption("model.option.settlementLimitModifier",
            "gameOptions.map", 0);
        // Introduced: SAVEGAME_VERSION == 12
        addIntegerOption("model.option.startingPositions",
            "gameOptions.map", 0);
        // Introduced: SAVEGAME_VERSION == 12
        addBooleanOption(GameOptions.TELEPORT_REF,
            "gameOptions.map", false);
        // Introduced: SAVEGAME_VERSION == 12
        addIntegerOption(GameOptions.SHIP_TRADE_PENALTY,
            "gameOptions.map", -30);
        if (spec.getModifiers("model.modifier.shipTradePenalty") == null) {
            spec.addModifier(new Modifier("model.modifier.shipTradePenalty",
                    Specification.SHIP_TRADE_PENALTY_SOURCE,
                    -30.0f, Modifier.Type.PERCENTAGE));
        }
    }

    private void addBooleanOption(String id, String gr, boolean defaultValue) {
        Specification spec = game.getSpecification();
        if (!spec.hasOption(id)) {
            BooleanOption op = new BooleanOption(id);
            op.setGroup(gr);
            op.setValue(defaultValue);
            spec.addAbstractOption(op);
            if ("".equals(gr)) {
                for (OptionGroup level : spec.getDifficultyLevels()) {
                    level.add(op);
                }
            } else {
                spec.getOptionGroup(gr).add(op);
            }
        }
    }

    private void addIntegerOption(String id, String gr, int defaultValue) {
        Specification spec = game.getSpecification();
        if (!spec.hasOption(id)) {
            IntegerOption op = new IntegerOption(id);
            op.setGroup(gr);
            op.setValue(defaultValue);
            spec.addAbstractOption(op);
            if ("".equals(gr)) {
                for (OptionGroup level : spec.getDifficultyLevels()) {
                    level.add(op);
                }
            } else {
                spec.getOptionGroup(gr).add(op);
            }
        }
    }

    private void addStringOption(String id, String gr, String defaultValue) {
        Specification spec = game.getSpecification();
        if (!spec.hasOption(id)) {
            StringOption op = new StringOption(id);
            op.setGroup(gr);
            op.setValue(defaultValue);
            spec.addAbstractOption(op);
            if ("".equals(gr)) {
                for (OptionGroup level : spec.getDifficultyLevels()) {
                    level.add(op);
                }
            } else {
                spec.getOptionGroup(gr).add(op);
            }
        }
    }
    // end compatibility code


    /**
     * At savegame version 11 (new in 0.10.0) several classes were
     * added to serverObjects.  Evil hack here to scan the game for
     * objects in pre-v11 games that should now be in serverObjects,
     * and put them in.
     *
     * @param serverStrings A list of server object {type, id} pairs to add to.
     * @param fis The savegame to scan.
     * @compat 0.10.x
     */
    private static void v11FixServerObjects(List<String> serverStrings,
                                            final FreeColSavegameFile fis) {
        XMLStream xs = null;
        try {
            xs = createXMLStreamReader(fis);
            final XMLStreamReader xsr = xs.getXMLStreamReader();
            xsr.nextTag();
            final String owner = xsr.getAttributeValue(null, "owner");
            while (xsr.nextTag() != XMLStreamConstants.END_ELEMENT) {
                if (xsr.getLocalName().equals(Game.getXMLElementTagName())) {
                    Game game = new ServerGame(null, xsr,
                            new ArrayList<String>(serverStrings),
                            new FreeColTcFile("freecol").getSpecification());
                    Iterator<FreeColGameObject> objs
                        = game.getFreeColGameObjectIterator();
                    while (objs.hasNext()) {
                        FreeColGameObject fcgo = objs.next();
                        if (serverStrings.contains(fcgo.getId())) {
                            continue;
                        } else if (fcgo instanceof Europe) {
                            serverStrings.add("serverEurope");
                        } else if (fcgo instanceof Colony) {
                            serverStrings.add("serverColony");
                        } else if (fcgo instanceof Building) {
                            serverStrings.add("serverBuilding");
                        } else if (fcgo instanceof ColonyTile) {
                            serverStrings.add("serverColonyTile");
                        } else if (fcgo instanceof IndianSettlement) {
                            serverStrings.add("serverIndianSettlement");
                        } else if (fcgo instanceof Unit) {
                            serverStrings.add("serverUnit");
                        } else {
                            continue;
                        }
                        serverStrings.add(fcgo.getId());
                    }
                    break;
                }
                while (xsr.nextTag() != XMLStreamConstants.END_ELEMENT) {
                    xsr.nextTag();
                }
            }
        } catch (Exception e) {
            ;
        } finally {
            if (xs != null) xs.close();
        }
    }

    /**
     * Removes automatically created save games.
     * Call this function to delete the automatically created save games from
     * a previous game.
     */
    public static void removeAutosaves(final String prefix) {
        for (File autosaveFile : FreeCol.getAutosaveDirectory().listFiles()) {
            if (autosaveFile.getName().startsWith(prefix)) {
                autosaveFile.delete();
            }
        }
    }

    /**
     * Makes the entire map visible for all players. Used only when debugging
     * (will be removed).
     */
    public void revealMapForAllPlayers() {
        Iterator<Player> playerIterator = getGame().getPlayerIterator();
        while (playerIterator.hasNext()) {
            ServerPlayer player = (ServerPlayer) playerIterator.next();
            player.revealMap();
        }
        playerIterator = getGame().getPlayerIterator();
        while (playerIterator.hasNext()) {
            ServerPlayer player = (ServerPlayer) playerIterator.next();
            Element reconnect = DOMMessage.createNewRootElement("reconnect");
            try {
                player.getConnection().send(reconnect);
            } catch (IOException ex) {
                logger.warning("Could not send reconnect message!");
            }
        }
    }

    /**
     * Gets a <code>Player</code> specified by a connection.
     *
     * @param connection The connection to use while searching for a
     *            <code>ServerPlayer</code>.
     * @return The player.
     */
    public ServerPlayer getPlayer(Connection connection) {
        Iterator<Player> playerIterator = getGame().getPlayerIterator();
        while (playerIterator.hasNext()) {
            ServerPlayer player = (ServerPlayer) playerIterator.next();
            if (player.getConnection() == connection) {
                return player;
            }
        }
        return null;
    }

    /**
     * Get a unit by ID, validating the ID as much as possible.  Designed for
     * message unpacking where the ID should not be trusted.
     *
     * @param unitId The ID of the unit to be found.
     * @param serverPlayer The <code>ServerPlayer</code> to whom the unit must belong.
     *
     * @return The unit corresponding to the unitId argument.
     * @throws IllegalStateException on failure to validate the unitId
     *         in any way.
     *         In the worst case this may be indicative of a malign client.
     */
    public Unit getUnitSafely(String unitId, ServerPlayer serverPlayer)
        throws IllegalStateException {
        Game game = serverPlayer.getGame();
        FreeColGameObject obj;
        Unit unit;

        if (unitId == null || unitId.length() == 0) {
            throw new IllegalStateException("ID must not be empty.");
        }
        obj = game.getFreeColGameObjectSafely(unitId);
        if (obj == null) {
            throw new IllegalStateException("Not an object: " + unitId);
        } else if (!(obj instanceof Unit)) {
            throw new IllegalStateException("Unit expected, "
                                            + " got " + obj.getClass()
                                            + ": " + unitId);
        }
        unit = (Unit) obj;
        if (unit.getOwner() != serverPlayer) {
            throw new IllegalStateException("Not the owner of unit: " + unitId);
        }
        return unit;
    }

    /**
     * Get a settlement by ID, validating the ID as much as possible.
     * Designed for message unpacking where the ID should not be trusted.
     *
     * @param settlementId The ID of the <code>Settlement</code> to be found.
     * @param unit A <code>Unit</code> which must be adjacent
     *             to the <code>Settlement</code>.
     *
     * @return The settlement corresponding to the settlementId argument.
     * @throws IllegalStateException on failure to validate the settlementId
     *         in any way.
     *         In the worst case this may be indicative of a malign client.
     */
    public Settlement getAdjacentSettlementSafely(String settlementId, Unit unit)
        throws IllegalStateException {
        Game game = unit.getOwner().getGame();
        Settlement settlement;

        if (settlementId == null || settlementId.length() == 0) {
            throw new IllegalStateException("ID must not be empty.");
        } else if (!(game.getFreeColGameObject(settlementId) instanceof Settlement)) {
            throw new IllegalStateException("Not a settlement ID: " + settlementId);
        }
        settlement = (Settlement) game.getFreeColGameObject(settlementId);
        if (settlement.getTile() == null) {
            throw new IllegalStateException("Settlement is not on the map: "
                                            + settlementId);
        }
        if (unit.getTile() == null) {
            throw new IllegalStateException("Unit is not on the map: "
                                            + unit.getId());
        }
        if (unit.getTile().getDistanceTo(settlement.getTile()) > 1) {
            throw new IllegalStateException("Unit " + unit.getId()
                                            + " is not adjacent to settlement: " + settlementId);
        }
        if (unit.getOwner() == settlement.getOwner()) {
            throw new IllegalStateException("Unit: " + unit.getId()
                                            + " and settlement: " + settlementId
                                            + " are both owned by player: "
                                            + unit.getOwner().getId());
        }
        return settlement;
    }

    /**
     * Get an adjacent Indian settlement by ID, validating as much as possible,
     * including checking whether the nation involved has been contacted.
     * Designed for message unpacking where the ID should not be trusted.
     *
     * @param settlementId The ID of the <code>Settlement</code> to be found.
     * @param unit A <code>Unit</code> which must be adjacent
     *             to the <code>Settlement</code>.
     *
     * @return The settlement corresponding to the settlementId argument.
     * @throws IllegalStateException on failure to validate the settlementId
     *         in any way.
     *         In the worst case this may be indicative of a malign client.
     */
    public IndianSettlement getAdjacentIndianSettlementSafely(String settlementId, Unit unit)
        throws IllegalStateException {
        Settlement settlement = getAdjacentSettlementSafely(settlementId, unit);
        if (!(settlement instanceof IndianSettlement)) {
            throw new IllegalStateException("Not an indianSettlement: " + settlementId);
        }
        if (!unit.getOwner().hasContacted(settlement.getOwner())) {
            throw new IllegalStateException("Player has not established contact with the "
                                            + settlement.getOwner().getNation());
        }
        return (IndianSettlement) settlement;
    }


    /**
     * Adds a new AIPlayer to the Game.
     *
     * @param nation a <code>Nation</code> value
     * @return a <code>ServerPlayer</code> value
     */
    public ServerPlayer addAIPlayer(Nation nation) {
        String name = nation.getRulerNameKey();
        DummyConnection theConnection =
            new DummyConnection("Server connection - " + name, getInGameInputHandler());
        ServerPlayer aiPlayer
            = new ServerPlayer(getGame(), name, false, nation,
                               null, theConnection);
        aiPlayer.setAI(true);
        DummyConnection aiConnection
            = new DummyConnection("AI connection - " + name,
                                  new AIInGameInputHandler(this, aiPlayer, getAIMain()));

        aiConnection.setOutgoingMessageHandler(theConnection);
        theConnection.setOutgoingMessageHandler(aiConnection);

        getServer().addDummyConnection(theConnection);

        getGame().addPlayer(aiPlayer);

        // Send message to all players except to the new player:
        // TODO: null-destination-player is unnecessarily generous visibility
        Element addNewPlayer = DOMMessage.createNewRootElement("addPlayer");
        addNewPlayer.appendChild(aiPlayer.toXMLElement(null, addNewPlayer.getOwnerDocument()));
        getServer().sendToAll(addNewPlayer, theConnection);
        return aiPlayer;
    }

    /**
     * Gets the AI player corresponding to a given player.
     *
     * @param player The <code>Player</code> to look up.
     * @return The corresponding AI player, or null if not found.
     */
    public AIPlayer getAIPlayer(Player player) {
        return getAIMain().getAIPlayer(player);
    }

    
    
    /**
     * Get the <code>HighScores</code> value.
     *
     * @return a <code>List<HighScore></code> value
     */
    public List<HighScore> getHighScores() {
        if (highScores == null) {
            try {
                loadHighScores();
            } catch (Exception e) {
                logger.warning(e.toString());
                highScores = new ArrayList<HighScore>();
            }
        }
        return highScores;
    }

    /**
     * Adds a new high score for player and returns <code>true</code>
     * if possible.
     *
     * @param player a <code>Player</code> value
     * @return a <code>boolean</code> value
     */
    public boolean newHighScore(Player player) {
        getHighScores();
        if (!highScores.isEmpty() && player.getScore() <= highScores.get(highScores.size() - 1).getScore()) {
            return false;
        } else {
            highScores.add(new HighScore(player, new Date()));
            Collections.sort(highScores, highScoreComparator);
            if (highScores.size() == NUMBER_OF_HIGH_SCORES) {
                highScores.remove(NUMBER_OF_HIGH_SCORES - 1);
            }
            return true;
        }
    }

    /**
     * Saves high scores.
     *
     * @throws IOException If a problem was encountered while trying to open,
     *             write or close the file.
     */
    public void saveHighScores() throws IOException {
        if (highScores == null || highScores.isEmpty()) {
            return;
        }
        Collections.sort(highScores, highScoreComparator);
        XMLOutputFactory xof = XMLOutputFactory.newInstance();
        FileOutputStream fos = null;
        try {
            XMLStreamWriter xsw;
            fos = new FileOutputStream(new File(FreeCol.getDataDirectory(), HIGH_SCORE_FILE));

            xsw = xof.createXMLStreamWriter(fos, "UTF-8");
            xsw.writeStartDocument("UTF-8", "1.0");
            xsw.writeStartElement("highScores");
            int count = 0;
            for (HighScore score : highScores) {
                score.toXML(xsw);
                count++;
                if (count == NUMBER_OF_HIGH_SCORES) {
                    break;
                }
            }
            xsw.writeEndElement();
            xsw.writeEndDocument();
            xsw.flush();
            xsw.close();
        } catch (XMLStreamException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            logger.warning(sw.toString());
            throw new IOException("XMLStreamException.");
        } catch (Exception e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            logger.warning(sw.toString());
            throw new IOException(e.toString());
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                // do nothing
            }
        }
    }

    /**
     * Loads high scores.
     *
     * @throws IOException If a problem was encountered while trying to open,
     *             read or close the file.
     * @exception IOException if thrown while loading the game or if a
     *                <code>XMLStreamException</code> have been thrown by the
     *                parser.
     * @exception FreeColException if the savegame contains incompatible data.
     */
    public void loadHighScores() throws IOException, FreeColException {
        highScores = new ArrayList<HighScore>();
        File hsf = new File(FreeCol.getDataDirectory(), HIGH_SCORE_FILE);
        if (!hsf.exists()) return;
        XMLInputFactory xif = XMLInputFactory.newInstance();
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(hsf);
            XMLStreamReader xsr = xif.createXMLStreamReader(fis, "UTF-8");
            xsr.nextTag();
            while (xsr.nextTag() != XMLStreamConstants.END_ELEMENT) {
                if (xsr.getLocalName().equals("highScore")) {
                    highScores.add(new HighScore(xsr));
                }
            }
            xsr.close();
            Collections.sort(highScores, highScoreComparator);
        } catch (XMLStreamException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            logger.warning(sw.toString());
            throw new IOException("XMLStreamException.");
        } catch (Exception e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            logger.warning(sw.toString());
            throw new IOException(e.toString());
        } finally {
            if (fis != null) {
                fis.close();
            }
        }
    }


    public void shutdown() {
        server.shutdown();
    }


    /**
     * Get the internal state of a random number generator as a
     * string.  It would have been more convenient to simply return
     * the current seed, but unfortunately it is private.
     *
     * @param random The <code>Random</code> to use.
     * @return A <code>String</code> encapsulating the object state.
     */
    public static synchronized String getRandomState(Random random) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(random);
            oos.flush();
        } catch (IOException e) {
            throw new IllegalStateException("IO exception in memory!?", e);
        }
        byte[] bytes = bos.toByteArray();
        StringBuffer sb = new StringBuffer(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(HEX_DIGITS.charAt((b >> 4) & 0x0F));
            sb.append(HEX_DIGITS.charAt(b & 0x0F));
        }
        return sb.toString();
    }

    /**
     * Restore a previously saved state.
     *
     * @param state The saved state (@see #getRandomState()).
     * @return The restored <code>Random</code>.
     * @throws IOException if unable to restore state.
     */
    public static synchronized Random restoreRandomState(String state)
        throws IOException {
        byte[] bytes = new byte[state.length() / 2];
        int pos = 0;
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) HEX_DIGITS.indexOf(state.charAt(pos++));
            bytes[i] <<= 4;
            bytes[i] |= (byte) HEX_DIGITS.indexOf(state.charAt(pos++));
        }
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(bis);
        try {
            return (Random) ois.readObject();
        } catch (ClassNotFoundException e) {
            throw new IOException("Failed to restore ServerRandom!");
        }
    }
}
