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

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

/**
 * The <code>Wizard</code> class wraps around all SPOT wizard functionality.
 * All wizard GUI code resides in the <code>WizardGUI</code> class.  This
 * separation of GUI from functionality allows the GUI to be interchanged without
 * major code changes in the base wizard code.
 * 
 * @version 1.0, 12/9/1998
 * @author Jason Howes
 */
public class Wizard
{
	/**
	 * Lecture wizard project settings
	 */
	private PresentationInfo mPresentationInfo;
	private boolean mProjectOpen;

	/**
	 * PowerPoint control
	 */
	private PowerPointControl mPPTControl;
	private boolean mPPTPresentationOpen;

	/**
	 * Current presentation slide number
	 */
	private int mPresentationSlideNum;

	/**
	 * All recording objects
	 */
	private Channel mRecordChannel;
	private MicrophoneSource mSource;
	private DestinationObserver mObserver;
	private StreamDestination mDestination;
	private boolean mRecordingInProgress;
	private boolean mRecordingPaused;
	private int mLastSlideBeforePause;

	/**
	 * Current PAM
	 */
	private PAM mPAM;

	/**
	 * Open files
	 */
	private File mPAMFile;
	private File mRADFile;

	/**
	 * Output streams
	 */
	private FileOutputStream mPAMOutputStream;
	private FileOutputStream mRADOutputStream;

	/**
	 * Exception messages
	 */
	static final String PROJECT_OPEN_ERROR				= "Project open";
	static final String NO_PROJECT_OPEN_ERROR			= "No project open";
	static final String FILE_CREATION_ERROR				= "File creation error";
	static final String FILE_OPEN_ERROR					= "File open error";
	static final String PAM_WRITE_ERROR					= "PAM write error";
	static final String RECORDING_IN_PROGRESS_ERROR		= "Recording in progress";
	static final String PPT_OPEN_ERROR					= "PPT presenation open";
	static final String TOPIC_NOT_FOUND_ERROR			= "Topic not found";

	/**
	 * Class constructor.
	 */
	public Wizard()
	{
		mPPTControl = new PowerPointControl();
		mProjectOpen = false;
	}
	
	/**
	 * Creates a new SPOT Wizard project.
	 * 
	 * @param presentationName filename of the PPT presentation
	 * @throws <code>WizardException</code> on error.
	 */
	public void newProject(String presentationName) throws WizardException
	{
		if (mProjectOpen)
		{
			throw new WizardException(PROJECT_OPEN_ERROR);
		}
		mPresentationInfo = new PresentationInfo(presentationName, SPOTDefinitions.PRESENTATION_FILE_SUFFIX);

		// Success!
		mProjectOpen = true;
	}

	/**
	 * Opens, positions, and sizes the PPT presentation.
	 * 
	 * @param w width of the presentation window (in pixels).
	 * @param h height of the presentation window (in pixels).
	 * @param x horizontal screen position of the presentation window.
	 * @param y vertical screen position of the presentation window.
	 * @throws <code>WizardException</code>, <code>PowerPointControlException</code> on error.
	 */
	public void openPPTPresentation(
		int w, 
		int h, 
		float x, 
		float y) throws WizardException, PowerPointControlException
	{
		if (!mProjectOpen)
		{
			throw new WizardException(NO_PROJECT_OPEN_ERROR);
		}

		if (mPPTPresentationOpen)
		{
			throw new WizardException(PPT_OPEN_ERROR);
		}

		// Open the PPT presentation using the PowerPoint control
		mPPTControl.startPresentation(mPresentationInfo.getPresentationFilename(null));

		// Position and size the presentation
		mPPTControl.setSize(w, h);
		mPPTControl.setLocation(x, y);

		mPPTPresentationOpen = true;
	}

	/**
	 * Closes an open PPT presentation.
	 */
	public void closePPTPresentation()
	{	
		// Stop the presentation
		mPPTControl.stopPresentation();
		mPPTPresentationOpen = false;
	}

