/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2012 Jean-Pierre Charras, jp.charras at wanadoo.fr
 * Copyright (C) 1992-2012 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 3d_draw_basic_functions.cpp
 */

#include <fctsys.h>
#include <trigo.h>
#include <convert_basic_shapes_to_polygon.h>

#include <3d_viewer.h>
#include <info3d_visu.h>
#include <3d_draw_basic_functions.h>
#include <modelparsers.h>

// Number of segments to approximate a circle by segments
#define SEGM_PER_CIRCLE 16

#ifndef CALLBACK
#define CALLBACK
#endif

// Variables used to pass a value to call back openGL functions
static float s_textureScale;
static double s_currentZpos;
static double s_biuTo3Dunits;
bool s_useTextures;

// CALLBACK functions for GLU_TESS
static void CALLBACK    tessBeginCB( GLenum which );
static void CALLBACK    tessEndCB();
static void CALLBACK    tessErrorCB( GLenum errorCode );
static void CALLBACK    tessCPolyPt2Vertex( const GLvoid* data );

// 2 helper functions to set the current normal vector for gle items
static inline void SetNormalZpos()
{
    //glNormal3f( 0.0, 0.0, 1.0 );
}

static inline void SetNormalZneg()
{
    //glNormal3f( 0.0, 0.0, -1.0 );
}

void TransfertToGLlist( std::vector< S3D_VERTEX >& aVertices, double aBiuTo3DUnits );

/* Draw3D_VerticalPolygonalCylinder is a helper function.
 *
 * draws a "vertical cylinder" having a polygon shape
 * from Z position = aZpos to aZpos + aHeight
 * Used to create the vertical sides of 3D horizontal shapes with thickness.
 */
static void Draw3D_VerticalPolygonalCylinder( const CPOLYGONS_LIST& aPolysList,
                                              int aHeight, int aZpos,
                                              bool aInside, double aBiuTo3DUnits )
{
    if( aHeight == 0 )
        return;

    std::vector<S3D_VERTEX> coords;
    coords.resize( 4 );

    // Init Z position of the 4 points of a GL_QUAD
    if( aInside )
    {
        coords[0].z = aZpos;
        coords[1].z = aZpos + aHeight;
    }
    else
    {
        coords[0].z = aZpos + aHeight;
        coords[1].z = aZpos;
    }
    coords[2].z = coords[1].z;
    coords[3].z = coords[0].z;

    // Draw the vertical polygonal side
    int startContour = 0;
    for( unsigned ii = 0; ii < aPolysList.GetCornersCount(); ii++ )
    {
        unsigned jj = ii + 1;

        if( aPolysList.IsEndContour( ii ) || jj >= aPolysList.GetCornersCount() )
        {
            jj = startContour;
            startContour = ii + 1;
        }

        // Build the 4 vertices of each GL_QUAD
        coords[0].x = aPolysList.GetX( ii );
        coords[0].y = -aPolysList.GetY( ii );
        coords[1].x = coords[0].x;
        coords[1].y = coords[0].y;              // only z change
        coords[2].x = aPolysList.GetX( jj );
        coords[2].y = -aPolysList.GetY( jj );
        coords[3].x = coords[2].x;
        coords[3].y = coords[2].y;              // only z change

        // Creates the GL_QUAD
        TransfertToGLlist( coords, aBiuTo3DUnits );
    }
}


void SetGLColor( EDA_COLOR_T color, double alpha )
{
    const StructColors &colordata = g_ColorRefs[ColorGetBase( color )];

    float red     = colordata.m_Red / 255.0;
    float blue    = colordata.m_Blue / 255.0;
    float green   = colordata.m_Green / 255.0;
    glColor4f( red, green, blue, (float)alpha );
}


void SetGLColor( S3D_COLOR& aColor, float aTransparency )
{
    glColor4f( aColor.m_Red, aColor.m_Green, aColor.m_Blue, aTransparency );
}


