/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
 * Copyright (C) 1992-2019 KiCad Developers, see AUTHORS.txt for contributors.
 *
 * 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
 */

/**
 * @file  eda_3d_canvas.cpp
 * @brief Implementation of a 3d canvas
 */

#include <GL/glew.h>    // Must be included first
#include <wx/tokenzr.h>

#include "../common_ogl/openGL_includes.h"
#include "../common_ogl/ogl_utils.h"
#include "eda_3d_canvas.h"
#include "../3d_viewer_id.h"
#include "../3d_rendering/3d_render_raytracing/c3d_render_raytracing.h"
#include "../3d_viewer/eda_3d_viewer.h"
#include "../3d_rendering/test_cases.h"
#include <class_board.h>
#include "status_text_reporter.h"
#include <gl_context_mgr.h>
#include <profile.h>        // To use GetRunningMicroSecs or another profiling utility
#include <bitmaps.h>
#include <hotkeys_basic.h>
#include <menus_helpers.h>


/**
 * Flag to enable 3D canvas debug tracing.
 *
 * Use "KI_TRACE_EDA_3D_CANVAS" to enable.
 *
 * @ingroup trace_env_vars
 */
const wxChar * EDA_3D_CANVAS::m_logTrace = wxT( "KI_TRACE_EDA_3D_CANVAS" );

const float EDA_3D_CANVAS::m_delta_move_step_factor = 0.7f;


BEGIN_EVENT_TABLE( EDA_3D_CANVAS, wxGLCanvas )
    EVT_PAINT( EDA_3D_CANVAS::OnPaint )
    EVT_CHAR( EDA_3D_CANVAS::OnKeyEvent )
    EVT_CHAR_HOOK( EDA_3D_CANVAS::OnCharHook )

    // mouse events
    EVT_LEFT_DOWN( EDA_3D_CANVAS::OnLeftDown )
    EVT_LEFT_UP( EDA_3D_CANVAS::OnLeftUp )
    EVT_MIDDLE_UP( EDA_3D_CANVAS::OnMiddleUp )
    EVT_MIDDLE_DOWN( EDA_3D_CANVAS::OnMiddleDown)
    EVT_RIGHT_DOWN( EDA_3D_CANVAS::OnRightClick )
    EVT_MOUSEWHEEL( EDA_3D_CANVAS::OnMouseWheel )
    EVT_MOTION( EDA_3D_CANVAS::OnMouseMove )

#if wxCHECK_VERSION( 3, 1, 0 ) || defined( USE_OSX_MAGNIFY_EVENT )
    EVT_MAGNIFY( EDA_3D_CANVAS::OnMagnify )
#endif

    // other events
    EVT_ERASE_BACKGROUND( EDA_3D_CANVAS::OnEraseBackground )
    EVT_MENU_RANGE( ID_POPUP_3D_VIEW_START,
                    ID_POPUP_3D_VIEW_END, EDA_3D_CANVAS::OnPopUpMenu )

    EVT_CLOSE( EDA_3D_CANVAS::OnCloseWindow )
    EVT_SIZE(  EDA_3D_CANVAS::OnResize )
END_EVENT_TABLE()


EDA_3D_CANVAS::EDA_3D_CANVAS( wxWindow *aParent,
                              const int *aAttribList,
                              BOARD *aBoard,
                              CINFO3D_VISU &aSettings , S3D_CACHE *a3DCachePointer ) :
                  HIDPI_GL_CANVAS( aParent,
                              wxID_ANY,
                              aAttribList,
                              wxDefaultPosition,
                              wxDefaultSize,
                              wxFULL_REPAINT_ON_RESIZE
                            ),
                m_settings( aSettings )
{
    // Run test cases in debug mode, once.

    wxLogTrace( m_logTrace, "EDA_3D_CANVAS::EDA_3D_CANVAS" );

    m_editing_timeout_timer.SetOwner( this );
    Connect( m_editing_timeout_timer.GetId(),
             wxEVT_TIMER,
             wxTimerEventHandler( EDA_3D_CANVAS::OnTimerTimeout_Editing ),
             NULL,
             this );

    m_redraw_trigger_timer.SetOwner( this );
    Connect( m_redraw_trigger_timer.GetId(),
             wxEVT_TIMER,
             wxTimerEventHandler( EDA_3D_CANVAS::OnTimerTimeout_Redraw ),
             NULL,
             this );

    m_mouse_was_moved = false;
    m_mouse_is_moving = false;
    m_camera_is_moving = false;
    m_render_pivot = false;
    m_camera_moving_speed = 1.0f;

    m_strtime_camera_movement = 0;

    m_is_opengl_initialized = false;

    m_render_raytracing_was_requested = false;
    m_opengl_supports_raytracing = false;

    m_parentStatusBar = NULL;
    m_glRC = NULL;

    m_3d_render = NULL;

    m_3d_render_raytracing = new C3D_RENDER_RAYTRACING( aSettings );
    m_3d_render_ogl_legacy = new C3D_RENDER_OGL_LEGACY( aSettings );

    wxASSERT( m_3d_render_raytracing != NULL );
    wxASSERT( m_3d_render_ogl_legacy != NULL );

    RenderEngineChanged();

    wxASSERT( aBoard != NULL );
    m_settings.SetBoard( aBoard );

    wxASSERT( a3DCachePointer != NULL );
    m_settings.Set3DCacheManager( a3DCachePointer );
}


