/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2017 Jon Evans <jon@craftyjon.com>
 * Copyright (C) 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include <gerbview_painter.h>
#include <gal/graphics_abstraction_layer.h>
#include <settings/color_settings.h>
#include <convert_basic_shapes_to_polygon.h>
#include <convert_to_biu.h>
#include <gerbview.h>

#include <dcode.h>
#include <gerber_draw_item.h>
#include <gerber_file_image.h>

using namespace KIGFX;

GERBVIEW_RENDER_SETTINGS::GERBVIEW_RENDER_SETTINGS()
{
    m_backgroundColor = COLOR4D::BLACK;

    m_spotFill          = true;
    m_lineFill          = true;
    m_polygonFill       = true;
    m_showNegativeItems = false;
    m_showCodes         = false;
    m_diffMode          = true;

    m_componentHighlightString = "";
    m_netHighlightString       = "";
    m_attributeHighlightString = "";

    update();
}


void GERBVIEW_RENDER_SETTINGS::LoadColors( const COLOR_SETTINGS* aSettings )
{
    size_t palette_size = aSettings->m_Palette.size();
    size_t palette_idx = 0;

    for( int i = GERBVIEW_LAYER_ID_START;
         i < GERBVIEW_LAYER_ID_START + GERBER_DRAWLAYERS_COUNT; i++ )
    {
        COLOR4D baseColor = aSettings->GetColor( i );

        if( baseColor == COLOR4D::UNSPECIFIED )
            baseColor = aSettings->m_Palette[ ( palette_idx++ ) % palette_size ];

        if( m_diffMode )
            baseColor.a = 0.75;

        m_layerColors[i] = baseColor;
        m_layerColorsHi[i] = baseColor.Brightened( 0.5 );
        m_layerColorsSel[i] = baseColor.Brightened( 0.8 );
        m_layerColorsDark[i] = baseColor.Darkened( 0.25 );
    }

    for( int i = LAYER_DCODES; i < GERBVIEW_LAYER_ID_END; i++ )
        m_layerColors[i] = aSettings->GetColor( i );

    for( int i = GAL_LAYER_ID_START; i < GAL_LAYER_ID_END; i++ )
        m_layerColors[i] = aSettings->GetColor( i );

    update();
}


void GERBVIEW_RENDER_SETTINGS::LoadDisplayOptions( const GBR_DISPLAY_OPTIONS& aOptions )
{
    m_spotFill          = aOptions.m_DisplayFlashedItemsFill;
    m_lineFill          = aOptions.m_DisplayLinesFill;
    m_polygonFill       = aOptions.m_DisplayPolygonsFill;
    m_showNegativeItems = aOptions.m_DisplayNegativeObjects;
    m_showCodes         = aOptions.m_DisplayDCodes;
    m_diffMode          = aOptions.m_DiffMode;
    m_hiContrastEnabled = aOptions.m_HighContrastMode;
    m_showPageLimits    = aOptions.m_DisplayPageLimits;
    m_backgroundColor   = aOptions.m_BgDrawColor;

    update();
}


const COLOR4D& GERBVIEW_RENDER_SETTINGS::GetColor( const VIEW_ITEM* aItem, int aLayer ) const
{
    const EDA_ITEM* item = static_cast<const EDA_ITEM*>( aItem );
    static const COLOR4D transparent = COLOR4D( 0, 0, 0, 0 );
    const GERBER_DRAW_ITEM* gbrItem = nullptr;

    if( item && item->Type() == GERBER_DRAW_ITEM_T )
        gbrItem = static_cast<const GERBER_DRAW_ITEM*>( item );

    // All DCODE layers stored under a single color setting
    if( IsDCodeLayer( aLayer ) )
        return m_layerColors[ LAYER_DCODES ];

    if( item && item->IsSelected() )
        return m_layerColorsSel[aLayer];

    if( gbrItem && gbrItem->GetLayerPolarity() )
    {
        if( m_showNegativeItems )
            return m_layerColors[LAYER_NEGATIVE_OBJECTS];
        else
            return transparent;
    }

    if( !m_netHighlightString.IsEmpty() && gbrItem &&
        m_netHighlightString == gbrItem->GetNetAttributes().m_Netname )
        return m_layerColorsHi[aLayer];

    if( !m_componentHighlightString.IsEmpty() && gbrItem &&
        m_componentHighlightString == gbrItem->GetNetAttributes().m_Cmpref )
        return m_layerColorsHi[aLayer];

    if( !m_attributeHighlightString.IsEmpty() && gbrItem && gbrItem->GetDcodeDescr() &&
        m_attributeHighlightString == gbrItem->GetDcodeDescr()->m_AperFunction )
        return m_layerColorsHi[aLayer];

    // Return grayish color for non-highlighted layers in the high contrast mode
    if( m_hiContrastEnabled && m_activeLayers.count( aLayer ) == 0)
        return m_hiContrastColor[aLayer];

    // Catch the case when highlight and high-contraste modes are enabled
    // and we are drawing a not highlighted track
    if( m_highlightEnabled )
        return m_layerColorsDark[aLayer];

    // No special modificators enabled
    return m_layerColors[aLayer];
}


