/*
* Copyright (c) 2008 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of the License "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description:  Gesture helper implementation
*
*/


#include "gesturehelperimpl.h"

#include <e32base.h>
#include <w32std.h>

#include "gesture.h"
#include "gesturedefs.h"
#include "utils.h"
#include "gestureeventfilter.h"
#include "gesturehelpereventsender.h"
#include "flogger.h"

using namespace RT_GestureHelper;

namespace RT_GestureHelper
{

/// type of function in gesture helper to be called by the timer
/// when timer triggers
typedef void (CGestureHelperImpl::*CallbackFunctionL)();

NONSHARABLE_CLASS( CCallbackTimer ) : public CTimer
    {
public:
    /** Two-phase constructor */
    static CCallbackTimer* NewL( CGestureHelperImpl& aHelper, 
            CallbackFunctionL aCallbackFunctionL, TInt aDelay, TBool aIsEnabled )
        {
        CCallbackTimer* self = new ( ELeave ) CCallbackTimer( aHelper, 
            aCallbackFunctionL, aDelay, aIsEnabled );
        CleanupStack::PushL( self );
        self->ConstructL(); // construct base class
        CActiveScheduler::Add( self );
        CleanupStack::Pop( self );
        return self;
        }
        
    /** Destructor */
    ~CCallbackTimer()
        {
        Cancel();
        }
        
    /** Set whether sending holding events is currently enabled */
    void SetEnabled( TBool aEnabled )
        {
        iIsEnabled = aEnabled;
        // cancel in case hold timer is already running
        Cancel();
        }
        
    /** @return whether sending holding events is currently enabled */
    TBool IsEnabled() const
        {
        return iIsEnabled;
        }
        
    /** Start the timer. Calls CGestureHelperImpl::StartHoldingL upon completion */
    void Start()
        {
        // if sending hold events is disabled, do not ever start the hold timer, and 
        // hence hold events will never be triggered
        if ( iIsEnabled ) 
            {
            Cancel();
            After( iDelay );
            }
        }    
    void SetDelay(TInt aDelay) { iDelay = aDelay; }
    TInt GetDelay() { return iDelay; }
    
private:    
    /** Constructor */
    CCallbackTimer( CGestureHelperImpl& aHelper,
        CallbackFunctionL aCallbackFunctionL, TInt aDelay, TBool aIsEnabled )
            : CTimer( EPriorityUserInput - 1 ), // give higher priority to new pointer events with - 1
                iHelper( aHelper ), iCallbackFunctionL( aCallbackFunctionL ), 
                    iDelay( aDelay ), iIsEnabled( aIsEnabled ) 
        {
        }
        
    void RunL() // From CActive
        {
        (iHelper.*iCallbackFunctionL)();
        }

private:
    /// helper object that will be called back when timer is triggered
    CGestureHelperImpl& iHelper;
    /// Function in the iHelper object call 
    CallbackFunctionL iCallbackFunctionL;
    /// How long a time to wait befor calling back after Start()
    TInt iDelay;
    /// whether sending holding events is currently enabled
    TBool iIsEnabled;
    };

} // namespace GestureHelper

/** 
 * @return position from event. Use this instead of using aEvent direction to
 *         avoid accidentally using TPointerEvent::iPosition
 */
inline TPoint Position( const TPointerEvent& aEvent )
    {
    // use parent position, since the capturer is using full screen area,
    // and because the (Alfred) drag events are not local to visual even when
    // coming from the client
    
    return aEvent.iPosition;
    }

