// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright © Microsoft Corporation. All rights reserved

/****************************************************************************
*	recomgr.cpp
*		Support for queuing insertion points and correctly placing 
*       recognized text
*****************************************************************************/

#include "stdafx.h"
#include "resource.h"
#include <richedit.h>
#include "recomgr.h"

/****************************************************************************
* CRecoEventMgr::CRecoEventMgr *
*------------------------------*
*   Description:
*       Constructor for the recoevent manager
*****************************************************************************/
CRecoEventMgr::CRecoEventMgr( HINSTANCE hInstance ) :   
                            m_hInst( hInstance ), 
                            m_fPhraseStarted( false ),
                            m_pTextSel( NULL ),
                            m_pHeadLP( NULL ),
                            m_pHeadWM( NULL ),
                            m_pTailWM( NULL )
{
}   /* CRecoEventMgr::CRecoEventMgr */

/****************************************************************************
* CRecoEventMgr::~CRecoEventMgr *
*-------------------------------*
*   Description:
*       Destructor for the recoevent manager
*****************************************************************************/
CRecoEventMgr::~CRecoEventMgr()
{
    CleanUp();
}   /* CRecoEventMgr::~CRecoEventMgr */


/****************************************************************************
* CRecoEventMgr::PhraseStart *
*----------------------------*
*   Description:
*       Sets the flag to indicate that a phrase has been started.
*       Drops a listen point by calling SelNotify()
*   Return:
*       S_OK
*       E_OUTOFMEMORY
*       Return value of CRecoEventMgr::SelNotify()
*****************************************************************************/
HRESULT CRecoEventMgr::PhraseStart( ITextRange &rSelRange )
{
    // There should not be any phrases in progress or any listen points
    _ASSERTE( !m_fPhraseStarted && !m_pHeadLP );
    m_fPhraseStarted = true;

    // Should add a listening point right now
    HRESULT hr = SelNotify( rSelRange );
    if ( FAILED( hr ) )
    {
        return hr;
    }

    if ( !m_pHeadLP )
    {
        return E_OUTOFMEMORY;
    }

    m_pHeadLP->fFromPhraseStart = true;

    return S_OK;
}   /* CRecoEventMgr::PhraseStart */

/****************************************************************************
* CRecoEventMgr::SelNotify *
*--------------------------*
*   Description:
*       Called whenever the selection changes.
*       Drops a new listening point onto the list of listening points for
*       this phrase if there is a phrase currently being processed
*       being listened to.
*       Hands back the range to be eventually replaced by recognized
*       text (or deleted).
*   Return:
*       S_OK
*       S_FALSE if nothing had to be done
*       E_OUTOFMEMORY
*       Return value of ITextRange::GetDuplicate()
*       Return value of ITextRange::Collapse()
*****************************************************************************/
HRESULT CRecoEventMgr::SelNotify( ITextRange &rSelRange )
{
    // Only want to queue this listen if we are listening to a phrase
    HRESULT hr = S_FALSE;
    if ( m_fPhraseStarted )
    {
        // Get the time now
        FILETIME ftNow;
        ::CoFileTimeNow( &ftNow );

        // Does this range overlap any of the existing ranges in the list?
        LISTENPOINT *p;
        for ( p = m_pHeadLP; 
            p && AreDisjointRanges( &rSelRange, p->cpRangeToReplace ); 
            p = p->pNext )
            ;

        // If p is not NULL, that means that there is already some listen point
        // that is adjacent to or overlaps this range, so another listen point
        // is not necessary
        if ( !p )
        {
            // Add a new listen point
            LISTENPOINT *pNewPoint = new LISTENPOINT;
            if ( !pNewPoint )
            {
                return E_OUTOFMEMORY;
            }

            // Get the ranges
            rSelRange.GetDuplicate( &(pNewPoint->cpRangeToReplace) );

            // Get the timestamp
            pNewPoint->ftTime = ftNow;

            // This flag will be set by PhraseStart() if appropriate
            pNewPoint->fFromPhraseStart = false;

            // No hypothesis text here yet
            pNewPoint->fHasHypothesisText = false;

            // Put this new listenpoint onto the head of the list
            pNewPoint->pNext = m_pHeadLP;
            m_pHeadLP = pNewPoint;

            long lEndOfRangeToReplace;
            pNewPoint->cpRangeToReplace->GetEnd( &lEndOfRangeToReplace );

            // The selection should be forced to the end of the range to replace;
            // this keeps the selection out of ranges that might be replaced
            // by recognized text
            m_pTextSel->SetRange( 
                    lEndOfRangeToReplace, lEndOfRangeToReplace );

            // If we got here, we were successful
            hr = S_OK;
        }
    }
    return hr;
}   /* CRecoEventMgr::SelNotify */

