//------------------------------------------------------------------------------
// File: StrmCtl.cpp
//
// Desc: DirectShow base classes.
//
// Copyright (c) 1996-2001 Microsoft Corporation.  All rights reserved.
//------------------------------------------------------------------------------


#include <streams.h>
#include <strmctl.h>

CBaseStreamControl::CBaseStreamControl()
: m_StreamState(STREAM_FLOWING)
, m_StreamStateOnStop(STREAM_FLOWING) // means no pending stop
, m_tStartTime(MAX_TIME)
, m_tStopTime(MAX_TIME)
, m_dwStartCookie(0)
, m_dwStopCookie(0)
, m_pRefClock(NULL)
, m_FilterState(State_Stopped)
, m_bIsFlushing(FALSE)
, m_bStopSendExtra(FALSE) {
}

CBaseStreamControl::~CBaseStreamControl() {
    // Make sure we release the clock.
    SetSyncSource(NULL);
    return;
}


STDMETHODIMP CBaseStreamControl::StopAt(const REFERENCE_TIME * ptStop, BOOL bSendExtra, DWORD dwCookie) {
    CAutoLock lck(&m_CritSec);
    m_bStopSendExtra = FALSE;   // reset
    m_bStopExtraSent = FALSE;
    if(ptStop) {
        if(*ptStop == MAX_TIME) {
            DbgLog((LOG_TRACE,2,TEXT("StopAt: Cancel stop")));
            CancelStop();
            // If there's now a command to start in the future, we assume
            // they want to be stopped when the graph is first run
            if(m_FilterState == State_Stopped && m_tStartTime < MAX_TIME) {
                m_StreamState = STREAM_DISCARDING;
                DbgLog((LOG_TRACE,2,TEXT("graph will begin by DISCARDING")));
            }
            return NOERROR;
        }
        DbgLog((LOG_TRACE,2,TEXT("StopAt: %dms extra=%d"),
            (int)(*ptStop/10000), bSendExtra));
        // if the first command is to stop in the future, then we assume they
        // want to be started when the graph is first run
        if(m_FilterState == State_Stopped && m_tStartTime > *ptStop) {
            m_StreamState = STREAM_FLOWING;
            DbgLog((LOG_TRACE,2,TEXT("graph will begin by FLOWING")));
        }
        m_bStopSendExtra = bSendExtra;
        m_tStopTime = *ptStop;
        m_dwStopCookie = dwCookie;
        m_StreamStateOnStop = STREAM_DISCARDING;
    }
    else {
        DbgLog((LOG_TRACE,2,TEXT("StopAt: now")));
        // sending an extra frame when told to stop now would mess people up
        m_bStopSendExtra = FALSE;
        m_tStopTime = MAX_TIME;
        m_dwStopCookie = 0;
        m_StreamState = STREAM_DISCARDING;
        m_StreamStateOnStop = STREAM_FLOWING;   // no pending stop
    }
    // we might change our mind what to do with a sample we're blocking
    m_StreamEvent.Set();
    return NOERROR;
}


STDMETHODIMP CBaseStreamControl::StartAt
( const REFERENCE_TIME *ptStart, DWORD dwCookie ) {
    CAutoLock lck(&m_CritSec);
    if(ptStart) {
        if(*ptStart == MAX_TIME) {
            DbgLog((LOG_TRACE,2,TEXT("StartAt: Cancel start")));
            CancelStart();
            // If there's now a command to stop in the future, we assume
            // they want to be started when the graph is first run
            if(m_FilterState == State_Stopped && m_tStopTime < MAX_TIME) {
                DbgLog((LOG_TRACE,2,TEXT("graph will begin by FLOWING")));
                m_StreamState = STREAM_FLOWING;
            }
            return NOERROR;
        }
        DbgLog((LOG_TRACE,2,TEXT("StartAt: %dms"), (int)(*ptStart/10000)));
        // if the first command is to start in the future, then we assume they
        // want to be stopped when the graph is first run
        if(m_FilterState == State_Stopped && m_tStopTime >= *ptStart) {
            DbgLog((LOG_TRACE,2,TEXT("graph will begin by DISCARDING")));
            m_StreamState = STREAM_DISCARDING;
        }
        m_tStartTime = *ptStart;
        m_dwStartCookie = dwCookie;
        // if (m_tStopTime == m_tStartTime) CancelStop();
    }
    else {
        DbgLog((LOG_TRACE,2,TEXT("StartAt: now")));
        m_tStartTime = MAX_TIME;
        m_dwStartCookie = 0;
        m_StreamState = STREAM_FLOWING;
    }
    // we might change our mind what to do with a sample we're blocking
    m_StreamEvent.Set();
    return NOERROR;
}