// ----------------------------------------------------------------------------
// Two-phase constructor
// ----------------------------------------------------------------------------
//
CGestureHelperImpl* CGestureHelperImpl::NewL( MGestureObserver& aObserver )
    {
    CGestureHelperImpl* self = new ( ELeave ) CGestureHelperImpl( aObserver );
    CleanupStack::PushL( self );
    self->iEventSender = CGestureEventSender::NewL( aObserver );
    self->iDoubleTapTimer = CCallbackTimer::NewL( *self, EmitFirstTapEvent, 
            KMaxTapDuration, EFalse ); // double tap is disabled by default
    self->iHoldingTimer = CCallbackTimer::NewL( *self, StartHoldingL, 
        KHoldDuration, EFalse ); // holding is enabled by default
    
    self->iLongTouchTimer = CCallbackTimer::NewL( *self, HandleLongTouch, 
            KLongTapDuration, ETrue ); // holding is enabled by default
    
    self->iGesture = new ( ELeave ) CGesture();
    self->iUnusedGesture = new ( ELeave ) CGesture();
    TInt tapLimit = Mm2Pixels(KFingerSize_mm) / 2;
    self->iEventFilter = new (ELeave) CGestureEventFilter(tapLimit);
    CleanupStack::Pop( self );
    return self;
    }

// ----------------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------------
//
CGestureHelperImpl::CGestureHelperImpl( MGestureObserver& aObserver )
        : iObserver( aObserver )
    {
    }

// ----------------------------------------------------------------------------
// Destructor
// ----------------------------------------------------------------------------
//
CGestureHelperImpl::~CGestureHelperImpl()
    {
    delete iDoubleTapTimer;
    delete iHoldingTimer;
    delete iGesture;
    delete iPreviousTapGesture;
    delete iUnusedGesture;
    delete iLongTouchTimer;
    delete iEventFilter;
    delete iEventSender;
    }
    

// ----------------------------------------------------------------------------
// SetHoldingEnabled
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::SetHoldingEnabled( TBool aEnabled )
    {
    iHoldingTimer->SetEnabled( aEnabled );
    }

// ----------------------------------------------------------------------------
// IsHoldingEnabled
// ----------------------------------------------------------------------------
//
TBool CGestureHelperImpl::IsHoldingEnabled() const
    {
    return iHoldingTimer->IsEnabled();
    }

// ----------------------------------------------------------------------------
// SetHoldingEnabled
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::SetDoubleTapEnabled( TBool aEnabled )
    {
    iDoubleTapTimer->SetEnabled( aEnabled );
    }

// ----------------------------------------------------------------------------
// IsHoldingEnabled
// ----------------------------------------------------------------------------
//
TBool CGestureHelperImpl::IsDoubleTapEnabled() const
    {
    return iDoubleTapTimer->IsEnabled();
    }
    


// ----------------------------------------------------------------------------
// Reset state
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::Reset()
    {
    iHoldingTimer->Cancel();
    iLongTouchTimer->Cancel();
    iGesture->Reset();
    }

/** 
 * Helper function that calls Reset on the pointer to CGestureHelperImpl
 */
static void ResetHelper( TAny* aHelper )
    {
    static_cast< CGestureHelperImpl* >( aHelper )->Reset();
    }

// ----------------------------------------------------------------------------
// Handle a pointer event
// ----------------------------------------------------------------------------
//
TBool CGestureHelperImpl::HandlePointerEventL( const TPointerEvent& aEvent )
    {     
    TInt filterReason;
    SetLastEventTime();
    if (!iEventFilter->FilterDrag(aEvent, iLastEventTime, filterReason))
        {
        return noneAlf_HandlePointerEventL( aEvent );
        }
    else
        {
        /*
        TBuf<10> num;
        num.Num( filterReason );
        TBuf<128> str;
        str.AppendFormat(_L("Filter reason: %d"), filterReason);
        RFileLogger::Write( _L("gh"), _L("gh.txt"), EFileLoggingModeAppend, str);
        */
        return EFalse;
        }
    }


