/////////////////////////////////////////////////////////////////////////////

// Name:        3d_canvas.cpp
/////////////////////////////////////////////////////////////////////////////


#ifdef __GNUG__
#pragma implementation
#pragma interface
#endif

#include "fctsys.h"
#include "trigo.h"

#include "wx/image.h"

#if !wxUSE_GLCANVAS
#error Please set wxUSE_GLCANVAS to 1 in setup.h.
#endif

#include "wx/dataobj.h"
#include "wx/clipbrd.h"

#include "id.h"
#include "gestfich.h"

#include "3d_viewer.h"
#include "trackball.h"

/* Tool and button Bitmaps */
#include "bitmaps.h"

enum onrclick_id {
    ID_POPUP_3D_VIEW_START = 2000,
    ID_POPUP_ZOOMIN,
    ID_POPUP_ZOOMOUT,
    ID_POPUP_VIEW_XPOS,
    ID_POPUP_VIEW_XNEG,
    ID_POPUP_VIEW_YPOS,
    ID_POPUP_VIEW_YNEG,
    ID_POPUP_VIEW_ZPOS,
    ID_POPUP_VIEW_ZNEG,
    ID_POPUP_MOVE3D_LEFT,
    ID_POPUP_MOVE3D_RIGHT,
    ID_POPUP_MOVE3D_UP,
    ID_POPUP_MOVE3D_DOWN,
    ID_POPUP_3D_VIEW_END
};


/*
 * Pcb3D_GLCanvas implementation
 */

BEGIN_EVENT_TABLE( Pcb3D_GLCanvas, wxGLCanvas )
EVT_PAINT( Pcb3D_GLCanvas::OnPaint )
EVT_CHAR( Pcb3D_GLCanvas::OnChar )
EVT_MOUSE_EVENTS( Pcb3D_GLCanvas::OnMouseEvent )
EVT_ERASE_BACKGROUND( Pcb3D_GLCanvas::OnEraseBackground )
EVT_MENU_RANGE( ID_POPUP_3D_VIEW_START, ID_POPUP_3D_VIEW_END,
                Pcb3D_GLCanvas::OnPopUpMenu )
END_EVENT_TABLE()

/*************************************************************************/
Pcb3D_GLCanvas::Pcb3D_GLCanvas( WinEDA3D_DrawFrame* parent ) :
    wxGLCanvas( parent, -1, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE )
/*************************************************************************/
{
    m_init   = FALSE;
    m_gllist = 0;
    m_Parent = parent;
    DisplayStatus();
}


/*************************************/
Pcb3D_GLCanvas::~Pcb3D_GLCanvas()
/*************************************/
{
    ClearLists();
    m_init = FALSE;
}


/*************************************/
void Pcb3D_GLCanvas::ClearLists()
/*************************************/
{
    if( m_gllist > 0 )
        glDeleteLists( m_gllist, 1 );
    m_gllist = 0;
}


/*********************************************/
void Pcb3D_GLCanvas::OnChar( wxKeyEvent& event )
/*********************************************/
{
    SetView3D( event.GetKeyCode() );
    event.Skip();
}


