/**
 * <p>
 * Copyright © 2009-2010, Bruce-Robert Pocock
 * </p>
 * <p>
 * This program 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 3 of the License, or (at
 * your option) any later version.
 * </p>
 * <p>
 * This program 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.
 * </p>
 * <p>
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 * </p>
 * 
 * @author brpocock
 */

package org.starhope.vergil.net;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.json.JSONException;
import org.json.JSONObject;
import org.starhope.appius.except.UserDeadException;
import org.starhope.appius.game.AppiusClaudiusCaecus;
import org.starhope.appius.game.AppiusDatagram;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.util.LibMisc;
import org.starhope.util.types.CanProcessCommands;
import org.starhope.vergil.game.Virgil;

/**
 * @author brpocock
 * 
 */
public class AppianClient implements CanProcessCommands {
	/**
	 * Whether the client is busy processing … something … (currently
	 * not used for anything?)
	 */
	private boolean busyState = false;

	/**
	 * Datagrams that need to be returned to the server as soon as
	 * possible
	 */
	private final ConcurrentLinkedQueue <AppiusDatagram> futureDatagrams = new ConcurrentLinkedQueue <AppiusDatagram> ();

	/**
	 * Input stream (from the server)
	 */
	private BufferedReader in;
	/**
	 * Global flag to terminate the main loop
	 */
	private boolean keepRunning = true;
	/**
	 * WRITEME
	 */
	private long lastServerMessageTime = -1;
	/**
	 * Input buffer max limitation
	 */
	private final int maxInputSize = 4095;
	/**
	 * Output stream (to the server)
	 */
	private PrintWriter out;

	/**
	 * Socket connection to the server
	 */
	private Socket socket = null;

	/**
	 * Close the server connection and terminate the game
	 */
	private void close () {
		keepRunning = false;
		dropSocketConnection ();
	}

	/**
	 * This is the inner connection method that joins the socket to the
	 * server, opens the input and output streams, and sends the
	 * Infinity sign to the server to set the mode to Cubist
	 * 
	 * @throws UserDeadException If the connection is dropped
	 * @throws IOException If the connection fails due to I/O errors
	 */
	private void connect () throws UserDeadException, IOException {
		if (null == socket) throw new UserDeadException ();

		out = new PrintWriter (socket.getOutputStream (), true);
		in = new BufferedReader (new InputStreamReader (socket
				.getInputStream ()));
		if (null == out || null == in) throw new UserDeadException ();
		out.print ("∞\0");
	}

	/**
	 * Connect to the server based upon a hostname and port
	 * 
	 * @param serverAddress hostname or IP address of the server
	 * @param serverPort port number on the server
	 * @throws UnknownHostException If the hostname can't be interpreted
	 * @throws IOException If we're not able to connect to the server
	 * @throws UserDeadException If we're disconnected
	 */
	public void connect (final String serverAddress,
			final int serverPort)
	throws UnknownHostException, IOException, UserDeadException {
		socket = new Socket (serverAddress, serverPort);
		connect ();
	}

	/**
	 * Disconnect the socket connection to the server quite thoroughly
	 */
	private void dropSocketConnection () {
		synchronized (this) {
			if (null != out) {
				synchronized (out) {
					out.close ();
					out = null;
				}
			}
			if (null != in) {
				synchronized (in) {
					try {
						in.close ();
					} catch (final IOException e) {
						// yay
					}
					in = null;
				}
			}
			if (null != socket) {
				synchronized (socket) {
					if ( !socket.isClosed ()) {
						try {
							try {
								socket.shutdownInput ();
							} catch (final IOException e) {
								// yay
							}
							try {
								socket.shutdownOutput ();
							} catch (final IOException e) {
								// yay
							}
							try {
								socket.close ();
							} catch (final IOException e) {
								// yay
							}
						} catch (final Exception e) {
							AppiusClaudiusCaecus
							.reportBug (
									"Unexpected exception while disconnecting user",
									e);
						}
					}
					socket = null;
				}
			}
		}
		return;
	}

