/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2013 CERN
 * @author Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
 * Last changes: 2018
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, you may find one here:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * or you may search the http://www.gnu.org website for the version 2 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

#include <pcb_edit_frame.h>
#include <trace_helpers.h>

#include <tool/tool_manager.h>
#include <tool/tool_dispatcher.h>
#include <tool/actions.h>
#include <view/view.h>
#include <view/wx_view_controls.h>

#include <class_draw_panel_gal.h>
#include <pcbnew_id.h>

#include <core/optional.h>


///> Stores information about a mouse button state
struct TOOL_DISPATCHER::BUTTON_STATE
{
    BUTTON_STATE( TOOL_MOUSE_BUTTONS aButton, const wxEventType& aDownEvent,
                 const wxEventType& aUpEvent, const wxEventType& aDblClickEvent ) :
        dragging( false ),
        pressed( false ),
        dragMaxDelta( 0.0f ),
        button( aButton ),
        downEvent( aDownEvent ),
        upEvent( aUpEvent ),
        dblClickEvent( aDblClickEvent )
    {};

    ///> Flag indicating that dragging is active for the given button.
    bool dragging;

    ///> Flag indicating that the given button is pressed.
    bool pressed;

    ///> Point where dragging has started (in world coordinates).
    VECTOR2D dragOrigin;

    ///> Point where click event has occurred.
    VECTOR2D downPosition;

    ///> Difference between drag origin point and current mouse position (expressed as distance in
    ///> pixels).
    double dragMaxDelta;

    ///> Determines the mouse button for which information are stored.
    TOOL_MOUSE_BUTTONS button;

    ///> The type of wxEvent that determines mouse button press.
    wxEventType downEvent;

    ///> The type of wxEvent that determines mouse button release.
    wxEventType upEvent;

    ///> The type of wxEvent that determines mouse button double click.
    wxEventType dblClickEvent;

    ///> Time stamp for the last mouse button press event.
    wxLongLong downTimestamp;

    ///> Restores initial state.
    void Reset()
    {
        dragging = false;
        pressed = false;
    }

    ///> Checks the current state of the button.
    bool GetState() const
    {
        wxMouseState mouseState = wxGetMouseState();

        switch( button )
        {
        case BUT_LEFT:
            return mouseState.LeftIsDown();

        case BUT_MIDDLE:
            return mouseState.MiddleIsDown();

        case BUT_RIGHT:
            return mouseState.RightIsDown();

        default:
            assert( false );
            break;
        }

        return false;
    }
};


TOOL_DISPATCHER::TOOL_DISPATCHER( TOOL_MANAGER* aToolMgr, ACTIONS *aActions ) :
    m_toolMgr( aToolMgr ),
    m_actions( aActions )
{
    m_buttons.push_back( new BUTTON_STATE( BUT_LEFT, wxEVT_LEFT_DOWN,
                         wxEVT_LEFT_UP, wxEVT_LEFT_DCLICK ) );
    m_buttons.push_back( new BUTTON_STATE( BUT_RIGHT, wxEVT_RIGHT_DOWN,
                         wxEVT_RIGHT_UP, wxEVT_RIGHT_DCLICK ) );
    m_buttons.push_back( new BUTTON_STATE( BUT_MIDDLE, wxEVT_MIDDLE_DOWN,
                         wxEVT_MIDDLE_UP, wxEVT_MIDDLE_DCLICK ) );

    ResetState();
}


TOOL_DISPATCHER::~TOOL_DISPATCHER()
{
    for( BUTTON_STATE* st : m_buttons )
        delete st;
}


void TOOL_DISPATCHER::ResetState()
{
    for( BUTTON_STATE* st : m_buttons )
        st->Reset();
}


KIGFX::VIEW* TOOL_DISPATCHER::getView()
{
    return m_toolMgr->GetView();
}


