/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2016 Jean-Pierre Charras, jp.charras at wanadoo.fr
 * Copyright (C) 2011 Wayne Stambaugh <stambaughw@gmail.com>
 * Copyright (C) 1992-2023 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 dcode.cpp
 * @brief D_CODE class implementation
 */

#include <trigo.h>
#include <gerbview_frame.h>
#include <gerber_file_image.h>
#include <eda_units.h>
#include <convert_basic_shapes_to_polygon.h>

#define DCODE_DEFAULT_SIZE gerbIUScale.mmToIU( 0.1 )

/* Format Gerber: NOTES:
 * Tools and D_CODES
 *   tool number (identification of shapes)
 *   1 to 999
 *
 * D_CODES:
 *   D01 ... D9 = command codes:
 *      D01 = activating light (pen down) while moving
 *      D02 = light extinction (pen up) while moving
 *      D03 = Flash
 *      D04 to D09 = non used
 *   D10 ... D999 = Identification Tool (Shape id)
 *
 * For tools defining a shape):
 * DCode min = D10
 * DCode max = 999
 */


D_CODE::D_CODE( int num_dcode )
{
    m_Num_Dcode = num_dcode;
    Clear_D_CODE_Data();
}


D_CODE::~D_CODE()
{
}


void D_CODE::Clear_D_CODE_Data()
{
    m_Size.x     = DCODE_DEFAULT_SIZE;
    m_Size.y     = DCODE_DEFAULT_SIZE;
    m_ApertType  = APT_CIRCLE;
    m_Drill.x    = m_Drill.y = 0;
    m_DrillShape = APT_DEF_NO_HOLE;
    m_InUse      = false;
    m_Defined    = false;
    m_Macro      = nullptr;
    m_Rotation   = ANGLE_0;
    m_EdgesCount = 0;
    m_Polygon.RemoveAllContours();
}


const wxChar* D_CODE::ShowApertureType( APERTURE_T aType )
{
    const wxChar* ret;

    switch( aType )
    {
    case APT_CIRCLE:
        ret = wxT( "Round" );   break;

    case APT_RECT:
        ret = wxT( "Rect" );    break;

    case APT_OVAL:
        ret = wxT( "Oval" );    break;

    case APT_POLYGON:
        ret = wxT( "Poly" );    break;

    case APT_MACRO:
        ret = wxT( "Macro" );   break;

    default:
        ret = wxT( "???" );     break;
    }

    return ret;
}


int D_CODE::GetShapeDim( GERBER_DRAW_ITEM* aParent )
{
    int dim = 0;

    switch( m_ApertType )
    {
    case APT_CIRCLE:
        dim = m_Size.x;
        break;

    case APT_RECT:
    case APT_OVAL:
        dim = std::min( m_Size.x, m_Size.y );
        break;

    case APT_POLYGON:
        dim = std::min( m_Size.x, m_Size.y );
        break;

    case APT_MACRO:
        if( m_Macro )
        {
            if( m_Polygon.OutlineCount() == 0 )
                ConvertShapeToPolygon( aParent );

            BOX2I bbox = m_Polygon.BBox();
            dim = std::min( bbox.GetWidth(), bbox.GetHeight() );
        }
        break;

    default:
        break;
    }

    return dim;
}