EDA_3D_CANVAS::~EDA_3D_CANVAS()
{
    wxLogTrace( m_logTrace, "EDA_3D_CANVAS::~EDA_3D_CANVAS" );

    releaseOpenGL();
}


void EDA_3D_CANVAS::releaseOpenGL()
{
    if( m_glRC )
    {
        GL_CONTEXT_MANAGER::Get().LockCtx( m_glRC, this );

        delete m_3d_render_raytracing;
        m_3d_render_raytracing = NULL;

        delete m_3d_render_ogl_legacy;
        m_3d_render_ogl_legacy = NULL;

        // This is just a copy of a pointer, can safely be set to NULL
        m_3d_render = NULL;

        GL_CONTEXT_MANAGER::Get().UnlockCtx( m_glRC );
        GL_CONTEXT_MANAGER::Get().DestroyCtx( m_glRC );
        m_glRC = NULL;
    }
}


void EDA_3D_CANVAS::OnCloseWindow( wxCloseEvent &event )
{
    releaseOpenGL();

    event.Skip();
}


void EDA_3D_CANVAS::OnResize( wxSizeEvent &event )
{
    this->Request_refresh();
}


bool  EDA_3D_CANVAS::initializeOpenGL()
{
    wxLogTrace( m_logTrace, "EDA_3D_CANVAS::initializeOpenGL" );

    const GLenum err = glewInit();

    if( GLEW_OK != err )
    {
        const wxString msgError = (const char*) glewGetErrorString( err );

        wxLogMessage( msgError );

        return false;
    }
    else
    {
        wxLogTrace( m_logTrace, "EDA_3D_CANVAS::initializeOpenGL Using GLEW version %s",
                    FROM_UTF8( (char*) glewGetString( GLEW_VERSION ) ) );
    }

    wxString version = FROM_UTF8( (char *) glGetString( GL_VERSION ) );

    wxLogTrace( m_logTrace, "EDA_3D_CANVAS::%s OpenGL version string %s.",
                __WXFUNCTION__, version );

    // Extract OpenGL version from string.  This method is used because prior to OpenGL 2,
    // getting the OpenGL major and minor version as integers didn't exist.
    wxString tmp;

    wxStringTokenizer tokenizer( version );

    m_opengl_supports_raytracing = true;

    if( tokenizer.HasMoreTokens() )
    {
        long major = 0;
        long minor = 0;

        tmp = tokenizer.GetNextToken();

        tokenizer.SetString( tmp, wxString( "." ) );

        if( tokenizer.HasMoreTokens() )
            tokenizer.GetNextToken().ToLong( &major );

        if( tokenizer.HasMoreTokens() )
            tokenizer.GetNextToken().ToLong( &minor );

        if( major < 2 || ( (major == 2 ) && (minor < 1) ) )
        {
            wxLogTrace( m_logTrace, "EDA_3D_CANVAS::%s OpenGL ray tracing not supported.",
                        __WXFUNCTION__ );

            if( GetParent() )
            {
                wxCommandEvent evt( wxEVT_MENU, ID_DISABLE_RAY_TRACING );
                GetParent()->ProcessWindowEvent( evt );
            }

            m_opengl_supports_raytracing = false;
        }
    }

    m_is_opengl_initialized = true;

    return true;
}


void EDA_3D_CANVAS::GetScreenshot( wxImage &aDstImage )
{
    OGL_GetScreenshot( aDstImage );
}


void EDA_3D_CANVAS::ReloadRequest( BOARD *aBoard , S3D_CACHE *aCachePointer )
{
    if( aCachePointer != NULL )
        m_settings.Set3DCacheManager( aCachePointer );

    if( aBoard != NULL )
        m_settings.SetBoard( aBoard );

    if( m_3d_render )
        m_3d_render->ReloadRequest();
}


