/* ***************************************************************************+
 * ITX package (cnrg.itx) for telephony application programming.              *
 * Copyright (c) 1999  Cornell University, Ithaca NY                          *
 *                                                                            *
 * This program is free software; you can redistribute it and/or modify       *
 * it under the terms of the GNU General Public Liense as published by        *
 * the Free Software Foundation, either version 2 of the License, or (at      * 
 * your option) any later version.                                            *
 *                                                                            *
 * The ITX package 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.                                                          * 
 *                                                                            *
 * A copy of the license is distributed with this package.  Look in the docs  *
 * directory, filename GPL.                                                   *
 *                                                                            * 
 * Contact information:                                                       *
 * Donna Bergmark                                                             *
 * 484 Rhodes Hall                                                            *
 * Cornell University                                                         *
 * Ithaca, NY 14853-3801                                                      *
 * bergmark@cs.cornell.edu                                                    *
 ******************************************************************************/
package server;

import shared.*;
import cnrg.itx.datax.*;
import cnrg.itx.ds.*;
import cnrg.itx.signal.*;
import cnrg.itx.signal.SignalEvent.*;
import java.net.*;
import java.io.*;
import java.util.*;

/**
 * The <code>Server</code> class wraps around all SPOT server functionality.
 * All server UI code resides in the <code>ServerUI</code> class.  This
 * separation of UI from functionality allows the UI to be interchanged without
 * major code changes in the base server code.
 * 
 * @version 1.0, 3/16/1999
 * @author Jason Howes
 */
public class Server implements Runnable, SignalingObserver
{
	/**
	 * Server constants
	 */
	final static int MAX_CONNECTIONS = 25;

	/**
	 * Server Sockets and Sessions
	 */
	static private ServerSocket mServerSocket;
	static private Socket mNewConnectionSocket;
	static private Vector mActiveSessions;
	
	/**
	 * SPOT presentation root directory
	 */
	static private String mPresentationHome;
	
	/**
	 * Server state
	 */
	static public boolean mAlive;
	static private int mSessionNumberMaker;

	/**
	 * Command line variables
	 */
	static private int mPort;
	static private boolean mDebug;
	
	/**
	 * ITX Telephony objects and state
	 */
	private boolean mTelephonyEnabled;
	private boolean mTelephonyInitialized;
	private DesktopSignaling mTelephonySignaling;
	private Hashtable mTelephonyMap;
	private Hashtable mServerSessionMap;
	
	/**
	 * Server thread object
	 */
	private Thread mThread;
	
	/**
	 * Exception messages
	 */
	private static final String ARGUMENT_PARSE_ERROR			= new String("Error parsing command line arguments");
	private static final String INTERNAL_SERVER_ERROR			= new String("Internal server error");
	private static final String ITX_INTIALIZATION_ERROR			= new String("Error initializing ITX telephony");
	private static final String UI_INITIALIZATION_ERROR			= new String("Error initializing user interface");
	private static final String SERVER_SOCKET_CREATION_ERROR	= new String("Error creating server socket");
	private static final String ITX_ERROR						= new String("ITX telephony error");
	private static final String ITX_NOT_ENABLED					= new String("ITX telephony not enabled");
	private static final String ITX_REJECTION					= new String("SPOT server does not support ITX telephony invitations");
	
	/**
	 * Class constructor.
	 */
	public Server()
	{
		// Create objects
		mActiveSessions = new Vector();
		
		// Initialize server defaults
		mPresentationHome = new String(".");
		mPort = SPOTDefinitions.SERVER_PORT;
		mAlive = false;
		mDebug = true;
		mTelephonyEnabled = true;
		mTelephonyInitialized = false;
	}
	
	/**
	 * Initializes the server.
	 * 
	 * @throws <code>ServerException</code> on error
	 */
	public void initialize(String args[]) throws ServerException
	{
		// Parse command line arguments
		if (!parseArgs(args)) 
		{
			throw new ServerException("ARGUMENT_PARSE_ERROR");
		}
		
		// Print info
		System.out.println("  -Using port number: " + mPort);
		System.out.println("  -Using presentation home directory: " + mPresentationHome);
		if (mDebug)
		{
			System.out.println("  -Using debug output.");
		}
		if (mTelephonyEnabled)
		{
			System.out.println("  -Using ITX telephony connections.");
		}
		System.out.println("--------------------------------------------------\n");
	}
	