	/**
	 * Starts PAM and RAD recording.
	 * 
	 * @throws <code>WizardException</code> on error.
	 */
	public void startRecording() throws WizardException
	{
		PAMDescriptorEntry newEntry;

		if (!mProjectOpen)
		{
			throw new WizardException(NO_PROJECT_OPEN_ERROR);
		}

		if (mRecordingInProgress)
		{
			throw new WizardException(RECORDING_IN_PROGRESS_ERROR);
		}
		
		// Create the PAM and RAD files
		mPAMFile = new File(mPresentationInfo.getPAMFilename(null));
		mRADFile = new File(mPresentationInfo.getRADFilename(null));

		// Create files -> HACK
		try
		{
			if (!mPAMFile.exists())
			{
				new RandomAccessFile(mPAMFile, "rw");
			}
			if (!mRADFile.exists())
			{
				new RandomAccessFile(mRADFile, "rw");
			}
		}
		catch (IOException e)
		{
			mPAMFile = null;
			mRADFile = null;
			throw new WizardException(Wizard.FILE_CREATION_ERROR);
		}
		
		// Create output streams
		try
		{
			mPAMOutputStream = new FileOutputStream(mPAMFile);
			mRADOutputStream = new FileOutputStream(mRADFile);	
		}
		catch (IOException e)
		{
			mPAMFile = null;
			mRADFile = null;
			throw new WizardException(Wizard.FILE_OPEN_ERROR);
		}		

		// Create a new PAM
		mPAM = new PAM();

		// Reset slide numbers, etc.
		mPresentationSlideNum = 0;
		mRecordingInProgress = false;
		mRecordingPaused = false;
		
		// Give the PAM a title
		mPAM.setInfo(mPresentationInfo.getPresentationPath());

		// Create the first PAM entry
		newEntry = new PAMDescriptorEntry();
		newEntry.mRADOffset = 0;
		newEntry.mPresentationSlideNum = 0;

		// Get the current PPT slide number
		newEntry.mPPTSlideNum = mPPTControl.getCurrentSlide();
		
		// Start recording
		try
		{
			// Create the recording source, destinations, and channel
			mRecordChannel = new Channel();
			mSource = new MicrophoneSource(mRecordChannel);
			mObserver = new DestinationObserver();
			mDestination = new StreamDestination(mRADOutputStream);
			
			// Add the source and destination to the channel
			mRecordChannel.setSource(mSource);
			mRecordChannel.addDestination(mObserver);
			mRecordChannel.addDestination(mDestination);
			
			// Start recording
			mRecordChannel.open();
		}
		catch (DataException e)
		{
			throw new WizardException(e.getMessage());
		}
		
		// Add the entry to the PAM
		mPAM.addDescriptorEntry(newEntry);		

		// Success!
		mRecordingInProgress = true;
		mRecordingPaused = false;
	}

	/**
	 * Pauses RAD and PAM recording.
	 */
	public void pauseRecording()
	{
		if (!mRecordingInProgress)
		{
			return;
		}

		// Are we already paused?
		if (mRecordingPaused)
		{
			return;
		}

		// Mute the channel
		mRecordChannel.mute(true);

		// Record the current PPT slide
		mLastSlideBeforePause = mPPTControl.getCurrentSlide();
		
		mRecordingPaused = true;
	}

	/**
	 * Unpauses RAD and PAM recording.
	 */
	public void unpauseRecording()
	{
		PAMDescriptorEntry newEntry;
		int currentPPTSlideNum;

		// Are we recording?
		if (!mRecordingInProgress)
		{
			return;
		}

		// Are we already unpaused?
		if (!mRecordingPaused)
		{
			return;
		}

		// Get the current PPT slide
		currentPPTSlideNum = mPPTControl.getCurrentSlide();

		// Do we need to add a new PAM descriptor entry?
		if (currentPPTSlideNum != mLastSlideBeforePause)
		{
			// Create a new PAM entry
			newEntry = new PAMDescriptorEntry();
			newEntry.mRADOffset = mObserver.getStreamOffset();
			newEntry.mPPTSlideNum = currentPPTSlideNum;
			newEntry.mPresentationSlideNum = mPresentationSlideNum + 1;

			// Add the entry to the PAM
			mPAM.addDescriptorEntry(newEntry);
			mPresentationSlideNum++;
		}

		// Unmute the channel
		mRecordChannel.mute(false);
		mRecordingPaused = false;
	}