/*********************************************/
void Pcb3D_GLCanvas::SetView3D( int keycode )
/*********************************************/
{
    int    ii;
    double delta_move = 0.7 * g_Parm_3D_Visu.m_Zoom;

    switch( keycode )
    {
    case WXK_LEFT:
        g_Draw3d_dx -= delta_move;
        break;

    case WXK_RIGHT:
        g_Draw3d_dx += delta_move;
        break;

    case WXK_UP:
        g_Draw3d_dy += delta_move;
        break;

    case WXK_DOWN:
        g_Draw3d_dy -= delta_move;
        break;

    case WXK_HOME:
        g_Parm_3D_Visu.m_Zoom = 1.0;
        g_Draw3d_dx = g_Draw3d_dy = 0;
        trackball( g_Parm_3D_Visu.m_Quat, 0.0, 0.0, 0.0, 0.0 );
        break;

    case WXK_END:
        break;

    case WXK_F1:
        g_Parm_3D_Visu.m_Zoom /= 1.4;
        if( g_Parm_3D_Visu.m_Zoom <= 0.01 )
            g_Parm_3D_Visu.m_Zoom = 0.01;
        break;

    case WXK_F2:
        g_Parm_3D_Visu.m_Zoom *= 1.4;
        break;

    case '+':
        break;

    case '-':
        break;

    case 'r':
    case 'R':
        g_Draw3d_dx = g_Draw3d_dy = 0;
        for( ii = 0; ii < 4; ii++ )
            g_Parm_3D_Visu.m_Rot[ii] = 0.0;

        trackball( g_Parm_3D_Visu.m_Quat, 0.0, 0.0, 0.0, 0.0 );
        break;

    case 'x':
        for( ii = 0; ii < 4; ii++ )
            g_Parm_3D_Visu.m_Rot[ii] = 0.0;

        trackball( g_Parm_3D_Visu.m_Quat, 0.0, 0.0, 0.0, 0.0 );
        g_Parm_3D_Visu.m_ROTZ = -90;
        g_Parm_3D_Visu.m_ROTX = -90;
        break;

    case 'X':
        for( ii = 0; ii < 4; ii++ )
            g_Parm_3D_Visu.m_Rot[ii] = 0.0;

        trackball( g_Parm_3D_Visu.m_Quat, 0.0, 0.0, 0.0, 0.0 );
        g_Parm_3D_Visu.m_ROTZ = 90;
        g_Parm_3D_Visu.m_ROTX = -90;
        break;

    case 'y':
        for( ii = 0; ii < 4; ii++ )
            g_Parm_3D_Visu.m_Rot[ii] = 0.0;

        trackball( g_Parm_3D_Visu.m_Quat, 0.0, 0.0, 0.0, 0.0 );
        g_Parm_3D_Visu.m_ROTX = -90;
        break;

    case 'Y':
        for( ii = 0; ii < 4; ii++ )
            g_Parm_3D_Visu.m_Rot[ii] = 0.0;

        trackball( g_Parm_3D_Visu.m_Quat, 0.0, 0.0, 0.0, 0.0 );
        g_Parm_3D_Visu.m_ROTX = -90;
        g_Parm_3D_Visu.m_ROTZ = -180;
        break;

    case 'z':
        for( ii = 0; ii < 4; ii++ )
            g_Parm_3D_Visu.m_Rot[ii] = 0.0;

        trackball( g_Parm_3D_Visu.m_Quat, 0.0, 0.0, 0.0, 0.0 );
        break;

    case 'Z':
        for( ii = 0; ii < 4; ii++ )
            g_Parm_3D_Visu.m_Rot[ii] = 0.0;

        trackball( g_Parm_3D_Visu.m_Quat, 0.0, 0.0, 0.0, 0.0 );
        g_Parm_3D_Visu.m_ROTX = -180;
        break;

    default:
        return;
    }

    DisplayStatus();
    Refresh( FALSE );
}