void EDA_3D_CANVAS::RenderRaytracingRequest()
{
    m_3d_render = m_3d_render_raytracing;

    if( m_3d_render )
        m_3d_render->ReloadRequest();

    m_render_raytracing_was_requested = true;
    //m_mouse_was_moved = true;

    Request_refresh();
}


void EDA_3D_CANVAS::DisplayStatus()
{
    if( m_parentStatusBar )
    {
        wxString msg;

        msg.Printf( "dx %3.2f", m_settings.CameraGet().GetCameraPos().x );
        m_parentStatusBar->SetStatusText( msg, 1 );

        msg.Printf( "dy %3.2f", m_settings.CameraGet().GetCameraPos().y );
        m_parentStatusBar->SetStatusText( msg, 2 );
    }
}


void EDA_3D_CANVAS::OnPaint( wxPaintEvent &event )
{
    // Please have a look at:
    // https://lists.launchpad.net/kicad-developers/msg25149.html
    // wxPaintDC( this );
    // event.Skip( false );

    // SwapBuffer requires the window to be shown before calling
    if( !IsShownOnScreen() )
    {
        wxLogTrace( m_logTrace, "EDA_3D_CANVAS::OnPaint !IsShown" );
        return;
    }

    // Because the board to draw is handled by the parent viewer frame,
    // ensure this parent is still alive. When it is closed before the viewer
    // frame, a paint event can be generated after the parent is closed,
    // therefore with invalid board.
    // This is dependent of the platform.
    // Especially on OSX, but also on Windows, it frequently happens
    if( !GetParent()->GetParent()->IsShown() )
        return; // The parent board editor frame is no more alive

    wxString err_messages;

    // !TODO: implement error reporter
    //WX_STRING_REPORTER errorReporter( &err_messages );
    STATUS_TEXT_REPORTER activityReporter( m_parentStatusBar, 0 );

    unsigned strtime = GetRunningMicroSecs();

    // "Makes the OpenGL state that is represented by the OpenGL rendering
    //  context context current, i.e. it will be used by all subsequent OpenGL calls.
    //  This function may only be called when the window is shown on screen"

    // Explicitly create a new rendering context instance for this canvas.
    if( m_glRC == NULL )
        m_glRC = GL_CONTEXT_MANAGER::Get().CreateCtx( this );

    GL_CONTEXT_MANAGER::Get().LockCtx( m_glRC, this );

    // Set the OpenGL viewport according to the client size of this canvas.
    // This is done here rather than in a wxSizeEvent handler because our
    // OpenGL rendering context (and thus viewport setting) is used with
    // multiple canvases: If we updated the viewport in the wxSizeEvent
    // handler, changing the size of one canvas causes a viewport setting that
    // is wrong when next another canvas is repainted.
    wxSize clientSize = GetNativePixelSize();

    const bool windows_size_changed = m_settings.CameraGet().SetCurWindowSize( clientSize );

    // Initialize openGL if need
    // /////////////////////////////////////////////////////////////////////////
    if( !m_is_opengl_initialized )
    {
        if( !initializeOpenGL() )
        {
            GL_CONTEXT_MANAGER::Get().UnlockCtx( m_glRC );

            return;
        }
    }

    // Don't attend to ray trace if OpenGL doesn't support it.
    if( !m_opengl_supports_raytracing )
    {
        m_3d_render = m_3d_render_ogl_legacy;
        m_render_raytracing_was_requested = false;
        m_settings.RenderEngineSet( RENDER_ENGINE_OPENGL_LEGACY );
    }

    // Check if a raytacing was requested and need to switch to raytracing mode
    if( m_settings.RenderEngineGet() == RENDER_ENGINE_OPENGL_LEGACY )
    {
        const bool was_camera_changed = m_settings.CameraGet().ParametersChanged();

        // It reverts back to OpenGL mode if it was requested a raytracing
        // render of the current scene. AND the mouse / camera is moving
        if( ( m_mouse_is_moving ||
              m_camera_is_moving ||
              was_camera_changed ||
              windows_size_changed ) &&
            m_render_raytracing_was_requested )
        {
            m_render_raytracing_was_requested = false;
            m_3d_render = m_3d_render_ogl_legacy;
        }
    }

    float curtime_delta_s = 0.0f;

    if( m_camera_is_moving )
    {
        const unsigned curtime_delta = GetRunningMicroSecs() - m_strtime_camera_movement;
        curtime_delta_s = (curtime_delta / 1e6) * m_camera_moving_speed;
        m_settings.CameraGet().Interpolate( curtime_delta_s );

        if( curtime_delta_s > 1.0f )
        {
            m_render_pivot = false;
            m_camera_is_moving = false;
            m_mouse_was_moved = true;

            restart_editingTimeOut_Timer();
            DisplayStatus();
        }
        else
        {
            Request_refresh();
        }
    }

    // It will return true if the render request a new redraw
    bool requested_redraw = false;

    if( m_3d_render )
    {
        m_3d_render->SetCurWindowSize( clientSize );

        requested_redraw = m_3d_render->Redraw( m_mouse_was_moved || m_camera_is_moving,
                                                &activityReporter );
    }

    if( m_render_pivot )
    {
        const float scale = glm::min( m_settings.CameraGet().ZoomGet(), 1.0f );
        render_pivot( curtime_delta_s, scale * scale );
    }

    // "Swaps the double-buffer of this window, making the back-buffer the
    //  front-buffer and vice versa, so that the output of the previous OpenGL
    //  commands is displayed on the window."
    SwapBuffers();

    GL_CONTEXT_MANAGER::Get().UnlockCtx( m_glRC );

    if( !activityReporter.HasMessage() )
    {
        if( m_mouse_was_moved || m_camera_is_moving )
        {
            // Calculation time in milliseconds
            const double calculation_time = (double)( GetRunningMicroSecs() - strtime) / 1e3;

            activityReporter.Report( wxString::Format( _( "Render time %.0f ms ( %.1f fps)" ),
                                     calculation_time, 1000.0 / calculation_time ) );
        }
    }

    // This will reset the flag of camera parameters changed
    m_settings.CameraGet().ParametersChanged();

    if( !err_messages.IsEmpty() )
        wxLogMessage( err_messages );

    if( (!m_camera_is_moving) && requested_redraw )
    {
        m_mouse_was_moved = false;
        Request_refresh( false );
    }
}


