//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//===========================================================================//
#include <Assert.h>
#include <stdio.h>
#include <math.h>
#include "hlfaceposer.h"
#include "PhonemeEditor.h"
#include "PhonemeEditorColors.h"
#include "snd_audio_source.h"
#include "snd_wave_source.h"
#include "ifaceposersound.h"
#include "choreowidgetdrawhelper.h"
#include "mxBitmapButton.h"
#include "phonemeproperties.h"
#include "tier2/riff.h"
#include "StudioModel.h"
#include "expressions.h"
#include "expclass.h"
#include "InputProperties.h"
#include "phonemeextractor/PhonemeExtractor.h"
#include "PhonemeConverter.h"
#include "choreoevent.h"
#include "choreoscene.h"
#include "ChoreoView.h"
#include "filesystem.h"
#include "UtlBuffer.h"
#include "AudioWaveOutput.h"
#include "StudioModel.h"
#include "viewerSettings.h"
#include "ControlPanel.h"
#include "faceposer_models.h"
#include "tier1/strtools.h"
#include "tabwindow.h"
#include "MatSysWin.h"
#include "soundflags.h"
#include "mdlviewer.h"
#include "filesystem_init.h"
#include "WaveBrowser.h"
#include "tier2/p4helpers.h"
#include "vstdlib/random.h"

extern IUniformRandomStream *random;

float SnapTime( float input, float granularity );

#define MODE_TAB_OFFSET 20

// 10x magnification
#define MAX_TIME_ZOOM 1000
// 10% per step
#define TIME_ZOOM_STEP 2

#define SCRUBBER_HEIGHT	15

#define TAG_TOP ( 25 + SCRUBBER_HEIGHT )
#define TAG_BOTTOM ( TAG_TOP + 20 )

#define PLENTY_OF_TIME 99999.9
#define MINIMUM_WORD_GAP 0.02f
#define MINIMUM_PHONEME_GAP 0.01f
#define DEFAULT_WORD_LENGTH 0.25f
#define DEFAULT_PHONEME_LENGTH 0.1f

#define WORD_DATA_EXTENSION	".txt"

// #define ITEM_GAP_EPSILON 0.0025f
struct PhonemeEditorColor
{
	int				color_number; // For readability
	int				mode_number; // -1 for all
	COLORREF		root_color;
	COLORREF		gray_color;  // if mode is wrong...
};

static PhonemeEditorColor g_PEColors[ NUM_COLORS ] =
{
	{ COLOR_PHONEME_BACKGROUND,					-1, RGB( 240, 240, 220 ) },
	{ COLOR_PHONEME_TEXT,						-1,	RGB( 63, 63, 63 ) },
	{ COLOR_PHONEME_LIGHTTEXT,					0,	RGB( 180, 180, 120 ) },
	{ COLOR_PHONEME_PLAYBACKTICK,				0,	RGB( 255, 0, 0 ) },
	{ COLOR_PHONEME_WAVDATA,					0,	RGB( 128, 31, 63 ) },
	{ COLOR_PHONEME_TIMELINE,					0,	RGB( 31, 31, 127 ) },
	{ COLOR_PHONEME_TIMELINE_MAJORTICK,			0,	RGB( 200, 200, 255 ) },
	{ COLOR_PHONEME_TIMELINE_MINORTICK,			0,	RGB( 210, 210, 240 ) },
	{ COLOR_PHONEME_EXTRACTION_RESULT_FAIL,		0,	RGB( 180, 180, 0 ) },
	{ COLOR_PHONEME_EXTRACTION_RESULT_SUCCESS,	0,	RGB( 100, 180, 100 ) },
	{ COLOR_PHONEME_EXTRACTION_RESULT_ERROR,	0,	RGB( 255, 31, 31 ) },
	{ COLOR_PHONEME_EXTRACTION_RESULT_OTHER,	0,	RGB( 63, 63, 63 ) },
	{ COLOR_PHONEME_TAG_BORDER,					0,	RGB( 160, 100, 100 ) },
	{ COLOR_PHONEME_TAG_BORDER_SELECTED,		0,	RGB( 255, 40, 60 ) },
	{ COLOR_PHONEME_TAG_FILLER_NORMAL,			0,	RGB( 210, 210, 190 ) },
	{ COLOR_PHONEME_TAG_SELECTED,				0,	RGB( 200, 130, 130 ) },
	{ COLOR_PHONEME_TAG_TEXT,					0,	RGB( 63, 63, 63 ) },
	{ COLOR_PHONEME_TAG_TEXT_SELECTED,			0,	RGB( 250, 250, 250 ) },
	{ COLOR_PHONEME_WAV_ENDPOINT,				0,	RGB( 0, 0, 200 ) },
	{ COLOR_PHONEME_AB,							0,	RGB( 63, 190, 210 ) },
	{ COLOR_PHONEME_AB_LINE,					0,	RGB( 31, 150, 180 ) },
	{ COLOR_PHONEME_AB_TEXT,					0,	RGB( 100, 120, 120 ) },
	{ COLOR_PHONEME_ACTIVE_BORDER,				0,	RGB( 150, 240, 180 ) },
	{ COLOR_PHONEME_SELECTED_BORDER,			0,	RGB( 255, 0, 0 ) },
	{ COLOR_PHONEME_TIMING_TAG,					-1,	RGB( 0, 100, 200 ) },

	{ COLOR_PHONEME_EMPHASIS_BG,				1,	RGB( 230, 230, 200 ) },
	{ COLOR_PHONEME_EMPHASIS_BG_STRONG,			1,	RGB( 163, 201, 239 ) },
	{ COLOR_PHONEME_EMPHASIS_BG_WEAK,			1,	RGB( 237, 239, 163 ) },
	{ COLOR_PHONEME_EMPHASIS_BORDER,			1,	RGB( 200, 200, 200 ) },
	{ COLOR_PHONEME_EMPHASIS_LINECOLOR,			1,	RGB( 0, 0, 255 ) },
	{ COLOR_PHONEME_EMPHASIS_DOTCOLOR,			1,	RGB( 0, 0, 255 ) },
	{ COLOR_PHONEME_EMPHASIS_DOTCOLOR_SELECTED,	1,	RGB( 240, 80, 20 ) },
	{ COLOR_PHONEME_EMPHASIS_TEXT,				1,	RGB( 0, 0, 0 ) },
	{ COLOR_PHONEME_EMPHASIS_MIDLINE,			1,	RGB( 100, 150, 200 ) },
};

struct Extractor
{
	PE_APITYPE			apitype;
	CSysModule			*module;
	IPhonemeExtractor	*extractor;
};

CUtlVector< Extractor >	g_Extractors;


bool DoesExtractorExistFor( PE_APITYPE type )
{
	for ( int i=0; i < g_Extractors.Count(); i++ )
	{
		if ( g_Extractors[i].apitype == type )
			return true;
	}
	return false;
}


//-----------------------------------------------------------------------------
// Purpose: Implements the RIFF i/o interface on stdio
//-----------------------------------------------------------------------------
class StdIOReadBinary : public IFileReadBinary
{
public:
	int open( const char *pFileName )
	{
		return (int)filesystem->Open( pFileName, "rb" );
	}

	int read( void *pOutput, int size, int file )
	{
		if ( !file )
			return 0;

		return filesystem->Read( pOutput, size, (FileHandle_t)file );
	}

	void seek( int file, int pos )
	{
		if ( !file )
			return;

		filesystem->Seek( (FileHandle_t)file, pos, FILESYSTEM_SEEK_HEAD );
	}

	unsigned int tell( int file )
	{
		if ( !file )
			return 0;

		return filesystem->Tell( (FileHandle_t)file );
	}

	unsigned int size( int file )
	{
		if ( !file )
			return 0;

		return filesystem->Size( (FileHandle_t)file );
	}

	void close( int file )
	{
		if ( !file )
			return;

		filesystem->Close( (FileHandle_t)file );
	}
};

class StdIOWriteBinary : public IFileWriteBinary
{
public:
	int create( const char *pFileName )
	{
		MakeFileWriteable( pFileName );
		return (int)filesystem->Open( pFileName, "wb" );
	}

	int write( void *pData, int size, int file )
	{
		return filesystem->Write( pData, size, (FileHandle_t)file );
	}

	void close( int file )
	{
		filesystem->Close( (FileHandle_t)file );
	}

	void seek( int file, int pos )
	{
		filesystem->Seek( (FileHandle_t)file, pos, FILESYSTEM_SEEK_HEAD );
	}

	unsigned int tell( int file )
	{
		return filesystem->Tell( (FileHandle_t)file );
	}
};

// Interface objects
static StdIOWriteBinary io_out;
static StdIOReadBinary io_in;

class CPhonemeModeTab : public CTabWindow
{
public:
	typedef CTabWindow BaseClass;

	CPhonemeModeTab( mxWindow *parent, int x, int y, int w, int h, int id = 0, int style = 0 ) :
		CTabWindow( parent, x, y, w, h, id, style )
	{
		SetInverted( true );
	}

	virtual void ShowRightClickMenu( int mx, int my )
	{
		// Nothing
	}

	void	Init( void )
	{
		add( "Phonemes" );
		add( "Emphasis" );
		select( 0 );
	}
};

