/* ***************************************************************************+
 * 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.ds.*;
import cnrg.itx.datax.*;
import cnrg.itx.datax.devices.*;
import java.io.*;
import java.net.*;
import java.util.*;

/**
 * A <code>ServerSession</code> is a thread that handles an independent session
 * with a SPOT client.
 * 
 * @version 1.0, 3/16/1999
 * @author Jason Howes
 * @see cnrg.apps.spot.server.Server
 */
public class ServerSession implements Runnable
{	
	/**
	 * SPOT server
	 */
	private Server					mServer;
	
	/**
	 * Session I/O objects
	 */
	private Socket					mSessionSocket;
	private ObjectInputStream		mSessionInputStream;
	private ObjectOutputStream		mSessionOutputStream;
	
	/**
	 * Client IP address
	 */
	protected String				mClientAddress;
	
	/**
	 * Session state variables
	 */
	private boolean					mAlive;
	private int						mSessionNum;
	private Thread					mThread;

	/**
	 * Presentation variables
	 */
	private PresentationInfo		mPresentationInfo;
	private	boolean					mPresentationActive;
	private PAM						mPAM;
	private ServerSPOTControl		mPresentationControl;
	
	/**
	 * ITX objects
	 */
	private boolean					mTelephonyEnabled;
	private boolean					mActiveConnection;
	private Connection				mSessionConnection;
	private Channel					mSessionOutputChannel;
	private RADInputStream			mSessionRADInput;
	private UserID					mUserID;

	/**
	 * Semephores
	 */
	private Object					mSendMonitor = new Object();
	
	/**
	 * Exception messages
	 */
	private static final String SESSION_INITIALIZATION_ERROR	= "Error initializing session";
	private static final String SESSION_TERMINATION_ERROR		= "Error terminating session";	

	/**
	 * Class constructor.
	 *
	 * @param server reference to the creating server
	 * @param sessionSocket socket to be used by this session
	 * @param sessionNum session number
	 */
	public ServerSession(Server server, Socket sessionSocket, int sessionNum, boolean telephonyEnabled)
	{
		// Set session variables
		mServer = server;
		mSessionSocket = sessionSocket;
		mSessionNum = sessionNum;
		mAlive = false;
		mTelephonyEnabled = telephonyEnabled;
		mActiveConnection = false;
		
		// Reset presentation variables
		mPresentationInfo = new PresentationInfo(SPOTDefinitions.DEFAULT_PRESENTATION_NAME);
		mPresentationActive = false;
		
		// Record the client address
		mClientAddress = sessionSocket.getInetAddress().toString();
	}
	
	/**
	 * Starts a session with a SPOT client.
	 * 
	 * @param sessionSocket socket to be used by this sessi
	 * @param sessionNum port of the server
	 * @throws <code>ClientSessionException</code> on error 
	 */
	public void startSession() throws ServerSessionException
	{
		// Start the session.
		try
		{
			mSessionOutputStream = new ObjectOutputStream(mSessionSocket.getOutputStream());
			mSessionInputStream = new ObjectInputStream(mSessionSocket.getInputStream());
		}
		catch (IOException e)
		{
			throw new ServerSessionException(SESSION_INITIALIZATION_ERROR);
		}
		
		// Create and start the session thread
		mThread = new Thread(this);
		mThread.start();
		mAlive = true;
		
		// Add ourself to the server session list
		mServer.addServerSession(this);
	}
	
	/**
	 * Stops a session with a SPOT client.
	 * 
	 * @throws <code>ServerSessionException</code> on error 
	 */
	public void endSession() throws ServerSessionException
	{
		SPOTMessage response;
		
		// Do we need to end the session?
		if (mAlive && mThread.isAlive())
		{
			// Clean up the session
			cleanupSession(false);
		}
	}

	/**
	 * Session thread function.
	 */
	public void run()
	{
		SPOTMessage message;

		// Loop until we die
		while (mAlive) 
		{
			try 
			{
				// Get the next SPOTMessage
				message = receiveMessage();
				
				// Handle the message
				dispatchMessage(message);
			}
			catch (ServerSessionException e) 
			{
				// We're done
				mAlive = false;
			}
		}
		
		// Cleanup the session
		try
		{
			if (mActiveConnection)
			{
				if (mTelephonyEnabled)
				{
					mServer.closeTelephonyConnection(this);
				}
				else
				{
					// TODO: Non ITX support
				}
				mActiveConnection = false;
			}
			cleanupSession(true);
		}
		catch (ServerSessionException e)
		{
		}
		catch (ServerException e)
		{
		}
	}