void EDA_3D_CANVAS::OnEraseBackground( wxEraseEvent &event )
{
    wxLogTrace( m_logTrace, "EDA_3D_CANVAS::OnEraseBackground" );
    // Do nothing, to avoid flashing.
}


void EDA_3D_CANVAS::OnMouseWheel( wxMouseEvent &event )
{
    bool mouseActivity = false;

    wxLogTrace( m_logTrace, "EDA_3D_CANVAS::OnMouseWheel" );

    if( m_camera_is_moving )
        return;

    float delta_move = m_delta_move_step_factor * m_settings.CameraGet().ZoomGet();

    if( m_settings.GetFlag( FL_MOUSEWHEEL_PANNING ) )
        delta_move *= (0.01f * event.GetWheelRotation());
    else
        if( event.GetWheelRotation() < 0 )
            delta_move = -delta_move;

    // mousewheel_panning enabled:
    //      wheel           -> pan;
    //      wheel + shift   -> horizontal scrolling;
    //      wheel + ctrl    -> zooming;
    // mousewheel_panning disabled:
    //      wheel + shift   -> vertical scrolling;
    //      wheel + ctrl    -> horizontal scrolling;
    //      wheel           -> zooming.

    if( m_settings.GetFlag( FL_MOUSEWHEEL_PANNING ) && !event.ControlDown() )
    {
        if( event.GetWheelAxis() == wxMOUSE_WHEEL_HORIZONTAL || event.ShiftDown() )
            m_settings.CameraGet().Pan( SFVEC3F( -delta_move, 0.0f, 0.0f ) );
        else
            m_settings.CameraGet().Pan( SFVEC3F( 0.0f, -delta_move, 0.0f ) );

        mouseActivity = true;
    }
    else if( event.ShiftDown() && !m_settings.GetFlag( FL_MOUSEWHEEL_PANNING ) )
    {
        m_settings.CameraGet().Pan( SFVEC3F( 0.0f, -delta_move, 0.0f ) );
        mouseActivity = true;
    }
    else if( event.ControlDown() && !m_settings.GetFlag( FL_MOUSEWHEEL_PANNING ) )
    {
        m_settings.CameraGet().Pan( SFVEC3F( delta_move, 0.0f, 0.0f ) );
        mouseActivity = true;
    }
    else
    {
        mouseActivity = m_settings.CameraGet().Zoom( event.GetWheelRotation() > 0 ? 1.1f : 1/1.1f );
    }

    // If it results on a camera movement
    if( mouseActivity )
    {
        DisplayStatus();
        Request_refresh();

        m_mouse_is_moving = true;
        m_mouse_was_moved = true;

        restart_editingTimeOut_Timer();
    }

    // Update the cursor current mouse position on the camera
    m_settings.CameraGet().SetCurMousePosition( event.GetPosition() );
}