/****************************************************************************
* CRecoEventMgr::IsEditable *
*---------------------------*
*   Description:
*       Determines whether pRange overlaps any of the current listen points.
*       If it does, and if pNextEditableRange is non-NULL, sets 
*       pNextEditableRange to the next range that can be edited with
*       impunity
*   Return:
*       true iff the RecoEventMgr is okay with this text being edited
*****************************************************************************/
bool CRecoEventMgr::IsEditable( ITextRange *pRange, 
                               ITextRange **ppNextEditableRange )
{
    if ( !pRange )
    {
        return false;
    }
    if ( !m_fPhraseStarted )
    {
        // Always editable if there's no reco computation
        return true;
    }

    // Does this range overlap or abut any of the existing ranges in the list?
    LISTENPOINT *p;
    for ( p = m_pHeadLP; 
        p && AreDisjointRanges( pRange, p->cpRangeToReplace ); 
        p = p->pNext )
        ;

    if ( p )
    {
        // There is a phrase that overlaps or abuts this one.
        // If pRange dovetails onto the end of it, then the 
        // range is editable; otherwise it is not
        long lRangeStart = 0;
        long lLPEnd = 0;
        pRange->GetStart( &lRangeStart );
        p->cpRangeToReplace->GetEnd( &lLPEnd );

        if ( lRangeStart < lLPEnd )
        {
            // If pNextEditableRange is non-NULL, set it a degenerate range
            // at the end of the listen point range
            if ( ppNextEditableRange )
            {
                // Release whatever is already there
                if ( *ppNextEditableRange )
                {
                    (*ppNextEditableRange)->Release();
                }

                // Get a duplicate of the range to replace and collapse at the end
                HRESULT hr = p->cpRangeToReplace->GetDuplicate( ppNextEditableRange );
                if ( SUCCEEDED( hr ) )
                {
                    (*ppNextEditableRange)->Collapse( tomEnd );
                }
                else
                {
                    *ppNextEditableRange = NULL;
                }
            }
            return false;
        }
    }

    // pRange is editable
    // Leave ppNextEditableRange, whatever it is

    return true;
}   /* CRecoEventMgr::IsEditable */

/****************************************************************************
* CRecoEventMgr::QueueCommand *
*-----------------------------*
*   Description:
*       Called when the engine is currently processing a phrase.
*       Puts the command at the tail of the command queue.
*****************************************************************************/
void CRecoEventMgr::QueueCommand( HWND hWnd, UINT message, 
                                 WPARAM wParam, LPARAM lParam )
{
    if ( !m_fPhraseStarted )
    {
        // Don't queue commands unless a phrase is being processed
        return;
    }

    QUEUEDWM *pWM = new QUEUEDWM;
    if ( !pWM )
    {
        // Out of memory: This WM_COMMAND will get dropped
        return;
    }
    pWM->hWnd = hWnd;
    pWM->message = message;
    pWM->wParam = wParam;
    pWM->lParam = lParam;
    pWM->pNext = NULL;

    if ( m_pTailWM )
    {
        // Non-empty queue
        m_pTailWM->pNext = pWM;
        m_pTailWM = pWM;
    }
    else
    {
        // Empty queue
        m_pHeadWM = m_pTailWM = pWM;
    }
}   /* CRecoEventMgr::QueueCommand */
 