bool TOOL_DISPATCHER::handleMouseButton( wxEvent& aEvent, int aIndex, bool aMotion )
{
    BUTTON_STATE* st = m_buttons[aIndex];
    wxEventType type = aEvent.GetEventType();
    OPT<TOOL_EVENT> evt;
    bool isClick = false;

//    bool up = type == st->upEvent;
//    bool down = type == st->downEvent;
    bool up = false, down = false;
    bool dblClick = type == st->dblClickEvent;
    bool state = st->GetState();

    if( !dblClick )
    {
        // Sometimes the dispatcher does not receive mouse button up event, so it stays
        // in the dragging mode even if the mouse button is not held anymore
        if( st->pressed && !state )
            up = true;
        // Don't apply same logic to down events as it kills touchpad tapping
        else if( !st->pressed && type == st->downEvent )
            down = true;
    }

    int mods = decodeModifiers( static_cast<wxMouseEvent*>( &aEvent ) );
    int args = st->button | mods;

    if( down )      // Handle mouse button press
    {
        st->downTimestamp = wxGetLocalTimeMillis();

        if( !st->pressed )      // save the drag origin on the first click only
            st->dragOrigin = m_lastMousePos;

        st->downPosition = m_lastMousePos;
        st->dragMaxDelta = 0;
        st->pressed = true;
        evt = TOOL_EVENT( TC_MOUSE, TA_MOUSE_DOWN, args );
    }
    else if( up )   // Handle mouse button release
    {
        st->pressed = false;

        if( st->dragging )
        {
            wxLongLong t = wxGetLocalTimeMillis();

            // Determine if it was just a single click or beginning of dragging
            if( t - st->downTimestamp < DragTimeThreshold &&
                    st->dragMaxDelta < DragDistanceThreshold )
                isClick = true;
            else
                evt = TOOL_EVENT( TC_MOUSE, TA_MOUSE_UP, args );
        }
        else
            isClick = true;

        if( isClick )
            evt = TOOL_EVENT( TC_MOUSE, TA_MOUSE_CLICK, args );

        st->dragging = false;
    }
    else if( dblClick )
    {
        evt = TOOL_EVENT( TC_MOUSE, TA_MOUSE_DBLCLICK, args );
    }

    if( st->pressed && aMotion )
    {
        st->dragging = true;
        double dragPixelDistance =
            getView()->ToScreen( m_lastMousePos - st->dragOrigin, false ).EuclideanNorm();
        st->dragMaxDelta = std::max( st->dragMaxDelta, dragPixelDistance );

        wxLongLong t = wxGetLocalTimeMillis();

        if( t - st->downTimestamp > DragTimeThreshold || st->dragMaxDelta > DragDistanceThreshold )
        {
            evt = TOOL_EVENT( TC_MOUSE, TA_MOUSE_DRAG, args );
            evt->setMouseDragOrigin( st->dragOrigin );
            evt->setMouseDelta( m_lastMousePos - st->dragOrigin );
        }
    }

    if( evt )
    {
        evt->SetMousePosition( isClick ? st->downPosition : m_lastMousePos );
        m_toolMgr->ProcessEvent( *evt );

        return true;
    }

    return false;
}


// Helper function to know if a special key ( see key list ) should be captured
// or if the event can be skipped
// on Linux, the event must be passed to the GUI if they are not used by KiCad,
// especially the wxEVENT_CHAR_HOOK, if it is not handled
// unfortunately, m_toolMgr->ProcessEvent( const TOOL_EVENT& aEvent)
// does not return info about that. So the event is filtered before passed to
// the GUI. These key codes are known to be used in Pcbnew to move the cursor
// or change active layer, and have a default action (moving scrollbar button) if
// the event is skipped
bool isKeySpecialCode( int aKeyCode )
{
    const enum wxKeyCode special_keys[] =
    {
        WXK_UP, WXK_DOWN, WXK_LEFT, WXK_RIGHT,
        WXK_PAGEUP, WXK_PAGEDOWN,
        WXK_NUMPAD_UP,  WXK_NUMPAD_DOWN, WXK_NUMPAD_LEFT,  WXK_NUMPAD_RIGHT,
        WXK_NUMPAD_PAGEUP,  WXK_NUMPAD_PAGEDOWN
    };

    bool isInList = false;

    for( unsigned ii = 0; ii < DIM( special_keys ) && !isInList; ii++ )
    {
        if( special_keys[ii] == aKeyCode )
            isInList = true;
    }

    return isInList;
}


/* aHelper class that convert some special key codes to an equivalent.
 *  WXK_NUMPAD_UP to WXK_UP,
 *  WXK_NUMPAD_DOWN to WXK_DOWN,
 *  WXK_NUMPAD_LEFT to WXK_LEFT,
 *  WXK_NUMPAD_RIGHT,
 *  WXK_NUMPAD_PAGEUP,
 *  WXK_NUMPAD_PAGEDOWN
 * note:
 * wxEVT_CHAR_HOOK does this conversion when it is skipped by firing a wxEVT_CHAR
 * with this converted code, but we do not skip these key events because they also
 * have default action (scroll the panel)
 */