	/**
	 * SPOT message dispatcher.
	 * 
	 * TODO: Add error checking for out of sequence messages!
	 *
	 * @param message the SPOT message
	 */
	private void dispatchMessage(SPOTMessage message) throws ServerSessionException
	{
		// Dispatch the message to the appropriate message handler
		switch (message.mType) 
		{
			case SPOTMessage.START_SESSION:
				messageStartSession();
				break;
				
			case SPOTMessage.REQUEST_PRESENTATION_TOPICS:
				messageRequestPresentationTopics((PresentationInfo)message.mObject);
				break;
				
			case SPOTMessage.REQUEST_NUM_PRESENTATION_SLIDES:
				messageRequestNumPresentationSlides((PresentationInfo)message.mObject);
				break;
				
			case SPOTMessage.REQUEST_PRESENTATION:
				messageRequestPresentation((PresentationInfo)message.mObject);
				break;
				
			case SPOTMessage.OPEN_PRESENTATION:
				messageOpenPresentation((PresentationInfo)message.mObject);
				break;																	 			
				
			case SPOTMessage.CONNECT:
				messageConnect((UserID)message.mObject);
				break;
				
			case SPOTMessage.START_PRESENTATION:
				messageStartPresentation();
				break;					
				
			case SPOTMessage.REQUEST_PRESENTATION_SLIDE_CHANGE:
				messageRequestPresentationSlideChange((Integer)message.mObject);
				break;
				
			case SPOTMessage.REQUEST_PRESENTATION_TOPIC_SLIDE:
				messageRequestPresentationTopicSlide((String)message.mObject);
				break;
				
			case SPOTMessage.PAUSE_PRESENTATION:
				messagePausePresentation();
				break;
				
			case SPOTMessage.RESUME_PRESENTATION:
				messageResumePresentation();
				break;
				
			case SPOTMessage.STOP_PRESENTATION:
				messageStopPresentation();
				break;
				
			case SPOTMessage.END_SESSION:
				messageEndSession();
				break;
			default:
		}
	}

	/**
	 * Handles the START_SESSION client message.
	 * 
	 * @throws <code>ServerSessionException</code> on error.
	 */
	private void messageStartSession() throws ServerSessionException
	{
	}
	
	/**
	 * Handles the REQUEST_PRESENTATION_TOPICS client message.
	 * 
	 * @param presentationInfo descriptor of the presentation
	 * @throws <code>ServerSessionException</code> on error.	  
	 */
	private void messageRequestPresentationTopics(PresentationInfo presentationInfo) throws ServerSessionException
	{
		// Load the PAM
		if (!mPresentationInfo.equals(presentationInfo))
		{
			try
			{
				parsePAMFile(presentationInfo.getPAMFilename(mServer.getPresentationHome()));
			}
			catch (ServerSessionException e)
			{
				sendMessage(SPOTMessage.NACK, null);
				return;
			}
		}
		
		sendMessage(SPOTMessage.PRESENTATION_TOPICS, mPAM.getTopics());
	}
	
	/**
	 * Handles the REQUEST_NUM_PRESENTATION_SLIDES client message.
	 * 
	 * @param presentationInfo descriptor of the presentation
	 * @throws <code>ServerSessionException</code> on error.
	 */
	private void messageRequestNumPresentationSlides(PresentationInfo presentationInfo) throws ServerSessionException
	{
		// Load the PAM
		if (!mPresentationInfo.equals(presentationInfo))
		{
			try
			{
				parsePAMFile(presentationInfo.getPAMFilename(mServer.getPresentationHome()));
			}
			catch (ServerSessionException e)
			{
				sendMessage(SPOTMessage.NACK, null);
				return;
			}
		}
		
		sendMessage(SPOTMessage.NUM_PRESENTATION_SLIDES, new Integer(mPAM.getNumPresentationSlides()));
	}
	
