package simpleNetworking;

import java.io.IOException;
import java.net.Socket;
import java.security.SignatureException;
import java.util.ArrayList;

import javax.swing.JOptionPane;

/**
 * <pre>
 * A simple Server that you can use in your programs.
 * 
 * It has methods for:
 *   -- reading from each of the Clients attached to this Server
 *   -- writing to each of the Clients attached to this Server
 *   -- closing this Server's resources
 * 
 * It does a minimal-security validation protocol with each Client
 * who seeks to connect (the Client must use the Client's side of that protocol).
 * 
 * You can construct this Server directly, but it is probably easier for you to:
 *   1. Construct a new MultiServer.  It constructs a Server for you.
 *   2. Use the MultiServer's getServer(int n) method to get the Server
 *      after that Server has been connected to n validated Clients.
 * For example, if you want a Server that communicates with 2 Clients, use:
 *     MultiServer multiServer = new MultiServer();
 *     Server server = multiServer.getServer(2);
 *        ... code that uses
 *        ...      server.readLine
 *        ...      server.writeLine
 *        ...      server.close
 *        ... as you see fit.
 * </pre>
 * 
 * @author David Mutchler, based on the Java Tutorials on networking. May, 2009.
 */
public class Server {
	/**
	 * Password shared by the Server and all Clients in this
	 * minimal-security protocol.  Change the password if you wish.
	 */
	static final String PASSWORD = "Lucy in the Sky with Diamonds";
	
	private ArrayList<ReaderWriter> clients;
	
	/**
	 * Prepares to communicate with Clients, with no Clients accepted so far.
	 */
	public Server() {
		this.clients = new ArrayList<ReaderWriter>();
	}
	
	/**
	 * Prepares to communicate with Clients, with the given Client as the sole
	 * Client accepted so far (assuming that the given Client passes the validation protocol).
	 *
	 * @param socketToCommunicateWithClient
	 *        Socket to the sole Client with whom this Server can communicate so far
	 *        (assuming that the Client passes the validation protocol).
	 * @throws IOException if unable to construct a Reader/Writer to the Client.
	 * @throws SignatureException if unable to validate the given Client
	 */
	public Server(Socket socketToCommunicateWithClient) throws SignatureException, IOException {
		this();
		addClient(socketToCommunicateWithClient);
	}
	
	/**
	 * Adds the given Client to the list of clients with which this Server
	 * can communicate, if the Client passes the validation protocol.
	 * 
	 * @param socketToCommunicateWithClient
	 *        Socket to the Client to be added to the list of clients
	 *        with whom this Server can communicate,
	 *        if the Client passes the validation protocol.
	 * @throws IOException if unable to construct a Reader/Writer to the Client.
	 * @throws SignatureException if unable to validate the given Client
	 */
	public synchronized void addClient(Socket socketToCommunicateWithClient) throws SignatureException, IOException {
		try {
		    ReaderWriter readerWriter = new ReaderWriter(socketToCommunicateWithClient);
		    
			if (this.validate(readerWriter)) {
				this.clients.add(readerWriter);
				
			} else {
				throw new SignatureException(
						"Validation of this Client failed!\n\n"
						+ "Is this a port-sniffing attack?\n"
						+ "Or is your Client not using the matching validation protocol?\n");
			}
			
		} catch (IOException exception) {
			throw new IOException(
					"Could not construct a Reader/Writer to Socket " + socketToCommunicateWithClient);
		}
	}
	
	/**
	 * Returns the next line available from this Server's nth Client,
	 * where n is the given parameter, stripping the terminating newline.
	 * Blocks (waits) if no line is available yet.
	 * 
	 * @param client Index in the array of Clients for this Server,
	 *               indicating which Client from which to read.
	 *
	 * @return the next line available from the nth Client,
	 *         but with the terminating newline stripped.
	 * @throws IOException if an IO error occurs while reading.
	 * @throws IndexOutOfBoundsException if the given index of the Client is out of bounds.
	 */
	public String readLine(int client) throws IOException, IndexOutOfBoundsException {
		// Blocks until a line is sent, the buffer is filled, or an IO exception occurs.
		try {
			return this.clients.get(client).readLine();
			
		} catch (IndexOutOfBoundsException exception) {
			throw new IndexOutOfBoundsException(
					"Attempt to access client " + client + ".\n"
					+ "There are only " + this.clients.size() + " clients that have been validated.\n");
		}
	}
	