	/**
	 * Grab an input string from the server
	 * 
	 * @return the input from the client
	 * @throws UserDeadException if the user disconnects
	 */
	private String grabInput () throws UserDeadException {
		final StringBuilder inputBuilder = new StringBuilder ();
		int chr = -1;
		while (keepRunning) {

			if (socket.isClosed ()) return null;

			/*
			 * All “remote” output enqueued
			 */
			if (futureDatagrams.size () > 0) {
				while (futureDatagrams.size () > 0) {
					final AppiusDatagram gram = futureDatagrams
					.remove ();
					sendRawDatagram (gram.getDatagram ());
					if (gram.isDisconnectAfterSending ()) {
						close ();
						return null;
					}
				}
			}

			/*
			 * Any input
			 */
			try {
				if (in.ready ()) {
					try {
						chr = in.read ();
					} catch (final java.net.SocketException e) {
						chr = -1;
					}
					if (chr == '\0') {
						break;
					}
					if (chr == -1) {
						close ();
						break;
					}
					inputBuilder.append ((char) chr);
					if (inputBuilder.length () > maxInputSize) {
						break;
					}
				} else { // no input pending
					try {
						Thread.yield ();
						Thread.sleep (AppiusConfig.getIntOrDefault (
								"org.starhope.appius.ioDelay", 100));
					} catch (final InterruptedException e) {
						// no problem, keep going
					}
				}
			} catch (final IOException e) {
				AppiusClaudiusCaecus.reportBug (e);
				close ();
				return null;
			}

		}
		final String inputLine = inputBuilder.toString ();

		return inputLine;
	}

	/**
	 * TODO: document this method (brpocock, Jan 15, 2010)
	 * 
	 * @throws UserDeadException WRITEME
	 */
	public void inAndOut () throws UserDeadException {
		sendPendingOutput ();
		processInput ();
	}

	/**
	 * @param input The error string returned by the server
	 */
	private void processError (final String input) {
		if (input.startsWith ("?ERR\t")) {
			// TODO: parse the error code
			System.err.println ("Unhandled error code: " + input);
		} else {
			// TODO: report to user?
			System.err.println ("Random error code: " + input);
		}
	}

	/**
	 * Read input from the server and process it
	 * 
	 * @throws UserDeadException WRITEME
	 */
	private void processInput () throws UserDeadException {
		final String input = grabInput ();
		if (input.charAt (0) == '{') {
			processJSON (input);
		} else if (input.charAt (0) == '<') {
			// ignore random XML
			if (Virgil.isDebug ()) {
				System.err.println ("Ignoring: " + input);
			}
		} else if (input.charAt (0) == '?') {
			processError (input);
		} else {
			System.err.println ("Unexpected input: " + input);
		}
	}

	/**
	 * @param input WRITEME
	 */
	private void processJSON (final String input) {
		try {
			final JSONObject jso = new JSONObject (input);
			if (jso.has ("from")) {
				LibMisc.commandJSON (jso.getString ("from"), jso, this,
						null, null);
			}
		} catch (final JSONException e) {
			sendRawDatagram ("?HUH\t" + e.toString ()); // FIXME
		}
	}

	/**
	 * @see org.starhope.util.types.CanProcessCommands#sendError_RAW(java.lang.String,
	 *      java.lang.String)
	 */
	public void sendError_RAW (final String cmd, final String string) {
		// TODO
	}

	/**
	 * Send any pending output to the server
	 */
	private void sendPendingOutput () {
		for (final AppiusDatagram gram : futureDatagrams) {
			sendRawDatagram (gram.getDatagram ());
			if (gram.isDisconnectAfterSending ()) {
				close ();
				return;
			}
		}

	}

	/**
	 * @param datagram the raw datagram to be sent
	 */
	private void sendRawDatagram (final String datagram) {
		if ( !datagram.isEmpty ()) {
			synchronized (out) {
				out.print (datagram);
				out.print ('\0');
				out.flush ();
				if (out.checkError ()) {
					Virgil.reportBug ("Error writing datagram");
				}
			}
		}
	}

	/**
	 * @see org.starhope.util.types.CanProcessCommands#sendResponse(org.json.JSONObject)
	 */
	public void sendResponse (final JSONObject ret)
	throws UserDeadException {
		futureDatagrams.add (new AppiusDatagram (ret.toString ()));
	}

	/**
	 * @see org.starhope.util.types.CanProcessCommands#setBusyState(boolean)
	 */
	public void setBusyState (final boolean b) {
		busyState = b;
	}

	/**
	 * @see org.starhope.util.types.CanProcessCommands#setLastInputTime(long)
	 */
	public void setLastInputTime (final long thatTime) {
		lastServerMessageTime = thatTime;
	}
}