	/**
	 * Stops PAM and RAD recording and flushes the PAM to disk.
	 */
	public void stopRecording() throws WizardException
	{	
		// Are we recording?
		if (!mRecordingInProgress)
		{
			return;
		}		

		// Break down the recording infrastructure
		mRecordChannel.close();
		mSource = null;
		mObserver = null;
		mDestination = null;
		mRecordChannel = null;

		// Flush the PAM to disk
		try
		{
			writePAM();
		}
		catch (WizardException e)
		{
			// Cleanup
			mPAM = null;
			mRecordingInProgress = false;
			mRecordingPaused = false;
			
			// Close the PAM and RAD streams
			try
			{
				mPAMOutputStream.close();
				mRADOutputStream.close();
			}
			catch (IOException exp)
			{
			}			
			
			// Throw the exception
			throw e;
		}
		
		// Close the PAM and RAD streams
		try
		{
			mPAMOutputStream.close();
			mRADOutputStream.close();
		}
		catch (IOException e)
		{
		}	

		// Destroy the PAM
		mPAM = null;

		mRecordingInProgress = false;
		mRecordingPaused = false;
	}

	/**
	 * Changes the current slide of the PPT presentation to be the
	 * first slide of the presentation, adding the appropriate PAM
	 * entries if the recording is NOT paused.
	 * 
	 * @throws <code>PowerPointControlException</code> on error.
	 */
	public void gotoFirstPPTSlide() throws PowerPointControlException
	{
		PAMDescriptorEntry newEntry;
		boolean slideChanged = true;

		if (!mPPTPresentationOpen)
		{
			return;
		}

		// Update PowerPoint presentation
		slideChanged = mPPTControl.firstSlide();

		// Do we need to make a PAM entry?
		if (mRecordingInProgress && !mRecordingPaused && slideChanged)
		{
			// Create a new entry
			newEntry = new PAMDescriptorEntry();

			// Get the current PPT slide num
			newEntry.mPPTSlideNum = mPPTControl.getCurrentSlide();

			// Set the presentation slide and offset
			newEntry.mRADOffset = mObserver.getStreamOffset();
			newEntry.mPresentationSlideNum = mPresentationSlideNum + 1;

			// Add the entry
			mPAM.addDescriptorEntry(newEntry);
			mPresentationSlideNum++;
		}
	}

	/**
	 * Changes the current slide of the PPT presentation to be the 
	 * last slide of the presentation, adding the appropriate PAM 
	 * entries if the recording is NOT paused.
	 * 
	 * @throws <code>PowerPointControlException</code> on error.
	 */
	public void gotoLastPPTSlide() throws PowerPointControlException
	{
		PAMDescriptorEntry newEntry;
		boolean slideChanged = true;

		if (!mPPTPresentationOpen)
		{
			return;
		}

		// Update PowerPoint presentation
		slideChanged = mPPTControl.lastSlide();

		// Do we need to make a PAM entry?
		if (mRecordingInProgress && !mRecordingPaused && slideChanged)
		{
			// Create a new entry
			newEntry = new PAMDescriptorEntry();

			// Get the current PPT slide num
			newEntry.mPPTSlideNum = mPPTControl.getCurrentSlide();

			// Set the presentation slide and offset
			newEntry.mRADOffset = mObserver.getStreamOffset();
			newEntry.mPresentationSlideNum = mPresentationSlideNum + 1;

			// Add the entry
			mPAM.addDescriptorEntry(newEntry);
			mPresentationSlideNum++;
		}
	}

	/**
	 * Changes the current slide of the PPT presentation to be the 
	 * next slide of the presentation, adding the appropriate PAM 
	 * entries if the recording is NOT paused.
	 * 
	 * @throws <code>PowerPointControlException</code> on error.
	 */
	public void gotoNextPPTSlide() throws PowerPointControlException
	{
		PAMDescriptorEntry newEntry;
		boolean slideChanged = true;

		if (!mPPTPresentationOpen)
		{
			return;
		}
		
		// Update PowerPoint presentation
		slideChanged = mPPTControl.nextSlide();

		// Do we need to make a PAM entry?
		if (mRecordingInProgress && !mRecordingPaused && slideChanged)
		{
			// Create a new entry
			newEntry = new PAMDescriptorEntry();

			// Get the current PPT slide num
			newEntry.mPPTSlideNum = mPPTControl.getCurrentSlide();

			// Set the presentation slide and offset
			newEntry.mRADOffset = mObserver.getStreamOffset();
			newEntry.mPresentationSlideNum = mPresentationSlideNum + 1;

			// Add the entry
			mPAM.addDescriptorEntry(newEntry);
			mPresentationSlideNum++;
		}	
	}