TBool CGestureHelperImpl::noneAlf_HandlePointerEventL( const TPointerEvent& aEvent)
    {
    switch ( aEvent.iType )
        {
        case TPointerEvent::EButton1Down:
            {
            HandleTouchDownL(aEvent);
            break;
            }
        case TPointerEvent::EDrag:
            {
            HandleMoveL(aEvent);
            break;
            }
        case TPointerEvent::EButton1Up:
            {
            if (KErrNone == AddPoint( aEvent ))
                {
                HandleTouchUp(aEvent);
                }
            else
                {
                EmitCancelEvent();
                }
            Reset();
            break;
            }
        default:
            break;
        }
    return ETrue;
    }

TBool CGestureHelperImpl::IsMovementGesture(TGestureCode aCode)
    {
    return (aCode == EGestureDrag || aCode == EGestureFlick || aCode == EGestureSwipeUp ||
            aCode == EGestureSwipeDown || aCode == EGestureSwipeRight || aCode == EGestureSwipeLeft);
    }

void CGestureHelperImpl::HandleLongTouch()
    {
    iDoubleTapTimer->Cancel();
    iGesture->SetLongTap(ETrue);
    iGesture->SetComplete();
    TPoint startPos = iGesture->StartPos();
    EmitEvent(*iGesture);
    iGesture->Reset();
    iGesture->AddPoint( startPos, GetLastEventTime() );
    }

void CGestureHelperImpl::HandleTouchDownL(const TPointerEvent& aEvent)
    {
    TGestureCode prevCode = iGesture->PreviousGestureCode();
    if (prevCode == EGestureStart) return;
    if (prevCode == EGestureDrag) 
        {
        iGesture->Reset();
        }
    AddPointL( aEvent );
    
    if (!iLongTouchTimer->IsActive())
        {
    iLongTouchTimer->Start();
        }
    if (!iDoubleTapTimer->IsActive())
        {
            EmitEvent( *iGesture );
        }
    }

void CGestureHelperImpl::HandleMoveL(const TPointerEvent& aEvent)
    {
    if (iGesture->IsLatestPoint( Position(aEvent))) return; // I'm not sure we need this
    //Cancel double tap time - it's neither tap nor double tap 
    iDoubleTapTimer->Cancel();
    iLongTouchTimer->Cancel();
    
    TBool isFirstPoint = IsIdle();
    
    AddPointL( aEvent );
    
    if (iPreviousTapGesture)
        {
        RecycleGesture(iPreviousTapGesture);
        }
    
    if (!isFirstPoint)
        {
        EmitEvent( *iGesture );
        }
    }

void CGestureHelperImpl::HandleTouchUp(const TPointerEvent& /*aEvent*/)
    {
    TGestureCode prevCode = iGesture->PreviousGestureCode();
    iLongTouchTimer->Cancel();
    iDoubleTapTimer->Cancel();
    TInt64 fromLastTouchUp = iLastEventTime.MicroSecondsFrom(iLastTouchUpTime).Int64();
    TInt64 fromLastDoubleTap = iLastEventTime.MicroSecondsFrom(iLastDoubleTapTime).Int64();
    /*
    TBuf<1024> str;
    str.AppendFormat(_L("fromLastTouchUp: %d, "), fromLastTouchUp);
    str.AppendFormat(_L("fromLastDoubleTap: %d, "), fromLastTouchUp);
    str.AppendFormat(_L("iPreviousTapGesture: %d, "), iPreviousTapGesture);
    RFileLogger::Write( _L("gh"), _L("gh.txt"), EFileLoggingModeAppend, str);
    */
    if ( prevCode == EGestureLongTap )
        {
        EmitReleasedEvent();
        }
    else if (IsMovementGesture(prevCode) || 
             !iDoubleTapTimer->IsEnabled() /* || !iGesture->IsTap()*/ ) 
        {
        iGesture->SetComplete();
        EmitEvent(*iGesture);
        }
    
    else 
        {
        if ( iPreviousTapGesture && 
         (fromLastTouchUp > KDoubleTapMinActivationInterval) &&       
         (fromLastTouchUp < KDoubleTapMaxActivationInterval) &&
         (fromLastDoubleTap > KDoubleTapIdleInterval))
            {
            // it's a double tap
            iLastTouchUpTime = iLastEventTime;
            iLastDoubleTapTime = iLastEventTime;
            EmitDoubleTapEvent();
            }
        else
            {
            // it's a first tap
            iLastTouchUpTime = iLastEventTime;
            if (iPreviousTapGesture)
                {
                   RecycleGesture(iPreviousTapGesture);
                }
                        
            iPreviousTapGesture = iGesture;
            iGesture = NewGesture();
            iDoubleTapTimer->Start(); 
            }
        }
    }