//  Retrieve information about current settings
STDMETHODIMP CBaseStreamControl::GetInfo(AM_STREAM_INFO *pInfo) {
    if(pInfo == NULL)
        return E_POINTER;

    pInfo->tStart = m_tStartTime;
    pInfo->tStop  = m_tStopTime;
    pInfo->dwStartCookie = m_dwStartCookie;
    pInfo->dwStopCookie  = m_dwStopCookie;
    pInfo->dwFlags = m_bStopSendExtra ? AM_STREAM_INFO_STOP_SEND_EXTRA : 0;
    pInfo->dwFlags |= m_tStartTime == MAX_TIME ? 0 : AM_STREAM_INFO_START_DEFINED;
    pInfo->dwFlags |= m_tStopTime == MAX_TIME ? 0 : AM_STREAM_INFO_STOP_DEFINED;
    switch(m_StreamState) {
        default:
            DbgBreak("Invalid stream state");
        case STREAM_FLOWING:
            break;
        case STREAM_DISCARDING:
            pInfo->dwFlags |= AM_STREAM_INFO_DISCARDING;
            break;
    }
    return S_OK;
}


void CBaseStreamControl::ExecuteStop() {
    ASSERT(CritCheckIn(&m_CritSec));
    m_StreamState = m_StreamStateOnStop;
    if(m_dwStopCookie && m_pSink) {
        DbgLog((LOG_TRACE,2,TEXT("*sending EC_STREAM_CONTROL_STOPPED (%d)"),
            m_dwStopCookie));
        m_pSink->Notify(EC_STREAM_CONTROL_STOPPED, (LONG_PTR)this, m_dwStopCookie);
    }
    CancelStop(); // This will do the tidy up
}

void CBaseStreamControl::ExecuteStart() {
    ASSERT(CritCheckIn(&m_CritSec));
    m_StreamState = STREAM_FLOWING;
    if(m_dwStartCookie) {
        DbgLog((LOG_TRACE,2,TEXT("*sending EC_STREAM_CONTROL_STARTED (%d)"),
            m_dwStartCookie));
        m_pSink->Notify(EC_STREAM_CONTROL_STARTED, (LONG_PTR)this, m_dwStartCookie);
    }
    CancelStart(); // This will do the tidy up
}

void CBaseStreamControl::CancelStop() {
    ASSERT(CritCheckIn(&m_CritSec));
    m_tStopTime = MAX_TIME;
    m_dwStopCookie = 0;
    m_StreamStateOnStop = STREAM_FLOWING;
}

void CBaseStreamControl::CancelStart() {
    ASSERT(CritCheckIn(&m_CritSec));
    m_tStartTime = MAX_TIME;
    m_dwStartCookie = 0;
}


// This guy will return one of the three StreamControlState's.  Here's what the caller
// should do for each one:
//
// STREAM_FLOWING:      Proceed as usual (render or pass the sample on)
// STREAM_DISCARDING:   Calculate the time 'til *pSampleStart and wait that long
//                      for the event handle (GetStreamEventHandle()).  If the
//                      wait expires, throw the sample away.  If the event
//          fires, call me back, I've changed my mind.
//          I use pSampleStart (not Stop) so that live sources don't
//          block for the duration of their samples, since the clock
//          will always read approximately pSampleStart when called


// All through this code, you'll notice the following rules:
// - When start and stop time are the same, it's as if start was first
// - An event is considered inside the sample when it's >= sample start time
//   but < sample stop time
// - if any part of the sample is supposed to be sent, we'll send the whole
//   thing since we don't break it into smaller pieces
// - If we skip over a start or stop without doing it, we still signal the event
//   and reset ourselves in case somebody's waiting for the event, and to make
//   sure we notice that the event is past and should be forgotten
// Here are the 19 cases that have to be handled (x=start o=stop <-->=sample):
//
// 1.   xo<-->      start then stop
// 2.   ox<-->      stop then start
// 3.    x<o->      start
// 4.    o<x->      stop then start
// 5.    x<-->o     start
// 6.    o<-->x     stop
// 7.     <x->o     start
// 8.     <o->x     no change
// 9.     <xo>      start
// 10.    <ox>      stop then start
// 11.    <-->xo    no change
// 12.    <-->ox    no change
// 13.   x<-->      start
// 14.    <x->      start
// 15.    <-->x     no change
// 16.   o<-->      stop
// 17.    <o->      no change
// 18.    <-->o     no change
// 19.    <-->      no change