#if wxCHECK_VERSION( 3, 1, 0 ) || defined( USE_OSX_MAGNIFY_EVENT )
void EDA_3D_CANVAS::OnMagnify( wxMouseEvent& event )
{
    SetFocus();

    if( m_camera_is_moving )
        return;

    //m_is_moving_mouse = true;
    restart_editingTimeOut_Timer();

    float magnification = ( event.GetMagnification() + 1.0f );

    m_settings.CameraGet().Zoom( magnification );

    DisplayStatus();
    Request_refresh();
}
#endif


void EDA_3D_CANVAS::OnMouseMove( wxMouseEvent &event )
{
    //wxLogTrace( m_logTrace, wxT( "EDA_3D_CANVAS::OnMouseMove" ) );

    if( m_camera_is_moving )
        return;

    m_settings.CameraGet().SetCurWindowSize( GetNativePixelSize() );

    if( event.Dragging() )
    {
        if( event.LeftIsDown() )            // Drag
            m_settings.CameraGet().Drag( event.GetPosition() );
        else if( event.MiddleIsDown() )     // Pan
            m_settings.CameraGet().Pan( event.GetPosition() );

        m_mouse_is_moving = true;
        m_mouse_was_moved = true;

        // orientation has changed, redraw mesh
        DisplayStatus();
        Request_refresh();
    }

    const wxPoint eventPosition = event.GetPosition();
    m_settings.CameraGet().SetCurMousePosition( eventPosition );
}


void EDA_3D_CANVAS::OnLeftDown( wxMouseEvent &event )
{
    SetFocus();
    stop_editingTimeOut_Timer();
}


void EDA_3D_CANVAS::OnLeftUp( wxMouseEvent &event )
{
    if( m_camera_is_moving )
        return;

    if( m_mouse_is_moving )
    {
        m_mouse_is_moving = false;
        restart_editingTimeOut_Timer();
    }
}


void EDA_3D_CANVAS::OnMiddleDown( wxMouseEvent &event )
{
    SetFocus();
    stop_editingTimeOut_Timer();
}


void EDA_3D_CANVAS::OnMiddleUp( wxMouseEvent &event )
{
    if( m_camera_is_moving )
        return;

    if( m_mouse_is_moving )
    {
        m_mouse_is_moving = false;
        restart_editingTimeOut_Timer();
    }
    else
    {
        move_pivot_based_on_cur_mouse_position();
    }
}


void EDA_3D_CANVAS::OnRightClick( wxMouseEvent &event )
{
    wxLogTrace( m_logTrace, "EDA_3D_CANVAS::OnRightClick" );

    SetFocus();

    if( m_camera_is_moving )
        return;

    wxPoint     pos;
    wxMenu      PopUpMenu;
    wxString msg;

    pos.x = event.GetX();
    pos.y = event.GetY();

    msg = AddHotkeyName( _( "Zoom +" ), GetHotkeyConfig(),
                         ID_POPUP_ZOOMIN );
    AddMenuItem( &PopUpMenu, ID_POPUP_ZOOMIN,
                 msg, KiBitmap( zoom_in_xpm ) );


    msg = AddHotkeyName( _( "Zoom -" ), GetHotkeyConfig(),
                         ID_POPUP_ZOOMOUT );
    AddMenuItem( &PopUpMenu, ID_POPUP_ZOOMOUT,
                 msg, KiBitmap( zoom_out_xpm ) );

    PopUpMenu.AppendSeparator();

    msg = AddHotkeyName( _( "Top View" ), GetHotkeyConfig(),
                         ID_POPUP_VIEW_ZPOS );
    AddMenuItem( &PopUpMenu, ID_POPUP_VIEW_ZPOS,
                 msg, KiBitmap( axis3d_top_xpm ) );

    msg = AddHotkeyName( _( "Bottom View" ), GetHotkeyConfig(),
                         ID_POPUP_VIEW_ZNEG );
    AddMenuItem( &PopUpMenu, ID_POPUP_VIEW_ZNEG,
                 msg, KiBitmap( axis3d_bottom_xpm ) );

    PopUpMenu.AppendSeparator();

    msg = AddHotkeyName( _( "Right View" ), GetHotkeyConfig(),
                         ID_POPUP_VIEW_XPOS );
    AddMenuItem( &PopUpMenu, ID_POPUP_VIEW_XPOS,
                 msg, KiBitmap( axis3d_right_xpm ) );

    msg = AddHotkeyName( _( "Left View" ), GetHotkeyConfig(),
                         ID_POPUP_VIEW_XNEG );
    AddMenuItem( &PopUpMenu, ID_POPUP_VIEW_XNEG,
                 msg, KiBitmap( axis3d_left_xpm ) );

    PopUpMenu.AppendSeparator();

    msg = AddHotkeyName( _( "Front View" ), GetHotkeyConfig(),
                         ID_POPUP_VIEW_YPOS );
    AddMenuItem( &PopUpMenu, ID_POPUP_VIEW_YPOS,
                 msg, KiBitmap( axis3d_front_xpm ) );

    msg = AddHotkeyName( _( "Back View" ), GetHotkeyConfig(),
                         ID_POPUP_VIEW_YNEG );
    AddMenuItem( &PopUpMenu, ID_POPUP_VIEW_YNEG,
                 msg, KiBitmap( axis3d_back_xpm ) );

    PopUpMenu.AppendSeparator();

    msg = AddHotkeyName( _( "Move Left <-" ), GetHotkeyConfig(),
                         ID_POPUP_MOVE3D_LEFT );
    AddMenuItem( &PopUpMenu, ID_POPUP_MOVE3D_LEFT,
                 msg, KiBitmap( left_xpm ) );

    msg = AddHotkeyName( _( "Move Right ->" ), GetHotkeyConfig(),
                         ID_POPUP_MOVE3D_RIGHT );
    AddMenuItem( &PopUpMenu, ID_POPUP_MOVE3D_RIGHT,
                 msg, KiBitmap( right_xpm ) );

    msg = AddHotkeyName( _( "Move Up ^" ), GetHotkeyConfig(),
                         ID_POPUP_MOVE3D_UP );
    AddMenuItem( &PopUpMenu, ID_POPUP_MOVE3D_UP,
                 msg, KiBitmap( up_xpm ) );

    msg = AddHotkeyName( _( "Move Down" ), GetHotkeyConfig(),
                         ID_POPUP_MOVE3D_DOWN );
    AddMenuItem( &PopUpMenu, ID_POPUP_MOVE3D_DOWN,
                 msg, KiBitmap( down_xpm ) );

    PopupMenu( &PopUpMenu, pos );
}