/****************************************************************************
* CRecoEventMgr::Hypothesis *
*---------------------------*
*   Description:
*       Called whenever a hypothesis comes back to DictationPad.
*       Looks for the latest listening point whose timestamp predates ftRecoTime
*       or for the phrase-start-generated listening point,
*       whichever came later.
*       Hands back the range to be replaced by the recognized text, or NULL
*       if this hypothesis is to be dropped (i.e. did not come between
*       a PHRASE_START and a RECOGNITION/FALSE_RECOGNITION
*   Return:
*       A pointer to an ITextRange in which the hypothesis should go.
*       NULL in case of error.
*****************************************************************************/
ITextRange * CRecoEventMgr::Hypothesis( FILETIME ftRecoTime )
{
    if ( !m_fPhraseStarted || !m_pHeadLP )
    {
        // This hypothesis did not come between a PHRASE_START and a RECOGNITION,
        // so we drop it
        return NULL;
    }

    // Since the listen points are ordered by most recent first, we are
    // looking for the first element whose timestamp predates ftRecoTime
    LISTENPOINT *p;
    for ( p = m_pHeadLP; 
        p && (CompareFiletimes( ftRecoTime, p->ftTime ) < 0) && 
                !(p->fFromPhraseStart);
        p = p->pNext )
        ;
        
    if ( !p )
    {
        // Should not happen!  There should at least be a listen point queued
        // from PhraseStart() on this phrase
        _ASSERTE( false );
        return NULL;
    }

    // This listen point has now been marked to receive hypothesis text
    p->fHasHypothesisText = true;
    
    return p->cpRangeToReplace;
}   /* CRecoEventMgr::Hypothesis */

/****************************************************************************
* CRecoEventMgr::Recognition *
*----------------------------*
*   Description:
*       Called whenever a recognition comes back to DictationPad.
*       Looks for the latest listening point whose timestamp predates 
*       ftRecoTime or for the phrase-start-generated listening point,
*       whichever came later.
*       Deletes all earlier listening points. 
*       Hands back a duplicate of the range to be replaced by the recognized 
*       text.
*   Return:
*       S_OK
*       S_FALSE if the recognition was from some phrase that was started
*           when our grammars were inactive.
*       Return value from ITextRange::GetDuplicate()
*****************************************************************************/
HRESULT CRecoEventMgr::Recognition( FILETIME ftRecoTime, ITextRange **ppRecoRange )
{

    // A SPEI_RECOGNITION without a SPEI_PHRASE_START means that the phrase was
    // started when our grammars were inactive, so we'd like to ignore this one.
    if ( !m_fPhraseStarted || !m_pHeadLP )
    {
        return S_FALSE;
    }

    // Since the listen points are ordered by most recent first, we are
    // looking for the first element whose timestamp predates ftRecoTime
    LISTENPOINT *p;
    for ( p = m_pHeadLP; 
        p && (CompareFiletimes( ftRecoTime, p->ftTime ) < 0) && !(p->fFromPhraseStart);
        p = p->pNext )
        ;
        
    _ASSERTE( p );
    if ( !p )
    {
        // Should not happen!
        return E_UNEXPECTED;
    }

    // Get rid of all subsequent (earlier) listening points
    DeleteAfter( p );

    // Get the range to return.
    // Make a duplicate, since this range will be destroyed before the caller
    // can AddRef it.
    HRESULT hr = p->cpRangeToReplace->GetDuplicate( ppRecoRange );

    // This listen point will now contain real (non-hypothesis) text
    p->fHasHypothesisText = false;

    // p will get cleaned up later when DoneProcessingPhrase() is called

    return hr;
}   /* CRecoEventMgr::Recognition */

/****************************************************************************
* CRecoEventMgr::FalseRecognition *
*---------------------------------*
*   Description:
*       Called whenever a false recognition comes back to DictationPad.
*       Finds the phrase-start-marked listen point
*       and deletes everything after it
*****************************************************************************/
void CRecoEventMgr::FalseRecognition()
{
    if ( !m_fPhraseStarted || !m_pHeadLP )
    {
        // This means that this is a RECO_OTHER_CONTEXT, or a recognition for
        // a grammar other than dictation within our own context, neither of
        // which we care about
        return;
    }

    // Clean up anything that happened before the start of this utterance
    LISTENPOINT *p;
    for ( p = m_pHeadLP; p && !(p->fFromPhraseStart); p = p->pNext )
    {
        ;
    }
    if ( p )
    {
        DeleteAfter( p );
    }

    // p will get cleaned up later when DoneProcessingPhrase() is called

}   /* CRecoEventMgr::FalseRecognition */

