package simpleNetworking;

import java.io.IOException;
import java.net.BindException;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;

import javax.swing.JOptionPane;

/**
 * <pre>
 * A simple Client that you can use in your programs.
 * 
 * It has methods for:
 *   -- reading from the Server
 *   -- writing to the Server
 *   -- closing its resources
 * 
 * It does a minimal-security validation protocol with the Server
 * (who must use the Server's side of that protocol).
 * </pre>
 * 
 * @author David Mutchler, based on the Java Tutorials on networking. May, 2009.
 */
public class Client {
	private Socket socket;
	private ReaderWriter readerWriter;

	/**
	 * Connects to the given Server, getting a Socket,
	 * and prepares for reading/writer to the Server based on that Socket.
	 * Also does a minimal-security validation.
	 *
	 * @param serverHostName Name (e.g. "mutchler-3.rose-hulman.edu")
	 *                       or IP address (e.g. "137.112.250.33")
	 *                       of the Server to which you want to connect.
	 * @throws SocketException 
	 */
	public Client(String serverHostName) throws SocketException {
		try {
		    this.socket = new Socket(serverHostName, MultiServer.PORT);
		    this.readerWriter = new ReaderWriter(this.socket);
		    
		    if (! this.validate()) {
		    	this.finishUpFailedConstruction(null, serverHostName,
		    			"Validation of this Client failed!\n"
		    			+ "Is the Server using the matching validation protocol?\n\n");
			}
		    
		} catch (UnknownHostException exception) {
			this.finishUpFailedConstruction(exception, serverHostName,
					"Client could not connect to   " + serverHostName
					+ "    because that is an unknown host.\n\n"
					+ "The Server may still running; fix that by running a Client\n"
					+ "(without starting a Server).\n\n");

		} catch (ConnectException exception) {
			this.finishUpFailedConstruction(exception, serverHostName,
					"Client could not connect to   " + serverHostName
					+ "   either because:\n"
					+ " -- the Server is not yet started, or \n"
					+ " -- the host does not accept connections on port " + MultiServer.PORT + ".\n\n"
					+ "The Server may still running; fix that by running a Client\n"
					+ "(without starting a Server).\n\n");

		} catch (NoRouteToHostException exception) {
			this.finishUpFailedConstruction(exception, serverHostName,
					"Client could not connect to   " + serverHostName
					+ "   perhaps because\n"
					+ "the hostname   " + serverHostName
					+ "   does not allow connections on port   " + MultiServer.PORT + ".\n\n"
					+ "The Server may still running; fix that by running a Client\n"
					+ "(without starting a Server).\n\n");

		} catch (BindException exception) {
			this.finishUpFailedConstruction(exception, serverHostName,
					"Client could not connect to   " + serverHostName
					+ "   perhaps because\n"
					+ "port   " + MultiServer.PORT + "   is in use.\n"
					+ "The Server is probably still running; stop it by getting a Client to run.\n\n"
					+ "The Server may still running; fix that by running a Client\n"
					+ "(without starting a Server).\n\n");

		} catch (SocketException exception) {
			this.finishUpFailedConstruction(exception, serverHostName,
					"Client ended, either because it could not start\n"
					+ "or (more likely) because the user closed the Server.\n\n");

		} catch (IOException exception) {
			this.finishUpFailedConstruction(exception, serverHostName,
					"Client ended, probably because the user closed the Server.\n\n");

		} catch (Throwable exception) {
			exception.printStackTrace();
			this.finishUpFailedConstruction(exception, serverHostName,
					"Client ended abnormally.  See stack trace.\n\n");
		}
	}


	/**
	 * Returns the next line available from the Server, stripping the terminating newline.
	 * Blocks (waits) if no line is available yet.
	 *
	 * @return the next line available from the Server,
	 *         but with the terminating newline stripped.
	 * @throws IOException if an IO error occurs while reading.
	 */
	public String readLine() throws IOException {
		// Blocks until a line is sent, the buffer is filled, or an IO exception occurs.
		return this.readerWriter.readLine();
	}

	/**
	 * Sends the given String, appending a newline to it, to the Server.
	 * 
	 * @param stringToWrite String to send (with a newline appended) to the Server.
	 * @throws IOException if an error occurs while writing.
	 */
	public void writeLine(String stringToWrite) throws IOException {
		this.readerWriter.writeLine(stringToWrite);
	}
	
	/**
	 * Closes the resources associated with this Client.
	 */
	public void close() {
		JOptionPane.showMessageDialog(null, "Closing the Client's resources.");
		
		this.readerWriter.close();
	}
	
	/**
	 * To validate a proposed Client:
	 *   -- This Client sends the (announced) password to the Server.
	 *   -- The Server should receive the password and check whether it is OK.
	 *      -- If it is not OK, the Server should not accept the attempted connection.
	 *   -- The Server should echo back the sent password.
	 * This method is the Client side of the above.
	 * 
	 * Subclasses can override this, simply returning 'true' for no validation
	 * or doing their own validation protocol with their Server.
	 * 
	 * @return true if the validation succeeded
	 */
	protected boolean validate() {
		try {
			this.readerWriter.writeLine(Server.PASSWORD);
			String passwordSentBack = this.readerWriter.readLine();
			return passwordSentBack.equals(Server.PASSWORD);

		} catch (IOException exception) {
			exception.printStackTrace();
			return false;
		}
	}
	
	/**
	 * Something went wrong in constructing this Client.
	 * Close the Client, 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.
	 * @param serverHostName to which this Client tried to connect.
	 * 
	 * @throws SocketException so that the caller knows that construction failed.
	 */
	void finishUpFailedConstruction(Throwable exception, String serverHostName, String errorMessage) throws SocketException {
		this.close();
		if (exception != null) {
			System.out.println(exception);
		}
    	JOptionPane.showMessageDialog(null, errorMessage);
    	throw new SocketException(
    			"Could not connect this Client to   " + serverHostName + "\n"
    			+ "using port   " + MultiServer.PORT + ".\n\n");
	}
}