	/**
	 * Starts the server.
	 * 
	 * @throws <code>ServerException</code> on error
	 */
	public void start() throws ServerException
	{
		// Are we already started?
		if (mAlive)
		{
			return;
		}
		
		// Initialize the ITX telephony subsystem
		if (mTelephonyEnabled)
		{
			try
			{
				initializeTelephony();
			}
			catch (ServerException e)
			{
				throw e;
			}
		}		

		// Initialize the server socket
		try 
		{
			mServerSocket = new ServerSocket(mPort);
		}
		catch (IOException e) 
		{
			throw new ServerException(SERVER_SOCKET_CREATION_ERROR);
		}
		
		// Start the server thread
		mAlive = true;
		mThread = new Thread(this);
		mThread.start();
	}
	
	/**
	 * Server thread function.
	 */
	public void run()
	{	
		ServerSession newSession;
		
		// Main server loop
		while (mAlive) 
		{
			try 
			{
				// Accept a new client
				mNewConnectionSocket = mServerSocket.accept();
				
				// Do we have too many clients?
				if (getNumServerSessions() == Server.MAX_CONNECTIONS)
				{
					mNewConnectionSocket.close();
				}
				else
				{
					newSession = new ServerSession(this, mNewConnectionSocket, mSessionNumberMaker++, mTelephonyEnabled);
					newSession.startSession();
				}
			}
			catch(IOException e) 
			{
				printDebugString(e.getMessage());
				break;
			}
			catch (ServerSessionException e)
			{
				printDebugString(e.getMessage());
			}
		}		
	}

	/**
	 * Stops the server.
	 */
	public void shutdown() throws ServerException
	{
		ServerSession currentSession;
		
		// Do we need to shut down the server?
		if (!mAlive)
		{
			return;
		}
		
		// Shutdown the server thread
		try
		{
			mServerSocket.close();
			mThread.join();
		}
		catch (IOException e)
		{
			throw new ServerException(e.getMessage());
		}
		catch (InterruptedException e)
		{
		}
			
		// Stop all active server sessions
		for (Enumeration enum = mActiveSessions.elements(); enum.hasMoreElements() ; )
		{
			try
			{
				((ServerSession)enum.nextElement()).endSession();
			}
			catch (ServerSessionException e)
			{
			}
		}	

		// Shutdown the telephony subsystem
		if (mTelephonyEnabled)
		{
			shutdownTelephony();
		}
		
		mThread = null;
		mAlive = false;
	}
	
	/**
	 * Returns the number of active sessions.
	 * 
	 * @return the number of active sessions
	 */
	public int getNumServerSessions()
	{
		return mActiveSessions.size();
	}
	
	/**
	 * Returns the server state.
	 * 
	 * @return <code>true</code> if the server is running, <code>false</code> otherwise
	 */
	public boolean getServerState()
	{
		return mAlive;
	}

	/**
	 * Returns the presentation home directory.
	 */
	public String getPresentationHome() 
	{
		return mPresentationHome;
	}
	
	/**
	 * This method informs the application that a peer application's invitation
	 * has been received.
	 * 
	 * @param ise is the InviteSignalEvent that contains all the information about
	 * the caller application.
	 * @return  void
	 * 
	 * @see cnrg.itx.signal.SignalEvent.InviteSignalEvent
	 * @see cnrg.itx.signal.client.SignalingObserver	 
	 */
	public void onInvite(InviteSignalEvent ise)
	{
		ise.reject(ITX_REJECTION);
	}

	/**
	 * This method informs the application that a peer application has sent
	 * a confirmation and the call setup is complete.
	 * 
	 * @param  c the SignalConnection the Application should use for data exchange.
	 *         The connection within SignalConnection may be instantiated by the application.
	 * 
	 * @see cnrg.itx.signal.client.SignalConnection
	 * @see cnrg.itx.signal.client.SignalingObserver	 
	 */
	public void onStartCall(SignalConnection c)
	{
		// This should never happen, but just in case, hang up the connection
		try
		{
			mTelephonySignaling.Hangup(c);
		}
		catch (Exception e)
		{
		}
	}
	
	/**
	 * This method informs the application that it should abort the call it was
	 * waiting for.
	 * 
	 * @param ase ithe AbortSignalEvent describing the reason for the abort and
	 * which indicates the user that aborted the invite and returns the connection object
	 * the onInvite call gave signaling, if any.
	 * 
	 * @see cnrg.itx.signal.SignalEvent.AbortSignalEvent
	 * @see cnrg.itx.signal.client.SignalingObserver 
	 */
	public void onAbortCall(AbortSignalEvent ase)
	{
	}
	