	/**
	 * Handles the REQUEST_PRESENTATIONclient message.
	 * 
	 * @param presentationInfo descriptor of the presentation
	 * @throws <code>ServerSessionException</code> on error.
	 */
	private void messageRequestPresentation(PresentationInfo presentationInfo) throws ServerSessionException
	{	
		// Load the PAM
		if (!mPresentationInfo.equals(presentationInfo))
		{
			try
			{
				parsePAMFile(presentationInfo.getPAMFilename(mServer.getPresentationHome()));
			}
			catch (ServerSessionException e)
			{
				sendMessage(SPOTMessage.NACK, null);
				return;
			}
		}
		
		// Transfer the file
		sendMessage(SPOTMessage.BEGIN_PRESENTATION_TRANSFER, null);
		sendFile(presentationInfo.getPresentationFilename(mServer.getPresentationHome()));
		sendMessage(SPOTMessage.END_PRESENTATION_TRANSFER, null);
	}
	
	/**
	 * Handles the OPEN_PRESENTATION client message.
	 * 
	 * @throws <code>ServerSessionException</code> on error.
	 */	
	private void messageOpenPresentation(PresentationInfo presentationInfo) throws ServerSessionException
	{
		mSessionOutputChannel = new Channel();
		
		// Load the PAM
		if (!mPresentationInfo.equals(presentationInfo))
		{
			try
			{
				parsePAMFile(presentationInfo.getPAMFilename(mServer.getPresentationHome()));
			}
			catch (ServerSessionException e)
			{
				sendMessage(SPOTMessage.NACK, null);
				return;
			}
		}
		
		// Create the output channel
		try
		{
			// Create the connection audio infrastructure
			mSessionRADInput = new RADInputStream(presentationInfo.getRADFilename(mServer.getPresentationHome()));
			
			StreamSource newRADSource = new StreamSource(mSessionRADInput, mSessionOutputChannel, SPOTDefinitions.AUDIO_BUFFER_TIME);
			NetworkDestination newDestination = new NetworkDestination();
			
			// Attach the sources and destinations to the channels
			mSessionOutputChannel.setSource(newRADSource);
			mSessionOutputChannel.addDestination(newDestination);
			
			// Pause the RADInputStream
			mSessionRADInput.setPaused(true);
			
			// Create the presentation control
			mPresentationControl = new ServerSPOTControl(this, mPAM, mSessionRADInput);			
		}
		catch (Exception e)
		{
			sendMessage(SPOTMessage.NACK, null);
			return;			
		}
		
		// Notify the client that the presentation is now open
		sendMessage(SPOTMessage.ACK, null);		
	}
	
	/**
	 * Handles the CONNECT client message.
	 * 
	 * @throws <code>ServerSessionException</code> on error.
	 */
	private void messageConnect(UserID userID) throws ServerSessionException
	{		
		try
		{
			// Try to start a connection with the user
			if (mTelephonyEnabled)
			{
				mSessionConnection = mServer.openTelephonyConnection(this, 
																	 userID, 
																	 null, 
																	 mSessionOutputChannel);
				
				mSessionConnection.open();
				mActiveConnection = true;
			}
			else
			{
				// TODO: Non ITX support
			}
		}
		catch (Exception e)
		{
			sendMessage(SPOTMessage.NACK, null);
			return;			
		}
		
		// Notify the client that we've connected
		sendMessage(SPOTMessage.ACK, null);
		
		mPresentationActive = true;
	}	
	
	/**
	 * Handles the START_PRESENTATION client message
	 * 
	 * @param presentationInfo descriptor of the presentation
	 * @throws <code>ServerSessionException</code> on error.
	 */
	private void messageStartPresentation() throws ServerSessionException
	{	
		// Create the presentation control
		try
		{
			mSessionRADInput.setPaused(false);
			mPresentationControl.startPresentation();
		}
		catch (Exception e)
		{
			try
			{
				mServer.closeTelephonyConnection(this);
			}
			catch (ServerException exp)
			{
			}
			
			sendMessage(SPOTMessage.NACK, null);
			return;				
		}		
		
		// Notify the user that we are ready to start the presentation
		sendMessage(SPOTMessage.ACK, null);
		mPresentationActive = true;
	}
	