void D_CODE::DrawFlashedShape( const GERBER_DRAW_ITEM* aParent, wxDC* aDC, const COLOR4D& aColor,
                               const VECTOR2I& aShapePos, bool aFilledShape )
{
    int radius;

    switch( m_ApertType )
    {
    case APT_CIRCLE:
        radius = m_Size.x >> 1;

        if( !aFilledShape )
        {
            GRCircle( aDC, aParent->GetABPosition(aShapePos), radius, 0, aColor );
        }
        else if( m_DrillShape == APT_DEF_NO_HOLE )
        {
            GRFilledCircle( aDC, aParent->GetABPosition(aShapePos), radius, 0, aColor, aColor );
        }
        else if( m_DrillShape == APT_DEF_ROUND_HOLE )    // round hole in shape
        {
            int width = (m_Size.x - m_Drill.x ) / 2;
            GRCircle( aDC,  aParent->GetABPosition(aShapePos), radius - (width / 2), width, aColor );
        }
        else                            // rectangular hole
        {
            if( m_Polygon.OutlineCount() == 0 )
                ConvertShapeToPolygon( aParent );

            DrawFlashedPolygon( aParent, aDC, aColor, aFilledShape, aShapePos );
        }

        break;

    case APT_RECT:
    {
        VECTOR2I start;
        start.x = aShapePos.x - m_Size.x / 2;
        start.y = aShapePos.y - m_Size.y / 2;
        VECTOR2I end = start + m_Size;
        start = aParent->GetABPosition( start );
        end = aParent->GetABPosition( end );

        if( !aFilledShape )
        {
            GRRect( aDC, start, end, 0, aColor );
        }
        else if( m_DrillShape == APT_DEF_NO_HOLE )
        {
            GRFilledRect( aDC, start, end, 0, aColor, aColor );
        }
        else
        {
            if( m_Polygon.OutlineCount() == 0 )
                ConvertShapeToPolygon( aParent );

            DrawFlashedPolygon( aParent, aDC, aColor, aFilledShape, aShapePos );
        }
    }
    break;

    case APT_OVAL:
    {
        VECTOR2I start = aShapePos;
        VECTOR2I end = aShapePos;

        if( m_Size.x > m_Size.y )   // horizontal oval
        {
            int delta = ( m_Size.x - m_Size.y ) / 2;
            start.x -= delta;
            end.x   += delta;
            radius   = m_Size.y;    // Width in fact
        }
        else   // vertical oval
        {
            int delta = ( m_Size.y - m_Size.x ) / 2;
            start.y -= delta;
            end.y   += delta;
            radius   = m_Size.x;    // Width in fact
        }

        start = aParent->GetABPosition( start );
        end = aParent->GetABPosition( end );

        if( !aFilledShape )
        {
            GRCSegm( aDC, start, end, radius, aColor );
        }
        else if( m_DrillShape == APT_DEF_NO_HOLE )
        {
            GRFilledSegment( aDC, start, end, radius, aColor );
        }
        else
        {
            if( m_Polygon.OutlineCount() == 0 )
                ConvertShapeToPolygon( aParent );

            DrawFlashedPolygon( aParent, aDC, aColor, aFilledShape, aShapePos );
        }
    }

    break;

    case APT_MACRO:
    case APT_POLYGON:
        if( m_Polygon.OutlineCount() == 0 )
            ConvertShapeToPolygon( aParent );

        DrawFlashedPolygon( aParent, aDC, aColor, aFilledShape, aShapePos );
        break;
    }
}


void D_CODE::DrawFlashedPolygon( const GERBER_DRAW_ITEM* aParent, wxDC* aDC,
                                 const COLOR4D& aColor,
                                 bool aFilled, const VECTOR2I& aPosition )
{
    if( m_Polygon.OutlineCount() == 0 )
        return;

    int pointCount = m_Polygon.VertexCount();
    std::vector<VECTOR2I> points;
    points.reserve( pointCount );

    for( int ii = 0; ii < pointCount; ii++ )
    {
        VECTOR2I p( m_Polygon.CVertex( ii ).x, m_Polygon.CVertex( ii ).y );
        points[ii] = p + aPosition;
        points[ii] = aParent->GetABPosition( points[ii] );
    }

    GRClosedPoly( aDC, pointCount, &points[0], aFilled, aColor );
}


// TODO(snh): Remove the hard-coded count
#define SEGS_CNT 64     // number of segments to approximate a circle


// A helper function for D_CODE::ConvertShapeToPolygon().   Add a hole to a polygon
static void addHoleToPolygon( SHAPE_POLY_SET* aPolygon, APERTURE_DEF_HOLETYPE aHoleShape,
                              const VECTOR2I& aSize, const VECTOR2I& aAnchorPos );