	/**
	 * This method informs the application that a peer application has hung up.
	 * 
	 * @param hse the HangupSignalEvent that contains all the information about
	 * the application that has hung up.
	 * 
	 * @see cnrg.itx.signal.SignalEvent.HangupSignalEvent
	 * @see cnrg.itx.signal.client.SignalingObserver
	 */
	public void onHangup(HangupSignalEvent hse)
	{	
		ServerSession caller;
		SignalConnection connection;
		
		// Get the signal connection from the event
		connection = hse.getSignalConnection();
		
		// Get the caller from the telephony map
		caller = (ServerSession)mTelephonyMap.get(connection);
		
		// Clean up the telephony maps
		if (caller != null)
		{
			try
			{
				caller.messageStopPresentation();
			}
			catch (ServerSessionException e)
			{
			}
		}
	}
	
	/**
	 * This method informs the application that a DTMF has been received.
	 * 
	 * @param dse the DTMFSignalEvent that contains the tone(s).
	 * 
	 * @see cnrg.itx.signal.SignalEvent.DTMFSignalEvent
	 * @see cnrg.itx.signal.client.SignalingObserver
	 */
	public void onDTMF(DTMFSignalEvent dtmfse)
	{
		SignalConnection connection = dtmfse.getSignalConnection();
	}
	
	/**
	 * Places an ITX telephony call.
	 * 
	 * @param caller the calling <code>ServerSession</code> thread
	 * @param userID the callee's <code>UserID</code>
	 * @param inputChannel the session input audio channel
	 * @param outputChannel the session output audio channel
	 * @return the <code>Connection</code> to use during the session
	 * @throws <code>ServerException</code> on error
	 */
	protected synchronized Connection openTelephonyConnection(ServerSession caller, 
														 UserID callee,
														 Channel inputChannel,
														 Channel outputChannel) throws ServerException
	{
		SignalConnection newConnection;
		
		// Can calls be placed?
		if (!mTelephonyEnabled)
		{
			throw new ServerException(ITX_NOT_ENABLED);
		}
		
		// Try to dial the user
		try
		{
			newConnection = mTelephonySignaling.Dial(callee.toString(), inputChannel, outputChannel);
		}
		catch (Exception e) 
		{
		   throw new ServerException(e.getMessage());
		}
		
		// Add the new SignalConnection to the telephony maps
		try
		{
			mTelephonyMap.put(newConnection, caller);
			mServerSessionMap.put(caller, newConnection);
		}
		catch (NullPointerException e)
		{
			// Should never happen, so log exception
			printDebugString("Error adding entry in telephony map!");
			
			try
			{
				mTelephonySignaling.Hangup(newConnection);
			}
			catch (Exception exp)
			{
			}
			
			throw new ServerException(INTERNAL_SERVER_ERROR);
		}
	
		return newConnection.getConnection();			
	}
	
	/**
	 * Closes a telephony connection.
	 * 
	 * @param caller the ServerSession that made the call
	 * @throws <code>ServerException</code> on error
	 */
	protected synchronized void closeTelephonyConnection(ServerSession caller) throws ServerException
	{
		SignalConnection signalConnection;
		
		// Can calls be placed?
		if (!mTelephonyEnabled)
		{
			throw new ServerException(ITX_NOT_ENABLED);
		}
		
		// Look up the SignalConnection associated with the caller
		if ((signalConnection = (SignalConnection)mServerSessionMap.get(caller)) == null)
		{
			return;
		}
		
		// Hang up the connection
		try
		{
			mTelephonySignaling.Hangup(signalConnection);
		}
		catch (Exception e)
		{
			throw new ServerException(e.getMessage());
		}
		
		// Clean up the telephony maps
		mServerSessionMap.remove(caller);
		mTelephonyMap.remove(signalConnection);
	}
	
	/**
	 * Adds a <code>ServerSession</code> to the server
	 * 
	 * @param session session to add
	 */
	protected void addServerSession(ServerSession session)
	{
		mActiveSessions.addElement(session);
		
		// Output session info
		System.out.println("\t-Accepted connection from: " + session.mClientAddress);
	}

	/**
	 * Decrements the number of SPOT server sessions.
	 */
	protected void removeServerSession(ServerSession session)
	{
		if (mActiveSessions.removeElement(session))
		{
			// Output session info
			System.out.println("\t-Terminated connection from: " + session.mClientAddress);
		}
	}
	