	/**
	 * Convenience method intended for when there is a single Client.
	 *
	 * @return the next line available from the first (and presumably only) Client,
	 *         but with the terminating newline stripped.
	 * @throws IOException if an IO error occurs while reading.
	 */
	public String readLine() throws IOException {
		return this.readLine(0);
	}
	
	/**
	 * Returns the next line available from this Server's nth Client,
	 * where n is the given parameter, stripping the terminating newline.
	 * Returns immediately (returning null) if no line is available yet.
	 * 
	 * @param client Index in the array of Clients for this Server,
	 *               indicating which Client from which to read.
	 * @return the next line available from the nth Client,
	 *         but with the terminating newline stripped,
	 *         or null if no line is available yet.
	 * @throws IOException if an IO error occurs while reading.
	 * @throws IndexOutOfBoundsException if the given index of the Client is out of bounds.
	 */
	public String readLineIfReady(int client) throws IOException, IndexOutOfBoundsException {
		try {
			return this.clients.get(client).readLineIfReady();
			
		} catch (IndexOutOfBoundsException exception) {
			throw new IndexOutOfBoundsException(
					"Attempt to access client " + client + ".\n"
					+ "There are only " + this.clients.size() + " clients that have been validated.\n");
		}
	}
	
	/**
	 * Convenience method intended for when there is a single Client.
	 *
	 * @return the next line available from the first (and presumably only) Client,
	 *         but with the terminating newline stripped,
	 *         or null if no line is available yet.
	 * @throws IOException if an IO error occurs while reading.
	 */
	public String readLineIfReady() throws IOException {
		return this.readLineIfReady(0);
	}
	
	/**
	 * Sends the given String, appending a newline to it,
	 * to this Server's nth Client, where n is the given parameter.
	 * 
	 * @param stringToWrite String to send (with a newline appended)
	 *                      to this Server's nth Client.
	 * @param client Index in the array of Clients for this Server,
	 *               indicating which Client to which to write.
	 * @throws IOException if an error occurs while writing.
	 * @throws IndexOutOfBoundsException if the given index of the Client is out of bounds.
	 */
	public void writeLine(String stringToWrite, int client) throws IOException, IndexOutOfBoundsException {
		try {
			this.clients.get(client).writeLine(stringToWrite);
			
		} catch (IndexOutOfBoundsException exception) {
			throw new IndexOutOfBoundsException(
					"Attempt to access client " + client + ".\n"
					+ "There are only " + this.clients.size() + " clients that have been validated.\n");
		}
	}
	
	/**
	 * Convenience method intended for when there is a single Client.
	 *
	 * @param stringToWrite String to send (with a newline appended)
	 *                      to this Server's first (and presumably only) Client.
	 * @throws IOException if an IO error occurs while reading.
	 */
	public void writeLine(String stringToWrite) throws IOException {
		this.writeLine(stringToWrite, 0);
	}
	
	/**
	 * To validate a proposed Client:
	 *   -- The Client should send the (announced) password to this Server.
	 *   -- This Server receives the password and checks whether it is OK.
	 *      -- If it is not OK, this Server returns that the proposed connection is not validated
	 *         and this Server does no further communication with the proposed Client.
	 *   -- This Server echoes back the sent password.
	 * This method is the Server side of the above.
	 * 
	 * Subclasses can override this, simply returning 'true' for no validation
	 * or doing their own validation protocol with their Clients.
	 * 
	 * @param connectionToClient ReaderWriter to Client to use for validation.
	 * @return true if the Client is valid according to this validation protocol.
	 */
	protected boolean validate(ReaderWriter connectionToClient) {
		try {
			String passwordSent = connectionToClient.readLine();
			connectionToClient.writeLine(passwordSent);
			return passwordSent.equals(Server.PASSWORD);

		} catch (IOException exception) {
			exception.printStackTrace();
			return false;
		}
	}

	/**
	 * Closes the resources associated with communicating to the given Client.
	 * @param client Index in the array of Clients for this Server,
	 *               indicating which Client to close resources for communicating.
	 */
	public void close(int client) {
		this.clients.get(client).close();
	}
	
	/**
	 * For every Client currently associated with this Server,
	 * closes the resources associated with communicating with that Client.
	 */
	public void close() {
		JOptionPane.showMessageDialog(null, "Closing the Servers's resources.");
		
		for (ReaderWriter readerWriter : this.clients) {
			readerWriter.close();
		}
	}
	
	/**
	 * Returns the number of validated Clients currently associated with this Server.
	 * Blocks (waits) if a validation is in progress.
	 *
	 * @return the number of validated Clients currently associated with this Server.
	 */
	public synchronized int numberOfValidatedClients() {
		return this.clients.size();
	}
}