void D_CODE::ConvertShapeToPolygon( const GERBER_DRAW_ITEM* aParent )
{
    VECTOR2I initialpos;
    VECTOR2I currpos;

    m_Polygon.RemoveAllContours();

    switch( m_ApertType )
    {
    case APT_CIRCLE:        // creates only a circle with rectangular hole
        {
        // Adjust the allowed approx error to convert arcs to segments:
        int arc_to_seg_error = gerbIUScale.mmToIU( 0.005 );    // Allow 5 microns
        TransformCircleToPolygon( m_Polygon, initialpos, m_Size.x >> 1, arc_to_seg_error,
                                  ERROR_INSIDE );
        addHoleToPolygon( &m_Polygon, m_DrillShape, m_Drill, initialpos );
        }
        break;

    case APT_RECT:
        m_Polygon.NewOutline();
        currpos.x  = m_Size.x / 2;
        currpos.y  = m_Size.y / 2;
        initialpos = currpos;
        m_Polygon.Append( VECTOR2I( currpos ) );
        currpos.x -= m_Size.x;
        m_Polygon.Append( VECTOR2I( currpos ) );
        currpos.y -= m_Size.y;
        m_Polygon.Append( VECTOR2I( currpos ) );
        currpos.x += m_Size.x;
        m_Polygon.Append( VECTOR2I( currpos ) );
        currpos.y += m_Size.y;
        m_Polygon.Append( VECTOR2I( currpos ) );    // close polygon
        m_Polygon.Append( VECTOR2I( initialpos ) );

        addHoleToPolygon( &m_Polygon, m_DrillShape, m_Drill, initialpos );
        break;

    case APT_OVAL:
    {
        m_Polygon.NewOutline();
        int delta, radius;

        // we create an horizontal oval shape. then rotate if needed
        if( m_Size.x > m_Size.y )   // horizontal oval
        {
            delta = ( m_Size.x - m_Size.y ) / 2;
            radius = m_Size.y / 2;
        }
        else   // vertical oval
        {
            delta  = (m_Size.y - m_Size.x) / 2;
            radius = m_Size.x / 2;
        }

        currpos.y  = radius;
        initialpos = currpos;
        m_Polygon.Append( VECTOR2I( currpos ) );

        // build the right arc of the shape
        unsigned ii = 0;

        for( ; ii <= SEGS_CNT / 2; ii++ )
        {
            currpos = initialpos;
            RotatePoint( currpos, ANGLE_360 * ii / SEGS_CNT );
            currpos.x += delta;
            m_Polygon.Append( VECTOR2I( currpos ) );
        }

        // build the left arc of the shape
        for( ii = SEGS_CNT / 2; ii <= SEGS_CNT; ii++ )
        {
            currpos = initialpos;
            RotatePoint( currpos, ANGLE_360 * ii / SEGS_CNT );
            currpos.x -= delta;
            m_Polygon.Append( currpos );
        }

        m_Polygon.Append( initialpos );      // close outline

        if( m_Size.y > m_Size.x )                   // vertical oval, rotate polygon.
            m_Polygon.Rotate( ANGLE_90 );

        addHoleToPolygon( &m_Polygon, m_DrillShape, m_Drill, initialpos );
    }
        break;

    case APT_POLYGON:
        m_Polygon.NewOutline();
        currpos.x  = m_Size.x >> 1;     // first point is on X axis
        initialpos = currpos;

        // rs274x said: m_EdgesCount = 3 ... 12
        if( m_EdgesCount < 3 )
            m_EdgesCount = 3;

        if( m_EdgesCount > 12 )
            m_EdgesCount = 12;

        for( int ii = 0; ii < m_EdgesCount; ii++ )
        {
            currpos = initialpos;
            RotatePoint( currpos, ANGLE_360 * ii / m_EdgesCount );
            m_Polygon.Append( currpos );
        }

        addHoleToPolygon( &m_Polygon, m_DrillShape, m_Drill, initialpos );

        if( !m_Rotation.IsZero() )    // rotate polygonal shape:
            m_Polygon.Rotate( m_Rotation );

        break;

    case APT_MACRO:
        APERTURE_MACRO* macro = GetMacro();
        SHAPE_POLY_SET* macroShape = macro->GetApertureMacroShape( aParent, initialpos );
        m_Polygon.Append( *macroShape );
        break;
    }
}


// The helper function for D_CODE::ConvertShapeToPolygon().
// Add a hole to a polygon
static void addHoleToPolygon( SHAPE_POLY_SET* aPolygon, APERTURE_DEF_HOLETYPE aHoleShape,
                              const VECTOR2I& aSize, const VECTOR2I& aAnchorPos )
{
    VECTOR2I       currpos;
    SHAPE_POLY_SET holeBuffer;

    if( aHoleShape == APT_DEF_ROUND_HOLE )
    {
        // Adjust the allowed approx error to convert arcs to segments:
        int arc_to_seg_error = gerbIUScale.mmToIU( 0.005 );    // Allow 5 microns
        TransformCircleToPolygon( holeBuffer, VECTOR2I( 0, 0 ), aSize.x / 2, arc_to_seg_error,
                                  ERROR_INSIDE );
    }
    else if( aHoleShape == APT_DEF_RECT_HOLE )
    {
        holeBuffer.NewOutline();
        currpos.x = aSize.x / 2;
        currpos.y = aSize.y / 2;
        holeBuffer.Append( VECTOR2I( currpos ) );       // link to hole and begin hole
        currpos.x -= aSize.x;
        holeBuffer.Append( VECTOR2I( currpos ) );
        currpos.y -= aSize.y;
        holeBuffer.Append( VECTOR2I( currpos ) );
        currpos.x += aSize.x;
        holeBuffer.Append( VECTOR2I( currpos ) );
        currpos.y += aSize.y;
        holeBuffer.Append( VECTOR2I( currpos ) );       // close hole
    }

    aPolygon->BooleanSubtract( holeBuffer, SHAPE_POLY_SET::PM_FAST );
    aPolygon->Fracture( SHAPE_POLY_SET::PM_FAST );
}