GERBVIEW_PAINTER::GERBVIEW_PAINTER( GAL* aGal ) :
    PAINTER( aGal )
{
}


// TODO(JE): Pull up to PAINTER?
int GERBVIEW_PAINTER::getLineThickness( int aActualThickness ) const
{
    // if items have 0 thickness, draw them with the outline
    // width, otherwise respect the set value (which, no matter
    // how small will produce something)
    if( aActualThickness == 0 )
        return m_gerbviewSettings.m_outlineWidth;

    return aActualThickness;
}


bool GERBVIEW_PAINTER::Draw( const VIEW_ITEM* aItem, int aLayer )
{
    const EDA_ITEM* item = static_cast<const EDA_ITEM*>( aItem );

    // the "cast" applied in here clarifies which overloaded draw() is called
    switch( item->Type() )
    {
    case GERBER_DRAW_ITEM_T:
        draw( static_cast<GERBER_DRAW_ITEM*>( const_cast<EDA_ITEM*>( item ) ), aLayer );
        break;

    default:
        // Painter does not know how to draw the object
        return false;
    }

    return true;
}


// TODO(JE) aItem can't be const because of GetDcodeDescr()
// Probably that can be refactored in GERBER_DRAW_ITEM to allow const here.
void GERBVIEW_PAINTER::draw( /*const*/ GERBER_DRAW_ITEM* aItem, int aLayer )
{
    VECTOR2D start( aItem->GetABPosition( aItem->m_Start ) );   // TODO(JE) Getter
    VECTOR2D end( aItem->GetABPosition( aItem->m_End ) );       // TODO(JE) Getter
    int      width = aItem->m_Size.x;   // TODO(JE) Getter
    bool     isFilled = true;
    COLOR4D  color;
    // TODO(JE) This doesn't actually work properly for ImageNegative
    bool     isNegative = ( aItem->GetLayerPolarity() ^ aItem->m_GerberImageFile->m_ImageNegative );

    // Draw DCODE overlay text
    if( IsDCodeLayer( aLayer ) )
    {
        wxString codeText;
        VECTOR2D textPosition;
        double textSize;
        double orient;

        if( !aItem->GetTextD_CodePrms( textSize, textPosition, orient ) )
            return;

        color = m_gerbviewSettings.GetColor( aItem, aLayer );
        codeText.Printf( "D%d", aItem->m_DCode );

        m_gal->SetIsStroke( true );
        m_gal->SetIsFill( false );
        m_gal->SetStrokeColor( color );
        m_gal->SetFillColor( COLOR4D( 0, 0, 0, 0 ) );
        m_gal->SetLineWidth( textSize/10 );
        m_gal->SetFontBold( false );
        m_gal->SetFontItalic( false );
        m_gal->SetTextMirrored( false );
        m_gal->SetGlyphSize( VECTOR2D( textSize, textSize) );
        m_gal->SetHorizontalJustify( GR_TEXT_HJUSTIFY_CENTER );
        m_gal->SetVerticalJustify( GR_TEXT_VJUSTIFY_CENTER );
        m_gal->BitmapText( codeText, textPosition, orient );

        return;
    }

    color = m_gerbviewSettings.GetColor( aItem, aLayer );

    // TODO: Should brightened color be a preference?
    if( aItem->IsBrightened() )
        color = COLOR4D( 0.0, 1.0, 0.0, 0.75 );

    m_gal->SetNegativeDrawMode( isNegative );
    m_gal->SetStrokeColor( color );
    m_gal->SetFillColor( color );
    m_gal->SetIsFill( isFilled );
    m_gal->SetIsStroke( !isFilled );

    switch( aItem->m_Shape )
    {
    case GBR_POLYGON:
    {
        isFilled = m_gerbviewSettings.m_polygonFill;
        m_gal->SetIsFill( isFilled );
        m_gal->SetIsStroke( !isFilled );

        if( isNegative && !isFilled )
        {
            m_gal->SetNegativeDrawMode( false );
            m_gal->SetStrokeColor( GetSettings()->GetColor( aItem, aLayer ) );
        }

        if( !isFilled )
            m_gal->SetLineWidth( m_gerbviewSettings.m_outlineWidth );

        std::vector<VECTOR2I> pts = aItem->m_Polygon.COutline( 0 ).CPoints();

        for( auto& pt : pts )
            pt = aItem->GetABPosition( pt );


        SHAPE_POLY_SET   absolutePolygon;
        SHAPE_LINE_CHAIN chain( pts );
        chain.SetClosed( true );
        absolutePolygon.AddOutline( chain );

        // Degenerated polygons (having < 3 points) are drawn as lines
        // to avoid issues in draw polygon functions
        if( !isFilled || absolutePolygon.COutline( 0 ).PointCount() < 3 )
            m_gal->DrawPolyline( absolutePolygon.COutline( 0 ) );
        else
        {
            // On Opengl, a not convex filled polygon is usually drawn by using triangles as primitives.
            // CacheTriangulation() can create basic triangle primitives to draw the polygon solid shape
            // on Opengl
            if( m_gal->IsOpenGlEngine() )
                absolutePolygon.CacheTriangulation();

            m_gal->DrawPolygon( absolutePolygon );
        }

        break;
    }

    case GBR_CIRCLE:
    {
        isFilled = m_gerbviewSettings.m_lineFill;
        double radius = GetLineLength( aItem->m_Start, aItem->m_End );
        m_gal->DrawCircle( start, radius );
        break;
    }

    case GBR_ARC:
    {
        isFilled = m_gerbviewSettings.m_lineFill;

        // These are swapped because wxDC fills arcs counterclockwise and GAL
        // fills them clockwise.
        wxPoint arcStart = aItem->m_End;
        wxPoint arcEnd = aItem->m_Start;

        // Gerber arcs are 3-point (start, center, end)
        // GAL needs center, radius, start angle, end angle
        double   radius = GetLineLength( arcStart, aItem->m_ArcCentre );
        VECTOR2D center = aItem->GetABPosition( aItem->m_ArcCentre );
        VECTOR2D startVec = VECTOR2D( aItem->GetABPosition( arcStart ) ) - center;
        VECTOR2D endVec = VECTOR2D( aItem->GetABPosition( arcEnd ) ) - center;

        m_gal->SetIsFill( isFilled );
        m_gal->SetIsStroke( !isFilled );
        m_gal->SetLineWidth( isFilled ? width : m_gerbviewSettings.m_outlineWidth );

        double startAngle = startVec.Angle();
        double endAngle = endVec.Angle();

        // GAL fills in direction of increasing angle, so we have to convert
        // the angle from the -PI to PI domain of atan2() to ensure that
        // the arc goes in the right direction
        if( startAngle > endAngle )
            endAngle += (2 * M_PI);

        // In Gerber, 360-degree arcs are stored in the file with start equal to end
        if( arcStart == arcEnd )
        {
            endAngle =  startAngle + 2*M_PI;
        }

        m_gal->DrawArcSegment( center, radius, startAngle, endAngle, width );

#if 0   // Arc Debugging only
        m_gal->SetIsFill( false );
        m_gal->SetIsStroke( true );
        m_gal->SetLineWidth( 5 );
        m_gal->SetStrokeColor( COLOR4D( 0.1, 0.5, 0.0, 0.5 ) );
        m_gal->DrawLine( center, aItem->GetABPosition( arcStart ) );
        m_gal->SetStrokeColor( COLOR4D( 0.6, 0.1, 0.0, 0.5 ) );
        m_gal->DrawLine( center, aItem->GetABPosition( arcEnd ) );
#endif

#if 0   // Bbox arc Debugging only
        m_gal->SetIsFill( false );
        m_gal->SetIsStroke( true );
        EDA_RECT box = aItem->GetBoundingBox();
        m_gal->SetLineWidth( 5 );
        m_gal->SetStrokeColor( COLOR4D(0.9, 0.9, 0, 0.4) );
        // box coordinates are already in AB position.
        m_gal->DrawRectangle( box.GetOrigin(), box.GetEnd() );
#endif
        break;
    }

    case GBR_SPOT_CIRCLE:
    case GBR_SPOT_RECT:
    case GBR_SPOT_OVAL:
    case GBR_SPOT_POLY:
    case GBR_SPOT_MACRO:
    {
        isFilled = m_gerbviewSettings.m_spotFill;
        drawFlashedShape( aItem, isFilled );
        break;
    }

    case GBR_SEGMENT:
    {
        /* Plot a line from m_Start to m_End.
         * Usually, a round pen is used, but some gerber files use a rectangular pen
         * In fact, any aperture can be used to plot a line.
         * currently: only a square pen is handled (I believe using a polygon gives a strange plot).
         */
        isFilled = m_gerbviewSettings.m_lineFill;
        m_gal->SetIsFill( isFilled );
        m_gal->SetIsStroke( !isFilled );

        if( isNegative && !isFilled )
            m_gal->SetStrokeColor( GetSettings()->GetColor( aItem, aLayer ) );

        // TODO(JE) Refactor this to allow const aItem
        D_CODE* code = aItem->GetDcodeDescr();
        if( code && code->m_Shape == APT_RECT )
        {
            if( aItem->m_Polygon.OutlineCount() == 0 )
                aItem->ConvertSegmentToPolygon();

            drawPolygon( aItem, aItem->m_Polygon, isFilled );
        }
        else
        {
            if( !isFilled )
                m_gal->SetLineWidth( m_gerbviewSettings.m_outlineWidth );

            m_gal->DrawSegment( start, end, width );
        }
        break;
    }

    default:
        wxASSERT_MSG( false, "GERBER_DRAW_ITEM shape is unknown!" );
        break;
    }

    // Enable for bounding box debugging
    #if 0
    const BOX2I& bb = aItem->ViewBBox();
    m_gal->SetIsStroke( true );
    m_gal->SetIsFill( true );
    m_gal->SetLineWidth( 3 );
    m_gal->SetStrokeColor( COLOR4D(0.9, 0.9, 0, 0.4) );
    m_gal->SetFillColor( COLOR4D(0.9, 0.9, 0, 0.1) );
    m_gal->DrawRectangle( bb.GetOrigin(), bb.GetEnd() );
    #endif
}