void SetGLTexture( GLuint text_id, float scale )
{
    glEnable( GL_TEXTURE_2D );
    glBindTexture( GL_TEXTURE_2D, text_id );
    s_textureScale = scale;     // for Tess callback functions
}


/* draw all solid polygons found in aPolysList
 * aZpos = z position in board internal units
 * aThickness = thickness in board internal units
 * If aThickness = 0, a polygon area is drawn in a XY plane at Z position = aZpos.
 * If aThickness > 0, a solid object is drawn.
 *  The top side is located at aZpos + aThickness / 2
 *  The bottom side is located at aZpos - aThickness / 2
 */
void Draw3D_SolidHorizontalPolyPolygons( const CPOLYGONS_LIST& aPolysList,
                                         int aZpos, int aThickness, double aBiuTo3DUnits,
                                         bool aUseTextures )
{
    // for Tess callback functions:
    s_biuTo3Dunits = aBiuTo3DUnits;
    s_useTextures = aUseTextures;

    GLUtesselator* tess = gluNewTess();

    gluTessCallback( tess, GLU_TESS_BEGIN, ( void (CALLBACK*) () )tessBeginCB );
    gluTessCallback( tess, GLU_TESS_END, ( void (CALLBACK*) () )tessEndCB );
    gluTessCallback( tess, GLU_TESS_ERROR, ( void (CALLBACK*) () )tessErrorCB );
    gluTessCallback( tess, GLU_TESS_VERTEX, ( void (CALLBACK*) () )tessCPolyPt2Vertex );

    GLdouble    v_data[3];
    double      zpos = ( aZpos + (aThickness / 2.0) ) * aBiuTo3DUnits;
    s_currentZpos = zpos;     // for Tess callback functions
    v_data[2] = aZpos + (aThickness / 2.0);

    // Set normal toward positive Z axis, for a solid object on the top side
    if( aThickness )
        SetNormalZpos();

    // gluTessProperty(tess, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_ODD);

    // Draw solid areas contained in this list
    CPOLYGONS_LIST polylist = aPolysList;    // temporary copy for gluTessVertex

    int startContour;

    for( int side = 0; side < 2; side++ )
    {
        startContour = 1;

        for( unsigned ii = 0; ii < polylist.GetCornersCount(); ii++ )
        {
            if( startContour == 1 )
            {
                gluTessBeginPolygon( tess, NULL );
                gluTessBeginContour( tess );
                startContour = 0;
            }

            // https://www.opengl.org/sdk/docs/man2/xhtml/gluTessNormal.xml
            gluTessNormal( tess, 0.0, 0.0, 0.0 );


            v_data[0]   = polylist.GetX( ii ) * aBiuTo3DUnits;
            v_data[1]   = -polylist.GetY( ii ) * aBiuTo3DUnits;
            // gluTessVertex store pointers on data, not data, so do not store
            // different corners values in a temporary variable
            // but send pointer on each CPolyPt value in polylist
            // before calling gluDeleteTess
            gluTessVertex( tess, v_data, &polylist[ii] );


            if( polylist.IsEndContour( ii ) )
            {
                gluTessEndContour( tess );
                gluTessEndPolygon( tess );
                startContour = 1;
            }
        }

        if( aThickness == 0 )
            break;

        // Prepare the bottom side of solid areas
        zpos = ( aZpos - (aThickness / 2.0) ) * aBiuTo3DUnits;
        s_currentZpos = zpos;     // for Tess callback functions
        v_data[2] = zpos;
        // Set normal toward negative Z axis, for a solid object on bottom side
        SetNormalZneg();
    }

    if( startContour == 0 )
    {
        gluTessEndContour( tess );
        gluTessEndPolygon( tess );
    }

    gluDeleteTess( tess );

    if( aThickness == 0 )
    {
        return;
    }

    // Build the 3D data : vertical side
    Draw3D_VerticalPolygonalCylinder( polylist, aThickness, aZpos - (aThickness / 2.0), false, aBiuTo3DUnits );
}


