package simpleNetworking;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.security.SignatureException;

import javax.swing.JOptionPane;

/**
 * <pre>
 * A MultiServer first constructs a Server.
 * 
 * Then the MultiServer, in its own Thread, repeatedly accepts connections
 * to Clients and sends them to the Server (who can accept or reject them).
 * 
 * After constructing a MultiServer, you can ask it to give you the Server
 * that the MultiServer constructs via:
 *   -- getServer(N): Returns the Server only after it has N validated Clients
 *   -- getServer():  Returns the Server with whatever Clients it currently has,
 *                    and the MultiServer continues to add Clients to the Server
 *   -- getServerAndQuit():  Returns the Server with whatever Clients
 *                    it currently has, but now the MultiServer stops (so
 *                    no new Clients are added to the Server by the MultiServer).
 * 
 * Or, you can stop the MultiServer at any point and receive the Server
 * that the 
 *
 * NOTE:
 *   1. The MultServer must be started before any Clients attempt to connect.
 *   2. Before running this program, you may need to tell your Firewall
 *      not to block the port that this program uses (4444, chosen arbitrarily).
 *      In Windows, do so by:
 *         Control Panel ~ Windows Firewall ~ Exceptions tab ~ Add Port
 *      and enter the port number (4444) with any name you like. 
 *</pre>
 *
 * @author David Mutchler, using examples.example1_one_client from the Java Tutorials.  May, 2009.
 */
public class MultiServer implements Runnable {
	/**
	 * Port to use for establishing connections to this server.
	 * Any open, unused port is fine, but ports less than 1024 are reserved.
	 */
	static final int PORT = 4444;
	
	private ServerSocket serverSocket;
	private Server server;
	private boolean isDone = false;

	/**
	 * <pre>
	 * Constructs a ServerSocket that this MultiServer uses to accept
	 * multiple connections to Clients.  If that ServerSocket is constructed
	 * successfully:
	 * 
	 * 1. Construct a Server that runs in its own Thread.
	 * 
	 * 2. In a new Thread, repeatedly:
	 *    a. Accept a connection, getting a Socket for communicating with a Client.
	 *    b. Ask the Server to add that Client.
	 *       -- The Server can run a validation protocol on the proposed Client,
	 *          accepting or rejecting the proposed Client as it chooses.
	 * </pre>
	 * 
	 * @throws SocketException if the MultiServer cannot construct a ServerSocket
	 *                         that uses the default port to accept connections from Clients.
	 */
	public MultiServer() throws SocketException {		
		try {
			// Construct a ServerSocket to accept connections to Clients.
		    this.serverSocket = new ServerSocket(MultiServer.PORT);
		    
		    // Construct a Server.
		    this.server = new Server();

			JOptionPane.showMessageDialog(null,
					"The MultiServer has started and will wait for a connection.\n\n"
					+ "Start the Client(s) AFTER pressing OK in this dialog.\n\n");
			
		    // Start a Thread for this MultiServer to accept connections to Clients.
		    new Thread(this).start();
		    
		} catch (Throwable exception) {
		    this.finishUpFailedConstruction(exception,
		    		"Could not construct a ServerSocket using port   " + MultiServer.PORT + ".\n\n");
		}
	}
	
	/**
	 * Repeatedly:
	 * 1. Accept a connection, getting a Socket to a proposed Client.
	 * 2. Ask the Server to add that Client to the list of Clients
	 *    with which it communicates.
	 *     -- The Server can run a validation protocol on the proposed Client,
	 *        accepting or rejecting the proposed Client as it chooses.
	 */
	@Override
	public void run() {
		Socket socketToTalkToClient = null;
		
		while (! this.isDone) {
			
			try {
				socketToTalkToClient = this.serverSocket.accept();
				this.server.addClient(socketToTalkToClient);
				
			} catch (SignatureException exception) {
				// The proposed Client did not pass the validation.
				// A break-in attempt?
				// Or the Clients are not doing the validation protocol correctly?
				// In any case, print a message and continue accepting clients.
				System.out.println("The client " + socketToTalkToClient
						+ " did not pass the validation protocol.");
				
			} catch (SocketException exception) {
				// Presumably the ServerSocket was closed, if not print error
				if (! exception.getMessage().equals("socket closed")) {
					exception.printStackTrace();
				}
				
			} catch (Exception exception) {
				exception.printStackTrace();
				// Something went wrong, but keep trying to accept connections.
			}
		}
		
		try {
			this.serverSocket.close();
			
		} catch (IOException exception) {
			// ServerSocket will usually be already closed,
			// so no error if we get here.  Just continue.
		}
	}

	/**
	 * Blocks (waits) until the Server created by this MultiServer
	 * has validated connections to at least the given number of Clients;
	 * then it returns that Server.
	 *
	 * @param requiredNumberOfClients Number of Clients required before this method returns.
	 * @return the Server created by this MultiServer, after that Server
	 *         has validated connections to at least the given number of Clients.
	 */
	public Server getServer(int requiredNumberOfClients) {
		while (this.server.numberOfValidatedClients() < requiredNumberOfClients) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException exception) {
				// Ignore exception, keep waiting.
			}
		}
		return this.server;
	}
	
	/**
	 * Stop this MultiThread (which is running in its own Thread)
	 * and close all its resources.
	 */
	public void stop() {
		this.isDone = true;
		
		try {
			this.serverSocket.close(); // This interrupts the Thread too
		} catch (IOException exception) {
			exception.printStackTrace();
			// Continue as best you can.
		}
	}
	
	/**
	 * Closes the ServerSocket that this MultiServer opened.
	 */
	private void close() {
		try {
			this.serverSocket.close();
			
		} catch (Exception exception) {
			// Ignore; we made our best effort at closing the resource.
		}
	}
	
	/**
	 * Something went wrong in constructing this MultiServer.
	 * Close the MultiServer, print an error message, and throw a SocketException.
	 * 
	 * @param exception that was thrown by the failed construction of this Client.
	 * @param errorMessage to display in a message dialog.
	 * 
	 * @throws SocketException so that the caller knows that construction failed.
	 */
	void finishUpFailedConstruction(Throwable exception, String errorMessage) throws SocketException {
		this.close();
		if (exception != null) {
			System.out.println(exception);
		}
    	JOptionPane.showMessageDialog(null, errorMessage);
    	throw new SocketException(
    			"Could not construct this MultiServer using port   " + MultiServer.PORT + ".\n\n");
	}
}