	/**
	 * Handles the REQUEST_PRESENTATION_SLIDE_CHANGE client message
	 * 
	 * @param presentationSlide new presentation slide number
	 */
	private void messageRequestPresentationSlideChange(Integer presentationSlide) throws ServerSessionException
	{
		mPresentationControl.gotoPresentationSlide(presentationSlide.intValue());
	}
	
	/**
	 * Handles the REQUEST_PRESENTATION_TOPIC_SLIDE client message
	 * 
	 * @param presentationTopic new presentation topic
	 */
	private void messageRequestPresentationTopicSlide(String presentationTopic) throws ServerSessionException
	{
		mPresentationControl.gotoPresentationTopic(presentationTopic);
	}
	
	/**
	 * Handles the PAUSE_PRESENTATION client message
	 * 
	 */
	private void messagePausePresentation()
	{
		mPresentationControl.pausePresentation();
	}	
	
	/**
	 * Handles the RESUME_PRESENTATION client message
	 * 
	 */
	private void messageResumePresentation()
	{
		mPresentationControl.resumePresentation();
	}
	
	/***
	 * Handles the END_SESSION client message.
	 * 
	 * @throws <code>ServerSessionException</code> on error.
	 */
	private void messageEndSession() throws ServerSessionException
	{
		if (mPresentationActive)
		{
			messageStopPresentation();
		}
		
		mAlive = false;
	}
	
	/**
	 * Handles the STOP_PRESENTATION client message
	 * 
	 * @throws <code>ServerSessionException</code> on error.
	 */
	protected synchronized void messageStopPresentation() throws ServerSessionException
	{
		try
		{
			mPresentationControl.stopPresentation();
			
			if (mActiveConnection)
			{
				mServer.closeTelephonyConnection(this);
				mActiveConnection = false;
			}
		}
		catch (ServerException e)
		{
			sendMessage(SPOTMessage.NACK, null);
			return;
		}
		
		// Notify the user that the presentation has stopped
		sendMessage(SPOTMessage.ACK, null);
		
		mPresentationActive = false;
	}	

	/**
	 * Parses a PAM file to extract the PAM.
	 * 
	 * @param PAMFilename name of the PAM file
	 * @throws <code>ServerSessionException</code> on error
	 */
	private void parsePAMFile(String PAMFilename) throws ServerSessionException
	{
		File PAMFile;
		FileInputStream	fileInput;
		ObjectInputStream objectInput;

		// Create a file object around the PAM file
		try
		{
			PAMFile = new File(PAMFilename);
		}
		catch (Exception e)
		{
			throw new ServerSessionException(e.getMessage());
		}

		// Get the PAM object from the PAM file
		try
		{
			fileInput = new FileInputStream(PAMFile);
			objectInput = new ObjectInputStream(fileInput);
			mPAM = (PAM)objectInput.readObject();
		}
		catch (Exception e)
		{
			throw new ServerSessionException(e.getMessage());
		}
	}
	
	/**
	 * Sends the contents of a file to the client.
	 * 
	 * @param filename name of the file to send
	 * @exception <code>ServerSessionException</code> on error
	 */
	private void sendFile(String filename) throws ServerSessionException
	{
		SPOTMessage message;
		RandomAccessFile theFile;
		int dataRead;
		int offset;
		byte[] buffer;
		
		// Try to open the file
		try
		{
			theFile = new RandomAccessFile(filename, "rw");
		}
		catch (Exception e)
		{
			throw new ServerSessionException(e.getMessage());
		}

		// Send all the data
		try
		{
			dataRead = 0;
			offset = 0;
			
			// Read until EOF
			while (dataRead != -1)
			{
				// Read the next file chuck
				while (true)
				{
					// "Recreate" the send buffer -> weird bug
					buffer = new byte[SPOTDefinitions.FILE_CHUNK_SIZE];
					
					// Read as much as we can
					dataRead = theFile.read(buffer, offset, SPOTDefinitions.FILE_CHUNK_SIZE - offset);
					
					// Did we reach end of file?
					if (dataRead == -1)
					{
						// Do we need to send a last chunk?
						if (offset > 0)
						{
							// Create the last chunk
							byte[] lastChunk = new byte[offset];
							System.arraycopy(buffer, 0, lastChunk, 0, offset);
							sendMessage(SPOTMessage.FILE_CHUNK, lastChunk);
						}
						break;
					}
					
					// Do we need to send a chunk?
					if ((dataRead + offset) == SPOTDefinitions.FILE_CHUNK_SIZE)
					{
						sendMessage(SPOTMessage.FILE_CHUNK, buffer);
						break;
					}
				
					// Update offset
					offset += dataRead;
				}
				
				// Reset the offset
				offset = 0;
			}
		}
		catch (IOException e)
		{
			throw new ServerSessionException(e.getMessage());
		}		
	}
	