	/**
	 * Outputs information on how to use the server.
	 */
	protected static void printServerSyntax() 
	{
		System.out.println("Syntax: Server [-port w][-home x][-debug y][-itx z]");
		System.out.println("  -port  w  : Use port w as the Server port.");
		System.out.println("  -home  x  : Use x as the presentation home directory.");
		System.out.println("  -debug y  : Set y to 'on' to enable Server debug output.");
		System.out.println("  -itx   z  : Set z to 'on' to enable ITX telephony connections.");
	}
	
	/**
	 * Prints a debug string.
	 * 
	 * @param s the debug string to print
	 */
	protected void printDebugString(String s)
	{
		if (mDebug)
		{
			System.out.println("<Server> :: " + s);
		}
	}
	
	/**
	 * Prints a debug string, based on a <code>ServerException</code>.
	 * 
	 * @param e the exception
	 */
	protected void printDebugString(ServerException e)
	{
		if (mDebug)
		{
			System.out.println("<Server> :: exception -> " + e.getMessage());
		}
	}	

	/**
	 * Parses all command line arguments for the Server.
	 * 
	 * @param args array of command line arguments
	 * @return <code>true</code> if parse was successful, <code>false</code> otherwise.
	 */
	private boolean parseArgs(String args[])
	{
		int numArgs;
		String sType;
		String sValue;

		// Make sure we were passed the correct number of arguments
		numArgs = args.length;
		if ((numArgs % 2) != 0) 
		{
			printServerSyntax();
			return false;
		}

		// Parse all the arguments
		for (int i = 0; i < numArgs; i += 2)
		{
			sType  = args[i];
			sValue = args[i+1];

			// -port x
			if(sType.equals("-port")) 
			{
				try 
				{
					mPort = (Integer.valueOf(sValue)).intValue();
				}
				catch (NumberFormatException e) 
				{
					printServerSyntax();
					return false;
				}
			} 

			// -root y
			else if (sType.equals("-root"))
			{
				mPresentationHome = sValue;
			}

			// -debug
			else if (sType.equals("-debug"))
			{
				if (sValue.equals("on")) 
				{
					mDebug = true;
				}
			}

			// -itx
			else if (sType.equals("-itx"))
			{
				if (sValue.equals("on"))
				{
					mTelephonyEnabled = true;
				}
			}

			// UNKNOWN
			else
			{
				printServerSyntax();
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Initializes the ITX telephony subsystem.
	 * 
	 * @throws <code>ServerException</code> on error
	 */
	private void initializeTelephony() throws ServerException
	{	
		// Create the telephony/ServerSession mapping hashtables
		mTelephonyMap = new Hashtable();
		mServerSessionMap = new Hashtable();
		
		// Create the telephony signaling object
		try
		{
			mTelephonySignaling = new DesktopSignaling(this, "spotsrv", "spotsrv");
		}
		catch (DirectoryServiceException e)
		{
			throw new ServerException(e.getMessage());
		}
	}
	
	/**
	 * Shuts down the ITX telephony subsystem.
	 */
	private void shutdownTelephony()
	{
		mTelephonySignaling.logout();
	}
	
	/**
	 * Entry point for the SPOT server application:
	 * <p>
	 * client [SPOT filename][PAM filename][?]
	 * 
	 * @param args command line arguments
	 */
	public static void main(String args[])
	{
		Server server = new Server();
		ServerUI serverUI;
		
		// Print some information
		System.out.println("SPOT Server:");
		
		// Initialize the server
		try
		{
			server.initialize(args);
		}
		catch (ServerException e)
		{
			server.printDebugString(e);
			System.out.println("Server initialization failed.");
			System.exit(1);
		}
		System.out.println("Server initialized.");
		
		// Start the server UI
		try
		{
			serverUI = new ServerUI(server);
			serverUI.start();
		}
		catch (IOException e)
		{
			server.printDebugString(e.getMessage());
			System.out.println("User interface startup failed.\n");
			System.exit(1);			
		}
		System.out.println("User interface started.\n");
	}
}

/**
 * A <code>ServerException</code> is an exception thrown by a <code>Server</code>.
 * 
 * @version 1.0, 3/16/1999
 * @author Jason Howes
 * @see cnrg.apps.spot.server.Server
 */
class ServerException extends Exception
{
	/**
	 * Class constructor.
	 * 
	 * @param msg exception information.
	 */
	public ServerException(String msg)
	{
		super("<ServerException> :: " + msg);
	}

	/**
	 * Class constructor.
	 */
	public ServerException()
	{
		super("<ServerException>");
	}
}