	/**
	 * Changes the current slide of the PPT presentation to be the 
	 * previous slide of the presentation, adding the appropriate PAM 
	 * entries if the recording is NOT paused.
	 * 
	 * @throws <code>PowerPointControlException</code> on error.
	 */
	public void gotoPreviousPPTSlide() throws PowerPointControlException
	{
		PAMDescriptorEntry newEntry;
		boolean slideChanged = true;

		if (!mPPTPresentationOpen)
		{
			return;
		}

		// Update PowerPoint presentation
		slideChanged = mPPTControl.previousSlide();

		// Do we need to make a PAM entry?
		if (mRecordingInProgress && !mRecordingPaused && slideChanged)
		{
			// Create a new entry
			newEntry = new PAMDescriptorEntry();

			// Get the current PPT slide num
			newEntry.mPPTSlideNum = mPPTControl.getCurrentSlide();

			// Set the presentation slide and offset
			newEntry.mRADOffset = mObserver.getStreamOffset();
			newEntry.mPresentationSlideNum = mPresentationSlideNum + 1;

			// Add the entry
			mPAM.addDescriptorEntry(newEntry);
			mPresentationSlideNum++;
		}
	}
	
	/**
	 * Set the current PPT slide to be the slide that corresponds to
	 * the supplied topic string.
	 * 
	 * @param topic the topic of interest.
	 * @throws <code>WizardException</code>, <code>PowerPointControlException</code> on error.
	 */
	public void gotoTopicPPTSlide(String topic) throws WizardException, PowerPointControlException
	{
		PAMDescriptorEntry newEntry;
		PAMTopicIndexEntry topicEntry;
		boolean slideChanged = true;
		int topicSlideNum;

		if (!mPPTPresentationOpen)
		{
			return;
		}

		// Find the topic
		if ((topicEntry = findTopic(topic)) == (PAMTopicIndexEntry)null)
		{
			throw new WizardException(TOPIC_NOT_FOUND_ERROR);
		}

		// Update the PowerPoint presentation
		slideChanged = mPPTControl.gotoSlide(topicEntry.mPPTSlideNum);

		// Do we need to make a PAM entry?
		if (mRecordingInProgress && !mRecordingPaused && slideChanged)
		{
			// Create a new entry
			newEntry = new PAMDescriptorEntry();

			// Set the presentation slide, PPT slide, and RAD offset
			newEntry.mPresentationSlideNum = mPresentationSlideNum + 1;
			newEntry.mPPTSlideNum = topicEntry.mPPTSlideNum;
			newEntry.mRADOffset = mObserver.getStreamOffset();

			// Add the entry
			mPAM.addDescriptorEntry(newEntry);
			mPresentationSlideNum++;
		}
	}

	/**
	 * Set the current presentation slide topic, adding the appropriate 
	 * PAM entry.  Recording of the presentation MUST be in progress
	 * at the time this function is called (that is, it must have been
	 * started and NOT paused).
	 * 
	 * @param topic the topic to set.
	 * @return <code>true</code> if a topic entry was added, <code>false</code> if one was replaced.
	 */
	public boolean setTopic(String topic)
	{
		PAMTopicIndexEntry newEntry;
		int currentPPTSlideNum;

		if (!mRecordingInProgress)
		{
			return false;
		}

		if (mRecordingPaused)
		{
			return false;
		}

		// Get the current PPT slide number
		currentPPTSlideNum = mPPTControl.getCurrentSlide();

		// Create a new topic index entry
		newEntry = new PAMTopicIndexEntry();
		newEntry.mTopic = new String(topic);
		newEntry.mPresentationSlideNum = mPresentationSlideNum;
		newEntry.mPPTSlideNum = currentPPTSlideNum;

		// Add the entry
		return addTopic(newEntry);
	}

	/**
	 * Returns a Vector of available topics (in String form).
	 * 
	 * @return a vector of available topics.
	 */
	public Vector getTopics()
	{	
		// Is a project open?
		if (!mProjectOpen)
		{
			return new Vector();
		}

		return mPAM.getTopics();
	}