enum CBaseStreamControl::StreamControlState CBaseStreamControl::CheckSampleTimes
( const REFERENCE_TIME * pSampleStart, const REFERENCE_TIME * pSampleStop ) {
    CAutoLock lck(&m_CritSec);

    ASSERT(!m_bIsFlushing);
    ASSERT(pSampleStart && pSampleStop);

    // Don't ask how I came up with the code below to handle all 19 cases...
    if(m_tStopTime >= *pSampleStart) {
        if(m_tStartTime >= *pSampleStop)
            return m_StreamState;       // cases  8 11 12 15 17 18 19

        if(m_tStopTime < m_tStartTime)
            ExecuteStop();          // case 10

        ExecuteStart();                         // cases 3 5 7 9 13 14
        return m_StreamState;
    }

    if(m_tStartTime >= *pSampleStop) {
        ExecuteStop();                          // cases 6 16
        return m_StreamState;
    }

    if(m_tStartTime <= m_tStopTime) {
        ExecuteStart();
        ExecuteStop();
    }
    else {
        ExecuteStop();
        ExecuteStart();
    }

    return m_StreamState;       // case 1, 2, or 4
}


enum CBaseStreamControl::StreamControlState CBaseStreamControl::CheckStreamState( IMediaSample * pSample ) {

    REFERENCE_TIME rtBufferStart, rtBufferStop;
    const BOOL bNoBufferTimes =
        pSample == NULL ||
        FAILED(pSample->GetTime(&rtBufferStart, &rtBufferStop));

    StreamControlState state;
    LONG lWait;

    do {
        // something has to break out of the blocking
        if(m_bIsFlushing || m_FilterState == State_Stopped)
            return STREAM_DISCARDING;

        if(bNoBufferTimes) {
            //  Can't do anything until we get a time stamp
            state = m_StreamState;
            break;
        }
        else {
            state = CheckSampleTimes(&rtBufferStart, &rtBufferStop);
            if(state == STREAM_FLOWING)
                break;

            // we aren't supposed to send this, but we've been
            // told to send one more than we were supposed to
            // (and the stop isn't still pending and we're streaming)
            if(m_bStopSendExtra && !m_bStopExtraSent &&
                m_tStopTime == MAX_TIME &&
                m_FilterState != State_Stopped) {
                m_bStopExtraSent = TRUE;
                DbgLog((LOG_TRACE,2,TEXT("%d sending an EXTRA frame"),
                    m_dwStopCookie));
                state = STREAM_FLOWING;
                break;
            }
        }

        // We're in discarding mode

        // If we've no clock, discard as fast as we can
        if(!m_pRefClock) {
            break;

            // If we're paused, we can't discard in a timely manner because
            // there's no such thing as stream times.  We must block until
            // we run or stop, or we'll end up throwing the whole stream away
            // as quickly as possible
        }
        else if(m_FilterState == State_Paused) {
            lWait = INFINITE;

        }
        else {
            // wait until it's time for the sample until we say "discard"
            // ("discard in a timely fashion")
            REFERENCE_TIME rtNow;
            EXECUTE_ASSERT(SUCCEEDED(m_pRefClock->GetTime(&rtNow)));
            rtNow -= m_tRunStart;   // Into relative ref-time
            lWait = LONG((rtBufferStart - rtNow)/10000); // 100ns -> ms
            if(lWait < 10) break; // Not worth waiting - discard early
        }

    } while(WaitForSingleObject(GetStreamEventHandle(), lWait) != WAIT_TIMEOUT);

    return state;
}


void CBaseStreamControl::NotifyFilterState( FILTER_STATE new_state, REFERENCE_TIME tStart ) {
    CAutoLock lck(&m_CritSec);

    // or we will get confused
    if(m_FilterState == new_state)
        return;

    switch(new_state) {
        case State_Stopped:

            DbgLog((LOG_TRACE,2,TEXT("Filter is STOPPED")));

            // execute any pending starts and stops in the right order,
            // to make sure all notifications get sent, and we end up
            // in the right state to begin next time (??? why not?)

            if(m_tStartTime != MAX_TIME && m_tStopTime == MAX_TIME) {
                ExecuteStart();
            }
            else if(m_tStopTime != MAX_TIME && m_tStartTime == MAX_TIME) {
                    ExecuteStop();
            }
            else if(m_tStopTime != MAX_TIME && m_tStartTime != MAX_TIME) {
                    if(m_tStartTime <= m_tStopTime) {
                        ExecuteStart();
                        ExecuteStop();
                    }
                    else {
                        ExecuteStop();
                        ExecuteStart();
                    }
            }
            // always start off flowing when the graph starts streaming
            // unless told otherwise
            m_StreamState = STREAM_FLOWING;
            m_FilterState = new_state;
            break;

        case State_Running:

            DbgLog((LOG_TRACE,2,TEXT("Filter is RUNNING")));

            m_tRunStart = tStart;
            // fall-through

        default: // case State_Paused:
            m_FilterState = new_state;
    }
    // unblock!
    m_StreamEvent.Set();
}


void CBaseStreamControl::Flushing(BOOL bInProgress) {
    CAutoLock lck(&m_CritSec);
    m_bIsFlushing = bInProgress;
    m_StreamEvent.Set();
}

