/**
 *  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.networking;


import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.logging.Logger;

import net.sf.freecol.FreeCol;
import net.sf.freecol.common.networking.Connection;
import net.sf.freecol.common.networking.MessageHandler;
import net.sf.freecol.server.FreeColServer;

import org.w3c.dom.Element;



/**
 * The networking server in which new clients can connect and methods
 * like <code>sendToAll</code> are kept.
 *
 * <br><br>
 *
 * When a new client connects to the server a new {@link Connection}
 * is made, with {@link net.sf.freecol.server.control.UserConnectionHandler}
 * as the control object.
 *
 * @see net.sf.freecol.common.networking
 */
public final class Server extends Thread {

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

    /** The public "well-known" socket to which clients may connect. */
    private ServerSocket serverSocket;

    /** A hash of Connection objects, keyed by the Socket they relate to. */
    private HashMap<Socket, Connection> connections = new HashMap<Socket, Connection>();

    /**
     * Whether to keep running the main loop that is awaiting new
     * client connections.
     */
    private boolean running = true;

    /** The owner of this <code>Server</code>. */
    private FreeColServer freeColServer;

    /** The TCP port that is beeing used for the public socket. */
    private int port;

    /** For information about this variable see the run method. */
    private final Object shutdownLock = new Object();

    /**
     * Creates a new network server. Use {@link #run server.start()} to start
     * listening for new connections.
     *
     * @param freeColServer The owner of this <code>Server</code>.
     * @param port The TCP port to use for the public socket.
     * @throws IOException if the public socket cannot be created.
     */
    public Server(FreeColServer freeColServer, int port) throws IOException {
        super(FreeCol.SERVER_THREAD+"Server");
        this.freeColServer = freeColServer;
        this.port = port;
        //serverSocket = new ServerSocket(port, freeColServer.getMaximumPlayers());
        serverSocket = new ServerSocket(port);
    }

    /**
     * Starts the thread's processing.  Contains the loop that is
     * waiting for new connections to the public socket.  When a new
     * client connects to the server a new {@link Connection} is made,
     * with {@link net.sf.freecol.server.control.UserConnectionHandler}
     * as the control object.
     */
    public void run() {
        // This method's entire body is synchronized to shutdownLock.
        // The reason why this is done is to prevent the shutdown
        // method from finishing before this thread is finished
        // working.  We have to do this because the
        // ServerSocket::close method keeps the server alive for
        // several milliseconds EVEN AFTER THE CLOSE METHOD IS
        // FINISHED. And because of this a new server can't be created
        // on the same port as this server right after closing this
        // server.
        //
        // Now that the shutdown method 'hangs' until the entire
        // server thread is finished you can be certain that the
        // ServerSocket is REALLY closed after execution of shutdown.
        synchronized (shutdownLock) {
            while (running) {
                Socket clientSocket = null;
                try {
                    clientSocket = serverSocket.accept();

                    logger.info("Got client connection from "
                                + clientSocket.getInetAddress().toString());
                    //Connection connection =
                        new Connection(clientSocket, freeColServer.getUserConnectionHandler(),
                                       FreeCol.SERVER_THREAD);
                    //connections.put(clientSocket, connection);
                } catch (IOException e) {
                    if (running) {
                        StringWriter sw = new StringWriter();
                        e.printStackTrace(new PrintWriter(sw));
                        logger.warning(sw.toString());
                    }
                }
            }
        }
    }

    /**
     * Sends a network message to all connections except
     * <code>exceptConnection</code> (if the argument is non-null).
     *
     * @param element The root element of the message to send.
     * @param exceptConnection If non-null, the
     *        <code>Connection</code> not to send to.
     */
    public void sendToAll(Element element, Connection exceptConnection) {
        Iterator<Connection> connectionIterator = getConnectionIterator();

        while (connectionIterator.hasNext()) {
            Connection connection = connectionIterator.next();
            if (connection != exceptConnection) {
                try {
                    connection.sendAndWait(element);
                } catch (IOException e) {
                    logger.warning("Exception while attempting to send to "
                                   + connection);
                }
            }
        }
    }

    /**
     * Sends a network message to all connections.
     *
     * @param element The root element of the message to send.
     */
    public void sendToAll(Element element) {
        sendToAll(element, null);
    }

    /**
     * Gets the TCP port that is beeing used for the public socket.
     *
     * @return The TCP port.
     */
    public int getPort() {
        return port;
    }

    /**
     * Sets the specified <code>MessageHandler</code> to all connections.
     *
     * @param messageHandler The <code>MessageHandler</code>.
     */
    public void setMessageHandlerToAllConnections(MessageHandler messageHandler) {
        Iterator<Connection> connectionIterator = getConnectionIterator();

        while (connectionIterator.hasNext()) {
            Connection connection = connectionIterator.next();
            connection.setMessageHandler(messageHandler);
        }
    }

    /**
     * Gets an iterator of every connection to this server.
     *
     * @return The <code>Iterator</code>.
     * @see Connection
     */
    public Iterator<Connection> getConnectionIterator() {
        return connections.values().iterator();
    }

    /**
     * Shuts down the server thread.
     */
    public void shutdown() {
        running = false;

        try {
            serverSocket.close();
            logger.fine("Closed server socket.");
        } catch (IOException e) {
            logger.warning("Could not close the server socket!");
        }

        synchronized (shutdownLock) {
            // Nothing to do here... just waiting for the server
            // thread to finish.  For more info see the run() method
        }

        Iterator<Connection> connectionsIterator = getConnectionIterator();
        while (connectionsIterator.hasNext()) {
            Connection c = connectionsIterator.next();

            try {
                if (c != null) {
                    //c.reallyClose();
                    c.close();
                }
            } catch (IOException e) {
                logger.warning("Could not close the connection.");
            }
        }

        freeColServer.removeFromMetaServer();

        logger.fine("Server shutdown.");
    }

    /**
     * Gets a <code>Connection</code> identified by a <code>Socket</code>.
     *
     * @param socket The <code>Socket</code> that identifies the
     *               <code>Connection</code>
     * @return The <code>Connection</code>.
     */
    public Connection getConnection(Socket socket) {
        return connections.get(socket);
    }

    /**
     * Adds a (usually Dummy)Connection into the hashmap.
     *
     * @param connection The connection to add.
     */
    public void addDummyConnection(Connection connection) {
        connections.put(new Socket(), connection);
    }

    /**
     * Adds a Connection into the hashmap.
     *
     * @param connection The connection to add.
     */
    public void addConnection(Connection connection) {
        connections.put(connection.getSocket(), connection);
    }

    /**
     * Removes the given connection.
     *
     * @param connection The connection that should be removed.
     */
    public void removeConnection(Connection connection) {
        connections.remove(connection.getSocket());
    }
}