	/**
	 * Sends a <code>SPOTMessage</code> to the SPOT client.
	 * 
	 * @param type type of the SPOTMessage to send
	 * @param object object to attach to the SPOTMessage (MUST be serializable)
	 * @throws <code>ServerSessionException</code> on error
	 */
	private void sendMessage(byte type, Object object) throws ServerSessionException
	{
		SPOTMessage message = new SPOTMessage(type, object);
		try
		{
			mSessionOutputStream.writeObject(message);
		}
		catch(InvalidClassException e)
		{
			throw new ServerSessionException(e.getMessage());
		}
		catch(NotSerializableException e)
		{
			throw new ServerSessionException(e.getMessage());
		}
		catch(IOException e)
		{
			throw new ServerSessionException(e.getMessage());
		}
	}
	
	/**
	 * Receives a <code>SPOTMessage</code> from the SPOT client.
	 * 
	 * @return the received message
	 */
	private SPOTMessage receiveMessage() throws ServerSessionException
	{
		SPOTMessage message;
		try
		{
			message = (SPOTMessage)mSessionInputStream.readObject();
		}
		catch(ClassNotFoundException e)
		{
			throw new ServerSessionException(e.getMessage());
		}
		catch(InvalidClassException e)
		{
			throw new ServerSessionException(e.getMessage());
		}
		catch(StreamCorruptedException e)
		{
			throw new ServerSessionException(e.getMessage());
		}
		catch(OptionalDataException e)
		{
			throw new ServerSessionException(e.getMessage());
		}
		catch(IOException e)
		{
			throw new ServerSessionException(e.getMessage());
		}
		return message;
	}
	
	/**
	 * Cleans up the session.
	 * 
	 * @param threadCall is this function called by the session thread?
	 * @throws <code>ServerSessionException</code> on error
	 */
	protected void cleanupSession(boolean threadCall) throws ServerSessionException
	{	
		// Send the Client a END_SESSION message
		try
		{
			sendMessage(SPOTMessage.END_SESSION, null);
		}
		catch (ServerSessionException e)
		{
		}
		
		// Close the session I/O objects -> should cause the thread to stop		
		try
		{
			mSessionInputStream.close();
			mSessionOutputStream.close();
			mSessionSocket.close();
		}
		catch(IOException e)
		{
			throw new ServerSessionException(SESSION_TERMINATION_ERROR);
		}
		
		// Wait for the thread to die
		try
		{
			if (!threadCall)
			{
				mThread.join();
			}
		}
		catch (InterruptedException e)
		{
		}
		
		// Remove ourself from the server session list
		mServer.removeServerSession(this);
	}
	
	/** 
	 * Sets the current presentation slide and sends info to client.
	 * 
	 * @param newSlide the descriptor of the new slide
	 * @throws <code>ServerSessionException</code> on error
	 */
	protected void setPresentationSlide(PAMDescriptorEntry newSlide) throws ServerSessionException
	{
		sendMessage(SPOTMessage.PRESENTATION_SLIDE_CHANGE, newSlide);
	}	
}

/**
 * A <code>ServerSessionException</code> is an exception thrown by a
 * <code>ServerSession</code>.
 */
class ServerSessionException extends Exception
{
	/**
	 * Class constructor.
	 * 
	 * @param msg description of exception
	 */
	public ServerSessionException(String msg)
	{
		super("<ServerSessionException> :: " + msg);
	}

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