/* ***************************************************************************+
 * 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;

// Imports
import shared.*;
import java.io.*;

/**
 * A <code>ClientSPOTControl</code> is the server-side SPOT presentation control.
 * It implements the <code>ClientPresentationControl</code> interface to 
 * synchronize client-side PPT slide transitions and the presentation audio stream 
 * with presentation slide changes (as specified by the PAM file).
 * 
 * @author Jason Howes
 * @version 1.0, 3/25/1999
 * @see cnrg.apps.spot.shared.RADInputStream
 * @see cnrg.apps.spot.client.ClientPresentationControl
 * @see cnrg.apps.spot.client.ClientException
 * @see cnrg.apps.spot.client.PowerPointControl
 * @see cnrg.apps.spot.shared.PowerPointException
 */
public class ServerSPOTControl extends Thread implements ServerPresentationControl
{
	/**
	 * Debug flag (if true, the control thread outputs control info).
	 */
	private static final boolean DEBUG = false;	
	
	/**
	 * Client controlling objects
	 */
	private PAM mControlPAM;
	
	/**
	 * RAD input stream
	 */
	private RADInputStream mRADInputStream;

	/**
	 * Current presentation state
	 */
	private int mCurrentPresentationSlideNum;
	private int mPausedPresentationSlideNum;
	private int mNumPresentationSlides;
	private boolean mPresentationPaused;

	/**
	 * The controlling session.
	 */
	private ServerSession mSession;

	/**
	 * Variables for controlling the control thread
	 */
	private boolean mAlive;
	private boolean mStart;
	private boolean mStop;
	private Object mSema;

	/**
	 * Class constructor.
	 * 
	 * @param session the calling control session
	 * @param controlPAM the PAM object that will be used to control the client.
	 * @param inputStream the input stream to the presentation RAD file
	 */	
	public ServerSPOTControl(ServerSession session, PAM controlPAM, RADInputStream inputStream)
	{
		mSession = session;
		mControlPAM = controlPAM;
		mRADInputStream = inputStream;
		mSema = new Object();
		mAlive = false;
		mStart = false;
		mStop = false;
		mCurrentPresentationSlideNum = 0;
		mPresentationPaused = false;
		mNumPresentationSlides = mControlPAM.getNumPresentationSlides();

		// Start the thread
		startup();
	}

	/**
	 * Starts the presentation.
	 * 
	 * @throws <code>ServerSessionException</code> on error
	 */
	public void startPresentation() throws ServerException
	{
		// Are we already started?
		if (mStart)
		{
			return;
		}

		// Start the control thread
		try
		{
			synchronized (mSema)
			{
				mAlive = true;
				mStart = true;
				mSema.notify();
			}
		}
		catch (Exception e)
		{
			stopPresentation();
			throw new ServerException(e.getMessage());
		}
	}
	
	/**
	 * Stops the slide presentation.
	 * 
	 * @throws <code>ServerException</code> on error.
	 */	
	public void stopPresentation() throws ServerException
	{
		// Have we started?
		if (!mStart)
		{
			return;
		}

		// Have we already stopped?
		if (mStop)
		{
			return;
		}

		// Wait for the control thread to stop
		try
		{
			synchronized(mSema)
			{
				mAlive = false;
				mStop = true;
				
				// Reset the input stream 
				mRADInputStream.setPaused(true);
				mRADInputStream.setOffset(0);				
				
				mSema.notify();
			}

			this.join();
		}
		catch (Exception e)
		{
		}
	}
	
	/**
	 * Synchronizes the PPT presentation and audio with a given presentation slide.
	 */
	public void gotoPresentationSlide(int slide) throws ServerSessionException
	{
		PAMDescriptorEntry nextEntry;
		
		if (setSlideNum(slide))
		{
			nextEntry = mControlPAM.descriptorEntryAt(getCurrentSlideNum());
			mSession.setPresentationSlide(nextEntry);
			
			// Set the new audio offset ONLY if the audio is NOT paused
			if (!mPresentationPaused)
			{				
				try
				{
					mRADInputStream.setOffset(nextEntry.mRADOffset);
				}
				catch (IOException e)
				{
				}
			}
		}
	}
	
	/**
	 * Synchronizes the PPT presentation and audio with a specified presentation topic.
	 * 
	 * @param topic the desired topic with which to synchronize.
	 */	
	public void gotoPresentationTopic(String topic) throws ServerSessionException
	{
		int newPresentationSlide;
		PAMDescriptorEntry nextEntry;
		
		// Find the topic entry
		if ((newPresentationSlide = findPresentationSlideNum(topic)) != -1)
		{
			if (setSlideNum(newPresentationSlide))
			{			
				nextEntry = mControlPAM.descriptorEntryAt(getCurrentSlideNum());
				mSession.setPresentationSlide(nextEntry);
					
				// Set the new audio offset ONLY if the audio is NOT paused
				if (!mPresentationPaused)
				{						
					try
					{
						mRADInputStream.setOffset(nextEntry.mRADOffset);
					}
					catch (IOException e)
					{
					}						
				}
			}			
		}
	}

	/**
	 * Pauses the presentation.
	 */
	public void pausePresentation()
	{
		// Have we started yet?
		if (!mStart)
		{
			return;
		}
		
		// Are we already paused?
		if (mPresentationPaused)
		{
			return;
		}
		
		// Pause the RAD input stream
		mRADInputStream.setPaused(true);
		
		// Record the paused presentation slide number
		mPausedPresentationSlideNum = getCurrentSlideNum();
		mPresentationPaused = true;
	}