/****************************************************************************
* CRecoEventMgr::DoneProcessingPhrase *
*-------------------------------------*
*   Description:
*       Sets m_fPhraseStarted to false.
*       Calls CleanUp() to clean up the listen point list 
*       and WM_COMMAND queue.
*****************************************************************************/
void CRecoEventMgr::DoneProcessingPhrase()
{
    m_fPhraseStarted = false;
    CleanUp();
}   /* CRecoEventMgr::DoneProcessingPhrase */

/****************************************************************************
* CRecoEventMgr::CleanUp *
*------------------------*
*   Description:
*       Deletes all nodes.
*       Resends the queued messages and cleans up the queue
*****************************************************************************/
void CRecoEventMgr::CleanUp()
{
    m_fPhraseStarted = false;
    
    // Remove any hypothesis text
    for ( LISTENPOINT *p = m_pHeadLP; p; p = p->pNext )
    {
        if ( p->fHasHypothesisText && p->cpRangeToReplace )
        {
            p->cpRangeToReplace->SetText( L"" );
        }
    }

    // Clean up the listen point list
    if ( m_pHeadLP )
    {
        DeleteAfter( m_pHeadLP );
        delete m_pHeadLP;
        m_pHeadLP = NULL;
    }

    // Resend the WM_x messages that had been waiting during the computation
    // on the phrase and clean up the queue
    QUEUEDWM *pWM;
    while ( m_pHeadWM )
    {
        pWM = m_pHeadWM;
        ::SendMessage( pWM->hWnd, pWM->message, pWM->wParam, pWM->lParam );

        m_pHeadWM = m_pHeadWM->pNext;
        delete pWM;
    }
    m_pHeadWM = m_pTailWM = NULL;
}   /* CRecoEventMgr::CleanUp */


/****************************************************************************
* CRecoEventMgr::DeleteAfter *
*----------------------------*
*   Description:
*       Deletes all nodes after pCutoff.
*****************************************************************************/
void CRecoEventMgr::DeleteAfter( LISTENPOINT *pCutoff )
{
    if ( pCutoff )
    {
        LISTENPOINT *p, *pNextInLine;
        for ( p = pCutoff->pNext; p; p = pNextInLine )
        {
            pNextInLine = p->pNext;
            delete p;
        }

        pCutoff->pNext = NULL;
    }
}   /* CRecoEventMgr::DeleteAfter */

/****************************************************************************
* CompareFiletimes *
*------------------*
*   Return:
*       -1 if ft1 is earlier than ft2
*       0 if ft1 == ft2
*       1 if ft1 is greater than ft2
*****************************************************************************/
int CompareFiletimes( FILETIME ft1, FILETIME ft2 )
{
    if ( ft1.dwHighDateTime < ft2.dwHighDateTime )
    {
        return -1;
    }
    else if ( ft1.dwHighDateTime > ft2.dwHighDateTime )
    {
        return 1;
    }
    else
    {
        if ( ft1.dwLowDateTime < ft2.dwLowDateTime )
        {
            return -1;
        }
        else if ( ft1.dwLowDateTime > ft2.dwLowDateTime )
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }
}   /* CompareFiletimes */

/****************************************************************************
* AreDisjointRanges *
*-------------------*
*   Description:
*       Suppose Range1 has limits cpMin1, cpMax1.
*       Suppose Range2 has limits cpMin2, cpMax2.
*       Range1 and Range2 are disjoint iff 
*           cpMin2 > cpMax1 OR cpMax2 < cpMin1.
*       That is, they are disjoint iff there is no overlap and they do not
*       dovetail.
*   Return:
*       true iff the two ranges neither overlap nor abut.
*****************************************************************************/
bool AreDisjointRanges( ITextRange *pRange1, ITextRange *pRange2 )
{
    if ( !( pRange1 && pRange2 ) )
    {
        return true;
    }

    long cpMin1, cpMax1, cpMin2, cpMax2;
    pRange1->GetStart( &cpMin1 );
    pRange1->GetEnd( &cpMax1 );
    pRange2->GetStart( &cpMin2 );
    pRange2->GetEnd( &cpMax2 );

    return (( cpMin2 > cpMax1 ) || ( cpMax2 < cpMin1 ));
}   /* AreDisjointRanges */