	/**
	 * Adds a topic index entry to the PAM.  If the specified 
	 * presentation slide already has a topic associated with it, the
	 * existing topic is replaced with the new topic.
	 * 
	 * @param newEntry new topic index entry to add.
	 * @return <code>true</code> if a topic entry was added, <code>false</code> 
	 * if one was replaced.
	 */
	private boolean addTopic(PAMTopicIndexEntry newEntry)
	{
		int numTopicEntries = mPAM.getNumTopicIndexEntries();
		PAMTopicIndexEntry oldEntry;

		// Do we need to replace an existing entry?
		for (int i = 0; i < numTopicEntries; i++)
		{
			// Get the next topic index entry
			try
			{
				oldEntry = mPAM.topicIndexEntryAt(i);
			
				if ((oldEntry != null) && 
					(oldEntry.mPresentationSlideNum == newEntry.mPresentationSlideNum))
				{
					mPAM.removeTopicIndexEntryAt(i);
					mPAM.addTopicIndexEntry(newEntry);
					return false;
				}
			}
			catch (ArrayIndexOutOfBoundsException e)
			{
			}			
		}

		// Add a new entry
		mPAM.addTopicIndexEntry(newEntry);
		return true;
	}
	
	/**
	 * Finds a topic index entry in the PAM based on topic. Note: if
	 * there exists two topics with the SAME topic string, the FIRST
	 * topic is returned.
	 * 
	 * @param topic the topic to find.
	 * @return the topic index entry that corresponds to the topic, if found;
	 * otherwise, <code>null</code>.
	 */
	private PAMTopicIndexEntry findTopic(String topic)
	{
		int numTopicEntries = mPAM.getNumTopicIndexEntries();
		PAMTopicIndexEntry currentEntry;

		// Scan for the desired topic
		for (int i = 0; i < numTopicEntries; i++)
		{
			// Get the next topic entry
			try
			{
				currentEntry = mPAM.topicIndexEntryAt(i);
			}
			catch (ArrayIndexOutOfBoundsException e)
			{
				return (PAMTopicIndexEntry)null;
			}
			
			// Have we found the topic?
			if (currentEntry.mTopic.compareTo(topic) == 0)
			{
				return currentEntry;
			}
		}

		return (PAMTopicIndexEntry)null;
	}

	/**
	 * Writes the PAM to the PAM file.
	 * 
	 * @throws <code>WizardException</code> on error.
	 */
	private void writePAM() throws WizardException
	{	
		// Write the PAM object
		try
		{
			ObjectOutputStream oos = new ObjectOutputStream(mPAMOutputStream);
			oos.writeObject(mPAM);
			oos.flush();
		}
		catch (Exception e)
		{
			throw new WizardException(PAM_WRITE_ERROR);
		}
	}

	/**
	 * Closes the current SPOT Wizard project.
	 * 
	 * @throws <code>WizardException</code> on error
	 */
	public void closeProject() throws WizardException
	{
		if (!mProjectOpen)
		{
			return;
		}

		// Stop recording, if necessary...
		if (mRecordingInProgress)
		{
			try
			{
				stopRecording();
			}
			catch (WizardException e)
			{
				// Close any open PPT presentation 
				closePPTPresentation();	
				
				mPPTPresentationOpen = false;		
				mProjectOpen = false;
				
				// Throw the exception
				throw e;
			}
		}
		
		// Close any open PPT presentation...
		if (mPPTPresentationOpen)
		{
			closePPTPresentation();
			mPPTPresentationOpen = false;
		}
		
		// Done..
		mProjectOpen = false;
	}

	/**
	 * Entry point for the SPOT Wizard application.
	 * 
	 * @param args any command line arguments.
	 */
	public static void main(String[] args)
	{
		// 'The' wizard object and GUI
		Wizard theWizard = new Wizard();
		WizardGUI theWizardGUI;

		// Attach the wizard GUI
		theWizardGUI = new WizardGUI(theWizard);

		// Layout the wizard GUI
		theWizardGUI.start();
	}
}

/**
 * A <code>WizardException</code> is a exception thrown
 * by a <code>Wizard</code>.
 * 
 * @author Jason Howes
 * @version 1.0, 12/13/1998
 * @see cnrg.apps.spot.wizard.Wizard
 */
class WizardException extends Exception
{
	/**
	 * Class constructor.
	 * 
	 * @param msg a message describing the exception.
	 */
	public WizardException(String msg)
	{
		super("<WizardException> :: " + msg);
	}

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