void EDA_3D_CANVAS::OnPopUpMenu( wxCommandEvent &event )
{
    int id = event.GetId();

    wxLogTrace( m_logTrace, "EDA_3D_CANVAS::OnPopUpMenu id:%d", id );

    int key = 0;

    switch( id )
    {
    case ID_POPUP_ZOOMIN:
        key = WXK_F1;
        break;

    case ID_POPUP_ZOOMOUT:
        key = WXK_F2;
        break;

    case ID_POPUP_VIEW_XPOS:
        key = 'X';
        break;

    case ID_POPUP_VIEW_XNEG:
        key = GR_KB_SHIFT + 'X';
        break;

    case ID_POPUP_VIEW_YPOS:
        key = 'Y';
        break;

    case ID_POPUP_VIEW_YNEG:
        key = GR_KB_SHIFT + 'Y';
        break;

    case ID_POPUP_VIEW_ZPOS:
        key = 'Z';
        break;

    case ID_POPUP_VIEW_ZNEG:
        key = GR_KB_SHIFT + 'Z';
        break;

    case ID_POPUP_MOVE3D_LEFT:
        key = WXK_LEFT;
        break;

    case ID_POPUP_MOVE3D_RIGHT:
        key = WXK_RIGHT;
        break;

    case ID_POPUP_MOVE3D_UP:
        key = WXK_UP;
        break;

    case ID_POPUP_MOVE3D_DOWN:
        key = WXK_DOWN;
        break;

    default:
        return;
    }

    SetView3D( key );
}


void EDA_3D_CANVAS::OnCharHook( wxKeyEvent &event )
{
    //wxLogTrace( m_logTrace, "EDA_3D_CANVAS::OnCharHook" );
    event.Skip();
}


void EDA_3D_CANVAS::OnKeyEvent( wxKeyEvent& event )
{
    //wxLogTrace( m_logTrace, "EDA_3D_CANVAS::OnKeyEvent" );
    int localkey = event.GetKeyCode();

    // Use only upper char values in comparisons
    // (the Shift modifier is a separate attribute)
    if( (localkey >= 'a') && (localkey <= 'z') )
        localkey += 'A' - 'a';

    if( m_camera_is_moving )
        return;

    if( event.ShiftDown() )
        localkey |= GR_KB_SHIFT;

    if( event.ControlDown() )
        localkey |= GR_KB_CTRL;

    if( event.AltDown() )
        localkey |= GR_KB_ALT;

    if( !SetView3D( localkey ) )
        event.Skip();
}