/********************************************************/
void Pcb3D_GLCanvas::OnMouseEvent( wxMouseEvent& event )
/********************************************************/
{
    wxSize size( GetClientSize() );
    double spin_quat[4];


    if( event.RightDown() )
    {
        OnRightClick( event ); return;
    }

    if( event.m_wheelRotation )
    {
        if( event.ShiftDown() )
        {
            if( event.GetWheelRotation() < 0 )
            {
                /* up */
                SetView3D( WXK_UP );
            }
            else
            {
                /* down */
                SetView3D( WXK_DOWN );
            }
        }
        else if( event.ControlDown() )
        {
            if( event.GetWheelRotation() > 0 )
            {
                /* right */
                SetView3D( WXK_RIGHT );
            }
            else
            {
                /* left */
                SetView3D( WXK_LEFT );
            }
        }
        else
        {
            if( event.GetWheelRotation() > 0 )
            {
                g_Parm_3D_Visu.m_Zoom /= 1.4;
                if( g_Parm_3D_Visu.m_Zoom <= 0.01 )
                    g_Parm_3D_Visu.m_Zoom = 0.01;
            }
            else
                g_Parm_3D_Visu.m_Zoom *= 1.4;
            DisplayStatus();
            Refresh( FALSE );
        }
    }

    if( event.Dragging() )
    {
        if( event.LeftIsDown() )
        {
            /* drag in progress, simulate trackball */
            trackball( spin_quat,
                       (2.0 * g_Parm_3D_Visu.m_Beginx - size.x) / size.x,
                       (size.y - 2.0 * g_Parm_3D_Visu.m_Beginy) / size.y,
                       (     2.0 * event.GetX() - size.x) / size.x,
                       ( size.y - 2.0 * event.GetY() ) / size.y );

            add_quats( spin_quat, g_Parm_3D_Visu.m_Quat, g_Parm_3D_Visu.m_Quat );
        }
        else if( event.MiddleIsDown() )
        {
            /* middle button drag -> pan */
            /* Current zoom and an additional factor are taken into account for the amount of panning. */
            const double PAN_FACTOR = 8.0 * g_Parm_3D_Visu.m_Zoom;
            g_Draw3d_dx -= PAN_FACTOR * ( g_Parm_3D_Visu.m_Beginx - event.GetX() ) / size.x;
            g_Draw3d_dy -= PAN_FACTOR * (event.GetY() - g_Parm_3D_Visu.m_Beginy) / size.y;
        }

        /* orientation has changed, redraw mesh */
        DisplayStatus();
        Refresh( FALSE );
    }

    g_Parm_3D_Visu.m_Beginx = event.GetX();
    g_Parm_3D_Visu.m_Beginy = event.GetY();
}


/*******************************************************/
void Pcb3D_GLCanvas::OnRightClick( wxMouseEvent& event )
/*******************************************************/

/* Construit et affiche un menu Popup lorsque on actionne le bouton droit
 *  de la souris
 */
{
    wxPoint     pos;
    wxMenu      PopUpMenu;

    pos.x = event.GetX(); pos.y = event.GetY();
    wxMenuItem* item = new wxMenuItem( &PopUpMenu, ID_POPUP_ZOOMIN,
                                      _( "Zoom +" ) );
    item->SetBitmap( zoom_in_xpm );
    PopUpMenu.Append( item );

    item = new wxMenuItem( &PopUpMenu, ID_POPUP_ZOOMOUT,
                          _( "Zoom -" ) );
    item->SetBitmap( zoom_out_xpm );
    PopUpMenu.Append( item );

    PopUpMenu.AppendSeparator();
    item = new wxMenuItem( &PopUpMenu, ID_POPUP_VIEW_ZPOS,
                          _( "Top View" ) );
    item->SetBitmap( axis3d_top_xpm );
    PopUpMenu.Append( item );

    item = new wxMenuItem( &PopUpMenu, ID_POPUP_VIEW_ZNEG,
                          _( "Bottom View" ) );
    item->SetBitmap( axis3d_bottom_xpm );
    PopUpMenu.Append( item );

    PopUpMenu.AppendSeparator();
    item = new wxMenuItem( &PopUpMenu, ID_POPUP_VIEW_XPOS,
                          _( "Right View" ) );
    item->SetBitmap( axis3d_right_xpm );
    PopUpMenu.Append( item );

    item = new wxMenuItem( &PopUpMenu, ID_POPUP_VIEW_XNEG,
                          _( "Left View" ) );
    item->SetBitmap( axis3d_left_xpm );
    PopUpMenu.Append( item );


    PopUpMenu.AppendSeparator();
    item = new wxMenuItem( &PopUpMenu, ID_POPUP_VIEW_YPOS,
                          _( "Front View" ) );
    item->SetBitmap( axis3d_front_xpm );
    PopUpMenu.Append( item );

    item = new wxMenuItem( &PopUpMenu, ID_POPUP_VIEW_YNEG,
                          _( "Back View" ) );
    item->SetBitmap( axis3d_back_xpm );
    PopUpMenu.Append( item );

    PopUpMenu.AppendSeparator();
    item = new wxMenuItem( &PopUpMenu, ID_POPUP_MOVE3D_LEFT,
                          _( "Move left <-" ) );
    item->SetBitmap( left_xpm );
    PopUpMenu.Append( item );

    item = new wxMenuItem( &PopUpMenu, ID_POPUP_MOVE3D_RIGHT,
                          _( "Move right ->" ) );
    item->SetBitmap( right_xpm );
    PopUpMenu.Append( item );

    item = new wxMenuItem( &PopUpMenu, ID_POPUP_MOVE3D_UP,
                          _( "Move Up ^" ) );
    item->SetBitmap( up_xpm );
    PopUpMenu.Append( item );

    item = new wxMenuItem( &PopUpMenu, ID_POPUP_MOVE3D_DOWN,
                          _( "Move Down" ) );
    item->SetBitmap( down_xpm );
    PopUpMenu.Append( item );

    PopupMenu( &PopUpMenu, pos );
}