	/**
	 * Resumes a paused presentation.
	 */
	public void resumePresentation()
	{
		PAMDescriptorEntry nextEntry;
		int currentSlideNum;

		// Have we started yet?
		if (!mStart)
		{
			return;
		}
		
		// Are we already unpaused?
		if (!mPresentationPaused)
		{
			return;
		}

		// Set the RAD input stream offset, if neccessary
		if ((currentSlideNum = getCurrentSlideNum()) != mPausedPresentationSlideNum)
		{
			// Set the new audio offset
			nextEntry = mControlPAM.descriptorEntryAt(currentSlideNum);
			try
			{
				mRADInputStream.setOffset(nextEntry.mRADOffset);
			}
			catch (IOException e)
			{
			}				
		}
		
		// Unpause the RAD input stream
		mRADInputStream.setPaused(false);
		mPresentationPaused = false;
	}
	
	/**
	 * Thread function.
	 */
	public void run()
	{
		int nextSlideNum;
		long nextOffset;
		PAMDescriptorEntry nextEntry = new PAMDescriptorEntry();

		// Wait until we are either started or stopped
		try
		{
			synchronized(mSema)
			{
				if (!mStart)
				{
					mSema.wait();
					if (mStop)
					{
						shutdown();
						return;
					}
				}
			}
		}
		catch (Exception e)
		{
			// Simply shutdown on an exception
			shutdown();
			return;
		}
		
		mAlive = true;

		// Main thread loop
		try
		{
			while (mAlive)
			{
				// Find the "next" offset
				if ((nextSlideNum = getNextSlideNum()) == mNumPresentationSlides)
				{
					nextOffset = Long.MAX_VALUE;
				}
				else
				{
					nextEntry = mControlPAM.descriptorEntryAt(nextSlideNum);
					nextOffset = nextEntry.mRADOffset;
				}
		
				// Wait for the next offset
				if(mRADInputStream.waitForOffset(nextOffset))
				{
					// Success!
					setSlideNum(nextSlideNum);
					mSession.setPresentationSlide(nextEntry);

					if (DEBUG)
					{
						System.out.println("<ServerSPOTControl> :: waitForOffset succeeded at offset = " + nextOffset);
					}
				}

				// Are we done?
				if (mStop)
				{
					break;
				}

				// Find the next entry
				nextEntry = mControlPAM.descriptorEntryAt(getCurrentSlideNum());
			}
		}
		catch (Exception e)
		{
			// Simply break on Exceptions
		}

		// Shutdown the thread
		shutdown();
	}	


	/**
	 * Thread startup function.
	 */
	private void startup()
	{
		this.start();
	}

	/**
	 * Thread shutdown function.
	 */
	private void shutdown()
	{
		if (DEBUG)
		{
			System.out.println("<ServerSPOTControl> :: thread terminated");
		}
	}
	
	/**
	 * Gets the current value of mCurrentPresentationSlideNum (thread safe).
	 * 
	 * @return the current value of mCurrentPresentationSlideNum.
	 */		
	private synchronized int getCurrentSlideNum()
	{
		return mCurrentPresentationSlideNum;
	}
	
	/**
	 * Gets the "next" value of  mCurrentPresentationSlideNum (thread safe).
	 * 
	 * @return the "next" value of mCurrentPresentationSlideNum.
	 */		
	private synchronized int getNextSlideNum()
	{
		return mCurrentPresentationSlideNum + 1;
	}	
	
	/**
	 * Sets mCurrentPresentationSlideNum (thread safe).
	 * 
	 * @param newSlideNum new value of mCurrentPresentationSlideNum
	 * @return true if mCurrentPresentationSlideNum changes, false otherwise.
	 */	
	private synchronized boolean setSlideNum(int newSlideNum) throws ServerSessionException
	{
		if (mCurrentPresentationSlideNum != newSlideNum)
		{
			if (newSlideNum < 0)
			{
				return false;
			}
			
			if (newSlideNum >= mNumPresentationSlides)
			{
				return false;
			}
			
			mCurrentPresentationSlideNum = newSlideNum;
			
			if (DEBUG)
			{
				System.out.println("<ServerSPOTControl> :: mCurrentPresentationSlideNum set to " + newSlideNum);
			}
			
			return true;
		}

		return false;
	}
	
	/**
	 * Finds the presentation slide number that corresponds to the given
	 * topic.
	 * <p>
	 * Note: if there exists two topics with the same topic string, the
	 * first topic is used.
	 * 
	 * @param topic the topic of interest.
	 * @return the corresponding presentation slide number, if found; otherwise, -1.
	 */
	private int findPresentationSlideNum(String topic)
	{
		int numTopicEntries = mControlPAM.getNumTopicIndexEntries();
		PAMTopicIndexEntry currentEntry;

		// Loop through all topic entries
		for (int i = 0; i < numTopicEntries; i++)
		{
			currentEntry = mControlPAM.topicIndexEntryAt(i);
			if (currentEntry.mTopic.compareTo(topic) == 0)
			{
				return currentEntry.mPresentationSlideNum;
			}
		}

		return -1;
	}	
}