void EDA_3D_CANVAS::OnTimerTimeout_Editing( wxTimerEvent &event )
{
    (void)event;

    m_mouse_is_moving = false;
    m_mouse_was_moved = false;

    Request_refresh();
}


void EDA_3D_CANVAS::stop_editingTimeOut_Timer()
{
    m_editing_timeout_timer.Stop();
}


void EDA_3D_CANVAS::restart_editingTimeOut_Timer()
{
    m_editing_timeout_timer.Start( m_3d_render->GetWaitForEditingTimeOut() , wxTIMER_ONE_SHOT );
}


void EDA_3D_CANVAS::OnTimerTimeout_Redraw( wxTimerEvent &event )
{
    (void)event;

    //Refresh();
    //Update();

    wxPaintEvent redrawEvent;
    wxPostEvent( this, redrawEvent );
}


void EDA_3D_CANVAS::Request_refresh( bool aRedrawImmediately )
{
    if( aRedrawImmediately )
    {
        // On some systems, just calling Refresh does not work always
        // (Issue experienced on Win7 MSYS2)
        //Refresh();
        //Update();

        // Using PostEvent will take priority to other events, like
        // mouse movements, keys, etc.
        wxPaintEvent redrawEvent;
        wxPostEvent( this, redrawEvent );

        // This behaves the same
        // wxQueueEvent( this,
                        // From wxWidget documentation: "The heap-allocated and
                        // non-NULL event to queue, the function takes ownership of it."
        //               new wxPaintEvent()
        //               );
    }
    else
    {
        // Schedule a timed redraw
        m_redraw_trigger_timer.Start( 10 , wxTIMER_ONE_SHOT );
    }
}


void EDA_3D_CANVAS::request_start_moving_camera( float aMovingSpeed, bool aRenderPivot )
{
    wxASSERT( aMovingSpeed > FLT_EPSILON );

    m_render_pivot = aRenderPivot;
    m_camera_moving_speed = aMovingSpeed;

    stop_editingTimeOut_Timer();

    DisplayStatus();
    Request_refresh();

    m_camera_is_moving = true;

    m_strtime_camera_movement = GetRunningMicroSecs();
}


void EDA_3D_CANVAS::move_pivot_based_on_cur_mouse_position()
{
    SFVEC3F rayOrigin;
    SFVEC3F rayDir;

    // Generate a ray origin and direction based on current mouser position and camera
    m_settings.CameraGet().MakeRayAtCurrrentMousePosition( rayOrigin, rayDir );

    RAY mouseRay;
    mouseRay.Init( rayOrigin, rayDir );

    float hit_t;

    // Test it with the board bounding box
    if( m_settings.GetBBox3DU().Intersect( mouseRay, &hit_t ) )
    {
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_BEZIER );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().SetLookAtPos_T1( mouseRay.at( hit_t ) );
        m_settings.CameraGet().ResetXYpos_T1();

        request_start_moving_camera();
    }
}