/*******************************************************/
void Pcb3D_GLCanvas::OnPopUpMenu( wxCommandEvent& event )
/*******************************************************/
{
    int key = 0;

    switch( event.GetId() )
    {
    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 = 'X';
        break;

    case ID_POPUP_VIEW_YPOS:
        key = 'y';
        break;

    case ID_POPUP_VIEW_YNEG:
        key = 'Y';
        break;

    case ID_POPUP_VIEW_ZPOS:
        key = 'z';
        break;

    case ID_POPUP_VIEW_ZNEG:
        key = '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 Pcb3D_GLCanvas::DisplayStatus()
/***************************************/
{
    wxString msg;

    msg.Printf( wxT( "dx %3.2f" ), g_Draw3d_dx );
    m_Parent->SetStatusText( msg, 1 );

    msg.Printf( wxT( "dy %3.2f" ), g_Draw3d_dy );
    m_Parent->SetStatusText( msg, 2 );

    msg.Printf( wxT( "View: %3.1f" ), 45 * g_Parm_3D_Visu.m_Zoom );
    m_Parent->SetStatusText( msg, 3 );
}


/*************************************************/
void Pcb3D_GLCanvas::OnPaint( wxPaintEvent& event )
/*************************************************/
{
    wxPaintDC dc( 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.
    const wxSize ClientSize = GetClientSize();

    glViewport( 0, 0, ClientSize.x, ClientSize.y );
    Redraw();
    event.Skip();
}


/***********************************************************/
void Pcb3D_GLCanvas::OnEraseBackground( wxEraseEvent& event )
/***********************************************************/
{
    // Do nothing, to avoid flashing.
}


/****************************/
void Pcb3D_GLCanvas::InitGL()
/****************************/

/* Int parametres generaux pour OPENGL
 */
{
    wxSize size = GetClientSize();

    if( !m_init )
    {
        m_init = TRUE;
        g_Parm_3D_Visu.m_Zoom = 1.0;
        ZBottom = 1.0; ZTop = 10.0;
    }

    /* set viewing projection */
    double ratio_HV = (double) size.x / size.y; // Ratio largeur /hauteur de la fenetre d'affichage
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();

#define MAX_VIEW_ANGLE 160.0 / 45.0
    if( g_Parm_3D_Visu.m_Zoom > MAX_VIEW_ANGLE )
        g_Parm_3D_Visu.m_Zoom = MAX_VIEW_ANGLE;
    gluPerspective( 45.0 * g_Parm_3D_Visu.m_Zoom, ratio_HV, 1, 10 );

//	glFrustum(-1., 1.1F, -1.1F, 1.1F, ZBottom, ZTop);

    /* position viewer */
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    glTranslatef( 0.0F, 0.0F, -( ZBottom + ZTop) / 2 );

    /* clear color and depth buffers */
    glClearColor( g_Parm_3D_Visu.m_BgColor.m_Red,
                  g_Parm_3D_Visu.m_BgColor.m_Green,
                  g_Parm_3D_Visu.m_BgColor.m_Blue, 1 );
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    /* Setup light souces: */
    SetLights();


    glDisable( GL_CULL_FACE );      // show back faces

    glEnable( GL_DEPTH_TEST );      // Enable z-buferring

    glEnable( GL_LINE_SMOOTH );
    glEnable( GL_COLOR_MATERIAL );
    glColorMaterial( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE );

    /* speedups */
    glEnable( GL_DITHER );
    glShadeModel( GL_SMOOTH );
    glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST );
    glHint( GL_POLYGON_SMOOTH_HINT, GL_FASTEST );

    /* blend */
    glEnable( GL_BLEND );
    glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
}


/***********************************/
void Pcb3D_GLCanvas::SetLights()
/***********************************/

/* Init sources lumineuses pour OPENGL
 */
{
    double  light;
    GLfloat light_color[4];

    /* set viewing projection */
    light_color[3] = 1.0;
    GLfloat Z_axis_pos[4]    = { 0.0, 0.0, 3.0, 0.0 };
    GLfloat lowZ_axis_pos[4] = { 0.0, 0.0, -3.0, 0.5 };

    /* activate light */
    light = 1.0;
    light_color[0] = light_color[1] = light_color[2] = light;
    glLightfv( GL_LIGHT0, GL_POSITION, Z_axis_pos );
    glLightfv( GL_LIGHT0, GL_DIFFUSE, light_color );
    light = 0.3;
    light_color[0] = light_color[1] = light_color[2] = light;
    glLightfv( GL_LIGHT1, GL_POSITION, lowZ_axis_pos );
    glLightfv( GL_LIGHT1, GL_DIFFUSE, light_color );
    glEnable( GL_LIGHT0 );      // White spot on Z axis
    glEnable( GL_LIGHT1 );      // White spot on Z axis ( bottom)
    glEnable( GL_LIGHTING );
}


/**********************************************************/
void Pcb3D_GLCanvas::TakeScreenshot( wxCommandEvent& event )
/**********************************************************/

/* Create a Screenshot of the current 3D view.
 *  Output file format is png or jpeg, or image is copied on clipboard
 */
{
    wxFileName fn( m_Parent->m_Parent->GetScreen()->m_FileName );
    wxString   FullFileName;
    wxString   file_ext, mask;
    bool       fmt_is_jpeg = FALSE;

    if( event.GetId() == ID_MENU_SCREENCOPY_JPEG )
        fmt_is_jpeg = TRUE;
    if( event.GetId() != ID_TOOL_SCREENCOPY_TOCLIBBOARD )
    {
        file_ext = fmt_is_jpeg ? wxT( "jpg" ) : wxT( "png"; )
                   mask = wxT( "*." ) + file_ext;
        FullFileName    = m_Parent->m_Parent->GetScreen()->m_FileName;
        fn.SetExt( file_ext );

        FullFileName =
            EDA_FileSelector( _( "3D Image filename:" ),
                              wxEmptyString,    /* Chemin par defaut */
                              fn.GetFullName(), /* nom fichier par defaut */
                              file_ext,         /* extension par defaut */
                              mask,             /* Masque d'affichage */
                              this,
                              wxFD_SAVE,
                              TRUE
                              );
        if( FullFileName.IsEmpty() )
            return;
    }

    Redraw( true );

    struct vieport_params
    {
        GLint originx;
        GLint originy;
        GLint x;
        GLint y;
    } viewport;

    glGetIntegerv( GL_VIEWPORT, (GLint*) &viewport );

    unsigned char* pixelbuffer = (unsigned char*) malloc( 3 * viewport.x * viewport.y );
    glReadPixels( 0, 0, viewport.x, viewport.y, GL_RGB, GL_UNSIGNED_BYTE, pixelbuffer );

    wxImage        image( viewport.x, viewport.y );

    image.SetData( pixelbuffer );
    image = image.Mirror();
    image = image.Rotate90().Rotate90();
    wxBitmap bitmap( image, -1 );

    if( event.GetId() == ID_TOOL_SCREENCOPY_TOCLIBBOARD )
    {
        wxBitmapDataObject* dobjBmp = new wxBitmapDataObject;
        dobjBmp->SetBitmap( bitmap );
        if( wxTheClipboard->Open() )
        {
            if( !wxTheClipboard->SetData( dobjBmp ) )
                wxLogError( _T( "Failed to copy image to clipboard" ) );
            wxTheClipboard->Flush();    /* the data on clipboard
                                         *  will stay available after the application exits */
            wxTheClipboard->Close();
        }
    }
    else
    {
        if( !image.SaveFile( FullFileName,
                             fmt_is_jpeg ? wxBITMAP_TYPE_JPEG : wxBITMAP_TYPE_PNG ) )
            wxLogError( wxT( "Can't save file" ) );

        image.Destroy();
    }
}