void CGestureHelperImpl::EmitDoubleTapEvent()
    {
    iPreviousTapGesture->SetDoubleTap();
    EmitFirstTapEvent();
    }


void CGestureHelperImpl::EmitReleasedEvent()
    {
    iGesture->SetComplete();
    iGesture->SetReleased();
    EmitEvent(*iGesture);
    }


// ----------------------------------------------------------------------------
// Is the helper idle?
// inline ok in cpp file for a private member function
// ----------------------------------------------------------------------------
//
inline TBool CGestureHelperImpl::IsIdle() const
    {
    return iGesture->IsEmpty();
    }

// ----------------------------------------------------------------------------
// Add a point to the sequence of points that together make up the gesture
// inline ok in cpp file for a private member function
// ----------------------------------------------------------------------------
//
inline void CGestureHelperImpl::AddPointL( const TPointerEvent& aEvent )
    {
    User::LeaveIfError( AddPoint( aEvent ) );
    }

// ----------------------------------------------------------------------------
// Add a point to the sequence of points that together make up the gesture
// inline ok in cpp file for a private member function
// ----------------------------------------------------------------------------
//
inline TInt CGestureHelperImpl::AddPoint( const TPointerEvent& aEvent )
    {
    TPoint pos = Position ( aEvent );
    return iGesture->AddPoint( pos, GetLastEventTime() );
    }

// ----------------------------------------------------------------------------
// StartHoldingTimer
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::StartHoldingTimer( const TPointerEvent& aNewEvent )
    {
    if ( !( iGesture->IsHolding() ||
            iGesture->IsNearHoldingPoint( Position( aNewEvent ) ) ) )
        {
        // restart hold timer, since pointer has moved
        iHoldingTimer->Start();
        // Remember the point in which holding was started
        iGesture->SetHoldingPoint();
        }
    }

/** 
 * Helper function that calls ContinueHolding on the pointer to TGesture
 */
static void ContinueHolding( TAny* aGesture )
    {
    static_cast< CGesture* >( aGesture )->ContinueHolding();
    }

// ----------------------------------------------------------------------------
// Add a point to the sequence of points that together make up the gesture
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::StartHoldingL()
    {
    // hold & tap event is specifically filtered out. Use case: in list fast 
    // scrolling activation (e.g. enhanced coverflow), tap & hold should not
    // start fast scroll. In addition, after long tap on start position,
    // drag and drag & hold swiping should emit normal swipe and swipe&hold
    // events. Therefore, tap & hold is not supported.
    __ASSERT_DEBUG( !iGesture->IsTap() && !iPreviousTapGesture, Panic( EGesturePanicIllegalLogic ) );
    
    // holding has just started, and gesture code should be provided to client.
    // set gesture state so that it produces a gesture code (other than drag)
    iGesture->StartHolding();
    
    // create an item in the cleanup stack that will set the gesture state
    // to holding-was-started-earlier state. NotifyL may leave, but the
    // holding-was-started-earlier state must still be successfully set,
    // otherwise, the holding gesture code will be sent twice
    CleanupStack::PushL( TCleanupItem( &ContinueHolding, iGesture ) );
    
    EmitEvent( *iGesture );
    
    // set holding state to "post holding"
    CleanupStack::PopAndDestroy( iGesture );
    }