PhonemeEditor * g_pPhonemeEditor = 0;

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *parent - 
//-----------------------------------------------------------------------------
PhonemeEditor::PhonemeEditor( mxWindow *parent ) : 
	IFacePoserToolWindow( "PhonemeEditor", "Phoneme Editor" ), 
	mxWindow( parent, 0, 0, 0, 0 )
{
	SetAutoProcess( false );

	m_flPlaybackRate	= 1.0f;

	m_flScrub			= 0.0f;
	m_flScrubTarget		= 0.0f;

	m_CurrentMode = MODE_PHONEMES;
	Emphasis_Init();
	SetupPhonemeEditorColors();

	m_bRedoPending = false;
	m_nUndoLevel = 0;

	m_bWordsActive = false;

	m_pWaveFile = NULL;
	m_pMixer	= NULL;
	m_pEvent	= NULL;
	m_nClickX = 0;

	m_WorkFile.m_bDirty = false;
	m_WorkFile.m_szWaveFile[ 0 ] = 0;
	m_WorkFile.m_szWorkingFile[ 0 ] = 0;
	m_WorkFile.m_szBasePath[ 0 ] = 0;

	m_nTickHeight = 20;

	m_flPixelsPerSecond = 500.0f;
	m_nTimeZoom = 100;
	m_nTimeZoomStep = TIME_ZOOM_STEP;

	m_pHorzScrollBar = new mxScrollbar( this, 0, 0, 18, 100, IDC_PHONEME_SCROLL, mxScrollbar::Horizontal );


	m_hPrevCursor		= 0;
	m_nStartX			= 0;
	m_nStartY			= 0;
	m_nLastX			= 0;
	m_nLastY			= 0;
	m_nDragType			= DRAGTYPE_NONE;

	SetClickedPhoneme( -1, -1 );

	m_nSelection[ 0 ] = m_nSelection[ 1 ] = 0;
	m_bSelectionActive = false;

	m_nSelectedPhonemeCount = 0;
	m_nSelectedWordCount = 0;

	m_btnSave					= new mxButton( this, 0, 0, 16, 16, "Save (Ctrl+S)", IDC_SAVE_LINGUISTIC );
	m_btnRedoPhonemeExtraction	= new mxButton( this, 38, 14, 80, 16, "Re-extract (Ctrl+R)", IDC_REDO_PHONEMEEXTRACTION );

	m_btnLoad					= new mxButton( this, 0, 0, 0, 0, "Load (Ctrl+O)", IDC_LOADWAVEFILE );
	m_btnPlay					= new mxButton( this, 0, 0, 16, 16, "Play (Spacebar)", IDC_PLAYBUTTON );

	m_pPlaybackRate				= new mxSlider( this, 0, 0, 16, 16, IDC_PLAYBACKRATE );
	m_pPlaybackRate->setRange( 0.0, 2.0, 40 );
	m_pPlaybackRate->setValue( m_flPlaybackRate );

	m_pModeTab					= new CPhonemeModeTab( this, 0, 0, 500, 20, IDC_MODE_TAB );
	m_pModeTab->Init();

	m_nLastExtractionResult		= SR_RESULT_NORESULT;

	ClearDragLimit();

	SetSuffix( " - Normal" );
	m_flScrubberTimeOffset = 0.0f;

	LoadPhonemeConverters();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::OnDelete()
{
	if ( m_pWaveFile )
	{
		char fn[ 512 ];
		Q_snprintf( fn, sizeof( fn ), "%s%s", m_WorkFile.m_szBasePath, m_WorkFile.m_szWorkingFile );
		filesystem->RemoveFile( fn, "GAME" );
	}

	delete m_pWaveFile;
	m_pWaveFile = NULL;

	m_Tags.Reset();
	m_TagsExt.Reset();

	UnloadPhonemeConverters();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool PhonemeEditor::CanClose()
{
	if ( !GetDirty() )
		return true;

	int retval = mxMessageBox( this, va( "Save current changes to %s", m_WorkFile.m_szWaveFile ),
		"Phoneme Editor", MX_MB_QUESTION | MX_MB_YESNOCANCEL );

	// Cancel
	if ( retval == 2 )
	{
		return false;
	}

	// Yes
	if ( retval == 0 )
	{
		CommitChanges();
	}

	return true;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
PhonemeEditor::~PhonemeEditor( void )
{
}

void PhonemeEditor::SetupPhonemeEditorColors( void )
{
	int i;
	for ( i = 0; i < NUM_COLORS; i++ )
	{
		PhonemeEditorColor *p = &g_PEColors[ i ];
		Assert( p->color_number == i );

		if ( p->mode_number == -1 )
		{
			p->gray_color = p->root_color;
		}
		else
		{
			COLORREF bgColor = g_PEColors[ COLOR_PHONEME_BACKGROUND ].root_color;

			int bgr, bgg, bgb;

			bgr = GetRValue( bgColor );
			bgg = GetGValue( bgColor );
			bgb = GetBValue( bgColor );
			
			int r, g, b;

			r = GetRValue( p->root_color );
			g = GetGValue( p->root_color );
			b = GetBValue( p->root_color );

			int avg = ( r + g + b ) / 3;
			int bgavg = ( bgr + bgg + bgb ) / 3;

			// Bias toward bg color
			avg += ( bgavg - avg ) / 2.5;

			p->gray_color = RGB( avg, avg, avg );
		}
	}
}

COLORREF PhonemeEditor::PEColor( int colornum )
{
	COLORREF clr = RGB( 0, 0, 0 );
	if ( colornum < 0 || colornum >= NUM_COLORS )
	{
		Assert( 0 );
		return clr;
	}

	PhonemeEditorColor *p = &g_PEColors[ colornum ];

	if ( p->mode_number == -1 )
	{
		return p->root_color;
	}

	int modenum = (int)GetMode();

	if ( p->mode_number == modenum )
	{
		return p->root_color;
	}
	
	return p->gray_color;
}

void PhonemeEditor::EditWord( CWordTag *pWord, bool positionDialog /*= false*/ )
{
	if ( !pWord )
	{
		Con_Printf( "PhonemeEditor::EditWord:  pWord == NULL\n" );
		return;
	}

	CInputParams params;
	memset( &params, 0, sizeof( params ) );
	strcpy( params.m_szDialogTitle, "Edit Word" );
	strcpy( params.m_szPrompt, "Current Word:" );
	V_strcpy_safe( params.m_szInputText, pWord->GetWord() );

	params.m_nLeft = -1;
	params.m_nTop = -1;

	params.m_bPositionDialog = positionDialog;
	if ( params.m_bPositionDialog )
	{
		RECT rcWord;
		GetWordRect( pWord, rcWord );

		// Convert to screen coords
		POINT pt;
		pt.x = rcWord.left;
		pt.y = rcWord.top;

		ClientToScreen( (HWND)getHandle(), &pt );

		params.m_nLeft	= pt.x;
		params.m_nTop	= pt.y;
	}

	int iret = InputProperties( &params );
	SetFocus( (HWND)getHandle() );
	if ( !iret )
	{
		return;
	}

	// Validate parameters
	if ( CSentence::CountWords( params.m_szInputText ) != 1 )
	{
		Con_ErrorPrintf( "Edit word:  %s has multiple words in it!!!\n", params.m_szInputText );
		return;
	}

	SetFocus( (HWND)getHandle() );

	SetDirty( true );

	PushUndo();

	// Set the word and clear out the phonemes
	// ->m_nPhonemeCode = TextToPhoneme( params.m_szName );
	pWord->SetWord( params.m_szInputText );

	PushRedo();

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *pPhoneme - 
//			positionDialog - 
//-----------------------------------------------------------------------------
void PhonemeEditor::EditPhoneme( CPhonemeTag *pPhoneme, bool positionDialog /*= false*/ )
{
	if ( !pPhoneme )
	{
		Con_Printf( "PhonemeEditor::EditPhoneme:  pPhoneme == NULL\n" );
		return;
	}

	CPhonemeParams params;
	memset( &params, 0, sizeof( params ) );
	strcpy( params.m_szDialogTitle, "Phoneme/Viseme Properties" );
	V_strcpy_safe( params.m_szName, ConvertPhoneme( pPhoneme->GetPhonemeCode() ) );

	params.m_nLeft = -1;
	params.m_nTop = -1;

	params.m_bPositionDialog = positionDialog;
	if ( params.m_bPositionDialog )
	{
		RECT rcPhoneme;
		GetPhonemeRect( pPhoneme, rcPhoneme );

		// Convert to screen coords
		POINT pt;
		pt.x = rcPhoneme.left;
		pt.y = rcPhoneme.top;

		ClientToScreen( (HWND)getHandle(), &pt );

		params.m_nLeft	= pt.x;
		params.m_nTop	= pt.y;
	}

	int iret = PhonemeProperties( &params );
	SetFocus( (HWND)getHandle() );

	if ( !iret )
	{
		return;
	}

	SetDirty( true );

	PushUndo();

	pPhoneme->SetPhonemeCode( TextToPhoneme( params.m_szName ) );

	PushRedo();

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::EditPhoneme( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CPhonemeTag *pPhoneme = GetClickedPhoneme();
	if ( !pPhoneme )
		return;

	EditPhoneme( pPhoneme, false );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::EditWord( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CWordTag *pWord = GetClickedWord();
	if ( !pWord )
		return;

	EditWord( pWord, false );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : dragtype - 
//			startx - 
//			cursor - 
//-----------------------------------------------------------------------------
void PhonemeEditor::StartDragging( int dragtype, int startx, int starty, HCURSOR cursor )
{
	m_nDragType = dragtype;
	m_nStartX	= startx;
	m_nLastX	= startx;
	m_nStartY	= starty;
	m_nLastY	= starty;
	
	if ( m_hPrevCursor )
	{
		SetCursor( m_hPrevCursor );
		m_hPrevCursor = NULL;
	}
	m_hPrevCursor = SetCursor( cursor );

	m_FocusRects.Purge();

	RECT rc;
	GetWorkspaceRect( rc );

	RECT rcStart;
	rcStart.left = startx;
	rcStart.right = startx;

	bool addrect = true;
	switch ( dragtype )
	{
	default:
	case DRAGTYPE_SCRUBBER:
		{
			RECT rcScrub;
			GetScrubHandleRect( rcScrub, true );

			rcStart = rcScrub;
			rcStart.left = ( rcScrub.left + rcScrub.right ) / 2;
			rcStart.right = rcStart.left;
			rcStart.bottom = h2() - 18 - MODE_TAB_OFFSET;
		}
		break;
	case DRAGTYPE_EMPHASIS_SELECT:
		{
			RECT rcEmphasis;
			Emphasis_GetRect( rc, rcEmphasis );

			rcStart.top = starty;
			rcStart.bottom = starty;
		}
		break;
	case DRAGTYPE_EMPHASIS_MOVE:
		{
			SetDirty( true );

			PushUndo();

			Emphasis_MouseDrag( startx, starty );
			m_Tags.Resort();

			addrect = false;
		}
		break;
	case DRAGTYPE_SELECTSAMPLES:
	case DRAGTYPE_MOVESELECTIONSTART:
	case DRAGTYPE_MOVESELECTIONEND:
		rcStart.top = rc.top;
		rcStart.bottom = rc.bottom;
		break;
	case DRAGTYPE_MOVESELECTION:
		{
			rcStart.top = rc.top;
			rcStart.bottom = rc.bottom;

			// Compute left/right pixels for selection
			rcStart.left = GetPixelForSample( m_nSelection[ 0 ] );
			rcStart.right = GetPixelForSample( m_nSelection[ 1 ] );
		}
		break;
	case DRAGTYPE_PHONEME:
		{
			GetPhonemeTrayTopBottom( rcStart );
			m_bWordsActive = false;
		}
		break;
	case DRAGTYPE_WORD:
		{
			GetWordTrayTopBottom( rcStart );
			m_bWordsActive = true;
		}
		break;
	case DRAGTYPE_MOVEWORD:
		{
			TraverseWords( &PhonemeEditor::ITER_AddFocusRectSelectedWords, 0.0f );
			addrect = false;
			m_bWordsActive = true;
		}
		break;
	case DRAGTYPE_MOVEPHONEME:
		{
			TraversePhonemes( &PhonemeEditor::ITER_AddFocusRectSelectedPhonemes, 0.0f );
			addrect = false;
			m_bWordsActive = false;
		}
		break;
	case DRAGTYPE_EVENTTAG_MOVE:
		{
			rcStart.top = TAG_TOP;
			rcStart.bottom = TAG_BOTTOM;
			rcStart.left -= 10;
			rcStart.right += 10;
		}
		break;
	}

	if ( addrect )
	{
		AddFocusRect( rcStart );
	}
	
	DrawFocusRect( "start" );

	SetDragLimit( m_nDragType );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *event - 
// Output : int
//-----------------------------------------------------------------------------
int PhonemeEditor::handleEvent( mxEvent *event )
{
	MDLCACHE_CRITICAL_SECTION_( g_pMDLCache );

	int iret = 0;
	
	if ( HandleToolEvent( event ) )
	{
		return iret;
	}

	switch ( event->event )
	{
	case mxEvent::Action:
		{
			iret = 1;
			switch ( event->action )
			{
			case IDC_EXPORT_SENTENCE:
				{
					OnExport();
				}
				break;
			case IDC_IMPORT_SENTENCE:
				{
					OnImport();
				}
				break;
			case IDC_PLAYBACKRATE:
				{
					m_flPlaybackRate = m_pPlaybackRate->getValue();
					redraw();
				}
				break;
			case IDC_MODE_TAB:
				{
					// The mode changed, so reset stuff here
					EditorMode newMode = (EditorMode)m_pModeTab->getSelectedIndex();
					bool needpaint = ( m_CurrentMode != newMode );
					m_CurrentMode = newMode;
					if ( needpaint )
					{
						switch ( GetMode() )
						{
						default:
						case MODE_PHONEMES:
							SetSuffix( " - Normal" );
							break;
						case MODE_EMPHASIS:
							SetSuffix( " - Emphasis Track" );
							break;
						}

						OnModeChanged();
						redraw();
					}
				}
				break;
			case IDC_EMPHASIS_DELETE:
				Emphasis_Delete();
				break;
			case IDC_EMPHASIS_DESELECT:
				Emphasis_DeselectAll();
				break;
			case IDC_EMPHASIS_SELECTALL:
				Emphasis_SelectAll();
				break;
			case IDC_API_SAPI:
				OnSAPI();
				break;
			case IDC_API_LIPSINC:
				OnLipSinc();
				break;
			case IDC_PLAYBUTTON:
				Play();
				break;
			case IDC_UNDO:
				Undo();
				break;
			case IDC_REDO:
				Redo();
				break;
			case IDC_CLEARUNDO:
				ClearUndo();
				break;
			case IDC_ADDTAG:
				AddTag();
				break;
			case IDC_DELETETAG:
				DeleteTag();
				break;
			case IDC_COMMITEXTRACTED:
				CommitExtracted();
				SetFocus( (HWND)getHandle() );
				break;
			case IDC_CLEAREXTRACTED:
				ClearExtracted();
				break;
			case IDC_SEPARATEPHONEMES:
				SeparatePhonemes();
				break;
			case IDC_SNAPPHONEMES:
				SnapPhonemes();
				break;
			case IDC_SEPARATEWORDS:
				SeparateWords();
				break;
			case IDC_SNAPWORDS:
				SnapWords();
				break;
			case IDC_EDITWORDLIST:
				EditWordList();
				break;
			case IDC_EDIT_PHONEME:
				EditPhoneme();
				break;
			case IDC_EDIT_WORD:
				EditWord();
				break;
			case IDC_EDIT_INSERTPHONEMEBEFORE:
				EditInsertPhonemeBefore();
				break;
			case IDC_EDIT_INSERTPHONEMEAFTER:
				EditInsertPhonemeAfter();
				break;
			case IDC_EDIT_INSERTWORDBEFORE:
				EditInsertWordBefore();
				break;
			case IDC_EDIT_INSERTWORDAFTER:
				EditInsertWordAfter();
				break;
			case IDC_EDIT_DELETEPHONEME:
				EditDeletePhoneme();
				break;
			case IDC_EDIT_DELETEWORD:
				EditDeleteWord();
				break;
			case IDC_EDIT_INSERTFIRSTPHONEMEOFWORD:
				EditInsertFirstPhonemeOfWord();
				break;
			case IDC_PHONEME_PLAY_ORIG:
				{
					StopPlayback();
					if ( m_pWaveFile )
					{
						// Make sure phonemes are loaded
						FacePoser_EnsurePhonemesLoaded();

						sound->PlaySound( m_pWaveFile, VOL_NORM, &m_pMixer );
					}
				}
				break;
			case IDC_PHONEME_SCROLL:
				if (event->modifiers == SB_THUMBTRACK)
				{
					MoveTimeSliderToPos( event->height );
				}
				else if ( event->modifiers == SB_PAGEUP )
				{
					int offset = m_pHorzScrollBar->getValue();
					
					offset -= 10;
					offset = max( offset, m_pHorzScrollBar->getMinValue() );

					MoveTimeSliderToPos( offset );
				}
				else if ( event->modifiers == SB_PAGEDOWN )
				{
					int offset = m_pHorzScrollBar->getValue();
					
					offset += 10;
					offset = min( offset, m_pHorzScrollBar->getMaxValue() );

					MoveTimeSliderToPos( offset );
				}
				break;	
			case IDC_REDO_PHONEMEEXTRACTION:
				if ( m_Tags.m_Words.Size() <= 0 )
				{
					// This calls redo LISET if some words are actually entered
					EditWordList();
				}
				else
				{
					RedoPhonemeExtraction();
				}
				SetFocus( (HWND)getHandle() );
				break;
			case IDC_REDO_PHONEMEEXTRACTION_SELECTION:
				{
					RedoPhonemeExtractionSelected();
				}
				SetFocus( (HWND)getHandle() );
				break;
			case IDC_DESELECT:
				Deselect();
				redraw();
				break;
			case IDC_PLAY_EDITED:
				PlayEditedWave( false );
				SetFocus( (HWND)getHandle() );
				break;
			case IDC_PLAY_EDITED_SELECTION:
				PlayEditedWave( true );
				SetFocus( (HWND)getHandle() );
				break;
			case IDC_SAVE_LINGUISTIC:
				CommitChanges();
				SetFocus( (HWND)getHandle() );
				break;
			case IDC_LOADWAVEFILE:
				LoadWaveFile();
				SetFocus( (HWND)getHandle() );
				break;
			case IDC_CANCELPLAYBACK:
				StopPlayback();
				SetFocus( (HWND)getHandle() );
				break;
			case IDC_SELECT_WORDSRIGHT:
				SelectWords( true );
				break;
			case IDC_SELECT_WORDSLEFT:
				SelectWords( false );
				break;
			case IDC_SELECT_PHONEMESRIGHT:
				SelectPhonemes( true );
				break;
			case IDC_SELECT_PHONEMESLEFT:
				SelectPhonemes( false );
				break;
			case IDC_DESELECT_PHONEMESANDWORDS:
				DeselectPhonemes();
				DeselectWords();
				redraw();
				break;
			case IDC_CLEANUP:
				CleanupWordsAndPhonemes( true );
				redraw();
				break;
			case IDC_REALIGNPHONEMES:
				RealignPhonemesToWords( true );
				redraw();
				break;
			case IDC_REALIGNWORDS:
				RealignWordsToPhonemes( true );
				redraw();
				break;
			case IDC_TOGGLE_VOICEDUCK:
				OnToggleVoiceDuck();
				break;
			}

			if ( iret == 1 )
			{
				SetActiveTool( this );
				SetFocus( (HWND)getHandle() );
			}
		}
		break;
	case mxEvent::MouseWheeled:
		{
			// Zoom time in  / out
			if ( event->height > 0 )
			{
				m_nTimeZoom = min( m_nTimeZoom + m_nTimeZoomStep, MAX_TIME_ZOOM );
			}
			else
			{
				m_nTimeZoom = max( m_nTimeZoom - m_nTimeZoomStep, m_nTimeZoomStep );
			}
			RepositionHSlider();
			iret = 1;
		}
		break;
	case mxEvent::Size:
		{
			int bw	= 100;
			int x	= 5;
			int by = h2() - 18 - MODE_TAB_OFFSET;

			m_pModeTab->setBounds( 0, h2() - MODE_TAB_OFFSET, w2(), MODE_TAB_OFFSET );

			m_btnRedoPhonemeExtraction->setBounds( x, by, bw, 16 );
			x += bw;
			m_btnSave->setBounds( x, by, bw, 16 );
			x += bw;
			m_btnLoad->setBounds( x, by, bw, 16 );
			x += bw;
			m_btnPlay->setBounds( x, by, bw, 16 );
			x += bw;

			m_pPlaybackRate->setBounds( x, by, 100, 16 );

			RepositionHSlider();
			iret = 1;
		}
		break;
	case mxEvent::MouseDown:
		{
			iret = 1;

			CPhonemeTag *pt;
			CWordTag *wt;
			
			pt = GetPhonemeTagUnderMouse( (short)event->x, (short)event->y );
			wt = GetWordTagUnderMouse( (short)event->x, (short)event->y );

			bool ctrldown = ( event->modifiers & mxEvent::KeyCtrl ) ? true : false;
			bool shiftdown = ( event->modifiers & mxEvent::KeyShift ) ? true : false;
			
			if ( event->buttons & mxEvent::MouseRightButton )
			{
				RECT rc;
				GetWorkspaceRect( rc );

				if ( IsMouseOverWordRow( (short)event->y ) )
				{
					ShowWordMenu( wt,  (short)event->x, (short)event->y );
				}
				else if ( IsMouseOverPhonemeRow( (short)event->y ) )
				{
					ShowPhonemeMenu( pt, (short)event->x, (short)event->y );
				}
				else if ( IsMouseOverTagRow( (short)event->y ) )
				{
					ShowTagMenu( (short)event->x, (short)event->y );
				}
				else if ( IsMouseOverScrubArea( event ) )
				{
					float t = GetTimeForPixel( (short)event->x );

					ClampTimeToSelectionInterval( t );

					SetScrubTime( t );
					SetScrubTargetTime( t );

					redraw();
				}
				else
				{
					ShowContextMenu( (short)event->x, (short)event->y );
				}
				return iret;
			}
			
			if ( m_nDragType == DRAGTYPE_NONE )
			{
				CountSelected();

				int type = IsMouseOverBoundary( event );

				if ( IsMouseOverScrubArea( event ) )
				{
					if ( IsMouseOverScrubHandle( event ) )
					{
						StartDragging( DRAGTYPE_SCRUBBER, 
							(short)event->x, 
							(short)event->y, 
							LoadCursor( NULL, IDC_SIZEWE ) );

						float t = GetTimeForPixel( (short)event->x );
						m_flScrubberTimeOffset = m_flScrub - t;
						float maxoffset = 0.5f * (float)SCRUBBER_HANDLE_WIDTH / GetPixelsPerSecond();
						m_flScrubberTimeOffset = clamp( m_flScrubberTimeOffset, -maxoffset, maxoffset );
						t += m_flScrubberTimeOffset;
						ClampTimeToSelectionInterval( t );

						SetScrubTime( t );
						SetScrubTargetTime( t );

						DrawScrubHandle();
						iret = true;
					}
					else
					{
						float t = GetTimeForPixel( (short)event->x );

						ClampTimeToSelectionInterval( t );

						SetScrubTargetTime( t );

						iret = true;

					}
					return iret;
				}
				else if ( GetMode() == MODE_EMPHASIS )
				{
					CEmphasisSample *sample = Emphasis_GetSampleUnderMouse( event );
					if ( sample )
					{
						if  ( shiftdown ) 
						{
							sample->selected = !sample->selected;
							redraw();
						}
						else if ( sample->selected )
						{
							StartDragging( DRAGTYPE_EMPHASIS_MOVE, (short)event->x, (short)event->y, LoadCursor( NULL, IDC_SIZEALL ) );
						}
						else
						{
							if  ( !shiftdown ) 
							{
								Emphasis_DeselectAll();
								redraw();
							}

							StartDragging( DRAGTYPE_EMPHASIS_SELECT, (short)event->x, (short)event->y, NULL );
						}
						return true;
					}
					else if ( ctrldown )
					{
						// Add a sample point
						float t = GetTimeForPixel( (short)event->x );

						RECT rcWork;
						GetWorkspaceRect( rcWork );
						RECT rcEmphasis;
						Emphasis_GetRect( rcWork, rcEmphasis );

						int eh = rcEmphasis.bottom - rcEmphasis.top;
						int dy = (short)event->y - rcEmphasis.top;

						CEmphasisSample sample;
						sample.time = t;
						Assert( eh >= 0 );
						sample.value = (float)( dy ) / ( float ) eh;
						sample.value = 1.0f - clamp( sample.value, 0.0f, 1.0f );
						sample.selected = false;

						Emphasis_AddSample( sample );

						redraw();

						return true;
					}
					else
					{
						if  ( !shiftdown ) 
						{
							Emphasis_DeselectAll();
							redraw();
						}

						StartDragging( DRAGTYPE_EMPHASIS_SELECT, (short)event->x, (short)event->y, NULL );
						return true;
					}
				}
				else
				{
					if ( type == BOUNDARY_PHONEME && m_nSelectedPhonemeCount <= 1 )
					{
						StartDragging( DRAGTYPE_PHONEME, (short)event->x, (short)event->y, LoadCursor( NULL, IDC_SIZEWE ) );
						return true;
					}			
					else if ( type == BOUNDARY_WORD && m_nSelectedWordCount <= 1 )
					{
						StartDragging( DRAGTYPE_WORD, (short)event->x, (short)event->y, LoadCursor( NULL, IDC_SIZEWE ) );
						return true;
					}
					else if ( IsMouseOverSamples( (short)event->x, (short)event->y ) )
					{
						if ( !m_bSelectionActive )
						{
							StartDragging( DRAGTYPE_SELECTSAMPLES, (short)event->x, (short)event->y, LoadCursor( NULL, IDC_SIZEWE ) );
						}
						else
						{
							// Either move, move edge if ctrl key is held, or deselect
							if ( IsMouseOverSelection( (short)event->x, (short)event->y ) )
							{
								if ( IsMouseOverSelectionStartEdge( event ) )
								{
									StartDragging( DRAGTYPE_MOVESELECTIONSTART, (short)event->x, (short)event->y, LoadCursor( NULL, IDC_SIZEWE ) );
								}
								else if ( IsMouseOverSelectionEndEdge( event ) )
								{
									StartDragging( DRAGTYPE_MOVESELECTIONEND, (short)event->x, (short)event->y, LoadCursor( NULL, IDC_SIZEWE ) );
								}
								else
								{
									if ( shiftdown )
									{
										StartDragging( DRAGTYPE_MOVESELECTION, (short)event->x, (short)event->y, LoadCursor( NULL, IDC_SIZEALL ) );
									}
								}
							}
							else
							{
								Deselect();
								redraw();
								return iret;
							}
						}
						return true;
					}
				}
				
				if ( IsMouseOverTag( (short)event->x, (short)event->y ) )
				{
					StartDragging( DRAGTYPE_EVENTTAG_MOVE, (short)event->x, (short)event->y, LoadCursor( NULL, IDC_SIZEALL ) );
					return true;
				}
				else
				{
					if ( pt )
					{
						// Can only move when holding down shift key
						if ( shiftdown )
						{
							pt->m_bSelected = true;
							StartDragging( DRAGTYPE_MOVEPHONEME,
								(short)event->x, (short)event->y, LoadCursor( NULL, IDC_SIZEALL ) );
						}
						else
						{
							// toggle the selection
							pt->m_bSelected = !pt->m_bSelected;
						}


						m_bWordsActive = false;

						redraw();
						return true;
					}
					else if ( wt )
					{

						// Can only move when holding down shift key
						if ( shiftdown )
						{
							wt->m_bSelected = true;
							StartDragging( DRAGTYPE_MOVEWORD,
								(short)event->x, (short)event->y, LoadCursor( NULL, IDC_SIZEALL ) );
						}
						else
						{
							// toggle the selection
							wt->m_bSelected = !wt->m_bSelected;
						}

						m_bWordsActive = true;

						redraw();
						return true;
					}
					else if ( type == BOUNDARY_NONE )
					{
						DeselectPhonemes();
						DeselectWords();
						redraw();
						return true;
					}
				}
			}
		}
		break;
	case mxEvent::MouseMove:
	case mxEvent::MouseDrag:
		{
			OnMouseMove( event );
			iret = 1;
		}
		break;
	case mxEvent::MouseUp:
		{
			if ( m_nDragType != DRAGTYPE_NONE )
			{
				int mx = (short)event->x;

				LimitDrag( mx );

				event->x = (short)mx;

				DrawFocusRect( "finish" );

				if ( m_hPrevCursor )
				{
					SetCursor( m_hPrevCursor );
					m_hPrevCursor = 0;
				}

				switch ( m_nDragType )
				{
				case DRAGTYPE_WORD:
					FinishWordMove( m_nStartX, (short)event->x );
					break;
				case DRAGTYPE_PHONEME:
					FinishPhonemeMove( m_nStartX, (short)event->x );
					break;
				case DRAGTYPE_SELECTSAMPLES:
					FinishSelect( m_nStartX, (short)event->x );
					break;
				case DRAGTYPE_MOVESELECTION:
					FinishMoveSelection( m_nStartX, (short)event->x );
					break;
				case DRAGTYPE_MOVESELECTIONSTART:
					FinishMoveSelectionStart( m_nStartX, (short)event->x );
					break;
				case DRAGTYPE_MOVESELECTIONEND:
					FinishMoveSelectionEnd( m_nStartX, (short)event->x );
					break;
				case DRAGTYPE_MOVEWORD:
					FinishWordDrag( m_nStartX, (short)event->x );
					break;
				case DRAGTYPE_MOVEPHONEME:
					FinishPhonemeDrag( m_nStartX, (short)event->x );
					break;
				case DRAGTYPE_EVENTTAG_MOVE:
					FinishEventTagDrag( m_nStartX, (short)event->x );
					break;
				case DRAGTYPE_EMPHASIS_MOVE:
					{
						Emphasis_MouseDrag( (short)event->x, (short)event->y );
						m_Tags.Resort();

						PushRedo();

						redraw();
					}
					break;
				case DRAGTYPE_EMPHASIS_SELECT:
					{
						Emphasis_SelectPoints();
						redraw();
					}
					break;
				case DRAGTYPE_SCRUBBER:
					{
						float t = GetTimeForPixel( (short)event->x );
						t += m_flScrubberTimeOffset;
						m_flScrubberTimeOffset = 0.0f;

						ClampTimeToSelectionInterval( t );

						SetScrubTime( t );
						SetScrubTargetTime( t );

						sound->Flush(); 

						DrawScrubHandle();
					}
					break;
				default:
					break;
				}

				m_nDragType = DRAGTYPE_NONE;
			}
			iret = 1;
		}
		break;
	case mxEvent::KeyUp:
		{
			bool shiftDown = GetAsyncKeyState( VK_SHIFT ) ? true : false;
			bool ctrlDown = GetAsyncKeyState( VK_CONTROL ) ? true : false;

			switch( event->key )
			{
				case VK_TAB:
					{
						int direction = shiftDown ? -1 : 1;
						SelectNextWord( direction );
					}
					break;
				case VK_NEXT:
				case VK_PRIOR:
					{
						m_bWordsActive = event->key == VK_PRIOR ? true : false;
						redraw();
					}
					break;
				case VK_UP:
				case VK_RETURN:
					if ( m_bWordsActive )
					{
						if ( event->key == VK_UP ||
							ctrlDown )
						{
							CountSelected();

							if ( m_nSelectedWordCount == 1 )
							{
								// Find the selected one
								for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
								{
									CWordTag *word = m_Tags.m_Words[ i ];
									if ( !word || !word->m_bSelected )
										continue;

									EditWord( word, true );
								}
							}
						}
					}
					else
					{
						if ( event->key == VK_UP ||
							ctrlDown )
						{
							CountSelected();

							if ( m_nSelectedPhonemeCount == 1 )
							{
								// Find the selected one
								for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
								{
									CWordTag *word = m_Tags.m_Words[ i ];
									if ( !word )
										continue;

									for ( int j = 0; j < word->m_Phonemes.Size(); j++ )
									{
										CPhonemeTag *phoneme = word->m_Phonemes[ j ];
										if ( !phoneme )
											continue;

										if ( !phoneme->m_bSelected )
											continue;
								
										EditPhoneme( phoneme, true );
									}
								}
							}
						}
					}
					break;
				case VK_DELETE:
					if ( GetMode() == MODE_EMPHASIS )
					{
						Emphasis_Delete();
					}
					else
					{
						if ( m_bWordsActive )
						{
							EditDeleteWord();
						}
						else
						{
							EditDeletePhoneme();
						}
					}
					break;
				case VK_INSERT:
					if ( m_bWordsActive )
					{
						if ( shiftDown )
						{
							EditInsertWordBefore();
						}
						else
						{
							EditInsertWordAfter();
						}
					}
					else
					{
						if ( shiftDown )
						{
							EditInsertPhonemeBefore();
						}
						else
						{
							EditInsertPhonemeAfter();
						}
					}
					break;
				case VK_SPACE:
					if ( m_pWaveFile && sound->IsSoundPlaying( m_pMixer ) )
					{
						Con_Printf( "Stopping playback\n" );
						m_btnPlay->setLabel( "Play (Spacebar)" );
						StopPlayback();	
					}
					else
					{
						Con_Printf( "Playing .wav\n" );
						m_btnPlay->setLabel( "Stop[ (Spacebar)" );
						PlayEditedWave( m_bSelectionActive );
					}
					break;
				case VK_SHIFT:
				case VK_CONTROL:
					{
						// Force mouse move
						POINT pt;
						GetCursorPos( &pt );
						SetCursorPos( pt.x, pt.y );
						return 0;
					}
					break;
				case VK_ESCAPE:
					{
						// If playing sound, stop it, otherwise, deselect all
						if ( !StopPlayback() )
						{
							Deselect();
							DeselectPhonemes();
							DeselectWords();
							Emphasis_DeselectAll();
							redraw();
						}
					}
					break;
				case 'O':
					{
						if ( ctrlDown )
						{
							LoadWaveFile();
						}
					}
					break;
				case 'S':
					{
						if ( ctrlDown )
						{
							CommitChanges();
						}
					}
					break;
				case 'T':
					{
						if ( ctrlDown )
						{
							// Edit sentence text
							EditWordList();
						}
					}
					break;
				case 'G':
					{
						if ( ctrlDown )
						{
							// Commit extraction
							CommitExtracted();
						}
					}
					break;
				case 'R':
					{
						if ( ctrlDown )
						{
							RedoPhonemeExtraction();
						}
					}
					break;
				default:
					break;
			}

			SetFocus( (HWND)getHandle() );
			iret = 1;
		}
		break;
	case mxEvent::KeyDown:
		{
			switch ( event->key )
			{
			case 'Z':
				if ( GetAsyncKeyState( VK_CONTROL ) )
				{
					Undo();
				}
				break;
			case 'Y':
				if ( GetAsyncKeyState( VK_CONTROL ) )
				{
					Redo();
				}
				break;


			case VK_RIGHT:
			case VK_LEFT:
				{
					int direction = event->key == VK_LEFT ? -1 : 1;

					if ( !m_bWordsActive )
					{
						if ( GetAsyncKeyState( VK_CONTROL ) )
						{
							ExtendSelectedPhonemeEndTime( direction );
						}
						else if ( GetAsyncKeyState( VK_SHIFT ) )
						{
							ShiftSelectedPhoneme( direction );
						}
						else
						{
							SelectNextPhoneme( direction );
						}
					}
					else
					{
						if ( GetAsyncKeyState( VK_CONTROL ) )
						{
							ExtendSelectedWordEndTime( direction );
						}
						else if ( GetAsyncKeyState( VK_SHIFT ) )
						{
							ShiftSelectedWord( direction );
						}
						else
						{
							SelectNextWord( direction );
						}
					}
				}
				break;
			case VK_RETURN:
				{
				}
				break;
			case VK_SHIFT:
			case VK_CONTROL:
				{
					// Force mouse move
					POINT pt;
					GetCursorPos( &pt );
					//SetCursorPos( pt.x -1, pt.y );
					SetCursorPos( pt.x, pt.y );
					return 0;
				}
				break;
			default:
				break;
			}
			iret = 1;
		}
		break;
	}
	return iret;
}

void PhonemeEditor::DrawWords( CChoreoWidgetDrawHelper& drawHelper, RECT& rcWorkSpace, CSentence& sentence, int type, bool showactive /* = true */ )
{
	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	int ypos = rcWorkSpace.top + m_nTickHeight + 2;

	if ( type == 1 )
	{
		ypos += m_nTickHeight + 5;
	}

	const char *fontName = "Arial";

	bool drawselected;
	for ( int pass = 0; pass < 2 ; pass++ )
	{
		drawselected = pass == 0 ? false : true;

		for (int k = 0; k < sentence.m_Words.Size(); k++)
		{
			CWordTag *word = sentence.m_Words[ k ];
			if ( !word )
				continue;

			if ( word->m_bSelected != drawselected )
				continue;

			bool hasselectedphonemes = false;
			for ( int p = 0; p < word->m_Phonemes.Size() && !hasselectedphonemes; p++ )
			{
				CPhonemeTag *t = word->m_Phonemes[ p ];
				if ( t->m_bSelected )
				{
					hasselectedphonemes = true;
				}
			}

			float t1 = word->m_flStartTime;
			float t2 = word->m_flEndTime;

			// Tag it
			float frac = ( t1 - starttime ) / ( endtime - starttime );

			int xpos = ( int )( frac * rcWorkSpace.right );

			if ( frac <= 0.0 )
				xpos = 0;

			// Draw duration
			float frac2  = ( t2 - starttime ) / ( endtime - starttime );
			if ( frac2 < 0.0 )
				continue;

			int xpos2 = ( int )( frac2 * rcWorkSpace.right );

			// Draw line and vertical ticks
			RECT rcWord;
			rcWord.left = xpos;
			rcWord.right = xpos2;
			rcWord.top = ypos - m_nTickHeight + 1;
			rcWord.bottom = ypos;

			drawHelper.DrawFilledRect( 
				PEColor( word->m_bSelected ? COLOR_PHONEME_TAG_SELECTED : COLOR_PHONEME_TAG_FILLER_NORMAL ), 
				rcWord );

			COLORREF border = PEColor( word->m_bSelected ? COLOR_PHONEME_TAG_BORDER_SELECTED : COLOR_PHONEME_TAG_BORDER );

			if ( showactive && m_bWordsActive )
			{
				drawHelper.DrawFilledRect( PEColor( COLOR_PHONEME_ACTIVE_BORDER ), xpos, ypos - m_nTickHeight, xpos2, ypos - m_nTickHeight + 4 );
			}

			drawHelper.DrawColoredLine( border, PS_SOLID, 1, xpos, ypos, xpos2, ypos );
			drawHelper.DrawColoredLine( border, PS_SOLID, 1, xpos, ypos, xpos, ypos - m_nTickHeight );
			drawHelper.DrawColoredLine( border, PS_SOLID, 1, xpos2, ypos, xpos2, ypos - m_nTickHeight );
			drawHelper.DrawColoredLine( border, PS_SOLID, 1, xpos, ypos - m_nTickHeight, xpos2, ypos - m_nTickHeight );

			if ( hasselectedphonemes )
			{
				drawHelper.DrawFilledRect( PEColor( COLOR_PHONEME_SELECTED_BORDER ), xpos, ypos - 3, xpos2, ypos );
			}
		
			//if ( frac >= 0.0 && frac <= 1.0 )
			{
				int fontsize = 9;

				RECT rcText;
				rcText.left = xpos;
				rcText.right = xpos + 500;
				rcText.top = ypos - m_nTickHeight + 4;
				rcText.bottom = rcText.top + fontsize + 2;

				int length = drawHelper.CalcTextWidth( fontName, fontsize, FW_NORMAL, "%s", word->GetWord() );

				rcText.right = max( (LONG)xpos2 - 2, rcText.left + length + 1 );

				int w = rcText.right - rcText.left;
				if ( w > length )
				{
					rcText.left += ( w - length ) / 2;
				}

				drawHelper.DrawColoredText( 
					fontName, 
					fontsize, 
					FW_NORMAL, 
					PEColor( word->m_bSelected ? COLOR_PHONEME_TAG_TEXT_SELECTED : COLOR_PHONEME_TAG_TEXT ), 
					rcText,
					"%s", word->GetWord() );
			}

		}
	}
}

void PhonemeEditor::DrawPhonemes( CChoreoWidgetDrawHelper& drawHelper, RECT& rcWorkSpace, CSentence& sentence, int type, bool showactive /* = true */ )
{
	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	int ypos = rcWorkSpace.bottom - m_nTickHeight - 2;

	if ( type == 1 )
	{
		ypos -= ( m_nTickHeight + 5 );
	}

	const char *fontName = "Arial";

	bool drawselected;
	for ( int pass = 0; pass < 2 ; pass++ )
	{
		drawselected = pass == 0 ? false : true;

		for ( int i = 0; i < sentence.m_Words.Size(); i++ )
		{
			CWordTag *w = sentence.m_Words[ i ];
			if ( !w )
				continue;

			if ( w->m_bSelected != drawselected )
				continue;

			for ( int k = 0; k < w->m_Phonemes.Size(); k++ )
			{
				CPhonemeTag *pPhoneme = w->m_Phonemes[ k ];

				float t1 = pPhoneme->GetStartTime();
				float t2 = pPhoneme->GetEndTime();

				// Tag it
				float frac = ( t1 - starttime ) / ( endtime - starttime );

				int xpos = ( int )( frac * rcWorkSpace.right );
				if ( frac <= 0.0 )
				{
					xpos = 0;
				}

				// Draw duration
				float frac2  = ( t2 - starttime ) / ( endtime - starttime );
				if ( frac2 < 0.0 )
				{
					continue;
				}

				int xpos2 = ( int )( frac2 * rcWorkSpace.right );

				RECT rcFrame;
				rcFrame.left = xpos;
				rcFrame.right = xpos2;
				rcFrame.top = ypos - m_nTickHeight + 1;
				rcFrame.bottom = ypos;

				drawHelper.DrawFilledRect( 
					PEColor( pPhoneme->m_bSelected ? COLOR_PHONEME_TAG_SELECTED : COLOR_PHONEME_TAG_FILLER_NORMAL ),
					rcFrame );

				COLORREF border = PEColor( pPhoneme->m_bSelected ? COLOR_PHONEME_TAG_BORDER_SELECTED : COLOR_PHONEME_TAG_BORDER );

				if ( showactive && !m_bWordsActive )
				{
					drawHelper.DrawFilledRect( PEColor( COLOR_PHONEME_ACTIVE_BORDER ), xpos, ypos - 3, xpos2, ypos );
				}

				drawHelper.DrawColoredLine( border, PS_SOLID, 1, xpos, ypos - m_nTickHeight, xpos2, ypos - m_nTickHeight );
				drawHelper.DrawColoredLine( border, PS_SOLID, 1, xpos, ypos, xpos, ypos - m_nTickHeight );
				drawHelper.DrawColoredLine( border, PS_SOLID, 1, xpos2, ypos, xpos2, ypos - m_nTickHeight );
				drawHelper.DrawColoredLine( border, PS_SOLID, 1, xpos, ypos, xpos2, ypos );

				if ( w->m_bSelected )
				{
					drawHelper.DrawFilledRect( PEColor( COLOR_PHONEME_SELECTED_BORDER ), xpos, ypos - m_nTickHeight + 1, xpos2, ypos - m_nTickHeight + 4 );
				}

				//if ( frac >= 0.0 && frac <= 1.0 )
				{

					int fontsize = 9;

					RECT rcText;
					rcText.left = xpos;
					rcText.right = xpos + 500;
					rcText.top = ypos - m_nTickHeight + 4;
					rcText.bottom = rcText.top + fontsize + 2;

					int length = drawHelper.CalcTextWidth( fontName, fontsize, FW_NORMAL, "%s", ConvertPhoneme( pPhoneme->GetPhonemeCode() ) );

					rcText.right = max( (LONG)xpos2 - 2, rcText.left + length + 1 );

					int w = rcText.right - rcText.left;
					if ( w > length )
					{
						rcText.left += ( w - length ) / 2;
					}

					drawHelper.DrawColoredText( 
						fontName, 
						fontsize, 
						FW_NORMAL, 
						PEColor( pPhoneme->m_bSelected ? COLOR_PHONEME_TAG_TEXT_SELECTED : COLOR_PHONEME_TAG_TEXT ), 
						rcText,
						"%s", ConvertPhoneme( pPhoneme->GetPhonemeCode() ) );
				}
			}
		}
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : drawHelper - 
//			rc - 
//-----------------------------------------------------------------------------
void PhonemeEditor::DrawRelativeTags( CChoreoWidgetDrawHelper& drawHelper, RECT& rc )
{
	if ( !m_pEvent || !m_pWaveFile )
		return;

	drawHelper.DrawColoredText( "Arial", 9, FW_NORMAL, PEColor( COLOR_PHONEME_TIMING_TAG ), rc, "Timing Tags:" );

	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	for ( int i = 0; i < m_pEvent->GetNumRelativeTags(); i++ )
	{
		CEventRelativeTag *tag = m_pEvent->GetRelativeTag( i );
		if ( !tag )
			continue;

		// 
		float tagtime = tag->GetPercentage() * m_pWaveFile->GetRunningLength();
		if ( tagtime < starttime || tagtime > endtime )
			continue;

		float frac = ( tagtime - starttime ) / ( endtime - starttime );

		int left = rc.left + (int)( frac * ( float )( rc.right - rc.left ) + 0.5f );

		RECT rcMark;
		rcMark = rc;
		rcMark.top = rc.bottom - 8;
		rcMark.bottom = rc.bottom;
		rcMark.left = left - 4;
		rcMark.right = left + 4;

		drawHelper.DrawTriangleMarker( rcMark, PEColor( COLOR_PHONEME_TIMING_TAG ) );

		RECT rcText;
		rcText = rc;
		rcText.bottom = rc.bottom - 10;
		rcText.top = rcText.bottom - 10;
	
		int len = drawHelper.CalcTextWidth( "Arial", 9, FW_NORMAL, tag->GetName() );
		rcText.left = left - len / 2;
		rcText.right = rcText.left + len + 2;

		drawHelper.DrawColoredText( "Arial", 9, FW_NORMAL, PEColor( COLOR_PHONEME_TIMING_TAG ), rcText, tag->GetName() );
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::redraw( void )
{
	if ( !ToolCanDraw() )
		return;

	CChoreoWidgetDrawHelper drawHelper( this );
	HandleToolRedraw( drawHelper );

	if ( !m_pWaveFile )
		return;

	HDC dc = drawHelper.GrabDC();

	RECT rc;
	GetWorkspaceRect( rc );

	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	// Now draw the time legend
	RECT rcLabel;
	float granularity = 0.5f; 

	drawHelper.DrawColoredLine( PEColor( COLOR_PHONEME_TIMELINE ), PS_SOLID, 1, rc.left, rc.bottom - m_nTickHeight, rc.right, rc.bottom - m_nTickHeight );

	if ( GetMode() != MODE_EMPHASIS )
	{
		Emphasis_Redraw( drawHelper, rc );
	}

	sound->RenderWavToDC( 
		dc, 
		rc, 
		PEColor( COLOR_PHONEME_WAVDATA ), 
		starttime, 
		endtime, 
		m_pWaveFile,
		m_bSelectionActive,
		m_nSelection[ 0 ],
		m_nSelection[ 1 ] );

	float f = SnapTime( starttime, granularity );
	while ( f <= endtime )
	{
		float frac = ( f - starttime ) / ( endtime - starttime );
		if ( frac >= 0.0f && frac <= 1.0f )
		{
			drawHelper.DrawColoredLine( ( COLOR_PHONEME_TIMELINE_MAJORTICK ), PS_SOLID, 1, (int)( frac * rc.right ), rc.top, (int)( frac * rc.right ), rc.bottom - m_nTickHeight );

			rcLabel.left = (int)( frac * rc.right );
			rcLabel.bottom = rc.bottom;
			rcLabel.top = rcLabel.bottom - 10;

			char sz[ 32 ];
			sprintf( sz, "%.2f", f );
			int textWidth = drawHelper.CalcTextWidth( "Arial", 9, FW_NORMAL, sz );
			rcLabel.right = rcLabel.left + textWidth;
			OffsetRect( &rcLabel, -textWidth / 2, 0 );
			drawHelper.DrawColoredText( "Arial", 9, FW_NORMAL, PEColor( COLOR_PHONEME_TEXT ), rcLabel, sz );
		}
		f += granularity;
	}

	HBRUSH br = CreateSolidBrush( PEColor( COLOR_PHONEME_TEXT ) );

	FrameRect( dc, &rc, br );

	DeleteObject( br );

	RECT rcTags = rc;
	rcTags.top = TAG_TOP;
	rcTags.bottom = TAG_BOTTOM;

	DrawRelativeTags( drawHelper, rcTags );

	int fontsize = 9;
	RECT rcText = rc;
	rcText.top = rcText.bottom + 5;
	rcText.left += 5;
	rcText.bottom = rcText.top + fontsize + 1;
	rcText.right -= 5;

	int fontweight = FW_NORMAL;

	const char *font = "Arial";

	if ( m_nLastExtractionResult != SR_RESULT_NORESULT )
	{
		COLORREF clr = PEColor( COLOR_PHONEME_EXTRACTION_RESULT_OTHER );
		switch ( m_nLastExtractionResult )
		{
		case SR_RESULT_ERROR:
			clr = PEColor( COLOR_PHONEME_EXTRACTION_RESULT_ERROR );
			break;
		case SR_RESULT_SUCCESS:
			clr = PEColor( COLOR_PHONEME_EXTRACTION_RESULT_SUCCESS );
			break;
		case SR_RESULT_FAILED:
			clr = PEColor( COLOR_PHONEME_EXTRACTION_RESULT_FAIL );
			break;
		default:
			break;
		}

		drawHelper.DrawColoredText( font, fontsize, fontweight, clr, rcText,
			"Last Extraction Result:  %s", GetExtractionResultString( m_nLastExtractionResult ) );
	
		OffsetRect( &rcText, 0, fontsize + 1 );
	}

	if ( m_pEvent && !Q_stristr( m_pEvent->GetParameters(), ".wav" ) )
	{
		drawHelper.DrawColoredText( font, fontsize, fontweight, PEColor( COLOR_PHONEME_TEXT ), rcText,
			"Sound: '%s', file: %s, length %.2f seconds", 
			m_pEvent->GetParameters(),
			m_WorkFile.m_szWaveFile, 
			m_pWaveFile->GetRunningLength() );
	}
	else
	{
		drawHelper.DrawColoredText( font, fontsize, fontweight, PEColor( COLOR_PHONEME_TEXT ), rcText,
			"File: %s, length %.2f seconds", m_WorkFile.m_szWaveFile, m_pWaveFile->GetRunningLength() );
	}

	OffsetRect( &rcText, 0, fontsize + 1 );

	drawHelper.DrawColoredText( font, fontsize, fontweight, PEColor( COLOR_PHONEME_TEXT ), rcText,
		"Number of samples %i at %ikhz (%i bits/sample) %s", (int) (m_pWaveFile->GetRunningLength() * m_pWaveFile->SampleRate() ), m_pWaveFile->SampleRate(), (m_pWaveFile->SampleSize()<<3), m_Tags.GetVoiceDuck() ? "duck other audio" : "no ducking" );

	OffsetRect( &rcText, 0, fontsize + 1 );

	drawHelper.DrawColoredText( font, fontsize, fontweight, PEColor( COLOR_PHONEME_TEXT ), rcText,
		"[ %i ] Words [ %i ] Phonemes / Zoom %i %%", m_Tags.m_Words.Size(), m_Tags.CountPhonemes(), m_nTimeZoom );

	if ( m_pEvent )
	{
		OffsetRect( &rcText, 0, fontsize + 1 );

		drawHelper.DrawColoredText( font, fontsize, fontweight, PEColor( COLOR_PHONEME_TEXT ), rcText,
			"Event %s", m_pEvent->GetName() );
	}

	OffsetRect( &rcText, 0, fontsize + 1 );

	drawHelper.DrawColoredText( font, fontsize, fontweight, PEColor( COLOR_PHONEME_TEXT ), rcText,
		"Using:  %s", GetSpeechAPIName() );


	char text[ 4096 ];
	sprintf( text, "Sentence Text:  %s", m_Tags.GetText() );

	int halfwidth = ( rc.right - rc.left ) / 2;

	rcText = rc;
	rcText.left = halfwidth;
	rcText.top = rcText.bottom + 5;
	rcText.right = rcText.left + halfwidth * 0.6;

	drawHelper.CalcTextRect( font, fontsize, fontweight, halfwidth, rcText, text );

	drawHelper.DrawColoredTextMultiline( font, fontsize, fontweight, PEColor( COLOR_PHONEME_TEXT ), rcText,
		text );

	CWordTag *cw = GetSelectedWord();
	if ( cw )
	{
		char wordInfo[ 512 ];
		sprintf( wordInfo, "Word:  %s, start %.2f end %.2f, duration %.2f ms phonemes %i",
			cw->GetWord(), cw->m_flStartTime, cw->m_flEndTime, 1000.0f * ( cw->m_flEndTime - cw->m_flStartTime ),
			cw->m_Phonemes.Size() );

		int length = drawHelper.CalcTextWidth( font, fontsize, fontweight, wordInfo );

		OffsetRect( &rcText, 0, ( rcText.bottom - rcText.top ) + 2 );

		rcText.left = rcText.right - length - 10;
		rcText.bottom = rcText.top + fontsize + 1;

		drawHelper.DrawColoredText( font, fontsize, fontweight, PEColor( COLOR_PHONEME_TEXT ), rcText, wordInfo );
	}

	CPhonemeTag *cp = GetSelectedPhoneme();
	if ( cp )
	{
		char phonemeInfo[ 512 ];
		sprintf( phonemeInfo, "Phoneme:  %s, start %.2f end %.2f, duration %.2f ms",
			ConvertPhoneme( cp->GetPhonemeCode() ), cp->GetStartTime(), cp->GetEndTime(), 1000.0f * ( cp->GetEndTime() - cp->GetStartTime() ) );

		int length = drawHelper.CalcTextWidth( font, fontsize, fontweight, phonemeInfo );

		OffsetRect( &rcText, 0, ( rcText.bottom - rcText.top ) + 2 );

		rcText.left = rcText.right - length - 10;
		rcText.bottom = rcText.top + fontsize + 1;

		drawHelper.DrawColoredText( font, fontsize, fontweight, PEColor( COLOR_PHONEME_TEXT ), rcText, phonemeInfo );
	}

	// Draw playback rate
	{
		char sz[ 48 ];
		sprintf( sz, "Speed: %.2fx", m_flPlaybackRate );

		int length = drawHelper.CalcTextWidth( font, fontsize, fontweight, sz);
		
		rcText = rc;
		rcText.top = rc.bottom + 60;
		rcText.bottom = rcText.top + fontsize + 1;
		rcText.left = m_pPlaybackRate->x() + m_pPlaybackRate->w() - x();
		rcText.right = rcText.left + length + 2;

		drawHelper.DrawColoredText( font, fontsize, fontweight, 
			PEColor( COLOR_PHONEME_TEXT ), rcText, sz );
	}

	if ( m_UndoStack.Size() > 0 )
	{
		int length = drawHelper.CalcTextWidth( font, fontsize, fontweight, 
			"Undo levels:  %i/%i", m_nUndoLevel, m_UndoStack.Size() );

		rcText = rc;
		rcText.top = rc.bottom + 60;
		rcText.bottom = rcText.top + fontsize + 1;
		rcText.right -= 5;
		rcText.left = rcText.right - length - 10;

		drawHelper.DrawColoredText( font, fontsize, fontweight, PEColor( COLOR_PHONEME_EXTRACTION_RESULT_SUCCESS ), rcText,
			"Undo levels:  %i/%i", m_nUndoLevel, m_UndoStack.Size() );
	}

	float endfrac = ( m_pWaveFile->GetRunningLength() - starttime ) / ( endtime - starttime );
	if ( endfrac >= 0.0f && endfrac <= 1.0f )
	{
		int endpos = ( int ) ( rc.right * endfrac );

		drawHelper.DrawColoredLine( PEColor( COLOR_PHONEME_WAV_ENDPOINT ), PS_DOT, 2, endpos, rc.top, endpos, rc.bottom - m_nTickHeight );
	}

	DrawPhonemes( drawHelper, rc, m_Tags, 0 );

	DrawPhonemes( drawHelper, rc, m_TagsExt, 1, false );

	DrawWords( drawHelper, rc, m_Tags, 0 );

	DrawWords( drawHelper, rc, m_TagsExt, 1, false );

	if ( GetMode() == MODE_EMPHASIS )
	{
		Emphasis_Redraw( drawHelper, rc );
	}

	DrawScrubHandle( drawHelper );
}

#define MOTION_RANGE 3000
#define MOTION_MAXSTEP 500
//-----------------------------------------------------------------------------
// Purpose: Brown noise simulates brownian motion centered around 127.5 but we cap the walking
//  to just a couple of units
// Input  : *buffer - 
//			count - 
// Output : static void
//-----------------------------------------------------------------------------
static void WriteBrownNoise( void *buffer, int count )
{
	int currentValue = 127500;
	int maxValue = currentValue + ( MOTION_RANGE / 2 );
	int minValue = currentValue - ( MOTION_RANGE / 2 );

	unsigned char *pos = ( unsigned char *)buffer;

	while ( --count >= 0 )
	{
		currentValue += random->RandomInt( -MOTION_MAXSTEP, MOTION_MAXSTEP );
		currentValue = min( maxValue, currentValue );
		currentValue = max( minValue, currentValue );

		// Downsample to 0-255 range
		*pos++ = (unsigned char)( ( (float)currentValue / 1000.0f ) + 0.5f );
	}
}

//-----------------------------------------------------------------------------
// Purpose: Replace with brownian noice parts of the wav file that we dont' want processed by the
//  speech recognizer
// Input  : store - 
//			*format - 
//			chunkname - 
//			*buffer - 
//			buffersize - 
//-----------------------------------------------------------------------------
void PhonemeEditor::ResampleChunk( IterateOutputRIFF& store, void *format, int chunkname, char *buffer, int buffersize, int start_silence /*=0*/, int end_silence /*=0*/ )
{
	WAVEFORMATEX *pFormat = ( WAVEFORMATEX * )format;
	Assert( pFormat );

	if ( pFormat->wFormatTag == WAVE_FORMAT_PCM )
	{
		int silience_time = start_silence + end_silence;

		// Leave room for silence at start + end
		int resamplesize = buffersize + silience_time * pFormat->nSamplesPerSec;
		char *resamplebuffer = new char[ resamplesize + 4 ];
		memset( resamplebuffer, (unsigned char)128, resamplesize + 4 );

		int startpos = (int)( start_silence * pFormat->nSamplesPerSec );

		if ( startpos > 0 )
		{
			WriteBrownNoise( resamplebuffer, startpos );
		}

		if ( startpos + buffersize < resamplesize )
		{
			WriteBrownNoise( &resamplebuffer[ startpos + buffersize ], resamplesize - ( startpos + buffersize ) );
		}

		memcpy( &resamplebuffer[ startpos ], buffer, buffersize );

		store.ChunkWriteData( resamplebuffer, resamplesize );
		return;
	}

	store.ChunkWriteData( buffer, buffersize );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::ReadLinguisticTags( void )
{
	if ( !m_pWaveFile )
		return;

	CAudioSource *wave = sound->LoadSound( m_WorkFile.m_szWorkingFile );
	if ( !wave )
		return;

	m_Tags.Reset();

	CSentence *sentence = wave->GetSentence();
	if ( sentence )
	{
		// Copy data from sentence to m_Tags
		m_Tags.Reset();
		m_Tags = *sentence;
	}

	delete wave;
}

//-----------------------------------------------------------------------------
// Purpose: Switch wave files
// Input  : *wavefile - 
//			force - 
//-----------------------------------------------------------------------------
void PhonemeEditor::SetCurrentWaveFile( const char *wavefile, bool force /*=false*/, CChoreoEvent *event /*=NULL*/ )
{
	// No change?
	if ( !force && !stricmp( m_WorkFile.m_szWaveFile, wavefile ) )
		return;

	StopPlayback();

	if ( GetDirty() )
	{
		int retval = mxMessageBox( this, va( "Save current changes to %s", m_WorkFile.m_szWaveFile ),
			"Phoneme Editor", MX_MB_QUESTION | MX_MB_YESNOCANCEL );

		// Cancel
		if ( retval == 2 )
			return;

		// Yes
		if ( retval == 0 )
		{
			CommitChanges();
		}
	}

	ClearExtracted();

	m_Tags.Reset();
	m_TagsExt.Reset();

	Deselect();

	if ( m_pWaveFile )
	{
		char fn[ 512 ];
		Q_snprintf( fn, sizeof( fn ), "%s%s", m_WorkFile.m_szBasePath, m_WorkFile.m_szWorkingFile );
		filesystem->RemoveFile( fn, "GAME" );
	}

	delete m_pWaveFile;
	m_pWaveFile = NULL;

	SetDirty( false );

	// Set up event and scene
	m_pEvent = event;

	// Try an dload new sound
	m_pWaveFile = sound->LoadSound( wavefile );
	Q_strncpy( m_WorkFile.m_szWaveFile, wavefile, sizeof( m_WorkFile.m_szWaveFile ) );

	char fullpath[ 512 ];
	filesystem->RelativePathToFullPath( wavefile, "GAME", fullpath, sizeof( fullpath ) );
	int len = Q_strlen( fullpath );
	int charstocopy = len - Q_strlen( wavefile ) + 1;
	m_WorkFile.m_szBasePath[ 0 ] = 0;
	if ( charstocopy >= 0 )
	{
		Q_strncpy( m_WorkFile.m_szBasePath, fullpath, charstocopy );
		m_WorkFile.m_szBasePath[ charstocopy ] = 0;
	}
	Q_StripExtension( wavefile, m_WorkFile.m_szWorkingFile, sizeof( m_WorkFile.m_szWorkingFile ) );
	Q_strncat( m_WorkFile.m_szWorkingFile, "_work.wav", sizeof( m_WorkFile.m_szWorkingFile ), COPY_ALL_CHARACTERS );

	Q_FixSlashes( m_WorkFile.m_szWaveFile );
	Q_FixSlashes( m_WorkFile.m_szWorkingFile );
	Q_FixSlashes( m_WorkFile.m_szBasePath );

	if ( !m_pWaveFile )
	{
		Con_ErrorPrintf( "Couldn't set current .wav file to %s\n", m_WorkFile.m_szWaveFile );
		return;
	}

	Con_Printf( "Current .wav file set to %s\n", m_WorkFile.m_szWaveFile );

	g_pWaveBrowser->SetCurrent( m_WorkFile.m_szWaveFile );

	// Copy over and overwrite file
	FPCopyFile( m_WorkFile.m_szWaveFile, m_WorkFile.m_szWorkingFile, false );
	// Make it writable
	MakeFileWriteable( m_WorkFile.m_szWorkingFile );

	ReadLinguisticTags();

	Deselect();

	RepositionHSlider();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : x - 
//-----------------------------------------------------------------------------
void PhonemeEditor::MoveTimeSliderToPos( int x )
{
	m_nLeftOffset = x;
	m_pHorzScrollBar->setValue( m_nLeftOffset );
	InvalidateRect( (HWND)m_pHorzScrollBar->getHandle(), NULL, TRUE );
	redraw();
}


//-----------------------------------------------------------------------------
// Purpose: 
// Output : int
//-----------------------------------------------------------------------------
int PhonemeEditor::ComputeHPixelsNeeded( void )
{
	int pixels = 0;

	if ( m_pWaveFile )
	{
		float maxtime = m_pWaveFile->GetRunningLength();
		maxtime += 1.0f;
		pixels = (int)( maxtime * GetPixelsPerSecond() );
	}

	return pixels;

}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::RepositionHSlider( void )
{
	int pixelsneeded = ComputeHPixelsNeeded();

	if ( pixelsneeded <= w2() )
	{
		m_pHorzScrollBar->setVisible( false );
	}
	else
	{
		m_pHorzScrollBar->setVisible( true );
	}

	m_pHorzScrollBar->setBounds( 0, GetCaptionHeight(), w2(), 12 );

	m_pHorzScrollBar->setRange( 0, pixelsneeded );
	m_pHorzScrollBar->setValue( 0 );
	m_nLeftOffset = 0;

	m_pHorzScrollBar->setPagesize( w2() );

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : float
//-----------------------------------------------------------------------------
float PhonemeEditor::GetPixelsPerSecond( void )
{
	return m_flPixelsPerSecond * GetTimeZoomScale();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : float
//-----------------------------------------------------------------------------
float PhonemeEditor::GetTimeZoomScale( void )
{
	return ( float )m_nTimeZoom / 100.0f;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : scale - 
//-----------------------------------------------------------------------------
void PhonemeEditor::SetTimeZoomScale( int scale )
{
	m_nTimeZoom = scale;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : dt - 
//-----------------------------------------------------------------------------
void PhonemeEditor::Think( float dt )
{
	if ( !m_pWaveFile )
		return;

	bool scrubbing = ( m_nDragType == DRAGTYPE_SCRUBBER ) ? true : false;
	ScrubThink( dt, scrubbing );

	if ( m_pMixer && !sound->IsSoundPlaying( m_pMixer ) )
	{
		m_pMixer = NULL;
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : mx - 
//			my - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
int PhonemeEditor::IsMouseOverBoundary( mxEvent *event )
{
	int mx, my;

	mx = (short)event->x;
	my = (short)event->y;

	// Deterime if phoneme boundary is under the cursor
	//
	if ( !m_pWaveFile )
		return BOUNDARY_NONE;

	if ( !(event->modifiers & mxEvent::KeyCtrl ) ) 
	{
		return BOUNDARY_NONE;
	}

	RECT rc;
	GetWorkspaceRect( rc );

	if ( IsMouseOverPhonemeRow( my ) )
	{
		float starttime = m_nLeftOffset / GetPixelsPerSecond();
		float endtime = w2() / GetPixelsPerSecond() + starttime;

		int		mouse_tolerance = 3;

		for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
		{
			CWordTag *word = m_Tags.m_Words[ i ];

			for ( int k = 0; k < word->m_Phonemes.Size(); k++ )
			{
				CPhonemeTag *pPhoneme = word->m_Phonemes[ k ];

				float t1 = pPhoneme->GetStartTime();
				float t2 = pPhoneme->GetEndTime();

				// Tag it
				float frac1 = ( t1 - starttime ) / ( endtime - starttime );
				float frac2 = ( t2 - starttime ) / ( endtime - starttime );

				int xpos1 = ( int )( frac1 * w2() );
				int xpos2 = ( int )( frac2 * w2() );
				if ( abs( xpos1 - mx ) <= mouse_tolerance ||
					 abs( xpos2 - mx ) <= mouse_tolerance )
				{
					return BOUNDARY_PHONEME;
				}
			}
		}
	}

	if ( IsMouseOverWordRow( my ) )
	{
		float starttime = m_nLeftOffset / GetPixelsPerSecond();
		float endtime = w2() / GetPixelsPerSecond() + starttime;

		int		mouse_tolerance = 3;

		for ( int k = 0; k < m_Tags.m_Words.Size(); k++ )
		{
			CWordTag *word = m_Tags.m_Words[ k ];

			float t1 = word->m_flStartTime;
			float t2 = word->m_flEndTime;

			// Tag it
			float frac1 = ( t1 - starttime ) / ( endtime - starttime );
			float frac2 = ( t2 - starttime ) / ( endtime - starttime );

			int xpos1 = ( int )( frac1 * w2() );
			int xpos2 = ( int )( frac2 * w2() );
			if ( ( abs( xpos1 - mx ) <= mouse_tolerance ) ||
				 ( abs( xpos2 - mx ) <= mouse_tolerance ) )
			{
				return BOUNDARY_WORD;
			}
		}
	}

	return BOUNDARY_NONE;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::DrawFocusRect( char *reason )
{
	HDC dc = GetDC( NULL );

	for ( int i = 0; i < m_FocusRects.Size(); i++ )
	{
		RECT rc = m_FocusRects[ i ].m_rcFocus;

		::DrawFocusRect( dc, &rc );
	}

	ReleaseDC( NULL, dc );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : &rc - 
//-----------------------------------------------------------------------------
void PhonemeEditor::GetWorkspaceRect( RECT &rc )
{
	GetClientRect( (HWND)getHandle(), &rc );
	
	rc.top += TAG_BOTTOM;
	rc.bottom = rc.bottom - 75 - MODE_TAB_OFFSET;

	InflateRect( &rc, -1, -1 );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : mx - 
//			my - 
//-----------------------------------------------------------------------------
void PhonemeEditor::ShowWordMenu( CWordTag *word, int mx, int my )
{
	CountSelected();

	mxPopupMenu *pop = new mxPopupMenu();
	Assert( pop );

	pop->add( va( "Edit sentence text..." ), IDC_EDITWORDLIST );
	
	if ( m_nSelectedWordCount > 0 && word )
	{
		pop->addSeparator();

		pop->add( va( "Delete %s", m_nSelectedWordCount > 1 ? "words" : va( "'%s'", word->GetWord() ) ), IDC_EDIT_DELETEWORD );

		if ( m_nSelectedWordCount == 1 )
		{
			int index = IndexOfWord( word );
			bool valid = false;
			if ( index != -1 )
			{
				SetClickedPhoneme( index, -1 );
				valid = true;
			}

			if ( valid )
			{
				pop->add( va( "Edit word '%s'...", word->GetWord() ), IDC_EDIT_WORD );

				float nextGap = GetTimeGapToNextWord( true, word );
				float prevGap = GetTimeGapToNextWord( false, word );

				if ( nextGap > MINIMUM_WORD_GAP ||
					 prevGap > MINIMUM_WORD_GAP )
				{
					pop->addSeparator();
					if ( prevGap > MINIMUM_WORD_GAP )
					{
						pop->add( va( "Insert word before '%s'...", word->GetWord() ), IDC_EDIT_INSERTWORDBEFORE );
					}
					if ( nextGap > MINIMUM_WORD_GAP )
					{
						pop->add( va( "Insert word after '%s'...", word->GetWord() ), IDC_EDIT_INSERTWORDAFTER );
					}
				}

				if ( word->m_Phonemes.Size() == 0 )
				{
					pop->addSeparator();
					pop->add( va( "Add phoneme to '%s'...", word->GetWord() ), IDC_EDIT_INSERTFIRSTPHONEMEOFWORD );
				}

				pop->addSeparator();
				pop->add( va( "Select all words after '%s'", word->GetWord() ), IDC_SELECT_WORDSRIGHT );
				pop->add( va( "Select all words before '%s'", word->GetWord() ), IDC_SELECT_WORDSLEFT );
			}
		}
	}

	if ( AreSelectedWordsContiguous() && m_nSelectedWordCount > 1 )
	{
		pop->addSeparator();
		pop->add( va( "Merge words" ), IDC_SNAPWORDS );
		
		if ( m_nSelectedWordCount == 2 )
		{
			pop->add( va( "Separate words" ), IDC_SEPARATEWORDS );
		}
	}

	if ( m_nSelectedWordCount > 0 )
	{
		pop->addSeparator();

		pop->add( va( "Deselect all" ), IDC_DESELECT_PHONEMESANDWORDS );
	}

	if ( m_Tags.m_Words.Size() > 0 )
	{
		pop->addSeparator();
		pop->add( va( "Cleanup words/phonemes" ), IDC_CLEANUP );
	}

	if ( m_Tags.m_Words.Size() > 0 )
	{
		pop->addSeparator();
		pop->add( va( "Realign phonemes to words" ), IDC_REALIGNPHONEMES );
	}


	pop->popup( this, mx, my );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : mx - 
//			my - 
//-----------------------------------------------------------------------------
void PhonemeEditor::ShowPhonemeMenu( CPhonemeTag *pho, int mx, int my )
{
	CountSelected();

	SetClickedPhoneme( -1, -1 );

	if ( !pho )
		return;

	if ( m_Tags.CountPhonemes() == 0 )
	{
		Con_Printf( "No phonemes, try extracting from .wav first\n" );
		return;
	}

	mxPopupMenu *pop = new mxPopupMenu();
	bool valid = false;
	CWordTag *tag = m_Tags.GetWordForPhoneme( pho );
	if ( tag )
	{
		int wordNum = IndexOfWord( tag );
		int pi = tag->IndexOfPhoneme( pho );

		SetClickedPhoneme( wordNum, pi );
		valid = true;
	}

	if ( valid )
	{
		if ( m_nSelectedPhonemeCount == 1 )
		{
			pop->add( va( "Edit '%s'...", ConvertPhoneme( pho->GetPhonemeCode() ) ), IDC_EDIT_PHONEME );

			float nextGap = GetTimeGapToNextPhoneme( true, pho );
			float prevGap = GetTimeGapToNextPhoneme( false, pho );

			if ( nextGap > MINIMUM_PHONEME_GAP ||
				 prevGap > MINIMUM_PHONEME_GAP )
			{
				pop->addSeparator();
				if ( prevGap > MINIMUM_PHONEME_GAP )
				{
					pop->add( va( "Insert phoneme before '%s'...", ConvertPhoneme( pho->GetPhonemeCode() ) ), IDC_EDIT_INSERTPHONEMEBEFORE );
				}
				if ( nextGap > MINIMUM_PHONEME_GAP )
				{
					pop->add( va( "Insert phoneme after '%s'...", ConvertPhoneme( pho->GetPhonemeCode() ) ), IDC_EDIT_INSERTPHONEMEAFTER );
				}
			}

			pop->addSeparator();
			pop->add( va( "Select all phonemes after '%s'", ConvertPhoneme( pho->GetPhonemeCode() ) ), IDC_SELECT_PHONEMESRIGHT );
			pop->add( va( "Select all phonemes before '%s'",ConvertPhoneme( pho->GetPhonemeCode() ) ), IDC_SELECT_PHONEMESLEFT );

			pop->addSeparator();
		}
				
		if ( AreSelectedPhonemesContiguous() && m_nSelectedPhonemeCount > 1 )
		{
			pop->add( va( "Merge phonemes" ), IDC_SNAPPHONEMES );
			if ( m_nSelectedPhonemeCount == 2 )
			{
				pop->add( va( "Separate phonemes" ), IDC_SEPARATEPHONEMES );
			}

			pop->addSeparator();
		}
		
		if ( m_nSelectedPhonemeCount >= 1 )
		{
			pop->add( va( "Delete %s", 
				m_nSelectedPhonemeCount == 1 ? va( "'%s'", ConvertPhoneme( pho->GetPhonemeCode() ) ) : "phonemes" ), IDC_EDIT_DELETEPHONEME );

			pop->addSeparator();
			pop->add( va( "Deselect all" ), IDC_DESELECT_PHONEMESANDWORDS );
		}
	}


	if ( m_Tags.m_Words.Size() > 0 )
	{
		pop->addSeparator();
		pop->add( va( "Cleanup words/phonemes" ), IDC_CLEANUP );
	}

	if ( m_Tags.m_Words.Size() > 0 )
	{
		pop->addSeparator();
		pop->add( va( "Realign words to phonemes" ), IDC_REALIGNWORDS );
	}

	pop->popup( this, mx, my );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : mx - 
// Output : float
//-----------------------------------------------------------------------------
float PhonemeEditor::GetTimeForPixel( int mx )
{
	RECT rc;
	GetWorkspaceRect( rc );

	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float time = (float)mx / GetPixelsPerSecond() + starttime;

	return time;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : time - 
//			**pp1 - 
//			**pp2 - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool PhonemeEditor::FindSpanningPhonemes( float time, CPhonemeTag **pp1, CPhonemeTag **pp2 )
{
	Assert( pp1 && pp2 );

	*pp1 = NULL;
	*pp2 = NULL;

	// Three pixels
	double time_epsilon = ( 1.0f / GetPixelsPerSecond() ) * 3;

	CPhonemeTag *previous = NULL;

	for ( int w = 0; w < m_Tags.m_Words.Size(); w++ )
	{
		CWordTag *word = m_Tags.m_Words[ w ];

		for ( int i = 0; i < word->m_Phonemes.Size(); i++ )
		{
			CPhonemeTag *current = word->m_Phonemes[ i ];
			double dt;

			if ( !previous )
			{
				dt = fabs( current->GetStartTime() - time );
				if ( dt < time_epsilon )
				{
					*pp2 = current;
					return true;
				}
			}
			else
			{
				int found = 0;

				dt = fabs( previous->GetEndTime() - time );
				if ( dt < time_epsilon )
				{
					*pp1 = previous;
					found++;
				}

				dt = fabs( current->GetStartTime() - time );
				if ( dt < time_epsilon )
				{
					*pp2 = current;
					found++;
				}

				if ( found != 0 )
				{
					return true;
				}
			}
		
			previous = current;
		}
	}

	if ( m_Tags.m_Words.Size() > 0 )
	{
		// Check last word, but only if it has some phonemes
		CWordTag *lastWord = m_Tags.m_Words[ m_Tags.m_Words.Size() - 1 ];
		if ( lastWord && 
			( lastWord->m_Phonemes.Size() > 0 ) )
		{

			CPhonemeTag *last = lastWord->m_Phonemes[ lastWord->m_Phonemes.Size() - 1 ];
			float dt;
			dt = fabs( last->GetEndTime() - time );
			if ( dt < time_epsilon )
			{
				*pp1 = last;
				return true;
			}
		}
	}

	return false;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : time - 
//			**pp1 - 
//			**pp2 - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool PhonemeEditor::FindSpanningWords( float time, CWordTag **pp1, CWordTag **pp2 )
{
	Assert( pp1 && pp2 );

	*pp1 = NULL;
	*pp2 = NULL;

	// Three pixels
	double time_epsilon = ( 1.0f / GetPixelsPerSecond() ) * 3;

	CWordTag *previous = NULL;
	for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
	{
		CWordTag *current = m_Tags.m_Words[ i ];
		double dt;

		if ( !previous )
		{
			dt = fabs( current->m_flStartTime - time );
			if ( dt < time_epsilon )
			{
				*pp2 = current;
				return true;
			}
		}
		else
		{
			int found = 0;

			dt = fabs( previous->m_flEndTime - time );
			if ( dt < time_epsilon )
			{
				*pp1 = previous;
				found++;
			}

			dt = fabs( current->m_flStartTime - time );
			if ( dt < time_epsilon )
			{
				*pp2 = current;
				found++;
			}

			if ( found != 0 )
			{
				return true;
			}
		}
	
		previous = current;
	}

	if ( m_Tags.m_Words.Size() > 0 )
	{
		CWordTag *last = m_Tags.m_Words[ m_Tags.m_Words.Size() - 1 ];
		float dt;
		dt = fabs( last->m_flEndTime - time );
		if ( dt < time_epsilon )
		{
			*pp1 = last;
			return true;
		}
	}

	return false;
}

int	PhonemeEditor::FindWordForTime( float time )
{
	for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
	{
		CWordTag *pCurrent = m_Tags.m_Words[ i ];

		if ( time < pCurrent->m_flStartTime )
			continue;

		if ( time > pCurrent->m_flEndTime )
			continue;

		return i;
	}

	return -1;
}

void PhonemeEditor::FinishWordDrag( int startx, int endx )
{
	float clicktime	= GetTimeForPixel( startx );
	float endtime	= GetTimeForPixel( endx );

	float dt = endtime - clicktime;

	SetDirty( true );

	PushUndo();

	TraverseWords( &PhonemeEditor::ITER_MoveSelectedWords, dt );

	RealignPhonemesToWords( false );
	CleanupWordsAndPhonemes( false );

	PushRedo();

	redraw();
}

void PhonemeEditor::FinishWordMove( int startx, int endx )
{
	float clicktime	= GetTimeForPixel( startx );
	float endtime	= GetTimeForPixel( endx );

	// Find the phonemes who have the closest start/endtime to the starting click time
	CWordTag *current, *next;

	if ( !FindSpanningWords( clicktime, &current, &next ) )
	{
		return;
	}

	SetDirty( true );

	PushUndo();

	if ( current && !next )
	{
		// cap movement
		current->m_flEndTime += ( endtime - clicktime );
	}
	else if ( !current && next )
	{
		// cap movement
		next->m_flStartTime += ( endtime - clicktime );
	}
	else
	{
		// cap movement
		endtime = min( endtime, next->m_flEndTime - 1.0f / GetPixelsPerSecond() );
		endtime = max( endtime, current->m_flStartTime + 1.0f / GetPixelsPerSecond() );

		current->m_flEndTime = endtime;
		next->m_flStartTime = endtime;
	}

	RealignPhonemesToWords( false );
	CleanupWordsAndPhonemes( false );

	PushRedo();

	redraw();
}

CPhonemeTag *PhonemeEditor::FindPhonemeForTime( float time )
{
	for ( int w = 0 ; w < m_Tags.m_Words.Size(); w++ )
	{
		CWordTag *word = m_Tags.m_Words[ w ];


		for ( int i = 0; i < word->m_Phonemes.Size(); i++ )
		{
			CPhonemeTag *pCurrent = word->m_Phonemes[ i ];

			if ( time < pCurrent->GetStartTime() )
				continue;

			if ( time > pCurrent->GetEndTime() )
				continue;

			return pCurrent;
		}
	}

	return NULL;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : phoneme - 
//			startx - 
//			endx - 
//-----------------------------------------------------------------------------
void PhonemeEditor::FinishPhonemeDrag( int startx, int endx )
{
	float clicktime	= GetTimeForPixel( startx );
	float endtime	= GetTimeForPixel( endx );

	float dt = endtime - clicktime;

	SetDirty( true );

	PushUndo();

	TraversePhonemes( &PhonemeEditor::ITER_MoveSelectedPhonemes, dt );

	RealignWordsToPhonemes( false );
	CleanupWordsAndPhonemes( false );

	PushRedo();

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : phoneme - 
//			startx - 
//			endx - 
//-----------------------------------------------------------------------------
void PhonemeEditor::FinishPhonemeMove( int startx, int endx )
{
	float clicktime	= GetTimeForPixel( startx );
	float endtime	= GetTimeForPixel( endx );

	// Find the phonemes who have the closest start/endtime to the starting click time
	CPhonemeTag *current, *next;

	if ( !FindSpanningPhonemes( clicktime, &current, &next ) )
	{
		return;
	}

	SetDirty( true );

	PushUndo();

	if ( current && !next )
	{
		// cap movement
		current->AddEndTime( endtime - clicktime );
	}
	else if ( !current && next )
	{
		// cap movement
		next->AddStartTime( endtime - clicktime );
	}
	else
	{
		// cap movement
		endtime = min( endtime, next->GetEndTime() - 1.0f / GetPixelsPerSecond() );
		endtime = max( endtime, current->GetStartTime() + 1.0f / GetPixelsPerSecond() );

		current->SetEndTime( endtime );
		next->SetStartTime( endtime );
	}

	RealignWordsToPhonemes( false );
	CleanupWordsAndPhonemes( false );

	PushRedo();

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : dirty - 
//-----------------------------------------------------------------------------
void PhonemeEditor::SetDirty( bool dirty, bool clearundo /*=true*/ )
{
	m_WorkFile.m_bDirty = dirty;

	if ( !dirty && clearundo )
	{
		WipeUndo();
		redraw();
	}

	SetPrefix( dirty ? "* " : "" );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool PhonemeEditor::GetDirty( void )
{
	return m_WorkFile.m_bDirty;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::EditInsertPhonemeBefore( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CPhonemeTag *cp = GetSelectedPhoneme();
	if ( !cp )
		return;

	float gap = GetTimeGapToNextPhoneme( false, cp );
	if ( gap < MINIMUM_PHONEME_GAP )
	{
		Con_Printf( "Can't insert before, gap of %.2f ms is too small\n", 1000.0f * gap );
		return;
	}

	// Don't have really long phonemes
	gap = min( gap, DEFAULT_PHONEME_LENGTH );

	CWordTag *word = m_Tags.GetWordForPhoneme( cp );
	if ( !word )
	{
		Con_Printf( "EditInsertPhonemeBefore:  phoneme not a member of any known word!!!\n" );
		return;
	}

	int clicked = word->IndexOfPhoneme( cp );
	if ( clicked < 0 )
	{
		Con_Printf( "EditInsertPhonemeBefore:  phoneme not a member of any specified word!!!\n" );
		Assert( 0 );
		return;
	}

	CPhonemeTag phoneme;

	CPhonemeParams params;
	memset( &params, 0, sizeof( params ) );
	strcpy( params.m_szDialogTitle, "Phoneme/Viseme Properties" );
	strcpy( params.m_szName, "" );

	int iret = PhonemeProperties( &params );
	SetFocus( (HWND)getHandle() );
	if ( !iret )
	{
		return;
	}

	SetDirty( true );

	PushUndo();

	phoneme.SetPhonemeCode( TextToPhoneme( params.m_szName ) );
	phoneme.SetTag( params.m_szName );

	phoneme.SetEndTime( cp->GetStartTime() );
	phoneme.SetStartTime( cp->GetStartTime() - gap );
	phoneme.m_bSelected = true;
	cp->m_bSelected = false;

	word->m_Phonemes.InsertBefore( clicked, new CPhonemeTag( phoneme ) );

	PushRedo();

	// Add it
	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::EditInsertPhonemeAfter( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CPhonemeTag *cp = GetSelectedPhoneme();
	if ( !cp )
		return;

	float gap = GetTimeGapToNextPhoneme( true, cp );
	if ( gap < MINIMUM_PHONEME_GAP )
	{
		Con_Printf( "Can't insert after, gap of %.2f ms is too small\n", 1000.0f * gap );
		return;
	}

	// Don't have really long phonemes
	gap = min( gap, DEFAULT_PHONEME_LENGTH );

	CWordTag *word = m_Tags.GetWordForPhoneme( cp );
	if ( !word )
	{
		Con_Printf( "EditInsertPhonemeAfter:  phoneme not a member of any known word!!!\n" );
		return;
	}

	int clicked = word->IndexOfPhoneme( cp );
	if ( clicked < 0 )
	{
		Con_Printf( "EditInsertPhonemeAfter:  phoneme not a member of any specified word!!!\n" );
		Assert( 0 );
		return;
	}

	CPhonemeTag phoneme;

	CPhonemeParams params;
	memset( &params, 0, sizeof( params ) );
	strcpy( params.m_szDialogTitle, "Phoneme/Viseme Properties" );
	strcpy( params.m_szName, "" );

	int iret = PhonemeProperties( &params );
	SetFocus( (HWND)getHandle() );

	if ( !iret )
	{
		return;
	}

	SetDirty( true );

	PushUndo();

	phoneme.SetPhonemeCode( TextToPhoneme( params.m_szName ) );
	phoneme.SetTag( params.m_szName );

	phoneme.SetEndTime( cp->GetEndTime() + gap );
	phoneme.SetStartTime( cp->GetEndTime() );
	phoneme.m_bSelected = true;
	cp->m_bSelected = false;
	
	word->m_Phonemes.InsertAfter( clicked, new CPhonemeTag( phoneme ) );

	PushRedo();

	// Add it
	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::EditInsertWordBefore( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CWordTag *cw = GetSelectedWord();
	if ( !cw )
		return;

	float gap = GetTimeGapToNextWord( false, cw );
	if ( gap < MINIMUM_WORD_GAP )
	{
		Con_Printf( "Can't insert before, gap of %.2f ms is too small\n", 1000.0f * gap );
		return;
	}

	// Don't have really long words
	gap = min( gap, DEFAULT_WORD_LENGTH );

	int clicked = IndexOfWord( cw );
	if ( clicked < 0 )
	{
		Con_Printf( "EditInsertWordBefore:  word not in sentence!!!\n" );
		Assert( 0 );
		return;
	}

	CInputParams params;
	memset( &params, 0, sizeof( params ) );
	strcpy( params.m_szDialogTitle, "Insert Word" );
	strcpy( params.m_szPrompt, "Word:" );
	strcpy( params.m_szInputText, "" );

	params.m_nLeft = -1;
	params.m_nTop = -1;

	params.m_bPositionDialog = true;
	if ( params.m_bPositionDialog )
	{
		RECT rcWord;
		GetWordRect( cw, rcWord );

		// Convert to screen coords
		POINT pt;
		pt.x = rcWord.left;
		pt.y = rcWord.top;

		ClientToScreen( (HWND)getHandle(), &pt );

		params.m_nLeft	= pt.x;
		params.m_nTop	= pt.y;
	}

	int iret = InputProperties( &params );
	SetFocus( (HWND)getHandle() );
	if ( !iret )
	{
		return;
	}

	if ( strlen( params.m_szInputText ) <= 0 )
	{
		return;
	}

	int wordCount = CSentence::CountWords( params.m_szInputText );
	if ( wordCount > 1 )
	{
		Con_Printf( "Can only insert one word at a time, %s has %i words in it!\n",
			params.m_szInputText, wordCount );
		return;
	}

	SetDirty( true );

	PushUndo();

	CWordTag newword;

	newword.SetWord( params.m_szInputText );

	newword.m_flEndTime		= cw->m_flStartTime;
	newword.m_flStartTime	= cw->m_flStartTime - gap;
	newword.m_bSelected = true;
	cw->m_bSelected = false;

	m_Tags.m_Words.InsertBefore( clicked, new CWordTag( newword ) );

	PushRedo();

	// Add it
	redraw();

	// Jump to phoneme insertion UI
	EditInsertFirstPhonemeOfWord();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::EditInsertWordAfter( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CWordTag *cw = GetSelectedWord();
	if ( !cw )
		return;

	float gap = GetTimeGapToNextWord( true, cw );
	if ( gap < MINIMUM_WORD_GAP )
	{
		Con_Printf( "Can't insert after, gap of %.2f ms is too small\n", 1000.0f * gap );
		return;
	}

	// Don't have really long words
	gap = min( gap, DEFAULT_WORD_LENGTH );

	int clicked = IndexOfWord( cw );
	if ( clicked < 0 )
	{
		Con_Printf( "EditInsertWordBefore:  word not in sentence!!!\n" );
		Assert( 0 );
		return;
	}

	CInputParams params;
	memset( &params, 0, sizeof( params ) );
	strcpy( params.m_szDialogTitle, "Insert Word" );
	strcpy( params.m_szPrompt, "Word:" );
	strcpy( params.m_szInputText, "" );

	params.m_nLeft = -1;
	params.m_nTop = -1;

	params.m_bPositionDialog = true;
	if ( params.m_bPositionDialog )
	{
		RECT rcWord;
		GetWordRect( cw, rcWord );

		// Convert to screen coords
		POINT pt;
		pt.x = rcWord.left;
		pt.y = rcWord.top;

		ClientToScreen( (HWND)getHandle(), &pt );

		params.m_nLeft	= pt.x;
		params.m_nTop	= pt.y;
	}

	int iret = InputProperties( &params );
	SetFocus( (HWND)getHandle() );
	if ( !iret )
	{
		return;
	}

	if ( strlen( params.m_szInputText ) <= 0 )
	{
		return;
	}

	int wordCount = CSentence::CountWords( params.m_szInputText );
	if ( wordCount > 1 )
	{
		Con_Printf( "Can only insert one word at a time, %s has %i words in it!\n",
			params.m_szInputText, wordCount );
		return;
	}

	SetDirty( true );

	PushUndo();

	CWordTag newword;

	newword.SetWord( params.m_szInputText );

	newword.m_flEndTime		= cw->m_flEndTime + gap;
	newword.m_flStartTime	= cw->m_flEndTime;
	newword.m_bSelected = true;
	cw->m_bSelected = false;
	
	CWordTag *w = new CWordTag( newword );
	Assert( w );
	if ( w )
	{
		m_Tags.m_Words.InsertAfter( clicked, w );
	}

	PushRedo();

	// Add it
	redraw();

	EditInsertFirstPhonemeOfWord();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::EditDeletePhoneme( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CountSelected();

	if ( m_nSelectedPhonemeCount < 1 )
	{
		return;
	}

	SetDirty( true );

	PushUndo();

	for ( int i = m_Tags.m_Words.Size() - 1; i >= 0; i-- )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		if ( !word )
			continue;

		for ( int j = word->m_Phonemes.Size() - 1; j >= 0; j-- )
		{
			CPhonemeTag *p = word->m_Phonemes[ j ];
			if ( !p || !p->m_bSelected )
				continue;

			// Delete it
			word->m_Phonemes.Remove( j );
		}
	}

	PushRedo();

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::EditDeleteWord( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CountSelected();

	if ( m_nSelectedWordCount < 1 )
	{
		return;
	}

	SetDirty( true );

	PushUndo();

	for ( int i = m_Tags.m_Words.Size() - 1; i >= 0; i-- )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		if ( !word || !word->m_bSelected )
			continue;

		m_Tags.m_Words.Remove( i );
	}

	PushRedo();

	redraw();
}
//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::PlayEditedWave( bool selection /* = false */ )
{
	StopPlayback();

	if ( !m_pWaveFile )
		return;

	// Make sure phonemes are loaded
	FacePoser_EnsurePhonemesLoaded();

	SaveLinguisticData();

	SetScrubTime( 0.0f );
	SetScrubTargetTime( m_pWaveFile->GetRunningLength() );
}

typedef struct channel_s
{
	int		leftvol;
	int		rightvol;
	int		rleftvol;
	int		rrightvol;
	float	pitch;
} channel_t;

bool PhonemeEditor::CreateCroppedWave( char const *filename, int startsample, int endsample )
{
	Assert( sound );

	CAudioWaveOutput *pWaveOutput = ( CAudioWaveOutput * )sound->GetAudioOutput();
	if ( !pWaveOutput )
		return false;

	CAudioSource *wave = sound->LoadSound( m_WorkFile.m_szWaveFile );
	if ( !wave )
		return false;

	CAudioMixer *pMixer = wave->CreateMixer();
	if ( !pMixer )
		return false;

	// Create out put file
	OutFileRIFF riffout( filename, io_out );
	// Create output iterator
	IterateOutputRIFF store( riffout );

	WAVEFORMATEX format;
	format.cbSize = sizeof( format );

	format.wFormatTag = WAVE_FORMAT_PCM;
	format.nAvgBytesPerSec = (int)wave->SampleRate();
	format.nChannels = 1;
	format.wBitsPerSample = 8;
	format.nSamplesPerSec = (int)wave->SampleRate();
	format.nBlockAlign = 1; // (int)wave->SampleSize();

	store.ChunkWrite( WAVE_FMT, &format, sizeof( format ) );

	// Pull in data and write it out

	int currentsample = 0;

	store.ChunkStart( WAVE_DATA );

	// need a bit of space
	short samples[ 2 ];
	channel_t channel;
	channel.leftvol = 255;
	channel.rightvol = 255;
	channel.pitch = 1.0;

	while ( 1 )
	{
		pWaveOutput->m_audioDevice.MixBegin();

		if ( !pMixer->MixDataToDevice( &pWaveOutput->m_audioDevice, &channel, currentsample, 1, wave->SampleRate(), true ) )
			break;

		pWaveOutput->m_audioDevice.TransferBufferStereo16( samples, 1 );

		currentsample = pMixer->GetSamplePosition();

		if ( currentsample >= startsample && currentsample <= endsample )
		{
			// left + right (2 channels ) * 16 bits
			float s1 = (float)( samples[ 0 ] >> 8 );
			float s2 = (float)( samples[ 1 ] >> 8 );

			float avg = ( s1 + s2 ) / 2.0f;
			unsigned char chopped = (unsigned char)( avg + 127.0f );

			store.ChunkWriteData( &chopped, sizeof( byte ) );
		}
	}

	store.ChunkFinish();

	delete pMixer;
	delete wave;

	return true;
}

void PhonemeEditor::SentenceFromString( CSentence& sentence, char const *str )
{
	sentence.Reset();

	if ( !str || !str[0] || CSentence::CountWords( str ) == 0 )
	{
		return;
	}

	char word[ 256 ];
	unsigned char const *in = (unsigned char *)str;
	char *out = word;
	
	while ( *in )
	{
		if ( *in > 32 )
		{
			*out++ = *in++;
		}
		else
		{
			*out = 0;

			while ( *in && *in <= 32 )
			{
				in++;
			}
			
			if ( strlen( word ) > 0 )
			{
				CWordTag *w = new CWordTag( (char *)word );
				Assert( w );
				if ( w )
				{
					sentence.m_Words.AddToTail( w );
				}
			}
			
			out = word;
		}
	}
	
	*out = 0;
	if ( strlen( word ) > 0 )
	{
		CWordTag *w = new CWordTag( (char *)word );
		Assert( w );
		if ( w )
		{
			sentence.m_Words.AddToTail( w );
		}
	}
	
	sentence.SetText( str );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::RedoPhonemeExtractionSelected( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	if ( !CheckSpeechAPI() )
		return;

	if ( !m_pWaveFile )
	{
		Con_Printf( "Can't redo extraction, no wavefile loaded!\n" );
		Assert( 0 );
		return;
	}

	if ( !m_bSelectionActive )
	{
		Con_Printf( "Please select a portion of the .wav from which to re-extract phonemes\n" );
		return;
	}

	// Now copy data back into original list, offsetting by samplestart time
	float numsamples = m_pWaveFile->GetRunningLength() * m_pWaveFile->SampleRate();
	float selectionstarttime = 0.0f;
	if ( numsamples > 0.0f )
	{
		// Convert sample #'s to time
		selectionstarttime = ( m_nSelection[ 0 ] / numsamples ) * m_pWaveFile->GetRunningLength();
		selectionstarttime = max( 0.0f, selectionstarttime );
	}
	else
	{
		Con_Printf( "Original .wav file %s has no samples!!!\n", m_WorkFile.m_szWaveFile );
		return;
	}

	int i;
	// Create input array of just selected words
	CSentence m_InputWords;
	CSentence m_Results;

	CountSelected();
	
	bool usingselection = true;

	if ( m_nSelectedWordCount == 0 )
	{
		// Allow user to type in text
		// Build word string
		char wordstring[ 1024 ];
		strcpy( wordstring, "" );

		CInputParams params;
		memset( &params, 0, sizeof( params ) );
		strcpy( params.m_szDialogTitle, "Phrase Word List" );
		strcpy( params.m_szPrompt, "Phrase" );

		strcpy( params.m_szInputText, wordstring );

		if ( !InputProperties( &params ) )
			return;

		if ( strlen( params.m_szInputText ) <= 0 )
		{
			Con_ErrorPrintf( "Edit word list:  No words entered!\n" );
			return;
		}

		SentenceFromString( m_InputWords, params.m_szInputText );

		if ( m_InputWords.m_Words.Size() == 0 )
		{
			Con_Printf( "You must either select words, or type in a set of words in order to extract phonemes!\n" );
			return;
		}

		usingselection = false;
	}
	else
	{
		if ( !AreSelectedWordsContiguous() )
		{
			Con_Printf( "Can only redo extraction on a contiguous subset of words\n" );
			return;
		}

		char temp[ 4096 ];
		bool killspace = false;
		Q_strncpy( temp, m_InputWords.GetText(), sizeof( temp ) );

		// Iterate existing words, looking for contiguous selected words
		for ( i = 0; i < m_Tags.m_Words.Size(); i++ )
		{
			CWordTag *word = m_Tags.m_Words[ i ];
			if ( !word || !word->m_bSelected )
				continue;

			// Now add "clean slate" to input list
			m_InputWords.m_Words.AddToTail( new CWordTag( *word ) );

			Q_strncat( temp, word->GetWord(), sizeof( temp ), COPY_ALL_CHARACTERS );
			Q_strncat( temp, " ", sizeof( temp ), COPY_ALL_CHARACTERS );
			killspace = true;
		}

		// Kill terminal space character
		int len = Q_strlen( temp );
		if ( killspace && ( len >= 1 ) )
		{
			Assert( temp[ len -1 ] == ' ' );
			temp[ len - 1 ] = 0;
		}

		m_InputWords.SetText( temp );
	}

	m_nLastExtractionResult		= SR_RESULT_NORESULT;

	char szCroppedFile[ 512 ];
	char szBaseFile[ 512 ];
	Q_StripExtension( m_WorkFile.m_szWaveFile, szBaseFile, sizeof( szBaseFile ) );
	Q_snprintf( szCroppedFile, sizeof( szCroppedFile ), "%s%s_work1.wav", m_WorkFile.m_szBasePath, szBaseFile );

	filesystem->RemoveFile( szCroppedFile, "GAME" );

	if ( !CreateCroppedWave( szCroppedFile, m_nSelection[ 0 ], m_nSelection[ 1 ] ) )
	{
		Con_Printf( "Unable to create cropped wave file %s from samples %i to %i\n",
			szCroppedFile,
			m_nSelection[ 0 ],
			m_nSelection[ 1 ] );
		return;
	}

	CAudioSource *m_pCroppedWave = sound->LoadSound( szCroppedFile );
	if ( !m_pCroppedWave )
	{
		Con_Printf( "Unable to load cropped wave file %s from samples %i to %i\n",
			szCroppedFile,
			m_nSelection[ 0 ],
			m_nSelection[ 1 ] );
		return;
	}

	// Save any pending stuff
	SaveLinguisticData();

	// Store off copy of complete sentence
	m_TagsExt = m_Tags;

	char filename[ 512 ];
	Q_snprintf( filename, sizeof( filename ), "%s%s", m_WorkFile.m_szBasePath, szCroppedFile );

	m_nLastExtractionResult = m_pPhonemeExtractor->Extract( 
		filename,
		(int)( m_pCroppedWave->GetRunningLength() * m_pCroppedWave->SampleRate() * m_pCroppedWave->TrueSampleSize() ),
		Con_Printf,
		m_InputWords,
		m_Results );

	if ( m_InputWords.m_Words.Size() != m_Results.m_Words.Size() )
	{
		Con_Printf( "Extraction returned %i words, source had %i, try adjusting selection\n",
			m_Results.m_Words.Size(), m_InputWords.m_Words.Size() );

		filesystem->RemoveFile( filename, "GAME" );

		redraw();
		return;
	}

	float bytespersecond = m_pCroppedWave->SampleRate() * m_pCroppedWave->TrueSampleSize();

	// Tracker 57389:
	// Total hack to fix a bug where the Lipsinc extractor is messing up the # channels on 16 bit stereo waves
	if ( m_pPhonemeExtractor->GetAPIType() == SPEECH_API_LIPSINC &&
		m_pCroppedWave->IsStereoWav() && 
		m_pCroppedWave->SampleSize() == 16 )
	{
		bytespersecond *= 2.0f;
	}

	// Now convert byte offsets to times
	for ( i = 0; i < m_Results.m_Words.Size(); i++ )
	{
		CWordTag *tag = m_Results.m_Words[ i ];
		Assert( tag );
		if ( !tag )
			continue;

		tag->m_flStartTime = ( float )(tag->m_uiStartByte ) / bytespersecond;
		tag->m_flEndTime = ( float )(tag->m_uiEndByte ) / bytespersecond;

		for ( int j = 0; j < tag->m_Phonemes.Size(); j++ )
		{
			CPhonemeTag *ptag = tag->m_Phonemes[ j ];
			Assert( ptag );
			if ( !ptag )
				continue;

			ptag->SetStartTime( ( float )(ptag->m_uiStartByte ) / bytespersecond );
			ptag->SetEndTime( ( float )(ptag->m_uiEndByte ) / bytespersecond );
		}
	}

	if ( usingselection )
	{
		// Copy data into m_TagsExt, offseting times by selectionstarttime
		CWordTag *from;
		CWordTag *to;

		int fromWord = 0;

		for ( i = 0; i < m_TagsExt.m_Words.Size() ; i++ )
		{
			to = m_TagsExt.m_Words[ i ];
			if ( !to || !to->m_bSelected )
				continue;

			// Found start of contiguous run
			if ( fromWord >= m_Results.m_Words.Size() )
				break;

			from = m_Results.m_Words[ fromWord++ ];
			Assert( from );
			if ( !from )
				continue;

			// Remove all phonemes from destination
			while ( to->m_Phonemes.Size() > 0 )
			{
				CPhonemeTag *p = to->m_Phonemes[ 0 ];
				Assert( p );
				to->m_Phonemes.Remove( 0 );
				delete p;
			}

			// Now copy phonemes from source
			for ( int j = 0; j < from->m_Phonemes.Size(); j++ )
			{
				CPhonemeTag *fromPhoneme = from->m_Phonemes[ j ];
				Assert( fromPhoneme );
				if ( !fromPhoneme )
					continue;

				CPhonemeTag newPhoneme( *fromPhoneme );
				// Offset start time
				newPhoneme.AddStartTime( selectionstarttime );
				newPhoneme.AddEndTime( selectionstarttime );

				// Add it back in with corrected timing data
				CPhonemeTag *p = new CPhonemeTag( newPhoneme );
				Assert( p );
				if ( p )
				{
					to->m_Phonemes.AddToTail( p );
				}
			}

			// Done
			if ( fromWord >= m_Results.m_Words.Size() )
				break;
		}

	}
	else
	{
		// Find word just before starting point of selection and
		//  place input words into list starting that that point

		int startWord = 0;

		CWordTag *firstWordOfPhrase = m_Results.m_Words[ 0 ];
		Assert( firstWordOfPhrase );
		
		for ( ; startWord < m_TagsExt.m_Words.Size(); startWord++ )
		{
			CWordTag *w = m_TagsExt.m_Words[ startWord ];
			Assert( w );
			if ( !w )
				continue;
			
			if ( w->m_flStartTime > firstWordOfPhrase->m_flStartTime + selectionstarttime )
				break;
		}

		for ( i = 0; i < m_Results.m_Words.Size(); i++ )
		{
			CWordTag *from = m_Results.m_Words[ i ];
			Assert( from );
			if ( !from )
				continue;

			CWordTag *to = new CWordTag( *from );
			Assert( to );

			to->m_flStartTime	+= selectionstarttime;
			to->m_flEndTime		+= selectionstarttime;

			// Now adjust phoneme times
			for ( int j = 0; j < to->m_Phonemes.Size(); j++ )
			{
				CPhonemeTag *toPhoneme = to->m_Phonemes[ j ];
				Assert( toPhoneme );
				if ( !toPhoneme )
					continue;

				// Offset start time
				toPhoneme->AddStartTime( selectionstarttime );
				toPhoneme->AddEndTime( selectionstarttime );
			}

			m_TagsExt.m_Words.InsertBefore( startWord++, to );
		}
	}

	Con_Printf( "Cleaning up...\n" );
	filesystem->RemoveFile( filename, "GAME" );

	SetFocus( (HWND)getHandle() );

	redraw();
}

void PhonemeEditor::RedoPhonemeExtraction( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	if ( !CheckSpeechAPI() )
		return;

	m_nLastExtractionResult		= SR_RESULT_NORESULT;

	if ( !m_pWaveFile )
		return;

	SaveLinguisticData();

	// Send m_WorkFile.m_szWorkingFile to extractor and retrieve resulting data
	// 

	m_TagsExt.Reset();

	Assert( m_pPhonemeExtractor );

	char filename[ 512 ];
	Q_snprintf( filename, sizeof( filename ), "%s%s", m_WorkFile.m_szBasePath, m_WorkFile.m_szWorkingFile );

	m_nLastExtractionResult = m_pPhonemeExtractor->Extract( 
		filename,
		(int)( m_pWaveFile->GetRunningLength() * m_pWaveFile->SampleRate() * m_pWaveFile->TrueSampleSize() ),
		Con_Printf,
		m_Tags,
		m_TagsExt );

	float bytespersecond = m_pWaveFile->SampleRate() * m_pWaveFile->TrueSampleSize();

	// Now convert byte offsets to times
	int i;
	for ( i = 0; i < m_TagsExt.m_Words.Size(); i++ )
	{
		CWordTag *tag = m_TagsExt.m_Words[ i ];
		Assert( tag );
		if ( !tag )
			continue;

		tag->m_flStartTime = ( float )(tag->m_uiStartByte ) / bytespersecond;
		tag->m_flEndTime = ( float )(tag->m_uiEndByte ) / bytespersecond;

		for ( int j = 0; j < tag->m_Phonemes.Size(); j++ )
		{
			CPhonemeTag *ptag = tag->m_Phonemes[ j ];
			Assert( ptag );
			if ( !ptag )
				continue;

			ptag->SetStartTime( ( float )(ptag->m_uiStartByte ) / bytespersecond );
			ptag->SetEndTime( ( float )(ptag->m_uiEndByte ) / bytespersecond );
		}
	}

	SetFocus( (HWND)getHandle() );

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::Deselect( void )
{
	m_nSelection[ 0 ] = m_nSelection[ 1 ] = 0;
	m_bSelectionActive = false;
}

void PhonemeEditor::ITER_SelectSpanningWords( CWordTag *word, float amount )
{
	Assert( word );
	word->m_bSelected = false;

	if ( !m_bSelectionActive )
		return;

	if ( !m_pWaveFile )
		return;

	float numsamples = m_pWaveFile->GetRunningLength() * m_pWaveFile->SampleRate();
	if ( numsamples > 0.0f )
	{
		// Convert sample #'s to time
		float starttime = ( m_nSelection[ 0 ] / numsamples ) * m_pWaveFile->GetRunningLength();
		float endtime	= ( m_nSelection[ 1 ] / numsamples ) * m_pWaveFile->GetRunningLength();

		if ( word->m_flEndTime >= starttime &&
			 word->m_flStartTime <= endtime )
		{
			word->m_bSelected = true;

			m_bWordsActive = true;
		}
	}
}


//-----------------------------------------------------------------------------
// Purpose: 
// Input  : start - 
//			end - 
//-----------------------------------------------------------------------------
void PhonemeEditor::SelectSamples( int start, int end )
{
	if ( !m_pWaveFile )
		return;

	// Make sure order is correct
	if ( end < start )
	{
		int temp = end;
		end = start;
		start = temp;
	}

	Deselect();

	m_nSelection[ 0 ] = start;
	m_nSelection[ 1 ] = end;
	m_bSelectionActive = true;

	// Select any words that span the selection
	//
	TraverseWords( &PhonemeEditor::ITER_SelectSpanningWords, 0.0f );

	redraw();
}

void PhonemeEditor::FinishMoveSelection( int startx, int mx )
{
	if ( !m_pWaveFile )
		return;

	int sampleStart = GetSampleForMouse( startx );
	int sampleEnd = GetSampleForMouse( mx );

	int delta = sampleEnd - sampleStart;

	for ( int i = 0; i < 2; i++ )
	{
		m_nSelection[ i ] += delta;
	}

	// Select any words that span the selection
	//
	TraverseWords( &PhonemeEditor::ITER_SelectSpanningWords, 0.0f );

	redraw();
}

void PhonemeEditor::FinishMoveSelectionStart( int startx, int mx )
{
	if ( !m_pWaveFile )
		return;

	int sampleStart = GetSampleForMouse( startx );
	int sampleEnd = GetSampleForMouse( mx );

	int delta = sampleEnd - sampleStart;

	m_nSelection[ 0 ] += delta;

	if ( m_nSelection[ 0 ] >= m_nSelection[ 1 ] )
	{
		Deselect();
	}

	// Select any words that span the selection
	//
	TraverseWords( &PhonemeEditor::ITER_SelectSpanningWords, 0.0f );

	redraw();
}

void PhonemeEditor::FinishMoveSelectionEnd( int startx, int mx )
{
	if ( !m_pWaveFile )
		return;

	int sampleStart = GetSampleForMouse( startx );
	int sampleEnd = GetSampleForMouse( mx );

	int delta = sampleEnd - sampleStart;

	m_nSelection[ 1 ] += delta;

	if ( m_nSelection[ 1 ] <= m_nSelection[ 0 ] )
	{
		Deselect();
	}

	// Select any words that span the selection
	//
	TraverseWords( &PhonemeEditor::ITER_SelectSpanningWords, 0.0f );

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : startx - 
//			mx - 
//-----------------------------------------------------------------------------
void PhonemeEditor::FinishSelect( int startx, int mx )
{
	if ( !m_pWaveFile )
		return;

	// Don't select really small areas
	if ( abs( startx - mx ) < 2 )
		return;

	int sampleStart = GetSampleForMouse( startx );
	int sampleEnd = GetSampleForMouse( mx );

	SelectSamples( sampleStart, sampleEnd );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : mx - 
//			my - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool PhonemeEditor::IsMouseOverSamples( int mx, int my )
{
	if ( GetMode() != MODE_PHONEMES )
		return false;

	// Deterime if phoneme boundary is under the cursor
	//
	if ( !m_pWaveFile )
		return false;

	RECT rc;
	GetWorkspaceRect( rc );

	// Over tag
	if ( my >= TAG_TOP && my <= TAG_BOTTOM )
		return false;

	if ( IsMouseOverPhonemeRow( my ) )
		return false;

	if ( IsMouseOverWordRow( my ) )
		return false;

	RECT rcWord;
	GetWordTrayTopBottom( rcWord );
	RECT rcPhoneme;
	GetPhonemeTrayTopBottom( rcPhoneme );

	if ( my < rcWord.bottom )
		return false;

	if ( my > rcPhoneme.top )
		return false;

	return true;
}

void PhonemeEditor::GetScreenStartAndEndTime( float &starttime, float& endtime )
{
	starttime = m_nLeftOffset / GetPixelsPerSecond();
	endtime = w2() / GetPixelsPerSecond() + starttime;
}

float PhonemeEditor::GetTimePerPixel( void )
{
	RECT rc;
	GetWorkspaceRect( rc );

	float starttime, endtime;
	GetScreenStartAndEndTime( starttime, endtime );

	if ( rc.right - rc.left <= 0 )
	{
		return ( endtime - starttime );
	}

	float timeperpixel = ( endtime - starttime ) / (float)( rc.right - rc.left );
	return timeperpixel;
}

int PhonemeEditor::GetPixelForSample( int sample )
{
	RECT rc;
	GetWorkspaceRect( rc );

	if ( !m_pWaveFile )
		return rc.left;

	// Determine start/stop positions
	int totalsamples = (int)( m_pWaveFile->GetRunningLength() * m_pWaveFile->SampleRate() );
	if ( totalsamples <= 0 )
	{
		return rc.left;
	}

	float starttime, endtime;
	GetScreenStartAndEndTime( starttime, endtime );

	float sampleFrac = (float)sample / (float)totalsamples;
	float sampleTime = sampleFrac * (float)m_pWaveFile->GetRunningLength();

	if ( endtime - starttime < 0.0f )
	{
		return rc.left;
	}

	float windowFrac = ( sampleTime - starttime ) / ( endtime - starttime );

	return rc.left + (int)( windowFrac * ( rc.right - rc.left ) );
}

int PhonemeEditor::GetSampleForMouse( int mx )
{
	if ( !m_pWaveFile )
		return 0;

	RECT rc;
	GetWorkspaceRect( rc );

	// Determine start/stop positions
	int totalsamples = (int)( m_pWaveFile->GetRunningLength() * m_pWaveFile->SampleRate() );

	float starttime, endtime;
	GetScreenStartAndEndTime( starttime, endtime );

	if ( GetPixelsPerSecond() <= 0 )
		return 0;

	// Start and end times
	float clickTime	= (float)mx / GetPixelsPerSecond() + starttime;

	// What sample do these correspond to
	if ( (float)m_pWaveFile->GetRunningLength() <= 0.0f )
		return 0;

	int sampleNumber	= (int) ( (float)totalsamples * clickTime / (float)m_pWaveFile->GetRunningLength() );

	return sampleNumber;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : mx - 
//			my - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool PhonemeEditor::IsMouseOverSelection( int mx, int my )
{
	if ( GetMode() != MODE_PHONEMES )
		return false;

	if ( !m_pWaveFile )
		return false;

	if ( !m_bSelectionActive )
		return false;

	if ( !IsMouseOverSamples( mx, my ) )
		return false;

	int sampleNumber = GetSampleForMouse( mx );

	if ( sampleNumber >= m_nSelection[ 0 ] - 3 &&
		 sampleNumber <= m_nSelection[ 1 ] + 3 )
	{
		return true;
	}

	return false;
}

bool PhonemeEditor::IsMouseOverSelectionStartEdge( mxEvent *event )
{
	if ( GetMode() != MODE_PHONEMES )
		return false;

	if ( !m_pWaveFile )
		return false;

	int mx, my;
	mx = (short)event->x;
	my = (short)event->y;

	if ( !(event->modifiers & mxEvent::KeyCtrl ) )
		return false;

	if ( !IsMouseOverSelection( mx, my ) )
		return false;

	int sample = GetSampleForMouse( mx );

	int		mouse_tolerance = 5;

	RECT rc;
	GetWorkspaceRect( rc );

	// Determine start/stop positions
	float timeperpixel = GetTimePerPixel();

	int samplesperpixel = (int)( timeperpixel * m_pWaveFile->SampleRate() );

	if ( abs( sample - m_nSelection[ 0 ] ) < mouse_tolerance * samplesperpixel )
	{
		return true;
	}
	return false;
}

bool PhonemeEditor::IsMouseOverSelectionEndEdge( mxEvent *event )
{
	if ( GetMode() != MODE_PHONEMES )
		return false;

	if ( !m_pWaveFile )
		return false;

	int mx, my;
	mx = (short)event->x;
	my = (short)event->y;

	if ( !(event->modifiers & mxEvent::KeyCtrl ) )
		return false;

	if ( !IsMouseOverSelection( mx, my ) )
		return false;

	int sample = GetSampleForMouse( mx );

	int		mouse_tolerance = 5;

	RECT rc;
	GetWorkspaceRect( rc );

	if ( GetPixelsPerSecond() <= 0.0f )
		return false;

	if ( ( rc.right - rc.left ) <= 0 )
		return false;

	// Determine start/stop positions
	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	float timeperpixel = ( endtime - starttime ) / (float)( rc.right - rc.left );
	int samplesperpixel = (int)( timeperpixel * m_pWaveFile->SampleRate() );

	if ( abs( sample - m_nSelection[ 1 ] ) < mouse_tolerance * samplesperpixel )
	{
		return true;
	}
	return false;
}

void PhonemeEditor::OnImport()
{
	char filename[ 512 ];
	if ( !FacePoser_ShowOpenFileNameDialog( filename, sizeof( filename ), "sound", "*" WORD_DATA_EXTENSION ) )
	{
		return;
	}

	ImportValveDataChunk( filename );
}

void PhonemeEditor::OnExport()
{
	if ( !m_pWaveFile )
		return;

	char filename[ 512 ];
	if ( !FacePoser_ShowSaveFileNameDialog( filename, sizeof( filename ), "sound", "*" WORD_DATA_EXTENSION ) )
	{
		return;
	}

	Q_SetExtension( filename, WORD_DATA_EXTENSION, sizeof( filename ) );

	ExportValveDataChunk( filename );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : store - 
//-----------------------------------------------------------------------------
void PhonemeEditor::StoreValveDataChunk( IterateOutputRIFF& store )
{
	// Buffer and dump data
	CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER );

	m_Tags.SaveToBuffer( buf );

	// Copy into store
	store.ChunkWriteData( buf.Base(), buf.TellPut() );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *tempfile - 
//-----------------------------------------------------------------------------
void PhonemeEditor::ExportValveDataChunk( char const *tempfile )
{
	if ( m_Tags.m_Words.Count() <= 0 )
	{
		Con_ErrorPrintf( "PhonemeEditor::ExportValveDataChunk:  Sentence has no word data\n" );
		return;
	}

	FileHandle_t fh = filesystem->Open( tempfile, "wb" );
	if ( !fh )
	{
		Con_ErrorPrintf( "PhonemeEditor::ExportValveDataChunk:  Unable to write to %s (read-only?)\n", tempfile );
		return;
	}
	else
	{
		// Buffer and dump data
		CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER );

		m_Tags.SaveToBuffer( buf );

		filesystem->Write( buf.Base(), buf.TellPut(), fh );
		filesystem->Close(fh);

		Con_Printf( "Exported %i words to %s\n", m_Tags.m_Words.Count(), tempfile );
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *tempfile - 
//-----------------------------------------------------------------------------
void PhonemeEditor::ImportValveDataChunk( char const *tempfile )
{
	FileHandle_t fh = filesystem->Open( tempfile, "rb" );
	if ( !fh )
	{
		Con_ErrorPrintf( "PhonemeEditor::ImportValveDataChunk:  Unable to read from %s\n", tempfile );
		return;
	}

	int len = filesystem->Size( fh );
	if ( len <= 4 )
	{
		Con_ErrorPrintf( "PhonemeEditor::ImportValveDataChunk:  File %s has length 0\n", tempfile );
		return;
	}

	ClearExtracted();

	unsigned char *buf = new unsigned char[ len + 1 ];

	filesystem->Read( buf, len, fh );
	filesystem->Close( fh );

	m_TagsExt.InitFromDataChunk( (void *)( buf ), len );

	delete[] buf;

	Con_Printf( "Imported %i words from %s\n", m_TagsExt.m_Words.Count(), tempfile );

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: Copy file over, but update phoneme lump with new data
//-----------------------------------------------------------------------------
void PhonemeEditor::SaveLinguisticData( void )
{
	if ( !m_pWaveFile )
		return;

	InFileRIFF riff( m_WorkFile.m_szWaveFile, io_in );
	Assert( riff.RIFFName() == RIFF_WAVE );

	// set up the iterator for the whole file (root RIFF is a chunk)
	IterateRIFF walk( riff, riff.RIFFSize() );

	char fullout[ 512 ];
	Q_snprintf( fullout, sizeof( fullout ), "%s%s", m_WorkFile.m_szBasePath, m_WorkFile.m_szWorkingFile );

	OutFileRIFF riffout( fullout, io_out );

	IterateOutputRIFF store( riffout );

	bool formatset = false;
	WAVEFORMATEX format;

	bool wordtrackwritten = false;

	// Walk input chunks and copy to output
	while ( walk.ChunkAvailable() )
	{
		unsigned int originalPos = store.ChunkGetPosition();

		store.ChunkStart( walk.ChunkName() );

		bool skipchunk = false;

		switch ( walk.ChunkName() )
		{
		case WAVE_VALVEDATA:
			// Overwrite data
			StoreValveDataChunk( store );
			wordtrackwritten = true;
			break;
		case WAVE_FMT:
			{
				formatset = true;
				
				char *buffer = new char[ walk.ChunkSize() ];
				Assert( buffer );
				walk.ChunkRead( buffer );

				format = *(WAVEFORMATEX *)buffer;

				store.ChunkWriteData( buffer, walk.ChunkSize() );

				delete[] buffer;
			}
			break;
		case WAVE_DATA:
			{
				Assert( formatset );
				
				char *buffer = new char[ walk.ChunkSize() ];
				Assert( buffer );
				
				walk.ChunkRead( buffer );
				// Resample it
				ResampleChunk( store, (void *)&format, walk.ChunkName(), buffer, walk.ChunkSize() );

				delete[] buffer;
			}
			break;
		default:
			store.CopyChunkData( walk );
			break;
		}

		store.ChunkFinish();
		if ( skipchunk )
		{
			store.ChunkSetPosition( originalPos );
		}

		walk.ChunkNext();
	}

	if ( !wordtrackwritten )
	{
		store.ChunkStart( WAVE_VALVEDATA );
		StoreValveDataChunk( store );
		store.ChunkFinish();
	}
}

//-----------------------------------------------------------------------------
// Purpose: Copy phoneme data in from wave file we sent for resprocessing
//-----------------------------------------------------------------------------
void PhonemeEditor::RetrieveLinguisticData( void )
{
	if ( !m_pWaveFile )
		return;

	m_Tags.Reset();

	ReadLinguisticTags();

	redraw();
}

bool PhonemeEditor::StopPlayback( void )
{
	bool bret = false;
	if ( m_pWaveFile )
	{
		SetScrubTargetTime( m_flScrub );

		if ( sound->IsSoundPlaying( m_pMixer ) )
		{
			sound->StopAll();
			bret = true;
		}
	}

	sound->Flush();

	return bret;
}

CPhonemeTag *PhonemeEditor::GetPhonemeTagUnderMouse( int mx, int my )
{
	if ( GetMode() != MODE_PHONEMES )
		return NULL;

	if ( !m_pWaveFile )
		return NULL;

	// FIXME:  Don't read from file, read from arrays after LISET finishes
	// Deterime if phoneme boundary is under the cursor
	//
	RECT rc;
	GetWorkspaceRect( rc );

	if ( !IsMouseOverPhonemeRow( my ) )
		return NULL;

	if ( GetPixelsPerSecond() <= 0 )
		return NULL;

	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	if ( endtime - starttime <= 0.0f )
		return NULL;

	for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		Assert( word );
		if ( !word )
			continue;

		for ( int k = 0; k < word->m_Phonemes.Size(); k++ )
		{
			CPhonemeTag *pPhoneme = word->m_Phonemes[ k ];
			Assert( pPhoneme );
			if ( !pPhoneme )
				continue;

			float t1 = pPhoneme->GetStartTime();
			float t2 = pPhoneme->GetEndTime();

			float frac1 = ( t1 - starttime ) / ( endtime - starttime );
			float frac2 = ( t2 - starttime ) / ( endtime - starttime );

			frac1 = min( 1.0f, frac1 );
			frac1 = max( 0.0f, frac1 );
			frac2 = min( 1.0f, frac2 );
			frac2 = max( 0.0f, frac2 );

			if ( frac1 == frac2 )
				continue;

			int x1 = ( int )( frac1 * w2() );
			int x2 = ( int )( frac2 * w2() );

			if ( mx >= x1 && mx <= x2 )
			{
				return pPhoneme;
			}
		}
	}

	return NULL;
}

CWordTag *PhonemeEditor::GetWordTagUnderMouse( int mx, int my )
{
	if ( GetMode() != MODE_PHONEMES )
		return NULL;

	// Deterime if phoneme boundary is under the cursor
	//
	if ( !m_pWaveFile )
		return NULL;

	RECT rc;
	GetWorkspaceRect( rc );

	if ( !IsMouseOverWordRow( my ) )
		return NULL;

	if ( GetPixelsPerSecond() <= 0 )
		return NULL;

	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	if ( endtime - starttime <= 0.0f )
		return NULL;

	for ( int k = 0; k < m_Tags.m_Words.Size(); k++ )
	{
		CWordTag *word = m_Tags.m_Words[ k ];
		Assert( word );
		if ( !word )
			continue;

		float t1 = word->m_flStartTime;
		float t2 = word->m_flEndTime;

		float frac1 = ( t1 - starttime ) / ( endtime - starttime );
		float frac2 = ( t2 - starttime ) / ( endtime - starttime );

		frac1 = min( 1.0f, frac1 );
		frac1 = max( 0.0f, frac1 );
		frac2 = min( 1.0f, frac2 );
		frac2 = max( 0.0f, frac2 );

		if ( frac1 == frac2 )
			continue;

		int x1 = ( int )( frac1 * w2() );
		int x2 = ( int )( frac2 * w2() );

		if ( mx >= x1 && mx <= x2 )
		{
			return word;
		}
	}

	return NULL;
}

void PhonemeEditor::DeselectWords( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	for ( int i = 0 ; i < m_Tags.m_Words.Size(); i++ )
	{
		CWordTag *w = m_Tags.m_Words[ i ];
		Assert( w );
		if ( !w )
			continue;
		w->m_bSelected = false;
	}

}

void PhonemeEditor::DeselectPhonemes( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	for ( int w = 0 ; w < m_Tags.m_Words.Size(); w++ )
	{
		CWordTag *word = m_Tags.m_Words[ w ];
		Assert( word );
		if ( !word )
			continue;

		for ( int i = 0 ; i < word->m_Phonemes.Size(); i++ )
		{
			CPhonemeTag *pt = word->m_Phonemes[ i ];
			Assert( pt );
			if ( !pt )
				continue;
			pt->m_bSelected = false;
		}
	}
}

void PhonemeEditor::SnapWords( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	if ( m_Tags.m_Words.Size() < 2 )
	{
		Con_Printf( "Can't snap, need at least two contiguous selected words\n" );
		return;
	}

	SetDirty( true );

	PushUndo();

	for ( int i = 0; i < m_Tags.m_Words.Size() - 1; i++ )
	{
		CWordTag *current = m_Tags.m_Words[ i ];
		CWordTag *next = m_Tags.m_Words[ i + 1 ];

		Assert( current && next );

		if ( !current->m_bSelected || !next->m_bSelected )
			continue;

		// Move next word to end of current
		next->m_flStartTime = current->m_flEndTime;
	}

	PushRedo();

	redraw();
}

void PhonemeEditor::SeparateWords( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	if ( GetPixelsPerSecond() <= 0.0f )
		return;

	if ( m_Tags.m_Words.Size() < 2 )
	{
		Con_Printf( "Can't separate, need at least two contiguous selected words\n" );
		return;
	}

	// Three pixels
	double time_epsilon = ( 1.0f / GetPixelsPerSecond() ) * 6;

	SetDirty( true );

	PushUndo();

	for ( int i = 0; i < m_Tags.m_Words.Size() - 1; i++ )
	{
		CWordTag *current = m_Tags.m_Words[ i ];
		CWordTag *next = m_Tags.m_Words[ i + 1 ];

		Assert( current && next );

		if ( !current->m_bSelected || !next->m_bSelected )
			continue;

		// Close enough?
		if ( fabs( current->m_flEndTime - next->m_flStartTime ) > time_epsilon )
		{
			Con_Printf( "Can't split %s and %s, already split apart\n",
				current->GetWord(), next->GetWord() );
			continue;
		}

		// Offset next word start time a bit
		next->m_flStartTime += time_epsilon;

		break;
	}

	PushRedo();

	redraw();
}

void PhonemeEditor::CreateEvenWordDistribution( const char *wordlist )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	if( !m_pWaveFile )
		return;

	Assert( wordlist );
	if ( !wordlist )
		return;

	m_Tags.CreateEventWordDistribution( wordlist, m_pWaveFile->GetRunningLength() );

	redraw();
}

void PhonemeEditor::EditWordList( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	if ( !m_pWaveFile )
		return;

	// Build word string
	char wordstring[ 1024 ];
	V_strcpy_safe( wordstring, m_Tags.GetText() );

	CInputParams params;
	memset( &params, 0, sizeof( params ) );
	strcpy( params.m_szDialogTitle, "Word List" );
	strcpy( params.m_szPrompt, "Sentence:" );

	strcpy( params.m_szInputText, wordstring );

	if ( !InputProperties( &params ) )
		return;

	if ( strlen( params.m_szInputText ) <= 0 )
	{
		// Could be foreign language...
		Warning( "Edit word list:  No words entered!\n" );
	}

	SetDirty( true );

	PushUndo();

	// Clear any current LISET results
	ClearExtracted();

	// Force text
	m_Tags.SetText( params.m_szInputText );

	if ( m_Tags.m_Words.Size() == 0 )
	{
		// First text we've seen, just distribute words evenly
		CreateEvenWordDistribution( params.m_szInputText );
	
		// Redo liset
		RedoPhonemeExtraction();
	}
	
	PushRedo();

	SetFocus( (HWND)getHandle() );

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: Overwrite original wave with changes
//-----------------------------------------------------------------------------
void PhonemeEditor::CommitChanges( void )
{
	SaveLinguisticData();

	// Make it writable - if possible
	MakeFileWriteable( m_WorkFile.m_szWaveFile );

	//Open a message box to warn the user if the file was unable to be made non-read only
	if ( !IsFileWriteable( m_WorkFile.m_szWaveFile ) )
	{
		mxMessageBox( NULL, va( "Unable to save file '%s'. File is read-only or in use.",	
				m_WorkFile.m_szWaveFile ), g_appTitle, MX_MB_OK );
	}
	else
	{
		// Copy over and overwrite file
		FPCopyFile( m_WorkFile.m_szWorkingFile, m_WorkFile.m_szWaveFile, true );
		Msg( "Changes saved to '%s'\n", m_WorkFile.m_szWaveFile );
		SetDirty( false, false );
	}

}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::LoadWaveFile( void )
{
	char filename[ 512 ];
	if ( !FacePoser_ShowOpenFileNameDialog( filename, sizeof( filename ), "sound", "*.wav" ) )
	{
		return;
	}

	StopPlayback();

	// Strip out the game directory
	SetCurrentWaveFile( filename );
}

void PhonemeEditor::SnapPhonemes( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	SetDirty( true );

	PushUndo();

	CPhonemeTag *prev = NULL;

	for ( int w = 0; w < m_Tags.m_Words.Size(); w++ )
	{
		CWordTag *word = m_Tags.m_Words[ w ];
		Assert( word );
		if ( !word )
			continue;

		for ( int i = 0; i < word->m_Phonemes.Size(); i++ )
		{
			CPhonemeTag *current = word->m_Phonemes[ i ];
			
			Assert( current );

			if ( current->m_bSelected )
			{
				if (prev)
				{
					// More start of next to end of previous
					prev->SetEndTime( current->GetStartTime() );
				}
				prev = current;
			}
			else
			{
				prev = NULL;
			}
		}
	}

	PushRedo();

	redraw();
}

void PhonemeEditor::SeparatePhonemes( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	SetDirty( true );

	PushUndo();

	// Three pixels
	double time_epsilon = ( 1.0f / GetPixelsPerSecond() ) * 6;

	CPhonemeTag *prev = NULL;

	for ( int w = 0; w < m_Tags.m_Words.Size(); w++ )
	{
		CWordTag *word = m_Tags.m_Words[ w ];
		Assert( word );
		if ( !word )
			continue;

		for ( int i = 0; i < word->m_Phonemes.Size(); i++ )
		{
			CPhonemeTag *current = word->m_Phonemes[ i ];

			Assert( current );

			if ( current->m_bSelected )
			{
				if ( prev )
				{
					// Close enough?
					if ( fabs( prev->GetEndTime() - current->GetStartTime() ) > time_epsilon )
					{
						Con_Printf( "Can't split already split apart\n" );
						continue;
					}

					current->AddStartTime( time_epsilon );
				}

				prev = current;
			}
			else
			{
				prev = NULL;
			}
		}
	}

	PushRedo();

	redraw();
}

bool PhonemeEditor::IsMouseOverWordRow( int my )
{
	if ( GetMode() != MODE_PHONEMES )
		return false;

	RECT rc;

	GetWordTrayTopBottom( rc );

	if ( my < rc.top )
		return false;
	
	if ( my > rc.bottom )
		return false;
	
	return true;
}

bool PhonemeEditor::IsMouseOverPhonemeRow( int my )
{
	if ( GetMode() != MODE_PHONEMES )
		return false;

	RECT rc;

	GetPhonemeTrayTopBottom( rc );

	if ( my < rc.top )
		return false;
	
	if ( my > rc.bottom )
		return false;
	
	return true;
}

void PhonemeEditor::GetPhonemeTrayTopBottom( RECT& rc )
{
	RECT wkrc;
	GetWorkspaceRect( wkrc );

	rc.top  = wkrc.bottom - 2 * m_nTickHeight;
	rc.bottom = wkrc.bottom - m_nTickHeight;
}

void PhonemeEditor::GetWordTrayTopBottom( RECT& rc )
{
	RECT wkrc;
	GetWorkspaceRect( wkrc );

	rc.top  = wkrc.top;
	rc.bottom = wkrc.top + m_nTickHeight;
}

int PhonemeEditor::GetMouseForTime( float time )
{
	RECT rc;
	GetWorkspaceRect( rc );

	if ( GetPixelsPerSecond() < 0.0f )
		return rc.left;

	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	if ( endtime - starttime <= 0.0f )
		return rc.left;

	float frac;

	frac = ( time - starttime ) / ( endtime - starttime );

	return rc.left + ( int )( rc.right * frac );
}

void PhonemeEditor::GetWordRect( const CWordTag *tag, RECT& rc )
{
	Assert( tag );
	GetWordTrayTopBottom( rc );
	
	rc.left = GetMouseForTime( tag->m_flStartTime );
	rc.right = GetMouseForTime( tag->m_flEndTime );

}

void PhonemeEditor::GetPhonemeRect( const CPhonemeTag *tag, RECT& rc )
{
	Assert( tag );

	GetPhonemeTrayTopBottom( rc );
	rc.left = GetMouseForTime( tag->GetStartTime() );
	rc.right = GetMouseForTime( tag->GetEndTime() );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::CommitExtracted( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	m_nLastExtractionResult		= SR_RESULT_NORESULT;

	if ( !m_TagsExt.m_Words.Size() )
		return;

	SetDirty( true );

	PushUndo();

	m_Tags.Reset();
	m_Tags = m_TagsExt;

	PushRedo();

	ClearExtracted();

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::ClearExtracted( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	m_nLastExtractionResult		= SR_RESULT_NORESULT;

	m_TagsExt.Reset();

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : resultCode - 
// Output : const char
//-----------------------------------------------------------------------------
const char *PhonemeEditor::GetExtractionResultString( int resultCode )
{
	switch ( resultCode )
	{
	case SR_RESULT_NORESULT:
		return "no extraction info.";
	case SR_RESULT_ERROR:
		return "an error occurred during extraction.";
	case SR_RESULT_SUCCESS:
		return "successful.";
	case SR_RESULT_FAILED:
		return "results retrieved, but full recognition failed.";
	default:
		break;
	}

	return "unknown result code.";
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : mx - 
// Output : CEventRelativeTag
//-----------------------------------------------------------------------------
CEventRelativeTag *PhonemeEditor::GetTagUnderMouse( int mx )
{
	if ( GetMode() != MODE_PHONEMES )
		return NULL;

	// Figure out tag positions
	if ( !m_pEvent || !m_pWaveFile )
		return NULL;
	
	RECT rc;
	GetWorkspaceRect( rc );
	RECT rcTags = rc;

	if ( GetPixelsPerSecond() <= 0.0f )
		return NULL;

	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	if ( endtime - starttime < 0 )
		return NULL;

	for ( int i = 0; i < m_pEvent->GetNumRelativeTags(); i++ )
	{
		CEventRelativeTag *tag = m_pEvent->GetRelativeTag( i );
		if ( !tag )
			continue;

		// 
		float tagtime = tag->GetPercentage() * m_pWaveFile->GetRunningLength();
		if ( tagtime < starttime || tagtime > endtime )
			continue;

		float frac = ( tagtime - starttime ) / ( endtime - starttime );

		int left = rcTags.left + (int)( frac * ( float )( rcTags.right - rcTags.left ) + 0.5f );

		if ( abs( mx - left ) < 10 )
			return tag;
	}
	return NULL;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : mx - 
//			my - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool PhonemeEditor::IsMouseOverTag( int mx, int my )
{
	if ( GetMode() != MODE_PHONEMES )
		return false;

	if ( !IsMouseOverTagRow( my ) )
		return false;

	CEventRelativeTag *tag = GetTagUnderMouse( mx );
	if ( !tag )
		return false;

	return true;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : startx - 
//			endx - 
//-----------------------------------------------------------------------------
void PhonemeEditor::FinishEventTagDrag( int startx, int endx )
{
	if ( !m_pWaveFile )
		return;

	if ( !m_pWaveFile->GetRunningLength() )
		return;

	// Find starting tag
	CEventRelativeTag *tag = GetTagUnderMouse( startx );
	if ( !tag )
		return;

	if ( GetPixelsPerSecond() <= 0 )
		return;

	// Convert mouse position to time
	float starttime = m_nLeftOffset / GetPixelsPerSecond();

	float clicktime = (float)endx / GetPixelsPerSecond() + starttime;

	float percent = clicktime / m_pWaveFile->GetRunningLength();
	percent = clamp( percent, 0.0f, 1.0f );
	
	tag->SetPercentage( percent );

	redraw();

	if ( g_pChoreoView )
	{
		g_pChoreoView->InvalidateLayout();
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : my - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool PhonemeEditor::IsMouseOverTagRow( int my )
{
	if ( GetMode() != MODE_PHONEMES )
		return false;

	if ( my < TAG_TOP || my > TAG_BOTTOM )
		return false;

	return true;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : mx - 
//			my - 
//-----------------------------------------------------------------------------
void PhonemeEditor::ShowTagMenu( int mx, int my )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	// Figure out tag positions
	if ( !m_pEvent || !m_pWaveFile )
		return;

	if ( !IsMouseOverTagRow( my ) )
		return;

	CEventRelativeTag *tag = GetTagUnderMouse( mx );

	mxPopupMenu *pop = new mxPopupMenu();

	if ( tag )
	{
		pop->add( va( "Delete tag '%s'", tag->GetName() ), IDC_DELETETAG );
	}
	else
	{
		pop->add( va( "Add tag..." ), IDC_ADDTAG );
	}

	m_nClickX = mx;

	pop->popup( this, mx, my );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::DeleteTag( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	// Figure out tag positions
	if ( !m_pEvent )
		return;

	CEventRelativeTag *tag = GetTagUnderMouse( m_nClickX );
	if ( !tag )
		return;

	// Remove it
	m_pEvent->RemoveRelativeTag( tag->GetName() );

	g_pChoreoView->InvalidateLayout();
	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::AddTag( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	// Figure out tag positions
	if ( !m_pEvent || !m_pWaveFile )
		return;

	CInputParams params;
	memset( &params, 0, sizeof( params ) );
	strcpy( params.m_szDialogTitle, "Event Tag Name" );
	strcpy( params.m_szPrompt, "Name:" );
	strcpy( params.m_szInputText, "" );

	if ( !InputProperties( &params ) )
		return;

	if ( strlen( params.m_szInputText ) <= 0 )
	{
		Con_ErrorPrintf( "Event Tag Name:  No name entered!\n" );
		return;
	}

	// Convert mouse position to time
	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float clicktime = (float)m_nClickX / GetPixelsPerSecond() + starttime;

	float percent = clicktime / m_pWaveFile->GetRunningLength();
	percent = min( 1.0f, percent );
	percent = max( 0.0f, percent );

	m_pEvent->AddRelativeTag( params.m_szInputText, percent );

	g_pChoreoView->InvalidateLayout();

	SetFocus( (HWND)getHandle() );

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::ClearEvent( void )
{
	m_pEvent = NULL;
	redraw();
}

void PhonemeEditor::TraverseWords( PEWORDITERFUNC pfn, float fparam )
{
	for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		if ( !word )
			continue;

		(this->*pfn)( word, fparam );
	}
}

void PhonemeEditor::TraversePhonemes( PEPHONEMEITERFUNC pfn, float fparam )
{
	for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		if ( !word )
			continue;

		for ( int j = 0; j < word->m_Phonemes.Size(); j++ )
		{
			CPhonemeTag *phoneme = word->m_Phonemes[ j ];
			if ( !phoneme )
				continue;

			(this->*pfn)( phoneme, word, fparam );
		}
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : amount - 
//-----------------------------------------------------------------------------
void PhonemeEditor::ITER_MoveSelectedWords( CWordTag *word, float amount )
{
	if ( !word->m_bSelected )
		return;

	word->m_flStartTime += amount;
	word->m_flEndTime += amount;
}

void PhonemeEditor::ITER_MoveSelectedPhonemes( CPhonemeTag *phoneme, CWordTag *word, float amount )
{
	if ( !phoneme->m_bSelected )
		return;

	phoneme->AddStartTime( amount );
	phoneme->AddEndTime( amount );

}

void PhonemeEditor::ITER_ExtendSelectedPhonemeEndTimes( CPhonemeTag *phoneme, CWordTag *word, float amount )
{
	if ( !phoneme->m_bSelected )
		return;

	if ( phoneme->GetEndTime() + amount <= phoneme->GetStartTime() )
		return;

	phoneme->AddEndTime( amount );

	// Fixme, check for extending into next phoneme
}


void PhonemeEditor::ITER_ExtendSelectedWordEndTimes( CWordTag *word, float amount )
{
	if ( !word->m_bSelected )
		return;

	if ( word->m_flEndTime + amount <= word->m_flStartTime )
		return;

	word->m_flEndTime += amount;

	// Fixme, check for extending into next word
}

void PhonemeEditor::ITER_AddFocusRectSelectedWords( CWordTag *word, float amount )
{
	if ( !word->m_bSelected )
		return;

	RECT wordRect;
	GetWordRect( word, wordRect );

	AddFocusRect( wordRect );
}

void PhonemeEditor::ITER_AddFocusRectSelectedPhonemes( CPhonemeTag *phoneme, CWordTag *word, float amount )
{
	if ( !phoneme->m_bSelected )
		return;

	RECT phonemeRect;
	GetPhonemeRect( phoneme, phonemeRect );

	AddFocusRect( phonemeRect );
}


void PhonemeEditor::AddFocusRect( RECT& rc )
{
	RECT rcFocus = rc;

	POINT offset;
	offset.x = 0;
	offset.y = 0;
	ClientToScreen( (HWND)getHandle(), &offset );
	OffsetRect( &rcFocus, offset.x, offset.y );

	// Convert to screen space?
	CFocusRect fr;
	fr.m_rcFocus = rcFocus;
	fr.m_rcOrig = rcFocus;

	m_FocusRects.AddToTail( fr );
}

void PhonemeEditor::CountSelected( void )
{
	m_nSelectedPhonemeCount = 0;
	m_nSelectedWordCount = 0;

	TraverseWords( &PhonemeEditor::ITER_CountSelectedWords, 0.0f );
	TraversePhonemes( &PhonemeEditor::ITER_CountSelectedPhonemes, 0.0f );
}

void PhonemeEditor::ITER_CountSelectedWords( CWordTag *word, float amount )
{
	if ( !word->m_bSelected )
		return;

	m_nSelectedWordCount++;

}

void PhonemeEditor::ITER_CountSelectedPhonemes( CPhonemeTag *phoneme, CWordTag *word, float amount )
{
	if ( !phoneme->m_bSelected )
		return;

	m_nSelectedPhonemeCount++;
}

// Undo/Redo
void PhonemeEditor::Undo( void )
{
	if ( m_UndoStack.Size() > 0 && m_nUndoLevel > 0 )
	{
		m_nUndoLevel--;
		PEUndo *u = m_UndoStack[ m_nUndoLevel ];
		Assert( u->undo );
		m_Tags = *(u->undo);

		SetClickedPhoneme( -1, -1 );
	}

	redraw();
}

void PhonemeEditor::Redo( void )
{
	if ( m_UndoStack.Size() > 0 && m_nUndoLevel <= m_UndoStack.Size() - 1 )
	{
		PEUndo *u = m_UndoStack[ m_nUndoLevel ];
		Assert( u->redo );
		m_Tags = *(u->redo);
		m_nUndoLevel++;

		SetClickedPhoneme( -1, -1 );
	}

	redraw();
}

void PhonemeEditor::PushUndo( void )
{
	Assert( !m_bRedoPending );
	m_bRedoPending = true;
	WipeRedo();

	// Copy current data
	CSentence *u = new CSentence();
	*u = m_Tags;
	PEUndo *undo = new PEUndo;
	undo->undo = u;
	undo->redo = NULL;
	m_UndoStack.AddToTail( undo );
	m_nUndoLevel++;
}

void PhonemeEditor::PushRedo( void )
{
	Assert( m_bRedoPending );
	m_bRedoPending = false;

	// Copy current data
	CSentence *r = new CSentence();
	*r = m_Tags;
	PEUndo *undo = m_UndoStack[ m_nUndoLevel - 1 ];
	undo->redo = r;
}

void PhonemeEditor::WipeUndo( void )
{
	while ( m_UndoStack.Size() > 0 )
	{
		PEUndo *u = m_UndoStack[ 0 ];
		delete u->undo;
		delete u->redo;
		delete u;
		m_UndoStack.Remove( 0 );
	}
	m_nUndoLevel = 0;
}

void PhonemeEditor::WipeRedo( void )
{
	// Wipe everything above level
	while ( m_UndoStack.Size() > m_nUndoLevel )
	{
		PEUndo *u = m_UndoStack[ m_nUndoLevel ];
		delete u->undo;
		delete u->redo;
		delete u;
		m_UndoStack.Remove( m_nUndoLevel );
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : word - 
//			phoneme - 
//-----------------------------------------------------------------------------
void PhonemeEditor::SetClickedPhoneme( int word, int phoneme )
{
	m_nClickedPhoneme = phoneme;
	m_nClickedWord = word;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : CPhonemeTag
//-----------------------------------------------------------------------------
CPhonemeTag *PhonemeEditor::GetClickedPhoneme( void )
{
	if ( m_nClickedPhoneme < 0 || m_nClickedWord < 0 )
		return NULL;

	if ( m_nClickedWord >= m_Tags.m_Words.Size() )
		return NULL;

	CWordTag *word = m_Tags.m_Words[ m_nClickedWord ];
	if ( !word )
		return NULL;

	if ( m_nClickedPhoneme >= word->m_Phonemes.Size() )
		return NULL;

	CPhonemeTag *phoneme = word->m_Phonemes[ m_nClickedPhoneme ];
	return phoneme;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : CWordTag
//-----------------------------------------------------------------------------
CWordTag *PhonemeEditor::GetClickedWord( void )
{
	if ( m_nClickedWord < 0 )
		return NULL;

	if ( m_nClickedWord >= m_Tags.m_Words.Size() )
		return NULL;

	CWordTag *word = m_Tags.m_Words[ m_nClickedWord ];
	return word;
}

void PhonemeEditor::ShowContextMenu_Phonemes( int mx, int my )
{
	CountSelected();

	// Construct main
	mxPopupMenu *pop = new mxPopupMenu();

	if ( m_pWaveFile )
	{
		mxPopupMenu *play = new mxPopupMenu;
		play->add( va( "Original" ), IDC_PHONEME_PLAY_ORIG );
		play->add( va( "Edited" ), IDC_PLAY_EDITED );
		if ( m_bSelectionActive )
		{
			play->add( va( "Selection" ), IDC_PLAY_EDITED_SELECTION );
		}

		pop->addMenu( "Play", play );

		if ( sound->IsSoundPlaying( m_pMixer ) )
		{
			pop->add( va( "Cancel playback" ), IDC_CANCELPLAYBACK );
		}

		pop->addSeparator();
	}

	pop->add( va( "Load..." ), IDC_LOADWAVEFILE );

	if ( m_pWaveFile )
	{
		pop->add( va( "Save" ), IDC_SAVE_LINGUISTIC );
	}

	if ( m_bSelectionActive )
	{
		pop->addSeparator();
		pop->add( va( "Deselect" ), IDC_DESELECT );
	}

	if ( m_pWaveFile )
	{
		pop->addSeparator();
		pop->add( va( "Redo Extraction" ), IDC_REDO_PHONEMEEXTRACTION );

		if ( m_nSelectedWordCount < 1 || AreSelectedWordsContiguous() )
		{
			pop->add( va( "Redo Extraction of selected words" ), IDC_REDO_PHONEMEEXTRACTION_SELECTION );
		}
	}

	if ( m_pWaveFile && m_TagsExt.m_Words.Size() )
	{
		pop->addSeparator();
		pop->add( va( "Commit extraction" ) , IDC_COMMITEXTRACTED );
		pop->add( va( "Clear extraction" ), IDC_CLEAREXTRACTED );
	}

	if ( m_nUndoLevel != 0 || m_nUndoLevel != m_UndoStack.Size()  )
	{
		pop->addSeparator();
		if ( m_nUndoLevel != 0 )
		{
			pop->add( va( "Undo" ), IDC_UNDO );
		}
		if ( m_nUndoLevel != m_UndoStack.Size() )
		{
			pop->add( va( "Redo" ), IDC_REDO );
		}
		pop->add( va( "Clear Undo Info" ), IDC_CLEARUNDO );
	}

	if ( m_Tags.m_Words.Size() > 0 )
	{
		pop->addSeparator();
		pop->add( va( "Cleanup words/phonemes" ), IDC_CLEANUP );
	}

	// Show hierarchical options menu
	{
		mxPopupMenu *api = 0;
		
		if ( DoesExtractorExistFor( SPEECH_API_SAPI ) )
		{
			api = new mxPopupMenu();
			api->add( "Microsoft Speech API", IDC_API_SAPI );
			if ( g_viewerSettings.speechapiindex == SPEECH_API_SAPI )
			{
				api->setChecked( IDC_API_SAPI, true );
			}
		}

		if ( DoesExtractorExistFor( SPEECH_API_LIPSINC ) )
		{
			if ( !api )
				api = new mxPopupMenu();

			api->add( "Lipsinc Speech API", IDC_API_LIPSINC );
			if ( g_viewerSettings.speechapiindex == SPEECH_API_LIPSINC )
			{
				api->setChecked( IDC_API_LIPSINC, true );
			}
		}

		pop->addSeparator();
		pop->addMenu( "Change Speech API", api );
	}

	// Import export menu
	if ( m_pWaveFile )
	{
		pop->addSeparator();
		if ( m_Tags.m_Words.Count() > 0 )
		{
			pop->add( "Export word data to " WORD_DATA_EXTENSION "...", IDC_EXPORT_SENTENCE );
		}
		pop->add( "Import word data from " WORD_DATA_EXTENSION "...", IDC_IMPORT_SENTENCE );
		pop->add( va("%s Voice Duck", m_Tags.GetVoiceDuck() ? "Disable" : "Enable" ), IDC_TOGGLE_VOICEDUCK );
	}

	pop->popup( this, mx, my );
}

void PhonemeEditor::ShowContextMenu_Emphasis( int mx, int my )
{
	Emphasis_CountSelected();

	// Construct main
	mxPopupMenu *pop = new mxPopupMenu();

	pop->add( va( "Select All" ), IDC_EMPHASIS_SELECTALL );
	if ( m_nNumSelected > 0 )
	{
		pop->add( va( "Deselect All" ), IDC_EMPHASIS_DESELECT );
	}

	if ( m_nUndoLevel != 0 || m_nUndoLevel != m_UndoStack.Size()  )
	{
		pop->addSeparator();

		if ( m_nUndoLevel != 0 )
		{
			pop->add( va( "Undo" ), IDC_UNDO );
		}
		if ( m_nUndoLevel != m_UndoStack.Size() )
		{
			pop->add( va( "Redo" ), IDC_REDO );
		}
		pop->add( va( "Clear Undo Info" ), IDC_CLEARUNDO );
	}
	pop->popup( this, mx, my );
}

void PhonemeEditor::ShowContextMenu( int mx, int my )
{
	switch ( GetMode() )
	{
	default:
	case MODE_PHONEMES:
		ShowContextMenu_Phonemes( mx, my );
		break;
	case MODE_EMPHASIS:
		ShowContextMenu_Emphasis( mx, my );
		break;
	}
}

void PhonemeEditor::ShiftSelectedPhoneme( int direction )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CountSelected();

	switch ( m_nSelectedPhonemeCount )
	{
	case 1:
		break;
	case 0:
		Con_Printf( "Can't shift phonemes, none selected\n" );
		return;
	default:
		Con_Printf( "Can only shift one phoneme at a time via keyboard\n" );
		return;
	}

	RECT rc;
	GetWorkspaceRect( rc );

	// Determine start/stop positions
	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	float timeperpixel = ( endtime - starttime ) / (float)( rc.right - rc.left );

	float movetime = timeperpixel * (float)direction;
	
	float maxmove = ComputeMaxPhonemeShift( direction > 0 ? true : false, false );

	if ( direction > 0 )
	{
		if ( movetime > maxmove )
		{
			movetime = maxmove;
			Con_Printf( "Further shift is blocked on right\n" );
		}
	}
	else
	{
		if ( movetime < -maxmove )
		{
			movetime = -maxmove;
			Con_Printf( "Further shift is blocked on left\n" );
		}
	}

	if ( fabs( movetime ) < 0.0001f )
		return;

	SetDirty( true );

	PushUndo();

	TraversePhonemes( &PhonemeEditor::ITER_MoveSelectedPhonemes, movetime );

	PushRedo();

	m_bWordsActive = false;

	redraw();

	Con_Printf( "Shift phoneme %s\n", direction == -1 ? "left" : "right" );
}

void PhonemeEditor::ExtendSelectedPhonemeEndTime( int direction )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CountSelected();

	if ( m_nSelectedPhonemeCount != 1 )
		return;

	RECT rc;
	GetWorkspaceRect( rc );

	// Determine start/stop positions
	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	float timeperpixel = ( endtime - starttime ) / (float)( rc.right - rc.left );

	float movetime = timeperpixel * (float)direction;

	SetDirty( true );

	PushUndo();

	TraversePhonemes( &PhonemeEditor::ITER_ExtendSelectedPhonemeEndTimes, movetime );

	PushRedo();

	m_bWordsActive = false;

	redraw();

	Con_Printf( "Extend phoneme end %s\n", direction == -1 ? "left" : "right" );
}

void PhonemeEditor::SelectNextPhoneme( int direction )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CountSelected();

	if ( m_nSelectedPhonemeCount != 1 )
	{
		if ( m_nSelectedWordCount == 1 )
		{
			CWordTag *word = GetSelectedWord();
			if ( word && word->m_Phonemes.Size() > 0 )
			{
				m_nSelectedPhonemeCount = 1;
				CPhonemeTag *p = word->m_Phonemes[ direction ? word->m_Phonemes.Size() - 1 : 0 ];
				p->m_bSelected = true;
			}
			else
			{
				return;
			}
		}
		else
		{
			return;
		}
	}

	Con_Printf( "Move to next phoneme %s\n", direction == -1 ? "left" : "right" );

	for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		if ( !word )
			continue;

		for ( int j = 0; j < word->m_Phonemes.Size(); j++ )
		{
			CPhonemeTag *phoneme = word->m_Phonemes[ j ];
			if ( !phoneme )
				continue;

			if ( !phoneme->m_bSelected )
				continue;

			// Deselect this one and move 
			int nextindex = j + direction;
			if ( nextindex < 0 )
			{
				nextindex = word->m_Phonemes.Size() - 1;
			}
			else if ( nextindex >= word->m_Phonemes.Size() )
			{
				nextindex = 0;
			}

			phoneme->m_bSelected = false;

			phoneme = word->m_Phonemes[ nextindex ];

			phoneme->m_bSelected = true;

			m_bWordsActive = false;

			redraw();
			return;
		}
	}
}

bool PhonemeEditor::IsPhonemeSelected( CWordTag *word )
{
	for ( int i = 0 ; i < word->m_Phonemes.Size(); i++ )
	{
		CPhonemeTag *p = word->m_Phonemes[ i ];
		if ( !p || !p->m_bSelected )
			continue;

		return true;
	}
	return false;
}

void PhonemeEditor::SelectNextWord( int direction )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CountSelected();

	if ( m_nSelectedWordCount != 1 &&
		 m_nSelectedPhonemeCount != 1 )
	{
		// Selected first word then
		if ( m_nSelectedWordCount == 0 && m_Tags.m_Words.Size() > 0 )
		{
			CWordTag *word = m_Tags.m_Words[ direction ? m_Tags.m_Words.Size() - 1 : 0 ];
			word->m_bSelected = true;
			m_nSelectedWordCount = 1;
		}
		else
		{
			return;
		}
	}

	Con_Printf( "Move to next word %s\n", direction == -1 ? "left" : "right" );

	for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		if ( !word )
			continue;

		if ( m_nSelectedWordCount == 1 )
		{
			if ( !word->m_bSelected )
				continue;
		}
		else 
		{
			if ( !IsPhonemeSelected( word ) )
				continue;
		}

		// Deselect word
		word->m_bSelected = false;

		for ( int j = 0; j < word->m_Phonemes.Size(); j++ )
		{
			CPhonemeTag *phoneme = word->m_Phonemes[ j ];
			if ( !phoneme )
				continue;

			if ( !phoneme->m_bSelected )
				continue;

			phoneme->m_bSelected = false;
		}

		// Deselect this one and move 
		int nextword = i + direction;
		if ( nextword < 0 )
		{
			nextword = m_Tags.m_Words.Size() - 1;
		}
		else if ( nextword >= m_Tags.m_Words.Size() )
		{
			nextword = 0;
		}

		word = m_Tags.m_Words[ nextword ];
		word->m_bSelected = true;

		if ( word->m_Phonemes.Size() > 0 )
		{
			CPhonemeTag *phoneme = NULL;

			if ( direction > 0 )
			{
				phoneme = word->m_Phonemes[ 0 ];
			}
			else
			{
				phoneme = word->m_Phonemes[ word->m_Phonemes.Size() - 1 ];
			}

			phoneme->m_bSelected = true;
		}

		m_bWordsActive = true;

		redraw();
		return;
	}
}

void PhonemeEditor::ShiftSelectedWord( int direction )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CountSelected();

	switch ( m_nSelectedWordCount )
	{
	case 1:
		break;
	case 0:
		Con_Printf( "Can't shift words, none selected\n" );
		return;
	default:
		Con_Printf( "Can only shift one word at a time via keyboard\n" );
		return;
	}

	RECT rc;
	GetWorkspaceRect( rc );

	// Determine start/stop positions
	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	float timeperpixel = ( endtime - starttime ) / (float)( rc.right - rc.left );

	float movetime = timeperpixel * (float)direction;

	float maxmove = ComputeMaxWordShift( direction > 0 ? true : false, false );

	if ( direction > 0 )
	{
		if ( movetime > maxmove )
		{
			movetime = maxmove;
			Con_Printf( "Further shift is blocked on right\n" );
		}
	}
	else
	{
		if ( movetime < -maxmove )
		{
			movetime = -maxmove;
			Con_Printf( "Further shift is blocked on left\n" );
		}
	}

	if ( fabs( movetime ) < 0.0001f )
		return;

	SetDirty( true );

	PushUndo();

	TraverseWords( &PhonemeEditor::ITER_MoveSelectedWords, movetime );

	PushRedo();

	m_bWordsActive = true;

	redraw();

	Con_Printf( "Shift word %s\n", direction == -1 ? "left" : "right" );
}

void PhonemeEditor::ExtendSelectedWordEndTime( int direction )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CountSelected();

	if ( m_nSelectedWordCount != 1 )
		return;

	RECT rc;
	GetWorkspaceRect( rc );

	// Determine start/stop positions
	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	float timeperpixel = ( endtime - starttime ) / (float)( rc.right - rc.left );

	float movetime = timeperpixel * (float)direction;

	SetDirty( true );

	PushUndo();

	TraverseWords( &PhonemeEditor::ITER_ExtendSelectedWordEndTimes, movetime );

	PushRedo();

	m_bWordsActive = true;

	redraw();

	Con_Printf( "Extend word end %s\n", direction == -1 ? "left" : "right" );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *word - 
// Output : int
//-----------------------------------------------------------------------------
int PhonemeEditor::IndexOfWord( CWordTag *word )
{
	for ( int i = 0 ; i < m_Tags.m_Words.Size(); i++ )
	{
		if ( m_Tags.m_Words[ i ] == word )
			return i;
	}
	return -1;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : forward - 
//			*currentWord - 
//			**nextWord - 
// Output : float
//-----------------------------------------------------------------------------
float PhonemeEditor::GetTimeGapToNextWord( bool forward, CWordTag *currentWord, CWordTag **ppNextWord /* = NULL */ )
{
	if ( ppNextWord )
	{
		*ppNextWord = NULL;
	}

	if ( !currentWord )
		return 0.0f;

	int wordnum = IndexOfWord( currentWord );
	if ( wordnum == -1 )
		return 0.0f;

	// Go in correct direction
	int newwordnum = wordnum + ( forward ? 1 : -1 );

	// There is no next word
	if ( newwordnum >= m_Tags.m_Words.Size() )
	{
		return PLENTY_OF_TIME;
	}

	// There is no previous word
	if ( newwordnum < 0 )
	{
		return PLENTY_OF_TIME;
	}

	if ( ppNextWord )
	{
		*ppNextWord = m_Tags.m_Words[ newwordnum ];
	}

	// Otherwise, figure out time gap
	if ( forward )
	{
		float currentEnd = currentWord->m_flEndTime;
		float nextStart = m_Tags.m_Words[ newwordnum ]->m_flStartTime;

		return ( nextStart - currentEnd );
	}
	else
	{
		float previousEnd = m_Tags.m_Words[ newwordnum ]->m_flEndTime;
		float currentStart = currentWord->m_flStartTime;

		return ( currentStart - previousEnd );
	}

	
	Assert( 0 );
	return 0.0f;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : forward - 
//			*currentPhoneme - 
//			**word - 
//			**phoneme - 
// Output : float
//-----------------------------------------------------------------------------
float PhonemeEditor::GetTimeGapToNextPhoneme( bool forward, CPhonemeTag *currentPhoneme, 
	CWordTag **ppword /* = NULL */, CPhonemeTag **ppphoneme /* = NULL */ )
{
	if ( ppword )
	{
		*ppword = NULL;
	}
	if ( ppphoneme )
	{
		*ppphoneme = NULL;
	}

	if ( !currentPhoneme )
		return 0.0f;

	CWordTag *word = m_Tags.GetWordForPhoneme( currentPhoneme );
	if ( !word )
		return 0.0f;

	int wordnum = IndexOfWord( word );
	Assert( wordnum != -1 );

	int phonemenum = word->IndexOfPhoneme( currentPhoneme );
	if ( phonemenum < 0  )
		return 0.0f;

	CPhonemeTag *nextPhoneme = NULL;

	int nextphoneme = phonemenum + ( forward ? 1 : -1 );

	// Try last phoneme of previous word
	if ( nextphoneme < 0 )
	{
		wordnum--;
		while ( wordnum >= 0 )
		{
			if ( ppword )
			{
				*ppword = m_Tags.m_Words[ wordnum ];
			}
			if ( m_Tags.m_Words.Size() > 0 )
			{
				if ( m_Tags.m_Words[ wordnum ]->m_Phonemes.Size() > 0 )
				{
					nextPhoneme = m_Tags.m_Words[ wordnum ]->m_Phonemes[ m_Tags.m_Words[ wordnum ]->m_Phonemes.Size() - 1 ];
					break;
				}
			}
			wordnum--;
		}
	}
	// Try first phoneme of next word, if there is one
	else if ( nextphoneme >= word->m_Phonemes.Size() )
	{
		wordnum++;
		while ( wordnum < m_Tags.m_Words.Size() )
		{
			if ( ppword )
			{
				*ppword = m_Tags.m_Words[ wordnum ];
			}
			// Really it can't be zero, but check anyway
			if ( m_Tags.m_Words.Size() > 0 )
			{
				if ( m_Tags.m_Words[ wordnum ]->m_Phonemes.Size() > 0 )
				{
					nextPhoneme = m_Tags.m_Words[ wordnum ]->m_Phonemes[ 0 ];
					break;
				}
			}
			wordnum++;
		}
	}
	else
	{
		nextPhoneme = word->m_Phonemes[ nextphoneme ];
	}

	if ( !nextPhoneme )
		return PLENTY_OF_TIME;

	if ( ppphoneme )
	{
		*ppphoneme = nextPhoneme;
	}

	// Now compute time delta
	float dt = 0.0f;
	if ( forward )
	{
		dt = nextPhoneme->GetStartTime() - currentPhoneme->GetEndTime();
	}
	else
	{
		dt = currentPhoneme->GetStartTime() - nextPhoneme->GetEndTime();
	}

	return dt;
}

CPhonemeTag *PhonemeEditor::GetSelectedPhoneme( void )
{
	CountSelected();

	if ( m_nSelectedPhonemeCount != 1 )
		return NULL;

	for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
	{
		CWordTag *w = m_Tags.m_Words[ i ];
		if ( !w )
			continue;

		for ( int j = 0; j < w->m_Phonemes.Size() ; j++ )
		{
			CPhonemeTag *p = w->m_Phonemes[ j ];
			if ( !p || !p->m_bSelected )
				continue;

			return p;
		}
	}
	return NULL;
}

CWordTag *PhonemeEditor::GetSelectedWord( void )
{
	CountSelected();

	if ( m_nSelectedWordCount != 1 )
		return NULL;

	for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
	{
		CWordTag *w = m_Tags.m_Words[ i ];
		if ( !w || !w->m_bSelected )
			continue;

		return w;
	}
	return NULL;
}

void PhonemeEditor::OnMouseMove( mxEvent *event )
{
	int mx = (short)event->x;

	LimitDrag( mx );

	event->x = (short)mx;

	if ( m_nDragType != DRAGTYPE_NONE )
	{
		DrawFocusRect( "moving old" );

		for ( int i = 0; i < m_FocusRects.Size(); i++ )
		{
			CFocusRect *f = &m_FocusRects[ i ];
			f->m_rcFocus = f->m_rcOrig;

			switch ( m_nDragType )
			{
			default:
				{
					// Only X Shifts supported
					OffsetRect( &f->m_rcFocus, ( (short)event->x - m_nStartX ),
						0 );
				}
				break;
			case DRAGTYPE_EMPHASIS_SELECT:
				{
					RECT rcWork;
					GetWorkspaceRect( rcWork );
					RECT rcEmphasis;
					Emphasis_GetRect( rcWork, rcEmphasis );

					RECT rcFocus;

					rcFocus = f->m_rcOrig;

					rcFocus.left = m_nStartX < (short)event->x ? m_nStartX : (short)event->x;
					rcFocus.right = m_nStartX < (short)event->x ? (short)event->x : m_nStartX;
					
					rcFocus.top = m_nStartY < (short)event->y ? m_nStartY : (short)event->y;
					rcFocus.bottom = m_nStartY < (short)event->y ? (short)event->y : m_nStartY;

					rcFocus.top = clamp( rcFocus.top, rcEmphasis.top, rcEmphasis.bottom );
					rcFocus.bottom = clamp( rcFocus.bottom, rcEmphasis.top, rcEmphasis.bottom );

					//OffsetRect( &rcFocus, 0, -rcEmphasis.top );

					POINT offset;
					offset.x = 0;
					offset.y = 0;
					ClientToScreen( (HWND)getHandle(), &offset );
					OffsetRect( &rcFocus, offset.x, offset.y );

					f->m_rcFocus = rcFocus;
				}
				break;
			}
		}

		if ( m_nDragType == DRAGTYPE_EMPHASIS_MOVE )
		{
			redraw();
		}

		DrawFocusRect( "moving new" );
	}
	else
	{
		if ( m_hPrevCursor )
		{
			SetCursor( m_hPrevCursor );
			m_hPrevCursor = NULL;
		}

		CountSelected();

		int overhandle = IsMouseOverBoundary( event );
		if ( overhandle == BOUNDARY_PHONEME && m_nSelectedPhonemeCount <= 1 )
		{
			m_hPrevCursor = SetCursor( LoadCursor( NULL, IDC_SIZEWE ) );
		}
		else if ( overhandle == BOUNDARY_WORD && m_nSelectedWordCount <= 1 )
		{
			m_hPrevCursor = SetCursor( LoadCursor( NULL, IDC_SIZEWE ) );
		}
		else if ( IsMouseOverSelection( (short)event->x, (short)event->y ) )
		{
			if ( IsMouseOverSelectionStartEdge( event ) )
			{
				m_hPrevCursor = SetCursor( LoadCursor( NULL, IDC_SIZEWE ) );
			}
			else if ( IsMouseOverSelectionEndEdge( event ) )
			{
				m_hPrevCursor = SetCursor( LoadCursor( NULL, IDC_SIZEWE ) );
			}
			else
			{
				if ( event->modifiers & mxEvent::KeyShift )
				{
					m_hPrevCursor = SetCursor( LoadCursor( NULL, IDC_SIZEALL ) );
				}
			}
		}
		else
		{
			if ( IsMouseOverTag( (short)event->x, (short)event->y ) )
			{
				m_hPrevCursor = SetCursor( LoadCursor( NULL, IDC_SIZEALL ) );
			}
			else
			{
				CPhonemeTag *pt = GetPhonemeTagUnderMouse( (short)event->x, (short)event->y );
				CWordTag *wt = GetWordTagUnderMouse( (short)event->x, (short)event->y );
				if ( wt || pt )
				{
					if ( pt )
					{
						// Select it
						SelectExpression( pt );
					}
					if ( event->modifiers & mxEvent::KeyShift )
					{
						m_hPrevCursor = SetCursor( LoadCursor( NULL, IDC_SIZEALL ) );
					}
				}
			}
		}
	}

	switch ( m_nDragType )
	{
	default:
		break;
	case DRAGTYPE_EMPHASIS_MOVE:
		{
			Emphasis_MouseDrag( (short)event->x, (short)event->y );
			m_Tags.Resort();
		}
		break;
	case DRAGTYPE_SCRUBBER:
		{
			float t = GetTimeForPixel( (short)event->x );
			t += m_flScrubberTimeOffset;

			ClampTimeToSelectionInterval( t );

			float dt = t - m_flScrub;

			SetScrubTargetTime( t );

			ScrubThink( dt, true );

			SetScrubTime( t );

			DrawScrubHandle();
		}
		break;
	}

	m_nLastX = (short)event->x;
	m_nLastY = (short)event->y;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::EditInsertFirstPhonemeOfWord( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CWordTag *cw = GetSelectedWord();
	if ( !cw )
		return;

	if ( cw->m_Phonemes.Size() != 0 )
	{
		Con_Printf( "Can't insert first phoneme into %s, already has phonemes\n", cw->GetWord() );
		return;
	}

	CPhonemeParams params;
	memset( &params, 0, sizeof( params ) );
	strcpy( params.m_szDialogTitle, "Phoneme/Viseme Properties" );
	strcpy( params.m_szName, "" );

	params.m_nLeft = -1;
	params.m_nTop = -1;

	params.m_bPositionDialog = true;
	params.m_bMultiplePhoneme = true;

	if ( params.m_bPositionDialog )
	{
		RECT rcWord;
		GetWordRect( cw, rcWord );

		// Convert to screen coords
		POINT pt;
		pt.x = rcWord.left;
		pt.y = rcWord.top;

		ClientToScreen( (HWND)getHandle(), &pt );

		params.m_nLeft	= pt.x;
		params.m_nTop	= pt.y;
	}

	int iret = PhonemeProperties( &params );
	SetFocus( (HWND)getHandle() );
	if ( !iret )
	{
		return;
	}

	int phonemeCount = CSentence::CountWords( params.m_szName );
	if ( phonemeCount <= 0 )
	{
		return;
	}

	float wordLength = cw->m_flEndTime - cw->m_flStartTime;
	float timePerPhoneme = wordLength / (float)phonemeCount;

	float currentTime = cw->m_flStartTime;

	SetDirty( true );

	PushUndo();

	unsigned char *in;
	char *out;

	char phonemeName[ 128 ];

	in = (unsigned char *)params.m_szName;

	do
	{
		out = phonemeName;

		while ( *in > 32 )
		{
			*out++ = *in++;
		}
		*out = 0;

		CPhonemeTag phoneme;

		phoneme.SetPhonemeCode( TextToPhoneme( phonemeName ) );
		phoneme.SetTag( phonemeName );

		phoneme.SetStartTime( currentTime );
		phoneme.SetEndTime( currentTime + timePerPhoneme );
		phoneme.m_bSelected = false;

		cw->m_Phonemes.AddToTail( new CPhonemeTag( phoneme ) );

		currentTime += timePerPhoneme;

		if ( !*in )
			break;

		// Skip whitespace
		in++;

	} while ( 1 );

	cw->m_Phonemes[ 0 ]->m_bSelected = true;

	PushRedo();

	// Add it
	redraw();
}

void PhonemeEditor::SelectPhonemes( bool forward )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CountSelected();

	if ( m_nSelectedPhonemeCount != 1 )
		return;

	CPhonemeTag *phoneme = GetSelectedPhoneme();
	if ( !phoneme )
		return;

	// Figure out it's word and index
	CWordTag *word = m_Tags.GetWordForPhoneme( phoneme );
	if ( !word )
		return;

	int wordNum = IndexOfWord( word );
	if ( wordNum == -1 )
		return;

	// Select remaining phonemes in current word
	int i;

	i = word->IndexOfPhoneme( phoneme );
	if ( i == -1 )
		return;

	if ( forward )
	{
		// Start at next one
		i++;

		for ( ; i < word->m_Phonemes.Size(); i++ )
		{
			phoneme = word->m_Phonemes[ i ];
			phoneme->m_bSelected = true;
		}

		// Now start at next word
		wordNum++;

		for ( ; wordNum < m_Tags.m_Words.Size(); wordNum++ )
		{
			word = m_Tags.m_Words[ wordNum ];

			for ( int j = 0; j < word->m_Phonemes.Size(); j++ )
			{
				phoneme = word->m_Phonemes[ j ];
				phoneme->m_bSelected = true;
			}
		}
	}
	else
	{
		// Start at previous
		i--;

		for ( ; i >= 0; i-- )
		{
			phoneme = word->m_Phonemes[ i ];
			phoneme->m_bSelected = true;
		}

		// Now start at previous word
		wordNum--;

		for ( ; wordNum >= 0 ; wordNum-- )
		{
			word = m_Tags.m_Words[ wordNum ];

			for ( int j = 0; j < word->m_Phonemes.Size(); j++ )
			{
				phoneme = word->m_Phonemes[ j ];
				phoneme->m_bSelected = true;
			}
		}
	}

	redraw();
}

void PhonemeEditor::SelectWords( bool forward )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	CountSelected();

	if ( m_nSelectedWordCount != 1 )
		return;

	// Figure out it's word and index
	CWordTag *word = GetSelectedWord();
	if ( !word )
		return;

	int wordNum = IndexOfWord( word );
	if ( wordNum == -1 )
		return;

	if ( forward )
	{
		wordNum++;

		for ( ; wordNum < m_Tags.m_Words.Size(); wordNum++ )
		{
			word = m_Tags.m_Words[ wordNum ];
			word->m_bSelected = true;
		}
	}
	else
	{
		wordNum--;

		for ( ; wordNum >= 0; wordNum-- )
		{
			word = m_Tags.m_Words[ wordNum ];
			word->m_bSelected = true;
		}

	}

	redraw();
}

bool PhonemeEditor::AreSelectedWordsContiguous( void )
{
	CountSelected();

	if ( m_nSelectedWordCount < 1 )
		return false;

	if ( m_nSelectedWordCount == 1 )
		return true;

	// Find first selected word
	int runcount = 0;
	bool parity = false;

	for ( int i = 0 ; i < m_Tags.m_Words.Size() ; i++ )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		if ( !word )
			continue;

		if ( word->m_bSelected )
		{
			if ( !parity )
			{
				parity = true;
				runcount++;
			}
		}
		else
		{
			if ( parity )
			{
				parity = false;
			}
		}
	}

	if ( runcount == 1 )
		return true;

	return false;
}

bool PhonemeEditor::AreSelectedPhonemesContiguous( void )
{
	CountSelected();

	if ( m_nSelectedPhonemeCount < 1 )
		return false;

	if ( m_nSelectedPhonemeCount == 1 )
		return true;

	// Find first selected word
	int runcount = 0;
	bool parity = false;

	for ( int i = 0 ; i < m_Tags.m_Words.Size() ; i++ )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		if ( !word )
			continue;

		for ( int j = 0 ; j < word->m_Phonemes.Size(); j++ )
		{
			CPhonemeTag *phoneme = word->m_Phonemes[ j ];
			if ( !phoneme )
				continue;

			if ( phoneme->m_bSelected )
			{
				if ( !parity )
				{
					parity = true;
					runcount++;
				}
			}
			else
			{
				if ( parity )
				{
					parity = false;
				}
			}
		}
	}

	if ( runcount == 1 )
		return true;

	return false;

}

void PhonemeEditor::SortWords( bool prepareundo )
{
	if ( prepareundo )
	{
		SetDirty( true );
		PushUndo();
	}

	// Just bubble sort by start time
	int c = m_Tags.m_Words.Count();

	int i;

	// check for start > end
	for ( i = 0; i < c; i++ )
	{
		CWordTag *p1 = m_Tags.m_Words[ i ];
		if (p1->m_flStartTime > p1->m_flEndTime )
		{
			float swap = p1->m_flStartTime;			
			p1->m_flStartTime = p1->m_flEndTime;
			p1->m_flEndTime = swap;
		}
	}

	for ( i = 0; i < c; i++ )
	{
		for ( int j = i + 1; j < c; j++ )
		{
			CWordTag *p1 = m_Tags.m_Words[ i ];
			CWordTag *p2 = m_Tags.m_Words[ j ];

			if ( p1->m_flStartTime < p2->m_flStartTime )
				continue;

			// Swap them
			m_Tags.m_Words[ i ] = p2;
			m_Tags.m_Words[ j ] = p1;
		}
	}

	if ( prepareundo )
	{
		PushRedo();
	}
}

void PhonemeEditor::SortPhonemes( bool prepareundo )
{
	if ( prepareundo )
	{
		SetDirty( true );
		PushUndo();
	}

	// Just bubble sort by start time
	int wc = m_Tags.m_Words.Count();
	for ( int w = 0; w < wc; w++ )
	{
		CWordTag *word = m_Tags.m_Words[ w ];
		Assert( word );

		int c = word->m_Phonemes.Count();
		int i;

		// check for start > end
		for ( i = 0; i < c; i++ )
		{
			CPhonemeTag *p1 = word->m_Phonemes[ i ];

			if (p1->GetStartTime() > p1->GetEndTime() )
			{
				float swap = p1->GetStartTime();			
				p1->SetStartTime( p1->GetEndTime() );
				p1->SetEndTime( swap );
			}
		}

		for ( i = 0; i < c; i++ )
		{
			for ( int j = i + 1; j < c; j++ )
			{
				CPhonemeTag *p1 = word->m_Phonemes[ i ];
				CPhonemeTag *p2 = word->m_Phonemes[ j ];

				if ( p1->GetStartTime() < p2->GetStartTime() )
					continue;

				// Swap them
				word->m_Phonemes[ i ] = p2;
				word->m_Phonemes[ j ] = p1;
			}
		}
	}

	if ( prepareundo )
	{
		PushRedo();
	}
}

void PhonemeEditor::CleanupWordsAndPhonemes( bool prepareundo )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	// 2 pixel gap
	float snap_epsilon = 2.49f / GetPixelsPerSecond();

	if ( prepareundo )
	{
		SetDirty( true );
		PushUndo();
	}

	SortWords( false );
	SortPhonemes( false );

	for ( int i = 0 ; i < m_Tags.m_Words.Size() ; i++ )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		if ( !word )
			continue;

		CWordTag *next = NULL;
		if ( i < m_Tags.m_Words.Size() - 1 )
		{
			next = m_Tags.m_Words[ i + 1 ];
		}

		if ( word && next )
		{
			// Check for words close enough
			float eps = next->m_flStartTime - word->m_flEndTime;
			if ( eps && eps <= snap_epsilon )
			{
				float t = (word->m_flEndTime + next->m_flStartTime) * 0.5;
				word->m_flEndTime = t;
				next->m_flStartTime = t;
			}
		}

		for ( int j = 0 ; j < word->m_Phonemes.Size(); j++ )
		{
			CPhonemeTag *phoneme = word->m_Phonemes[ j ];
			if ( !phoneme )
				continue;

			CPhonemeTag *next = NULL;
			if ( j < word->m_Phonemes.Size() - 1 )
			{
				next = word->m_Phonemes[ j + 1 ];
			}

			if ( phoneme && next )
			{
				float eps = next->GetStartTime() - phoneme->GetEndTime();
				if ( eps && eps <= snap_epsilon )
				{
					float t = (phoneme->GetEndTime() + next->GetStartTime() ) * 0.5;
					phoneme->SetEndTime( t );
					next->SetStartTime( t );
				}
			}
		}
	}

	if ( prepareundo )
	{
		PushRedo();
	}

	// NOTE: Caller must call "redraw()" to get screen to update
}



void PhonemeEditor::RealignPhonemesToWords( bool prepareundo )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	if ( prepareundo )
	{
		SetDirty( true );
		PushUndo();
	}

	SortWords( false );
	SortPhonemes( false );

	for ( int i = 0 ; i < m_Tags.m_Words.Size() ; i++ )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		if ( !word )
			continue;

		CWordTag *next = NULL;
		if ( i < m_Tags.m_Words.Size() - 1 )
		{
			next = m_Tags.m_Words[ i + 1 ];
		}

		float word_dt = word->m_flEndTime - word->m_flStartTime;

		CPhonemeTag *FirstPhoneme = word->m_Phonemes[ 0 ];
		if ( !FirstPhoneme )
			continue;

		CPhonemeTag *LastPhoneme = word->m_Phonemes[ word->m_Phonemes.Size() - 1 ];
		if ( !LastPhoneme )
			continue;

		float phoneme_dt = LastPhoneme->GetEndTime() - FirstPhoneme->GetStartTime();

		float phoneme_shift = FirstPhoneme->GetStartTime();

		for ( int j = 0 ; j < word->m_Phonemes.Size(); j++ )
		{
			CPhonemeTag *phoneme = word->m_Phonemes[ j ];
			if ( !phoneme )
				continue;

			CPhonemeTag *next = NULL;
			if ( j < word->m_Phonemes.Size() - 1 )
			{
				next = word->m_Phonemes[ j + 1 ];
			}

			if (j == 0)
			{
				float t = (phoneme->GetStartTime() - phoneme_shift ) * (word_dt / phoneme_dt) + word->m_flStartTime;
				phoneme->SetStartTime( t );
			}

			float t = (phoneme->GetEndTime() - phoneme_shift ) * (word_dt / phoneme_dt) + word->m_flStartTime;
			phoneme->SetEndTime( t );
			if (next)
			{
				next->SetStartTime( t );
			}
		}
	}

	if ( prepareundo )
	{
		PushRedo();
	}

	// NOTE: Caller must call "redraw()" to get screen to update
}


void PhonemeEditor::RealignWordsToPhonemes( bool prepareundo )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	if ( prepareundo )
	{
		SetDirty( true );
		PushUndo();
	}

	SortWords( false );
	SortPhonemes( false );

	for ( int i = 0 ; i < m_Tags.m_Words.Size() ; i++ )
	{
		CWordTag *word = m_Tags.m_Words[ i ];
		if ( !word )
			continue;

		CPhonemeTag *FirstPhoneme = word->m_Phonemes[ 0 ];
		if ( !FirstPhoneme )
			continue;

		CPhonemeTag *LastPhoneme = word->m_Phonemes[ word->m_Phonemes.Size() - 1 ];
		if ( !LastPhoneme )
			continue;

		word->m_flStartTime = FirstPhoneme->GetStartTime();
		word->m_flEndTime = LastPhoneme->GetEndTime();
	}

	if ( prepareundo )
	{
		PushRedo();
	}

	// NOTE: Caller must call "redraw()" to get screen to update
}



float PhonemeEditor::ComputeMaxWordShift( bool forward, bool allowcrop )
{
	// skipping selected words, figure out max time shift of words before they selection touches any
	// unselected words
	// if allowcrop is true, then the maximum extends up to end of next word
	float maxshift = PLENTY_OF_TIME;

	if ( forward )
	{
		for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
		{
			CWordTag *w1 = m_Tags.m_Words[ i ];
			if ( !w1 || !w1->m_bSelected )
				continue;

			CWordTag *w2 = NULL;
			for ( int search = i + 1; search < m_Tags.m_Words.Size() ; search++ )
			{
				CWordTag *check = m_Tags.m_Words[ search ];
				if ( !check || check->m_bSelected )
					continue;

				w2 = check;
				break;
			}

			if ( w2 )
			{
				float shift;
				if ( allowcrop )
				{
					shift = w2->m_flEndTime - w1->m_flEndTime;
				}
				else
				{
					shift = w2->m_flStartTime - w1->m_flEndTime;
				}

				if ( shift < maxshift )
				{
					maxshift = shift;
				}
			}
		}
	}
	else
	{
		for ( int i = m_Tags.m_Words.Size() -1; i >= 0; i-- )
		{
			CWordTag *w1 = m_Tags.m_Words[ i ];
			if ( !w1 || !w1->m_bSelected )
				continue;

			CWordTag *w2 = NULL;
			for ( int search = i - 1; search >= 0 ; search-- )
			{
				CWordTag *check = m_Tags.m_Words[ search ];
				if ( !check || check->m_bSelected )
					continue;

				w2 = check;
				break;
			}

			if ( w2 )
			{
				float shift;
				if ( allowcrop )
				{
					shift = w1->m_flStartTime - w2->m_flStartTime;
				}
				else
				{
					shift = w1->m_flStartTime - w2->m_flEndTime;
				}

				if ( shift < maxshift )
				{
					maxshift = shift;
				}
			}
		}
	}

	return maxshift;
}

float PhonemeEditor::ComputeMaxPhonemeShift( bool forward, bool allowcrop )
{
	// skipping selected phonemes, figure out max time shift of phonemes before they selection touches any
	// unselected words
	// if allowcrop is true, then the maximum extends up to end of next word
	float maxshift = PLENTY_OF_TIME;

	if ( forward )
	{
		for ( int i = 0; i < m_Tags.m_Words.Size(); i++ )
		{
			CWordTag *word = m_Tags.m_Words[ i ];
			if ( !word )
				continue;

			for ( int j = 0; j < word->m_Phonemes.Size(); j++ )
			{
				CPhonemeTag *p1 = word->m_Phonemes[ j ];
				if ( !p1 || !p1->m_bSelected )
					continue;

				// Find next unselected phoneme
				CPhonemeTag *p2 = NULL;

				CPhonemeTag *start = p1;
				do
				{
					CPhonemeTag *test = NULL;
					GetTimeGapToNextPhoneme( forward, start, NULL, &test );
					if ( !test )
						break;

					if ( test->m_bSelected )
					{
						start = test;
						continue;
					}

					p2 = test;
					break;
				} while ( 1 );

				if ( p2 )
				{
					float shift;
					if ( allowcrop )
					{
						shift = p2->GetEndTime() - p1->GetEndTime();
					}
					else
					{
						shift = p2->GetStartTime() - p1->GetEndTime();
					}

					if ( shift < maxshift )
					{
						maxshift = shift;
					}
				}
			}
		}
	}
	else
	{
		for ( int i = m_Tags.m_Words.Size() -1; i >= 0; i-- )
		{
			CWordTag *word = m_Tags.m_Words[ i ];
			if ( !word )
				continue;

			for ( int j = word->m_Phonemes.Size() - 1; j >= 0; j-- )
			{
				CPhonemeTag *p1 = word->m_Phonemes[ j ];
				if ( !p1 || !p1->m_bSelected )
					continue;

				// Find previous unselected phoneme
				CPhonemeTag *p2 = NULL;

				CPhonemeTag *start = p1;
				do
				{
					CPhonemeTag *test = NULL;
					GetTimeGapToNextPhoneme( forward, start, NULL, &test );
					if ( !test )
						break;

					if ( test->m_bSelected )
					{
						start = test;
						continue;
					}

					p2 = test;
					break;
				} while ( 1 );

				if ( p2 )
				{
					float shift;
					if ( allowcrop )
					{
						shift = p1->GetStartTime() - p2->GetStartTime();
					}
					else
					{
						shift = p1->GetStartTime() - p2->GetEndTime();
					}

					if ( shift < maxshift )
					{
						maxshift = shift;
					}
				}
			}
		}
	}

	return maxshift;
}

int PhonemeEditor::PixelsForDeltaTime( float dt )
{
	if ( !dt )
		return 0;

	RECT rc;
	GetWorkspaceRect( rc );

	float starttime = m_nLeftOffset / GetPixelsPerSecond();
	float endtime = w2() / GetPixelsPerSecond() + starttime;

	float timeperpixel = ( endtime - starttime ) / (float)( rc.right - rc.left );

	float pixels = dt / timeperpixel;

	return abs( (int)pixels );
}

void PhonemeEditor::ClearDragLimit( void )
{
	m_bLimitDrag = false;
	m_nLeftLimit = -1;
	m_nRightLimit = -1;
}

void PhonemeEditor::SetDragLimit( int dragtype )
{
	ClearDragLimit();

	float nextW, nextP;
	float prevW, prevP;

	nextW = ComputeMaxWordShift( true, false );
	prevW = ComputeMaxWordShift( false, false );
	nextP = ComputeMaxPhonemeShift( true, false );
	prevP = ComputeMaxPhonemeShift( false, false );

	/*
	Con_Printf( "+w %f -w %f +p %f -p %f\n",
		1000.0f * nextW,
		1000.0f * prevW,
		1000.0f * nextP,
		1000.0f * prevP );
	*/

	switch ( dragtype )
	{
	case DRAGTYPE_MOVEWORD:
		m_bLimitDrag = true;
		m_nLeftLimit = PixelsForDeltaTime( prevW );
		m_nRightLimit = PixelsForDeltaTime( nextW );
		break;
	case DRAGTYPE_MOVEPHONEME:
		m_bLimitDrag = true;
		m_nLeftLimit = PixelsForDeltaTime( prevP );
		m_nRightLimit = PixelsForDeltaTime( nextP );
		break;
	default:
		ClearDragLimit();
		break;
	}
}

void PhonemeEditor::LimitDrag( int& mousex )
{
	if ( m_nDragType == DRAGTYPE_NONE )
		return;

	if ( !m_bLimitDrag )
		return;

	int delta = mousex - m_nStartX;
	if ( delta > 0 )
	{
		if ( m_nRightLimit >= 0 )
		{
			if ( delta > m_nRightLimit )
			{
				mousex = m_nStartX + m_nRightLimit;
			}
		}
	}
	else if ( delta < 0 )
	{
		if ( m_nLeftLimit >= 0 )
		{
			if ( abs( delta ) > abs( m_nLeftLimit ) )
			{
				mousex = m_nStartX - m_nLeftLimit;
			}
		}
	}
}

//-----------------------------------------------------------------------------
// Purpose: Wipe undo/redo data
//-----------------------------------------------------------------------------
void PhonemeEditor::ClearUndo( void )
{
	WipeUndo();
	WipeRedo();

	SetDirty( false );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *tag - 
//-----------------------------------------------------------------------------
void PhonemeEditor::SelectExpression( CPhonemeTag *tag )
{
	if ( !models->GetActiveStudioModel() )
		return;

	CStudioHdr *hdr = models->GetActiveStudioModel()->GetStudioHdr();
	if ( !hdr )
		return;

	// Make sure phonemes are loaded
	FacePoser_EnsurePhonemesLoaded();

	CExpClass *cl = expressions->FindClass( "phonemes", true );
	if ( !cl )
	{
		Con_Printf( "Couldn't load expressions/phonemes.txt!\n" );
		return;
	}

	if ( expressions->GetActiveClass() != cl )
	{
		expressions->ActivateExpressionClass( cl );
	}

	CExpression *exp = cl->FindExpression( ConvertPhoneme( tag->GetPhonemeCode() ) );
	if ( !exp )
	{
		Con_Printf( "Couldn't find phoneme '%s'\n", ConvertPhoneme( tag->GetPhonemeCode() )  );
		return;
	}

	float *settings = exp->GetSettings();
	for (LocalFlexController_t i = LocalFlexController_t(0); i < hdr->numflexcontrollers(); i++)
	{
		int j = hdr->pFlexcontroller( i )->localToGlobal;

		models->GetActiveStudioModel()->SetFlexController( i, settings[j] );
	}
}

void PhonemeEditor::OnSAPI( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	g_viewerSettings.speechapiindex = SPEECH_API_SAPI;

	m_pPhonemeExtractor = NULL;

	CheckSpeechAPI();

	redraw();
}

void PhonemeEditor::OnLipSinc( void )
{
	if ( GetMode() != MODE_PHONEMES )
		return;

	g_viewerSettings.speechapiindex = SPEECH_API_LIPSINC;

	m_pPhonemeExtractor = NULL;

	CheckSpeechAPI();

	redraw();
}

void PhonemeEditor::LoadPhonemeConverters()
{
	m_pPhonemeExtractor = NULL;

	// Enumerate modules under bin folder of exe
	FileFindHandle_t findHandle;
	const char *pFilename = filesystem->FindFirstEx( "phonemeextractors/*.dll", "EXECUTABLE_PATH", &findHandle );
	while( pFilename )
	{	
		char fullpath[ 512 ];
		Q_snprintf( fullpath, sizeof( fullpath ), "phonemeextractors/%s", pFilename );

		Con_Printf( "Loading extractor from %s\n", fullpath );

		Extractor e;
		e.module = Sys_LoadModule( fullpath );
		if ( !e.module )
		{
			pFilename = filesystem->FindNext( findHandle );
			continue;
		}

		CreateInterfaceFn factory = Sys_GetFactory( e.module );
		if ( !factory )
		{
			pFilename = filesystem->FindNext( findHandle );
			continue;
		}

		e.extractor = ( IPhonemeExtractor * )factory( VPHONEME_EXTRACTOR_INTERFACE, NULL );
		if ( !e.extractor )
		{
			Warning( "Unable to get IPhonemeExtractor interface version %s from %s\n", VPHONEME_EXTRACTOR_INTERFACE, fullpath );
			pFilename = filesystem->FindNext( findHandle );
			continue;
		}

		e.apitype = e.extractor->GetAPIType();

		g_Extractors.AddToTail( e );
		pFilename = filesystem->FindNext( findHandle );
	}

	filesystem->FindClose( findHandle );
}

void PhonemeEditor::ValidateSpeechAPIIndex()
{
	if ( !DoesExtractorExistFor( (PE_APITYPE)g_viewerSettings.speechapiindex ) )
	{
		if ( g_Extractors.Count() > 0 )
			g_viewerSettings.speechapiindex = g_Extractors[0].apitype;
	}
}

void PhonemeEditor::UnloadPhonemeConverters()
{
	int c = g_Extractors.Count();
	for ( int i = c - 1; i >= 0; i-- )
	{
		Extractor *e = &g_Extractors[ i ];
		Sys_UnloadModule( e->module );
	}

	g_Extractors.RemoveAll();

	m_pPhonemeExtractor = NULL;
}

bool PhonemeEditor::CheckSpeechAPI( void )
{
	if ( GetMode() != MODE_PHONEMES )
	{
		return false;
	}

	if ( !m_pPhonemeExtractor )
	{
		int c = g_Extractors.Count();
		for ( int i = 0; i < c; i++ )
		{
			Extractor *e = &g_Extractors[ i ];
			if ( e->apitype == (PE_APITYPE)g_viewerSettings.speechapiindex )
			{
				m_pPhonemeExtractor = e->extractor;
				break;
			}
		}

		if ( !m_pPhonemeExtractor )
		{
			Con_ErrorPrintf( "Couldn't find phoneme extractor %i\n",
				g_viewerSettings.speechapiindex );
		}
	}

	return m_pPhonemeExtractor != NULL;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : char const
//-----------------------------------------------------------------------------
char const *PhonemeEditor::GetSpeechAPIName( void )
{
	CheckSpeechAPI();

	if ( m_pPhonemeExtractor )
	{
		return m_pPhonemeExtractor->GetName();
	}

	return "Unknown Speech API";
}


//-----------------------------------------------------------------------------
// Purpose: 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool PhonemeEditor::PaintBackground( void )
{
	redraw();
	return false;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : PhonemeEditor::EditorMode
//-----------------------------------------------------------------------------
PhonemeEditor::EditorMode PhonemeEditor::GetMode( void ) const
{
	return m_CurrentMode;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : rcWorkSpace - 
//			rcEmphasis - 
//-----------------------------------------------------------------------------
void PhonemeEditor::Emphasis_GetRect( RECT const & rcWorkSpace, RECT& rcEmphasis )
{
	rcEmphasis = rcWorkSpace;

	int ybottom = rcWorkSpace.bottom - 2 * m_nTickHeight - 2;
	int workspaceheight = rcWorkSpace.bottom - rcWorkSpace.top;
	
	// Just past midpoint
	rcEmphasis.top		= rcWorkSpace.top + workspaceheight / 2 + 2;
	// 60 units or 
	rcEmphasis.bottom = clamp( rcEmphasis.top + 60, rcEmphasis.top + 20, ybottom );
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::OnModeChanged( void )
{
	// Show/hide controls as necessary
}


//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *parent - 
//-----------------------------------------------------------------------------
void PhonemeEditor::Emphasis_Init( void )
{
	m_nNumSelected = 0;
}

CEmphasisSample *PhonemeEditor::Emphasis_GetSampleUnderMouse( mxEvent *event )
{
	if ( GetMode() != MODE_EMPHASIS )
		return NULL;

	if ( !m_pWaveFile )
		return NULL;

	if ( w2() <= 0 )
		return NULL;

	if ( GetPixelsPerSecond() <= 0 )
		return NULL;

	float timeperpixel = 1.0f / GetPixelsPerSecond();
	float closest_dist = 999999.0f;
	CEmphasisSample *bestsample = NULL;

	int samples = m_Tags.GetNumSamples();

	float clickTime = GetTimeForPixel( (short)event->x );

	for ( int i = 0; i < samples; i++ )
	{
		CEmphasisSample *sample = m_Tags.GetSample( i );

		float dist = fabs( sample->time - clickTime );
		if ( dist < closest_dist )
		{
			bestsample = sample;
			closest_dist = dist;
		}

	}

	// Not close to any of them!!!
	if ( closest_dist > ( 5.0f * timeperpixel ) )
	{
		return NULL;
	}

	return bestsample;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::Emphasis_DeselectAll( void )
{
	if ( GetMode() != MODE_EMPHASIS )
		return;

	for ( int i = 0; i < m_Tags.GetNumSamples(); i++ )
	{
		CEmphasisSample *sample = m_Tags.GetSample( i );
		sample->selected = false;
	}
	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::Emphasis_SelectAll( void )
{
	if ( GetMode() != MODE_EMPHASIS )
		return;

	for ( int i = 0; i <  m_Tags.GetNumSamples(); i++ )
	{
		CEmphasisSample *sample = m_Tags.GetSample( i );
		sample->selected = true;
	}
	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::Emphasis_Delete( void )
{
	if ( GetMode() != MODE_EMPHASIS )
		return;

	SetDirty( true );

	PushUndo();

	for ( int i = m_Tags.GetNumSamples() - 1; i >= 0 ; i-- )
	{
		CEmphasisSample *sample = m_Tags.GetSample( i );
		if ( !sample->selected )
			continue;

		m_Tags.m_EmphasisSamples.Remove( i );

		SetDirty( true );
	}

	PushRedo();

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : sample - 
//-----------------------------------------------------------------------------
void PhonemeEditor::Emphasis_AddSample( CEmphasisSample const& sample )
{
	if ( GetMode() != MODE_EMPHASIS )
		return;

	SetDirty( true );

	PushUndo();

	m_Tags.m_EmphasisSamples.AddToTail( sample );
	m_Tags.Resort();

	PushRedo();

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::Emphasis_CountSelected( void )
{
	m_nNumSelected = 0;

	for ( int i = 0; i < m_Tags.GetNumSamples(); i++ )
	{
		CEmphasisSample *sample = m_Tags.GetSample( i );
		if ( !sample || !sample->selected )
			continue;

		m_nNumSelected++;
	}
}

void PhonemeEditor::Emphasis_ShowContextMenu( mxEvent *event )
{
	if ( GetMode() != MODE_EMPHASIS )
		return;

	CountSelected();

	// Construct main menu
	mxPopupMenu *pop = new mxPopupMenu();

	if ( m_nNumSelected > 0 )
	{
		pop->add( va( "Delete" ), IDC_EMPHASIS_DELETE );
		pop->add( "Deselect all", IDC_EMPHASIS_DESELECT );
	}
	pop->add( "Select all", IDC_EMPHASIS_SELECTALL );

	pop->popup( this, (short)event->x, (short)event->y );
}

void PhonemeEditor::Emphasis_MouseDrag( int x, int y )
{
	if ( m_nDragType != DRAGTYPE_EMPHASIS_MOVE )
		return;

	RECT rcWork;
	GetWorkspaceRect( rcWork );

	RECT rc;
	Emphasis_GetRect( rcWork, rc );

	int height = rc.bottom - rc.top;

	int dx = x - m_nLastX;
	int dy = y - m_nLastY;

	float dfdx = (float)dx * GetTimePerPixel();
	float dfdy = (float)dy / (float)height;

	for ( int i = 0; i < m_Tags.GetNumSamples(); i++ )
	{
		CEmphasisSample *sample = m_Tags.GetSample( i );
		if ( !sample || !sample->selected )
			continue;

		sample->time += dfdx;
		//sample->time = clamp( sample->time, 0.0f, 1.0f );

		sample->value -= dfdy;
		sample->value = clamp( sample->value, 0.0f, 1.0f );
	}
}

void PhonemeEditor::Emphasis_Redraw( CChoreoWidgetDrawHelper& drawHelper, RECT& rcWorkSpace )
{
	if ( GetMode() != MODE_EMPHASIS &&
		 GetMode() != MODE_PHONEMES )
		return;

	bool fullmode = GetMode() == MODE_EMPHASIS;
	RECT rcClient;

	Emphasis_GetRect( rcWorkSpace, rcClient );

	RECT rcText;
	rcText = rcClient;

	InflateRect( &rcText, -15, 0 );

	OffsetRect( &rcText, 0, -20 );
	rcText.bottom = rcText.top + 20;

	if ( fullmode )
	{
		drawHelper.DrawColoredText( "Arial", 15, FW_BOLD, PEColor( COLOR_PHONEME_EMPHASIS_TEXT ), rcText, "Emphasis..." );
	}

	{
		int h = rcClient.bottom - rcClient.top;
		int offset = h/3;
		RECT rcSpot = rcClient;
		rcSpot.bottom = rcSpot.top + offset;
		
		drawHelper.DrawGradientFilledRect( 
			rcSpot, 
			PEColor( COLOR_PHONEME_EMPHASIS_BG_STRONG ), 
			PEColor( COLOR_PHONEME_EMPHASIS_BG ), 
			true );

		OffsetRect( &rcSpot, 0, offset );

		drawHelper.DrawFilledRect( PEColor( COLOR_PHONEME_EMPHASIS_BG ), rcSpot );

		OffsetRect( &rcSpot, 0, offset );

		drawHelper.DrawGradientFilledRect( 
			rcSpot, 
			PEColor( COLOR_PHONEME_EMPHASIS_BG ), 
			PEColor( COLOR_PHONEME_EMPHASIS_BG_WEAK ), 
			true );
	}

	COLORREF gray = PEColor( COLOR_PHONEME_EMPHASIS_MIDLINE );

	drawHelper.DrawOutlinedRect( PEColor( COLOR_PHONEME_EMPHASIS_BORDER ), PS_SOLID, 1, rcClient );

	COLORREF lineColor = PEColor( COLOR_PHONEME_EMPHASIS_LINECOLOR );
	COLORREF dotColor = PEColor( COLOR_PHONEME_EMPHASIS_DOTCOLOR );
	COLORREF dotColorSelected = PEColor( COLOR_PHONEME_EMPHASIS_DOTCOLOR_SELECTED );

	int midy = ( rcClient.bottom + rcClient.top ) / 2;

	drawHelper.DrawColoredLine( gray, PS_SOLID, 1, rcClient.left, midy,
		rcClient.right, midy );
	int height = rcClient.bottom - rcClient.top;
	int bottom = rcClient.bottom - 1;

	if ( !m_pWaveFile )
		return;

	float running_length = m_pWaveFile->GetRunningLength();

	// FIXME: adjust this based on framerate....
	float timeperpixel = GetTimePerPixel();

	float starttime, endtime;
	GetScreenStartAndEndTime( starttime, endtime );

	int prevx = 0;
	float prev_t = starttime;
	float prev_value = m_Tags.GetIntensity( prev_t, running_length );

	int dx = 5;

	for ( int x = 0; x < ( w2() + dx ); x += dx )
	{
		float t = GetTimeForPixel( x );

		float value = m_Tags.GetIntensity( t, running_length );

		// Draw segment
		drawHelper.DrawColoredLine( lineColor, PS_SOLID, 1,
			prevx, 
			bottom - prev_value * height,
			x, 
			bottom - value * height );

		prev_t = t;
		prev_value = value;
		prevx = x;

	}

	int numsamples = m_Tags.GetNumSamples();

	for ( int sample = 0; sample < numsamples; sample++ )
	{
		CEmphasisSample *start = m_Tags.GetSample( sample );

		int x = ( start->time - starttime ) / timeperpixel;

		float value = m_Tags.GetIntensity( start->time, running_length );
		int y = bottom - value * height;

		int dotsize = 4;
		int dotSizeSelected = 5;

		COLORREF clr = dotColor;
		COLORREF clrSelected = dotColorSelected;

		drawHelper.DrawCircle( 
			start->selected ? clrSelected : clr, 
			x, y, 
			start->selected ? dotSizeSelected : dotsize,
			true );

	}
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool PhonemeEditor::Emphasis_IsValid( void )
{
	if ( m_Tags.GetNumSamples() > 0 )
		return true;

	return false;
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void PhonemeEditor::Emphasis_SelectPoints( void )
{
	if ( GetMode() != MODE_EMPHASIS )
		return;

	RECT rcWork, rcEmphasis;
	GetWorkspaceRect( rcWork );

	Emphasis_GetRect( rcWork, rcEmphasis );

	RECT rcSelection;
	
	rcSelection.left = m_nStartX < m_nLastX ? m_nStartX : m_nLastX;
	rcSelection.right = m_nStartX < m_nLastX ? m_nLastX : m_nStartX;

	rcSelection.top = m_nStartY < m_nLastY ? m_nStartY : m_nLastY;
	rcSelection.bottom = m_nStartY < m_nLastY ? m_nLastY : m_nStartY;

	rcSelection.top = max( rcSelection.top, rcEmphasis.top );
	rcSelection.bottom = min( rcSelection.bottom, rcEmphasis.bottom );

	int eh, ew;

	eh = rcEmphasis.bottom - rcEmphasis.top;
	ew = rcEmphasis.right - rcEmphasis.left;

	InflateRect( &rcSelection, 5, 5 );

	if ( !w2() || !h2() )
		return;

	float fleft = GetTimeForPixel( rcSelection.left );
	float fright = GetTimeForPixel( rcSelection.right );

	float ftop = (float)( rcSelection.top - rcEmphasis.top ) / (float)eh;
	float fbottom = (float)( rcSelection.bottom- rcEmphasis.top ) / (float)eh;

	//fleft = clamp( fleft, 0.0f, 1.0f );
	//fright = clamp( fright, 0.0f, 1.0f );
	ftop = clamp( ftop, 0.0f, 1.0f );
	fbottom = clamp( fbottom, 0.0f, 1.0f );

	float eps = 0.005;

	for ( int i = 0; i < m_Tags.GetNumSamples(); i++ )
	{
		CEmphasisSample *sample = m_Tags.GetSample( i );
		
		if ( sample->time + eps < fleft )
			continue;

		if ( sample->time - eps > fright )
			continue;

		if ( (1.0f - sample->value ) + eps < ftop )
			continue;

		if ( (1.0f - sample->value ) - eps > fbottom )
			continue;

		sample->selected = true;
	}

	redraw();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : rcHandle - 
//-----------------------------------------------------------------------------
void PhonemeEditor::GetScrubHandleRect( RECT& rcHandle, bool clipped )
{
	float pixel = 0.0f;

	if ( m_pWaveFile )
	{
		float currenttime = m_flScrub;
		float starttime, endtime;
		GetScreenStartAndEndTime( starttime, endtime );

		float screenfrac = ( currenttime - starttime ) / ( endtime - starttime );

		pixel = screenfrac * w2();

		if ( clipped )
		{
			pixel = clamp( pixel, SCRUBBER_HANDLE_WIDTH/2, w2() - SCRUBBER_HANDLE_WIDTH/2 );
		}
	}

	rcHandle.left = pixel-SCRUBBER_HANDLE_WIDTH/2;
	rcHandle.right = pixel + SCRUBBER_HANDLE_WIDTH/2;
	rcHandle.top = 2 + GetCaptionHeight() + 12;
	rcHandle.bottom = rcHandle.top + SCRUBBER_HANDLE_HEIGHT;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : rcArea - 
//-----------------------------------------------------------------------------
void PhonemeEditor::GetScrubAreaRect( RECT& rcArea )
{
	rcArea.left = 0;
	rcArea.right = w2();
	rcArea.top = 2 + GetCaptionHeight() + 12;
	rcArea.bottom = rcArea.top + SCRUBBER_HEIGHT - 4;
}

void PhonemeEditor::DrawScrubHandle()
{
	RECT rcHandle;
	GetScrubHandleRect( rcHandle, true );
	rcHandle.left = 0;
	rcHandle.right = w2();

	CChoreoWidgetDrawHelper drawHelper( this, rcHandle );

	DrawScrubHandle( drawHelper );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : drawHelper - 
//			rcHandle - 
//-----------------------------------------------------------------------------
void PhonemeEditor::DrawScrubHandle( CChoreoWidgetDrawHelper& drawHelper )
{
	RECT rcHandle;
	GetScrubHandleRect( rcHandle, true );

	HBRUSH br = CreateSolidBrush( RGB( 0, 150, 100 ) );

	COLORREF areaBorder = RGB( 230, 230, 220 );

	drawHelper.DrawColoredLine( areaBorder,
		PS_SOLID, 1, 0, rcHandle.top, w2(), rcHandle.top );
	drawHelper.DrawColoredLine( areaBorder,
		PS_SOLID, 1, 0, rcHandle.bottom, w2(), rcHandle.bottom );

	drawHelper.DrawFilledRect( br, rcHandle );

	// 
	char sz[ 32 ];
	sprintf( sz, "%.3f", m_flScrub );

	int len = drawHelper.CalcTextWidth( "Arial", 9, 500, sz );

	RECT rcText = rcHandle;
	int textw = rcText.right - rcText.left;

	rcText.left += ( textw - len ) / 2;

	drawHelper.DrawColoredText( "Arial", 9, 500, RGB( 255, 255, 255 ), rcText, sz );

	DeleteObject( br );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : *event - 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool PhonemeEditor::IsMouseOverScrubHandle( mxEvent *event )
{
	RECT rcHandle;
	GetScrubHandleRect( rcHandle, true );
	InflateRect( &rcHandle, 2, 2 );

	POINT pt;
	pt.x = (short)event->x;
	pt.y = (short)event->y;
	if ( PtInRect( &rcHandle, pt ) )
	{
		return true;
	}
	return false;
}

bool PhonemeEditor::IsMouseOverScrubArea( mxEvent *event )
{
	RECT rcArea;

	rcArea.left = 0;
	rcArea.right = w2();
	rcArea.top = 2 + GetCaptionHeight() + 12;
	rcArea.bottom = rcArea.top + 10;

	InflateRect( &rcArea, 2, 2 );

	POINT pt;
	pt.x = (short)event->x;
	pt.y = (short)event->y;
	if ( PtInRect( &rcArea, pt ) )
	{
		return true;
	}

	return false;
}

float PhonemeEditor::GetTimeForSample( int sample )
{
	if ( !m_pWaveFile )
	{
		return 0.0f;
	}

	float duration = m_pWaveFile->GetRunningLength();
	int sampleCount = m_pWaveFile->SampleCount();
	if ( sampleCount <= 0 )
		return 0.0f;

	float frac = (float)sample / (float)sampleCount;

	return frac * duration;
}

void PhonemeEditor::ClampTimeToSelectionInterval( float& timeval )
{
	if ( !m_pWaveFile )
	{
		return;
	}
	if ( !m_pMixer || !sound->IsSoundPlaying( m_pMixer ) )
	{
		return;
	}

	if ( !m_bSelectionActive )
		return;

	float starttime = GetTimeForSample( m_nSelection[ 0 ] );
	float endtime = GetTimeForSample( m_nSelection[ 1 ] );

	Assert( starttime <= endtime );

	timeval = clamp( timeval, starttime, endtime );
}

void PhonemeEditor::ScrubThink( float dt, bool scrubbing )
{
	ClampTimeToSelectionInterval( m_flScrub );
	ClampTimeToSelectionInterval( m_flScrubTarget );

	if ( m_flScrubTarget == m_flScrub && !scrubbing )
	{
		if ( sound->IsSoundPlaying( m_pMixer ) )
		{
			sound->StopSound( m_pMixer );
		}
		return;
	}

	if ( !m_pWaveFile )
		return;

	bool newmixer = false;
	if ( !m_pMixer || !sound->IsSoundPlaying( m_pMixer ) )
	{
		m_pMixer = NULL;
		SaveLinguisticData();

		StudioModel *model = NULL;//models->GetActiveStudioModel();

		sound->PlaySound( model, VOL_NORM, m_WorkFile.m_szWorkingFile, &m_pMixer );
		newmixer = true;
	}

	if ( !m_pMixer )
	{
		return;
	}

	if ( m_flScrub > m_flScrubTarget )
	{
		m_pMixer->SetDirection( false );
	}
	else
	{
		m_pMixer->SetDirection( true );
	}

	float duration = m_pWaveFile->GetRunningLength();
	if ( !duration )
		return;

	float d = m_flScrubTarget - m_flScrub;
	int sign = d > 0.0f ? 1 : -1;

	float maxmove = dt * m_flPlaybackRate;

	if ( sign > 0 )
	{
		if ( d < maxmove )
		{
			m_flScrub = m_flScrubTarget;
		}
		else
		{
			m_flScrub += maxmove;
		}
	}
	else
	{
		if ( -d < maxmove )
		{
			m_flScrub = m_flScrubTarget;
		}
		else
		{
			m_flScrub -= maxmove;
		}
	}

	int sampleCount = m_pMixer->GetSource()->SampleCount();

	int cursample = sampleCount * ( m_flScrub / duration );

	int realsample = m_pMixer->GetSamplePosition();

	int dsample = cursample - realsample;

	int onehundredth = m_pMixer->GetSource()->SampleRate() * 0.01f;

	if ( abs( dsample ) > onehundredth )
	{
		m_pMixer->SetSamplePosition( cursample, true );
	}
	m_pMixer->SetActive( true );

	RECT rcArea;
	GetScrubAreaRect( rcArea );

	CChoreoWidgetDrawHelper drawHelper( this, rcArea );
	DrawScrubHandle( drawHelper );

	if ( scrubbing )
	{
		g_pMatSysWindow->Frame();
	}
}

void PhonemeEditor::SetScrubTime( float t )
{
	m_flScrub = t;
	ClampTimeToSelectionInterval( m_flScrub );
}

void PhonemeEditor::SetScrubTargetTime( float t )
{
	m_flScrubTarget = t;
	ClampTimeToSelectionInterval( m_flScrubTarget );
}


void PhonemeEditor::OnToggleVoiceDuck()
{
	SetDirty( true );
	PushUndo();
	m_Tags.SetVoiceDuck( !m_Tags.GetVoiceDuck() );
	PushRedo();
	redraw();
}

void PhonemeEditor::Play()
{
	PlayEditedWave( m_bSelectionActive );
}