void GERBVIEW_PAINTER::drawPolygon(
        GERBER_DRAW_ITEM* aParent, const SHAPE_POLY_SET& aPolygon, bool aFilled, bool aShift )
{
    wxASSERT( aPolygon.OutlineCount() == 1 );

    if( aPolygon.OutlineCount() == 0 )
        return;

    SHAPE_POLY_SET poly;
    poly.NewOutline();
    const std::vector<VECTOR2I> pts = aPolygon.COutline( 0 ).CPoints();
    VECTOR2I       offset = aShift ? VECTOR2I( aParent->m_Start ) : VECTOR2I( 0, 0 );

    for( auto& pt : pts )
        poly.Append( aParent->GetABPosition( pt + offset ) );

    if( !m_gerbviewSettings.m_polygonFill )
        m_gal->SetLineWidth( m_gerbviewSettings.m_outlineWidth );

    if( !aFilled )
    {
        m_gal->DrawPolyline( poly.COutline( 0 ) );
    }
    else
        m_gal->DrawPolygon( poly );
}


void GERBVIEW_PAINTER::drawFlashedShape( GERBER_DRAW_ITEM* aItem, bool aFilled )
{
    D_CODE* code = aItem->GetDcodeDescr();

    wxASSERT_MSG( code, "drawFlashedShape: Item has no D_CODE!" );

    if( !code )
        return;

    m_gal->SetIsFill( aFilled );
    m_gal->SetIsStroke( !aFilled );
    m_gal->SetLineWidth( m_gerbviewSettings.m_outlineWidth );

    switch( aItem->m_Shape )
    {
    case GBR_SPOT_CIRCLE:
    {
        int radius = code->m_Size.x >> 1;
        VECTOR2D start( aItem->GetABPosition( aItem->m_Start ) );

        if( !aFilled || code->m_DrillShape == APT_DEF_NO_HOLE )
        {
            m_gal->DrawCircle( start, radius );
        }
        else    // rectangular hole
        {
            if( code->m_Polygon.OutlineCount() == 0 )
                code->ConvertShapeToPolygon();

            drawPolygon( aItem, code->m_Polygon, aFilled, true );
        }

        break;
    }

    case GBR_SPOT_RECT:
    {
        wxPoint codeStart;
        wxPoint aShapePos = aItem->m_Start;
        codeStart.x = aShapePos.x - code->m_Size.x / 2;
        codeStart.y = aShapePos.y - code->m_Size.y / 2;
        wxPoint codeEnd = codeStart + code->m_Size;
        codeStart = aItem->GetABPosition( codeStart );
        codeEnd = aItem->GetABPosition( codeEnd );

        if( !aFilled || code->m_DrillShape == APT_DEF_NO_HOLE  )
        {
            m_gal->DrawRectangle( VECTOR2D( codeStart ), VECTOR2D( codeEnd ) );
        }
        else
        {
            if( code->m_Polygon.OutlineCount() == 0 )
                code->ConvertShapeToPolygon();

            drawPolygon( aItem, code->m_Polygon, aFilled, true );
        }
        break;
    }

    case GBR_SPOT_OVAL:
    {
        int radius = 0;

        wxPoint codeStart = aItem->m_Start;
        wxPoint codeEnd = aItem->m_Start;

        if( code->m_Size.x > code->m_Size.y )   // horizontal oval
        {
            int delta = (code->m_Size.x - code->m_Size.y) / 2;
            codeStart.x -= delta;
            codeEnd.x   += delta;
            radius   = code->m_Size.y;
        }
        else   // horizontal oval
        {
            int delta = (code->m_Size.y - code->m_Size.x) / 2;
            codeStart.y -= delta;
            codeEnd.y   += delta;
            radius   = code->m_Size.x;
        }

        codeStart = aItem->GetABPosition( codeStart );
        codeEnd = aItem->GetABPosition( codeEnd );

        if( !aFilled || code->m_DrillShape == APT_DEF_NO_HOLE )
        {
            m_gal->DrawSegment( codeStart, codeEnd, radius );
        }
        else
        {
            if( code->m_Polygon.OutlineCount() == 0 )
                code->ConvertShapeToPolygon();

            drawPolygon( aItem, code->m_Polygon, aFilled, true );
        }
        break;
    }

    case GBR_SPOT_POLY:
    {
        if( code->m_Polygon.OutlineCount() == 0 )
            code->ConvertShapeToPolygon();

        drawPolygon( aItem, code->m_Polygon, aFilled, true );
        break;
    }

    case GBR_SPOT_MACRO:
        drawApertureMacro( aItem, aFilled );
        break;

    default:
        wxASSERT_MSG( false, wxT( "Unknown Gerber flashed shape!" ) );
        break;
    }
}


void GERBVIEW_PAINTER::drawApertureMacro( GERBER_DRAW_ITEM* aParent, bool aFilled )
{
    D_CODE* code = aParent->GetDcodeDescr();
    APERTURE_MACRO* macro = code->GetMacro();

    SHAPE_POLY_SET* macroShape = macro->GetApertureMacroShape( aParent, aParent->m_Start );

    if( !m_gerbviewSettings.m_polygonFill )
        m_gal->SetLineWidth( m_gerbviewSettings.m_outlineWidth );

    if( !aFilled )
    {
        for( int i = 0; i < macroShape->OutlineCount(); i++ )
            m_gal->DrawPolyline( macroShape->COutline( i ) );
    }
    else
        m_gal->DrawPolygon( *macroShape );
}


const double GERBVIEW_RENDER_SETTINGS::MAX_FONT_SIZE = Millimeter2iu( 10.0 );