// ----------------------------------------------------------------------------
// RecyclePreviousTapGesture
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::RecyclePreviousTapGesture( TAny* aSelf )
    {
    CGestureHelperImpl& self = *reinterpret_cast<CGestureHelperImpl*>( aSelf );
    self.RecycleGesture( self.iPreviousTapGesture );
    }

// ----------------------------------------------------------------------------
// Emit the remainder of the previous tap event (tap + released)
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::EmitFirstTapEvent()
    {
    // when this function is called, a tap has turned out to _not_ be a double tap
    __ASSERT_DEBUG( IsDoubleTapEnabled(), Panic( EGesturePanicIllegalLogic ) );
    __ASSERT_DEBUG( iPreviousTapGesture, Panic( EGesturePanicIllegalLogic ) );
    
    iDoubleTapTimer->Cancel();
    CompleteAndEmit( *iPreviousTapGesture );
    RecycleGesture(iPreviousTapGesture);
     
    }

// ----------------------------------------------------------------------------
// EmitStartEventL
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::EmitStartEventL( const CGesture& aGesture )    
    {
    CGesture* startGesture = aGesture.AsStartEventLC();
    EmitEvent( *startGesture );
    CleanupStack::PopAndDestroy( startGesture );    
    }
    
// ----------------------------------------------------------------------------
// EmitCompletionEventsL
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::CompleteAndEmit( CGesture& aGesture )
    {
    aGesture.SetComplete();
    // send gesture code if holding has not been started. If holding has 
    // been started, client has already received a "hold swipe left" e.g. event, in which
    // case don't another "swipe left" event
    if ( !aGesture.IsHolding() )
        {
        // if client leaves, the state is automatically reset.
        // In this case the client will not get the released event
        EmitEvent( aGesture ); 
        }
    
    // send an event that stylus was lifted
    aGesture.SetReleased();
    EmitEvent( aGesture ); 
    }
    
// ----------------------------------------------------------------------------
// EmitCancelEventL
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::EmitCancelEvent()
    {
    iDoubleTapTimer->Cancel();

    
    CGesture& gestureToCancel = iPreviousTapGesture ? *iPreviousTapGesture : *iGesture;
    gestureToCancel.SetCancelled();
    EmitEvent( gestureToCancel );
    RecycleGesture(iPreviousTapGesture);
    
    }

// ----------------------------------------------------------------------------
// Notify observer
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::EmitEvent( const CGesture& aGesture )
    {
    // deallocation of the event is happening in CGestureEventSender::RunL() 
    TGestureEvent event;
    event.SetCode(const_cast<CGesture&>(aGesture).Code(EAxisBoth));
    event.SetCurrentPos(aGesture.CurrentPos());
    event.SetDistance(aGesture.Distance());
    event.SetStartPos(aGesture.StartPos());
    event.SetIsHolding(aGesture.IsHolding());
    event.SetSpeed(aGesture.Speed());
    iEventSender->AddEvent(event);
    }

// ----------------------------------------------------------------------------
// Return a fresh gesture from the gesture pool (pool of one gesture)
// ----------------------------------------------------------------------------
//
CGesture* CGestureHelperImpl::NewGesture()
    {
    __ASSERT_DEBUG( iUnusedGesture, Panic( EGesturePanicIllegalLogic ) ); // pool should no be empty
    
    iUnusedGesture->Reset();
    CGesture* freshGesture = iUnusedGesture;
    iUnusedGesture = NULL;
    return freshGesture;
    }

// ----------------------------------------------------------------------------
// Return a fresh gesture from the gesture pool (pool of one gesture)
// ----------------------------------------------------------------------------
//
void CGestureHelperImpl::RecycleGesture( CGesture*& aGesturePointer )
    {
    // only one object fits into the pool, and that should currently be enough
    // one pointer must be null, one non-null
    __ASSERT_DEBUG( !iUnusedGesture != !aGesturePointer, Panic( EGesturePanicIllegalLogic ) );
    if ( aGesturePointer )
        {
        iUnusedGesture = aGesturePointer;
        aGesturePointer = NULL;
        }
    }