/* draw the solid polygon found in aPolysList
 * The first polygon is the main polygon, others are holes
 * See Draw3D_SolidHorizontalPolyPolygons for more info
 */
void Draw3D_SolidHorizontalPolygonWithHoles( const CPOLYGONS_LIST& aPolysList,
                                             int aZpos, int aThickness,
                                             double aBiuTo3DUnits, bool aUseTextures )
{
    CPOLYGONS_LIST polygon;

    ConvertPolysListWithHolesToOnePolygon( aPolysList, polygon );
    Draw3D_SolidHorizontalPolyPolygons( polygon, aZpos, aThickness, aBiuTo3DUnits, aUseTextures );
}


/* draw a cylinder (a tube) using 3D primitives.
 * the cylinder axis is parallel to the Z axis
 * If aHeight = height of the cylinder is 0, only one ring will be drawn
 * If aThickness = 0, only one cylinder will be drawn
 */
void Draw3D_ZaxisCylinder( wxPoint aCenterPos, int aRadius,
                           int aHeight, int aThickness,
                           int aZpos, double aBiuTo3DUnits )
{
    const int slice = SEGM_PER_CIRCLE;
    CPOLYGONS_LIST outer_cornerBuffer;

    TransformCircleToPolygon( outer_cornerBuffer, aCenterPos,
                              aRadius + (aThickness / 2), slice );

    std::vector<S3D_VERTEX> coords;
    coords.resize( 4 );

    CPOLYGONS_LIST inner_cornerBuffer;
    if( aThickness )    // build the the vertical inner polygon (hole)
        TransformCircleToPolygon( inner_cornerBuffer, aCenterPos,
                                  aRadius - (aThickness / 2), slice );

    if( aHeight )
    {
        // Draw the vertical outer side
        Draw3D_VerticalPolygonalCylinder( outer_cornerBuffer,
                                      aHeight, aZpos, false, aBiuTo3DUnits );
        if( aThickness )
            // Draws the vertical inner side (hole)
            Draw3D_VerticalPolygonalCylinder( inner_cornerBuffer,
                                          aHeight, aZpos, true, aBiuTo3DUnits );
    }

    if( aThickness )
    {
        // draw top (front) and bottom (back) horizontal sides (rings)
        SetNormalZpos();
        outer_cornerBuffer.Append( inner_cornerBuffer );
        CPOLYGONS_LIST polygon;

        ConvertPolysListWithHolesToOnePolygon( outer_cornerBuffer, polygon );
        // draw top (front) horizontal ring
        Draw3D_SolidHorizontalPolyPolygons( polygon, aZpos + aHeight, 0, aBiuTo3DUnits, false );

        if( aHeight )
        {
            // draw bottom (back) horizontal ring
            SetNormalZneg();
            Draw3D_SolidHorizontalPolyPolygons( polygon, aZpos, 0, aBiuTo3DUnits, false );
        }
    }

    SetNormalZpos();
}


/*
 * Function Draw3D_ZaxisOblongCylinder:
 * draw a segment with an oblong hole.
 * Used to draw oblong holes
 * If aHeight = height of the cylinder is 0, only one ring will be drawn
 * If aThickness = 0, only one cylinder will be drawn
 */