int translateSpecialCode( int aKeyCode )
{
    switch( aKeyCode )
    {
    case WXK_NUMPAD_UP: return WXK_UP;
    case WXK_NUMPAD_DOWN: return WXK_DOWN;
    case WXK_NUMPAD_LEFT: return WXK_LEFT;
    case WXK_NUMPAD_RIGHT: return WXK_RIGHT;
    case WXK_NUMPAD_PAGEUP: return WXK_PAGEUP;
    case WXK_NUMPAD_PAGEDOWN: return WXK_PAGEDOWN;
    default: break;
    };

    return aKeyCode;
}


void TOOL_DISPATCHER::DispatchWxEvent( wxEvent& aEvent )
{
    bool motion = false, buttonEvents = false;
    OPT<TOOL_EVENT> evt;
    int key = 0;    // key = 0 if the event is not a key event
    bool keyIsSpecial = false;  // True if the key is a special key code

    int type = aEvent.GetEventType();

    // Sometimes there is no window that has the focus (it happens when another PCB_BASE_FRAME
    // is opened and is iconized on Windows).
    // In this case, gives the focus to the parent PCB_BASE_FRAME (for an obscure reason,
    // when happens, the GAL canvas itself does not accept the focus)
    if( wxWindow::FindFocus() == nullptr )
        static_cast<PCB_BASE_FRAME*>( m_toolMgr->GetEditFrame() )->SetFocus();

    // Mouse handling
    // Note: wxEVT_LEFT_DOWN event must always be skipped.
    if( type == wxEVT_MOTION || type == wxEVT_MOUSEWHEEL ||
#if wxCHECK_VERSION( 3, 1, 0 ) || defined( USE_OSX_MAGNIFY_EVENT )
        type == wxEVT_MAGNIFY ||
#endif
        type == wxEVT_LEFT_DOWN || type == wxEVT_LEFT_UP ||
        type == wxEVT_MIDDLE_DOWN || type == wxEVT_MIDDLE_UP ||
        type == wxEVT_RIGHT_DOWN || type == wxEVT_RIGHT_UP ||
        type == wxEVT_LEFT_DCLICK || type == wxEVT_MIDDLE_DCLICK || type == wxEVT_RIGHT_DCLICK ||
        // Event issued when mouse retains position in screen coordinates,
        // but changes in world coordinates (e.g. autopanning)
        type == KIGFX::WX_VIEW_CONTROLS::EVT_REFRESH_MOUSE )
    {
        wxMouseEvent* me = static_cast<wxMouseEvent*>( &aEvent );
        int mods = decodeModifiers( me );

        VECTOR2D pos = m_toolMgr->GetViewControls()->GetMousePosition();

        if( pos != m_lastMousePos )
        {
            motion = true;
            m_lastMousePos = pos;
        }

        for( unsigned int i = 0; i < m_buttons.size(); i++ )
            buttonEvents |= handleMouseButton( aEvent, i, motion );

        if( !buttonEvents && motion )
        {
            evt = TOOL_EVENT( TC_MOUSE, TA_MOUSE_MOTION, mods );
            evt->SetMousePosition( pos );
        }

#ifdef __APPLE__
        // TODO That's a big ugly workaround, somehow DRAWPANEL_GAL loses focus
        // after second LMB click and currently I have no means to do better debugging
        if( type == wxEVT_LEFT_UP )
            static_cast<PCB_BASE_FRAME*>( m_toolMgr->GetEditFrame() )->GetGalCanvas()->SetFocus();
#endif /* __APPLE__ */
    }
    else if( type == wxEVT_CHAR_HOOK || type == wxEVT_CHAR )
    {
        wxKeyEvent* ke = static_cast<wxKeyEvent*>( &aEvent );
        key = ke->GetKeyCode();
        keyIsSpecial = isKeySpecialCode( key );

        wxLogTrace( kicadTraceKeyEvent, "TOOL_DISPATCHER::DispatchWxEvent %s", dump( *ke ) );

        // if the key event must be skipped, skip it here if the event is a wxEVT_CHAR_HOOK
        // and do nothing.
        // a wxEVT_CHAR will be fired by wxWidgets later for this key.
        if( type == wxEVT_CHAR_HOOK )
        {
            if( !keyIsSpecial )
            {
                aEvent.Skip();
                return;
            }
            else
                key = translateSpecialCode( key );
        }

        int mods = decodeModifiers( ke );

        if( mods & MD_CTRL )
        {
            // wxWidgets maps key codes related to Ctrl+letter handled by CHAR_EVT
            // (http://docs.wxwidgets.org/trunk/classwx_key_event.html):
            // char events for ASCII letters in this case carry codes corresponding to the ASCII
            // value of Ctrl-Latter, i.e. 1 for Ctrl-A, 2 for Ctrl-B and so on until 26 for Ctrl-Z.
            // They are remapped here to be more easy to handle in code
            if( key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z )
                key += 'A' - 1;
        }

#ifdef __APPLE__
        if( mods & MD_ALT )
        {
            // OSX maps a bunch of commonly used extended-ASCII characters onto the keyboard
            // using the ALT key.  Since we use ALT for some of our hotkeys, we need to map back
            // to the underlying keys.  The kVK_ANSI_* values come from Apple and are said to be
            // hardware independant.
            switch( ke->GetRawKeyCode() )
            {
            case /* kVK_ANSI_1     */ 0x12: key = '1'; break;
            case /* kVK_ANSI_2     */ 0x13: key = '2'; break;
            case /* kVK_ANSI_3     */ 0x14: key = '3'; break;
            case /* kVK_ANSI_4     */ 0x15: key = '4'; break;
            case /* kVK_ANSI_6     */ 0x16: key = '6'; break;
            case /* kVK_ANSI_5     */ 0x17: key = '5'; break;
            case /* kVK_ANSI_Equal */ 0x18: key = '='; break;
            case /* kVK_ANSI_9     */ 0x19: key = '9'; break;
            case /* kVK_ANSI_7     */ 0x1A: key = '7'; break;
            case /* kVK_ANSI_Minus */ 0x1B: key = '-'; break;
            case /* kVK_ANSI_8     */ 0x1C: key = '8'; break;
            case /* kVK_ANSI_0     */ 0x1D: key = '0'; break;
            default: ;
            }
        }
#endif

        if( key == WXK_ESCAPE ) // ESC is the special key for canceling tools
            evt = TOOL_EVENT( TC_COMMAND, TA_CANCEL_TOOL );
        else
            evt = TOOL_EVENT( TC_KEYBOARD, TA_KEY_PRESSED, key | mods );
    }

    if( evt )
        m_toolMgr->ProcessEvent( *evt );

    // pass the event to the GUI, it might still be interested in it
    // Note wxEVT_CHAR_HOOK event is already skipped for special keys not used by KiCad
    // and wxEVT_LEFT_DOWN must be always Skipped.
    //
    // On OS X, key events are always meant to be caught.  An uncaught key event is assumed
    // to be a user input error by OS X (as they are pressing keys in a context where nothing
    // is there to catch the event).  This annoyingly makes OS X beep and/or flash the screen
    // in Pcbnew and the footprint editor any time a hotkey is used.  The correct procedure is
    // to NOT pass wxEVT_CHAR events to the GUI under OS X.
    //
    // On Windows, avoid to call wxEvent::Skip for special keys because some keys (ARROWS,
    // PAGE_UP, PAGE_DOWN have predefined actions (like move thumbtrack cursor), and we do
    // not want these actions executed (most are handled by KiCad)

    if( !evt || type == wxEVT_LEFT_DOWN )
        aEvent.Skip();

    // The suitable Skip is already called, but the wxEVT_CHAR
    // must be Skipped (sent to GUI).
    // Otherwise accelerators and shortcuts in main menu or toolbars are not seen.
#ifndef __APPLE__
    if( type == wxEVT_CHAR && !keyIsSpecial )
        aEvent.Skip();
#endif

    updateUI( aEvent );
}


void TOOL_DISPATCHER::DispatchWxCommand( wxCommandEvent& aEvent )
{
    OPT<TOOL_EVENT> evt = m_actions->TranslateLegacyId( aEvent.GetId() );

    if( evt )
        m_toolMgr->ProcessEvent( *evt );
    else
        aEvent.Skip();

    updateUI( aEvent );
}


void TOOL_DISPATCHER::updateUI( wxEvent& aEvent )
{
    // TODO I don't feel it is the right place for updating UI,
    // but at the moment I cannot think of a better one..

    auto frame = dynamic_cast<EDA_DRAW_FRAME*>( m_toolMgr->GetEditFrame() );
    if( frame )
    {
        frame->UpdateStatusBar();
        frame->SyncMenusAndToolbars( aEvent );
    }
}
