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

import shared.*;
import cnrg.itx.datax.*;
import cnrg.itx.datax.devices.*;
import java.io.*;

/**
 * A <code>ClientPAMControl</code> is a PAM-based SPOT client presentation control.
 * A PAM object is used to control the presentation, thus allowing local presentation
 * playback.  It implements the <code>ClientPresentationControl</code> interface to 
 * synchronize PPT slide transitions and the presentation audio stream with presentation
 * slide changes (as specified in the PAM object).
 * 
 * @author Jason Howes
 * @version 1.0, 2/24/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 ClientPAMControl extends Thread implements ClientPresentationControl
{
	/**
	 * Debug flag (if true, the control thread outputs control info).
	 */
	private static final boolean DEBUG = false;
	
	/**
	 * The controlling PAM.
	 */
	private PAM mControlPAM;

	/**
	 * PowerPoint control.
	 */
	private PowerPointControl mPPTControl;

	/**
	 * RAD filename and input stream.
	 */
	private String mRADFilename;
	private RADInputStream mRADInputStream;
	
	/**
	 * Audio source, channel, and destination.
	 */
	private Channel mAudioChannel;
	private StreamSource mAudioSource;
	private SpeakerDestination mAudioDestination;
	private DestinationObserver mAudioObserver;
	private boolean mAudioChannelMuted;

	/**
	 * Presentation slide numbers.
	 */
	private int mCurrentPresentationSlideNum;
	private int mPausedPresentationSlideNum;
	private int mNumPresentationSlides;

	/**
	 * ClientPAMControl thread flags and semaphore.
	 */
	private boolean mAlive;
	private boolean mStart;
	private boolean mStop;
	private Object mSema;

	/**
	 * Class constructor.
	 * 
	 * @param controlPAM the PAM object that will be used to control the client.
	 * @param RADFilename the filename of the presentation RAD audio file.
	 * @throws <code>ClientException</code> on error.
	 */
	public ClientPAMControl(PAM controlPAM, String RADFilename) throws ClientException
	{
		// Initialize object
		mControlPAM = controlPAM;
		mPPTControl = new PowerPointControl();
		mRADFilename = new String(RADFilename);
		mSema = new Object();
		mAlive = false;
		mStart = false;
		mStop = false;
		mCurrentPresentationSlideNum = 0;
		mNumPresentationSlides = mControlPAM.getNumPresentationSlides();
		mAudioChannelMuted = false;

		// Try to create the RADInputStream
		try
		{
			mRADInputStream = new RADInputStream(RADFilename);
		}
		catch (Exception e)
		{
			throw new ClientException(e.getMessage());
		}

		// Start the thread
		startup();
	}

	/**
	 * Starts the presentation.
	 * 
	 * @param PPTfilename name of the PPT presentation file.
	 * @param w the horizontal size of the PPT presentation window (in pixels).
	 * @param h the vertical size of the PPT presentation window (in pixels).
	 * @param x the horizontal screen position of the PPT presentation window.
	 * @param y the vertical screen position of the PPT presentation window.
	 * @throws <code>ClientException</code>, <code>PowerPointException</code>,
	 * <code>DataException</code> on error.
	 */
	public void startPresentation(String filename,
		int w, 
		int h, 
		float x, 
		float y) throws ClientException, PowerPointControlException, DataException
	{
		// Are we already started?
		if (mStart)
		{
			throw new ClientException(Client.PRESENTATION_IN_PROGRESS);
		}

		// Open the presentation
		mPPTControl.startPresentation(filename, w, h, x, y);

		// Create the audio source, destination, and channel
		mAudioChannel = new Channel();
		mAudioSource = new StreamSource(mRADInputStream, mAudioChannel, SPOTDefinitions.AUDIO_BUFFER_TIME);
		mAudioDestination = new SpeakerDestination();
		mAudioObserver = new DestinationObserver();
			
		// Add the source and destination to the channel
		mAudioChannel.setSource(mAudioSource);
		mAudioChannel.addDestination(mAudioDestination);
		mAudioChannel.addDestination(mAudioObserver);
		
		// Start the control thread
		try
		{
			synchronized(mSema)
			{
				mAlive = true;
				mStart = true;
				mStop = false;
				mSema.notify();
				
				// Start playback
				mAudioChannel.open();				
			}
		}
		catch (Exception e)
		{
			stopPresentation();
			throw new ClientException(e.getMessage());
		}		
	}
	
	/**
	 * Stops the presentation.
	 */
	public void stopPresentation()
	{
		// Have we started?
		if (!mStart)
		{
			return;
		}

		// Have we already stopped?
		if (mStop)
		{
			return;
		}
		
		// Tell the control thread to stop
		try
		{
			synchronized(mSema)
			{
				mAlive = false;
				mStop = true;
				mSema.notify();
			}

			if (DEBUG)
			{
				System.out.println("<ClientPAMControl> :: finished");
			}
		}
		catch (Exception e)
		{
		}

		// Reset the RADInputStream
		try
		{
			mRADInputStream.setOffset(0);
		}
		catch (IOException e)
		{
		}
		
		// Wait for the thread to join
		try
		{
			this.join();
		}
		catch (InterruptedException e)
		{
		}	
		
		// Deallocate the audio source, destination, and channel
		mAudioChannel.close();
		mAudioSource = null;
		mAudioDestination = null;
		mAudioObserver = null;
		mAudioChannel = null;
		
		// Stop the PowerPoint presentation
		mPPTControl.stopPresentation();
		
		// Reset start flag
		mStart = false;		
	}	

	/**
	 * Synchronizes the PPT presentation and audio with the first 
	 * presentation slide.
	 * 
	 * @throws <code>ClientException</code> on error.
	 */
	public void gotoFirstPresentationSlide() throws ClientException
	{
		PAMDescriptorEntry newEntry;

		// Have we started yet?
		if (!mStart)
		{
			return;
		}

		// Set the new presentation slide number and entry
		setSlideNum(0);
		try
		{
			newEntry = mControlPAM.descriptorEntryAt(getCurrentSlideNum());
		}
		catch (ArrayIndexOutOfBoundsException e)
		{
			throw new ClientException(e.getMessage());
		}

		// Set the new offset in the RADInputStream
		try
		{
			mRADInputStream.setOffset(newEntry.mRADOffset);
		}
		catch (IOException e)
		{
			throw new ClientException(e.getMessage());
		}
	}
	
	/**
	 * Synchronizes the PPT presentation and audio with the last 
	 * presentation slide.
	 * 
	 * @throws <code>ClientException</code> on error.
	 */
	public void gotoLastPresentationSlide() throws ClientException
	{
		PAMDescriptorEntry newEntry;

		// Have we started yet?
		if (!mStart)
		{
			return;
		}

		// Set the new presentation slide number and entry
		setSlideNum(mNumPresentationSlides - 1);
		try
		{
			newEntry = mControlPAM.descriptorEntryAt(getCurrentSlideNum());
		}
		catch (ArrayIndexOutOfBoundsException e)
		{
			throw new ClientException(e.getMessage());
		}

		// Set the new offset in the RADInputStream
		try
		{
			mRADInputStream.setOffset(newEntry.mRADOffset);
		}
		catch (IOException e)
		{
			throw new ClientException(e.getMessage());
		}
	}	

	/**
	 * Synchronizes the PPT presentation and audio with the next 
	 * presentation slide.
	 * 
	 * @throws <code>ClientException</code> on error.
	 */
	public void gotoNextPresentationSlide() throws ClientException
	{
		PAMDescriptorEntry newEntry;

		// Have we started yet?
		if (!mStart)
		{
			return;
		}

		// Set the new presentation slide number and entry
		if (!incrementSlideNum())
		{
			return;
		}
		try
		{
			newEntry = mControlPAM.descriptorEntryAt(getCurrentSlideNum());
		}
		catch (ArrayIndexOutOfBoundsException e)
		{
			throw new ClientException(e.getMessage());
		}

		// Set the new offset in the RADInputStream
		try
		{
			mRADInputStream.setOffset(newEntry.mRADOffset);
		}
		catch (IOException e)
		{
			throw new ClientException(e.getMessage());
		}
	}	
	
	/**
	 * Synchronizes the PPT presentation and audio with the previous 
	 * presentation slide.
	 * 
	 * @throws <code>ClientException</code> on error.
	 */
	public void gotoPreviousPresentationSlide() throws ClientException
	{
		PAMDescriptorEntry newEntry;

		// Have we started yet?
		if (!mStart)
		{
			return;
		}

		// Set the new presentation slide number and entry
		decrementSlideNum();
		try
		{
			newEntry = mControlPAM.descriptorEntryAt(getCurrentSlideNum());
		}
		catch (ArrayIndexOutOfBoundsException e)
		{
			throw new ClientException(e.getMessage());
		}

		// Set the new offset in the RADInputStream
		try
		{
			mRADInputStream.setOffset(newEntry.mRADOffset);
		}
		catch (IOException e)
		{
			throw new ClientException(e.getMessage());
		}
	}
	
	/**
	 * Synchronizes the PPT presentation and audio with a specified
	 * presentation topic.
	 * 
	 * @param topic the desired topic with which to synchronize.
	 * @throws <code>ClientException</code> on error.
	 */	
	public void gotoPresentationTopic(String topic) throws ClientException
	{
		PAMDescriptorEntry newEntry;
		int topicPresentationSlideNum;

		// Have we started yet?
		if (!mStart)
		{
			return;
		}
		
		// Does the topic exist?
		if ((topicPresentationSlideNum = findPresentationSlideNum(topic)) == -1)
		{
			throw new ClientException(ClientPresentationControl.NO_SUCH_TOPIC);
		}

		// Find the corresponding PAMDescriptorEntry
		setSlideNum(topicPresentationSlideNum);
		try
		{
			newEntry = mControlPAM.descriptorEntryAt(getCurrentSlideNum());
		}
		catch (ArrayIndexOutOfBoundsException e)
		{
			throw new ClientException(e.getMessage());
		}

		// Set the new offset in the RADInputStream
		try
		{
			mRADInputStream.setOffset(newEntry.mRADOffset);
		}
		catch (IOException e)
		{
			throw new ClientException(e.getMessage());
		}		
	}	

	/**
	 * Pauses the slide presentation.
	 */
	public void pausePresentation()
	{
		// Have we started yet?
		if (!mStart)
		{
			return;
		}
		
		// Mute the AudioChannel
		if (!mAudioChannelMuted)
		{
			mAudioChannel.mute(true);
			mAudioChannelMuted = true;

			// Record the current presentation slide number
			mPausedPresentationSlideNum = getCurrentSlideNum();
		}
	}

	/**
	 * Resumes a paused slide presentation.
	 * 
	 * @throws <code>ClientException</code> on error
	 */
	public void resumePresentation() throws ClientException
	{
		PAMDescriptorEntry currentEntry;

		// Have we started yet?
		if (!mStart)
		{
			return;
		}

		// Unmute the AudioChannel
		if (mAudioChannelMuted)
		{
			mAudioChannel.mute(false);
			mAudioChannelMuted = false;
			
			if (getCurrentSlideNum() != mPausedPresentationSlideNum)
			{
				// Get the current entry
				currentEntry = mControlPAM.descriptorEntryAt(getCurrentSlideNum());

				// Set the new offset in the RADInputStream
				try
				{
					mRADInputStream.setOffset(currentEntry.mRADOffset);
				}
				catch (IOException e)
				{
					throw new ClientException(e.getMessage());
				}				
			}
		}
	}

	/**
	 * Thread function.
	 */
	public void run()
	{
		int nextSlideNum;
		long nextOffset;
		PAMDescriptorEntry nextEntry;

		// 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;
		}

		// 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;
				}

				if (DEBUG)
				{
					System.out.println("<ClientPAMControl> :: nextSlideNum = " + nextSlideNum);
					System.out.println("<ClientPAMControl> :: nextOffset = " + nextOffset);
				}
		
				// Wait for the next offset
				if(mRADInputStream.waitForOffset(nextOffset))
				{
					// Success!
					setSlideNum(nextSlideNum);

					if (DEBUG)
					{
						System.out.println("<ClientPAMControl> :: waitForOffset succeeded at offset = " + nextOffset);
						System.out.println("<ClientPAMControl> ::: RADInputStream offset = " + mRADInputStream.getStreamOffset());
						System.out.println("<ClientPAMControl> :: DestinationObservers offset = " + mAudioObserver.getStreamOffset());
					}
				}

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

				// Find the next entry
				nextEntry = mControlPAM.descriptorEntryAt(getCurrentSlideNum());

				// Goto the specified PPT slide
				try
				{
					mPPTControl.gotoSlide(nextEntry.mPPTSlideNum);
				}
				catch (PowerPointControlException e)
				{
					break;
				}

				if (DEBUG)
				{
					System.out.println("<ClientPAMControl> :: changed PPT to slide " + nextEntry.mPPTSlideNum);
				}
			}
		}
		catch (InterruptedException e)
		{
		}

		// Shutdown the thread
		shutdown();
	}
	
	/**
	 * ClientPAMControl thread startup tasks.
	 */
	private void startup()
	{
		this.start();
		
		if (DEBUG)
		{
			System.out.println("<ClientPAMControl> :: control thread started");
		}
	}

	/**
	 * ClientPAMControl thread shutdown tasks.
	 */
	private void shutdown()
	{
		if (DEBUG)
		{
			System.out.println("<ClientPAMControl> :: control thread shutdown");
		}
	}	

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

	/**
	 * Increments mCurrentPresentationSildeNum (thread safe).
	 * 
	 * @return true if mCurrentPresentationSlideNum changes, false otherwise.
	 */
	private synchronized boolean incrementSlideNum()
	{
		if (mCurrentPresentationSlideNum < mNumPresentationSlides - 1)
		{
			mCurrentPresentationSlideNum++;
			return true;
		}

		return false;
	}

	/**
	 * Decrements mCurrentPresentationSlideNum (thread safe).
	 * 
	 * @return true if mCurrentPresentationSlideNum changes, false otherwise.
	 */
	private synchronized boolean decrementSlideNum()
	{
		if (mCurrentPresentationSlideNum > 0)
		{
			mCurrentPresentationSlideNum--;
			return true;
		}

		return false;
	}

	/**
	 * Sets mCurrentPresentationSlideNum (thread safe).
	 * 
	 * @param newSlideNum new value of mCurrentPresentationSlideNum
	 * @return true if mCurrentPresentationSlideNum changes, false otherwise.
	 */	
	private synchronized boolean setSlideNum(int newSlideNum)
	{
		if (mCurrentPresentationSlideNum != newSlideNum)
		{
			mCurrentPresentationSlideNum = newSlideNum;
			return true;
		}

		return false;
	}
}