void Draw3D_ZaxisOblongCylinder( wxPoint aAxis1Pos, wxPoint aAxis2Pos,
                                 int aRadius, int aHeight, int aThickness,
                                 int aZpos, double aBiuTo3DUnits  )
{
    const int slice = SEGM_PER_CIRCLE;

    // Build the points to approximate oblong cylinder by segments
    CPOLYGONS_LIST outer_cornerBuffer;

    int segm_width = (aRadius * 2) + aThickness;
    TransformRoundedEndsSegmentToPolygon( outer_cornerBuffer, aAxis1Pos,
                                          aAxis2Pos, slice, segm_width );

    // Draw the oblong outer cylinder
    if( aHeight )
        Draw3D_VerticalPolygonalCylinder( outer_cornerBuffer, aHeight, aZpos,
                                          false, aBiuTo3DUnits );

    if( aThickness )
    {
        CPOLYGONS_LIST inner_cornerBuffer;
        segm_width = aRadius * 2;
        TransformRoundedEndsSegmentToPolygon( inner_cornerBuffer, aAxis1Pos,
                                              aAxis2Pos, slice, segm_width );

        // Draw the oblong inner cylinder
        if( aHeight )
            Draw3D_VerticalPolygonalCylinder( inner_cornerBuffer, aHeight,
                                              aZpos, true, aBiuTo3DUnits );

        // Build the horizontal full polygon shape
        // (outer polygon shape - inner polygon shape)
        outer_cornerBuffer.Append( inner_cornerBuffer );

        CPOLYGONS_LIST polygon;
        ConvertPolysListWithHolesToOnePolygon( outer_cornerBuffer, polygon );

        // draw top (front) horizontal side (ring)
        SetNormalZpos();
        Draw3D_SolidHorizontalPolyPolygons( polygon, aZpos + aHeight, 0, aBiuTo3DUnits, false );

        if( aHeight )
        {
            // draw bottom (back) horizontal side (ring)
            SetNormalZneg();
            Draw3D_SolidHorizontalPolyPolygons( polygon, aZpos, 0, aBiuTo3DUnits, false );
        }
    }

    SetNormalZpos();
}


/* draw a thick segment using 3D primitives, in a XY plane
 * wxPoint aStart, wxPoint aEnd = YX position of end in board units
 * aWidth = width of segment in board units
 * aThickness = thickness of segment in board units
 * aZpos = z position of segment in board units
 */
void Draw3D_SolidSegment( const wxPoint& aStart, const wxPoint& aEnd,
                          int aWidth, int aThickness, int aZpos, double aBiuTo3DUnits )
{
    CPOLYGONS_LIST   cornerBuffer;
    const int               slice = SEGM_PER_CIRCLE;

    TransformRoundedEndsSegmentToPolygon( cornerBuffer, aStart, aEnd, slice, aWidth );

    Draw3D_SolidHorizontalPolyPolygons( cornerBuffer, aZpos, aThickness, aBiuTo3DUnits, false );
}


void Draw3D_ArcSegment( const wxPoint&  aCenterPos, const wxPoint& aStartPoint,
                        double aArcAngle, int aWidth, int aThickness,
                        int aZpos, double aBiuTo3DUnits )
{
    const int   slice = SEGM_PER_CIRCLE;

    CPOLYGONS_LIST cornerBuffer;
    TransformArcToPolygon( cornerBuffer, aCenterPos, aStartPoint, aArcAngle,
                           slice, aWidth );

    Draw3D_SolidHorizontalPolyPolygons( cornerBuffer, aZpos, aThickness, aBiuTo3DUnits, false );
}


// /////////////////////////////////////////////////////////////////////////////
// GLU_TESS CALLBACKS
// /////////////////////////////////////////////////////////////////////////////

void CALLBACK tessBeginCB( GLenum which )
{
    glBegin( which );
}


void CALLBACK tessEndCB()
{
    glEnd();
}


void CALLBACK tessCPolyPt2Vertex( const GLvoid* data )
{
    // cast back to double type
    const CPolyPt* ptr = (const CPolyPt*) data;

    if( s_useTextures )
    {
        glTexCoord2f( ptr->x * s_biuTo3Dunits * s_textureScale,
                      -ptr->y * s_biuTo3Dunits * s_textureScale);
    }

    glVertex3d( ptr->x * s_biuTo3Dunits, -ptr->y * s_biuTo3Dunits, s_currentZpos );
}


void CALLBACK tessErrorCB( GLenum errorCode )
{
#if defined(DEBUG)
    const GLubyte* errorStr;

    errorStr = gluErrorString( errorCode );

    // DEBUG //
    DBG( printf( "Tess ERROR: %s\n", errorStr ); )
#endif
}