bool EDA_3D_CANVAS::SetView3D( int aKeycode )
{
    if( m_camera_is_moving )
        return false;

    const float delta_move = m_delta_move_step_factor * m_settings.CameraGet().ZoomGet();
    const float arrow_moving_time_speed = 8.0f;
    bool handled = false;

    switch( aKeycode )
    {
    case WXK_SPACE:
        move_pivot_based_on_cur_mouse_position();
        return true;

    case WXK_LEFT:
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_LINEAR );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Pan_T1( SFVEC3F( -delta_move, 0.0f, 0.0f ) );
        request_start_moving_camera( arrow_moving_time_speed, false );
        return true;

    case WXK_RIGHT:
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_LINEAR );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Pan_T1( SFVEC3F( +delta_move, 0.0f, 0.0f ) );
        request_start_moving_camera( arrow_moving_time_speed, false );
        return true;

    case WXK_UP:
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_LINEAR );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Pan_T1( SFVEC3F( 0.0f, +delta_move, 0.0f ) );
        request_start_moving_camera( arrow_moving_time_speed, false );
        return true;

    case WXK_DOWN:
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_LINEAR );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Pan_T1( SFVEC3F( 0.0f, -delta_move, 0.0f ) );
        request_start_moving_camera( arrow_moving_time_speed, false );
        return true;

    case WXK_HOME:
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_BEZIER );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Reset_T1();
        request_start_moving_camera( glm::min( glm::max( m_settings.CameraGet().ZoomGet(), 0.5f ), 1.125f ) );
        return true;

    case WXK_END:
        break;

    case WXK_TAB:
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_EASING_IN_OUT );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().RotateZ_T1( glm::radians( 45.0f ) );
        request_start_moving_camera();
        handled = true;
        break;

    case WXK_F1:
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_BEZIER );
        m_settings.CameraGet().SetT0_and_T1_current_T();

        if( m_settings.CameraGet().Zoom_T1( 1.4f ) )
            request_start_moving_camera( 3.0f );

        return true;

    case WXK_F2:
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_BEZIER );
        m_settings.CameraGet().SetT0_and_T1_current_T();

        if( m_settings.CameraGet().Zoom_T1( 1/1.4f ) )
            request_start_moving_camera( 3.0f );

        return true;

    case '+':
    case '-':
        break;

    case 't':
    case 'T':
        m_settings.SetFlag( FL_MODULE_ATTRIBUTES_NORMAL,
                            !m_settings.GetFlag( FL_MODULE_ATTRIBUTES_NORMAL ) );
        ReloadRequest();
        handled = true;
        break;

    case 's':
    case 'S':
        m_settings.SetFlag( FL_MODULE_ATTRIBUTES_NORMAL_INSERT,
                            !m_settings.GetFlag( FL_MODULE_ATTRIBUTES_NORMAL_INSERT ) );
        ReloadRequest();
        handled = true;
        break;

    case 'v':
    case 'V':
        m_settings.SetFlag( FL_MODULE_ATTRIBUTES_VIRTUAL,
                            !m_settings.GetFlag( FL_MODULE_ATTRIBUTES_VIRTUAL ) );
        ReloadRequest();
        handled = true;
        break;

    case 'r':
    case 'R':
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_BEZIER );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Reset_T1();
        request_start_moving_camera( glm::min( glm::max( m_settings.CameraGet().ZoomGet(), 0.5f ), 1.125f ) );
        return true;

    case 'X':
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_BEZIER );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Reset_T1();
        m_settings.CameraGet().RotateZ_T1( glm::radians( -90.0f ) );
        m_settings.CameraGet().RotateX_T1( glm::radians( -90.0f ) );
        request_start_moving_camera();
        return true;

    case GR_KB_SHIFT + 'X':
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_BEZIER );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Reset_T1();
        m_settings.CameraGet().RotateZ_T1( glm::radians(  90.0f ) );
        m_settings.CameraGet().RotateX_T1( glm::radians( -90.0f ) );
        request_start_moving_camera();
        return true;

    case 'Y':
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_BEZIER );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Reset_T1();
        m_settings.CameraGet().RotateX_T1( glm::radians( -90.0f ) );
        request_start_moving_camera();
        return true;

    case GR_KB_SHIFT + 'Y':
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_BEZIER );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Reset_T1();
        m_settings.CameraGet().RotateX_T1( glm::radians(  -90.0f ) );
        // The rotation angle should be 180.
        // We use 179.999 (180 - epsilon) to avoid a full 360 deg rotation when
        // using 180 deg if the previous rotated position was already 180 deg
        m_settings.CameraGet().RotateZ_T1( glm::radians( -179.999f ) );
        request_start_moving_camera();
        return true;

    case 'Z':
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_BEZIER );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Reset_T1();
        request_start_moving_camera(
                    glm::min( glm::max( m_settings.CameraGet().ZoomGet(), 0.5f ), 1.125f ) );
        return true;

    case GR_KB_SHIFT + 'Z':
        m_settings.CameraGet().SetInterpolateMode( INTERPOLATION_BEZIER );
        m_settings.CameraGet().SetT0_and_T1_current_T();
        m_settings.CameraGet().Reset_T1();
        m_settings.CameraGet().RotateY_T1( glm::radians( 179.999f ) ); // Rotation = 180-epsilon
        request_start_moving_camera(
                    glm::min( glm::max( m_settings.CameraGet().ZoomGet(), 0.5f ), 1.125f ) );
        return true;

    default:
        return false;
    }

    m_mouse_was_moved = true;

    restart_editingTimeOut_Timer();

    DisplayStatus();
    Request_refresh();

    return handled;
}


void EDA_3D_CANVAS::RenderEngineChanged()
{

    switch( m_settings.RenderEngineGet() )
    {
    case RENDER_ENGINE_OPENGL_LEGACY:
        m_3d_render = m_3d_render_ogl_legacy;
        break;

    case RENDER_ENGINE_RAYTRACING:
        m_3d_render = m_3d_render_raytracing;
        break;

    default:
        m_3d_render = NULL;
        break;
    }

    if( m_3d_render )
        m_3d_render->ReloadRequest();

    m_mouse_was_moved = false;

    Request_refresh();
}
