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

#include <algorithm>

#include <confirm.h>
#include <trigo.h>
#include <pcb_base_frame.h>
#include <base_units.h>
#include <widgets/wx_grid.h>
#include <widgets/std_bitmap_button.h>
#include <footprint.h>
#include <math/util.h>      // for KiROUND

#include <bitmaps.h>
#include <wx/dcclient.h>

#include <dialog_pad_properties.h>
#include <dialog_pad_primitives_properties.h>

DIALOG_PAD_PRIMITIVES_PROPERTIES::DIALOG_PAD_PRIMITIVES_PROPERTIES( wxWindow* aParent,
                                                                    PCB_BASE_FRAME* aFrame,
                                                                    PCB_SHAPE* aShape ) :
        DIALOG_PAD_PRIMITIVES_PROPERTIES_BASE( aParent ),
        m_shape( aShape ),
        m_startX( aFrame, m_startXLabel, m_startXCtrl, m_startXUnits ),
        m_startY( aFrame, m_startYLabel, m_startYCtrl, m_startYUnits ),
        m_ctrl1X( aFrame, m_ctrl1XLabel, m_ctrl1XCtrl, m_ctrl1XUnits ),
        m_ctrl1Y( aFrame, m_ctrl1YLabel, m_ctrl1YCtrl, m_ctrl1YUnits ),
        m_ctrl2X( aFrame, m_ctrl2XLabel, m_ctrl2XCtrl, m_ctrl2XUnits ),
        m_ctrl2Y( aFrame, m_ctrl2YLabel, m_ctrl2YCtrl, m_ctrl2YUnits ),
        m_endX( aFrame, m_endXLabel, m_endXCtrl, m_endXUnits ),
        m_endY( aFrame, m_endYLabel, m_endYCtrl, m_endYUnits ),
        m_radius( aFrame, m_radiusLabel, m_radiusCtrl, m_radiusUnits ),
        m_thickness( aFrame, m_thicknessLabel, m_thicknessCtrl, m_thicknessUnits )
{
    SetInitialFocus( m_startXCtrl );

    TransferDataToWindow();

    SetupStandardButtons();

    finishDialogSettings();
}


bool DIALOG_PAD_PRIMITIVES_PROPERTIES::TransferDataToWindow()
{
    if( m_shape == nullptr )
        return false;

    m_thickness.SetValue( m_shape->GetWidth() );
    m_filledCtrl->SetValue( m_shape->IsFilled() );

    switch( m_shape->GetShape() )
    {
    case SHAPE_T::SEGMENT:
        SetTitle( _( "Segment" ) );
        m_startX.SetValue( m_shape->GetStart().x );
        m_startY.SetValue( m_shape->GetStart().y );
        m_endX.SetValue( m_shape->GetEnd().x );
        m_endY.SetValue( m_shape->GetEnd().y );
        m_ctrl1X.Show( false, true );
        m_ctrl1Y.Show( false, true );
        m_ctrl2X.Show( false, true );
        m_ctrl2Y.Show( false, true );
        m_staticTextPosCtrl1->Show( false );
        m_staticTextPosCtrl1->SetSize( 0, 0 );
        m_staticTextPosCtrl2->Show( false );
        m_staticTextPosCtrl2->SetSize( 0, 0 );
        m_radius.Show( false );
        m_filledCtrl->Show( false );
        break;

    case SHAPE_T::BEZIER:
        SetTitle( _( "Bezier" ) );
        m_startX.SetValue( m_shape->GetStart().x );
        m_startY.SetValue( m_shape->GetStart().y );
        m_endX.SetValue( m_shape->GetEnd().x );
        m_endY.SetValue( m_shape->GetEnd().y );
        m_ctrl1X.SetValue( m_shape->GetBezierC1().x );
        m_ctrl1Y.SetValue( m_shape->GetBezierC1().y );
        m_ctrl2X.SetValue( m_shape->GetBezierC2().x );
        m_ctrl2Y.SetValue( m_shape->GetBezierC2().y );
        m_radius.Show( false );
        m_filledCtrl->Show( false );
        break;

    case SHAPE_T::ARC:
        SetTitle( _( "Arc" ) );
        m_startX.SetValue( m_shape->GetStart().x );
        m_startY.SetValue( m_shape->GetStart().y );
        m_staticTextPosEnd->SetLabel( _( "Center Point:" ) );
        m_endX.SetValue( m_shape->GetCenter().x );
        m_endY.SetValue( m_shape->GetCenter().y );
        m_radiusLabel->SetLabel( _( "Angle:" ) );
        m_radius.SetUnits( EDA_UNITS::DEGREES );
        m_radius.SetAngleValue( m_shape->GetArcAngle() );
        m_ctrl1X.Show( false, true );
        m_ctrl1Y.Show( false, true );
        m_ctrl2X.Show( false, true );
        m_ctrl2Y.Show( false, true );
        m_staticTextPosCtrl1->Show( false );
        m_staticTextPosCtrl1->SetSize( 0, 0 );
        m_staticTextPosCtrl2->Show( false );
        m_staticTextPosCtrl2->SetSize( 0, 0 );
        m_filledCtrl->Show( false );
        break;

    case SHAPE_T::CIRCLE:
        if( m_shape->GetWidth() )
            SetTitle( _( "Ring" ) );
        else
            SetTitle( _( "Circle" ) );

        // End point does not exist for a circle or ring:
        m_staticTextPosEnd->Show( false );
        m_endX.Show( false );
        m_endY.Show( false );

        // Circle center uses position controls:
        m_staticTextPosStart->SetLabel( _( "Center Point:" ) );
        m_startX.SetValue( m_shape->GetStart().x );
        m_startY.SetValue( m_shape->GetStart().y );
        m_radius.SetValue( m_shape->GetRadius() );
        m_ctrl1X.Show( false, true );
        m_ctrl1Y.Show( false, true );
        m_ctrl2X.Show( false, true );
        m_ctrl2Y.Show( false, true );
        m_staticTextPosCtrl1->Show( false );
        m_staticTextPosCtrl1->SetSize( 0, 0 );
        m_staticTextPosCtrl2->Show( false );
        m_staticTextPosCtrl2->SetSize( 0, 0 );
        m_filledCtrl->Show( true );
        break;

    case SHAPE_T::RECT:
        if( m_shape->IsAnnotationProxy() )
            SetTitle( _( "Number Box" ) );
        else
            SetTitle( _( "Rectangle" ) );

        m_startX.SetValue( m_shape->GetStart().x );
        m_startY.SetValue( m_shape->GetStart().y );
        m_endX.SetValue( m_shape->GetEnd().x );
        m_endY.SetValue( m_shape->GetEnd().y );
        m_ctrl1X.Show( false, true );
        m_ctrl1Y.Show( false, true );
        m_ctrl2X.Show( false, true );
        m_ctrl2Y.Show( false, true );
        m_staticTextPosCtrl1->Show( false );
        m_staticTextPosCtrl1->SetSize( 0, 0 );
        m_staticTextPosCtrl2->Show( false );
        m_staticTextPosCtrl2->SetSize( 0, 0 );
        m_radius.Show( false );
        m_filledCtrl->Show( false );
        break;

    case SHAPE_T::POLY:
        // polygon has a specific dialog editor. So nothing here
        break;

    default:
        SetTitle( "Unknown Basic Shape" );
        break;
    }

    return true;
}


bool DIALOG_PAD_PRIMITIVES_PROPERTIES::TransferDataFromWindow()
{
    if( m_thickness.GetValue() == 0 && !m_filledCtrl->GetValue() )
    {
        DisplayError( this, _( "Line width may not be 0 for unfilled shapes." ) );
        m_thicknessCtrl->SetFocus();
        return false;
    }

    // Transfer data out of the GUI.
    STROKE_PARAMS stroke = m_shape->GetStroke();
    stroke.SetWidth( m_thickness.GetValue() );
    m_shape->SetStroke( stroke );

    m_shape->SetFilled( m_filledCtrl->GetValue() );

    switch( m_shape->GetShape() )
    {
    case SHAPE_T::SEGMENT:
    case SHAPE_T::RECT:
        m_shape->SetStart( VECTOR2I( m_startX.GetValue(), m_startY.GetValue() ) );
        m_shape->SetEnd( VECTOR2I( m_endX.GetValue(), m_endY.GetValue() ) );
        break;

    case SHAPE_T::BEZIER:
        m_shape->SetStart( VECTOR2I( m_startX.GetValue(), m_startY.GetValue() ) );
        m_shape->SetEnd( VECTOR2I( m_endX.GetValue(), m_endY.GetValue() ) );
        m_shape->SetBezierC1( VECTOR2I( m_ctrl1X.GetValue(), m_ctrl1Y.GetValue()));
        m_shape->SetBezierC2( VECTOR2I( m_ctrl2X.GetValue(), m_ctrl2Y.GetValue()));
        break;

    case SHAPE_T::ARC:
        m_shape->SetCenter( VECTOR2I( m_endX.GetValue(), m_endY.GetValue() ) );
        m_shape->SetStart( VECTOR2I( m_startX.GetValue(), m_startY.GetValue() ) );
        m_shape->SetArcAngleAndEnd( m_radius.GetAngleValue() );
        break;

    case SHAPE_T::CIRCLE:
        m_shape->SetStart( VECTOR2I( m_startX.GetValue(), m_startY.GetValue() ) );
        m_shape->SetEnd( m_shape->GetStart() + VECTOR2I( m_radius.GetValue(), 0 ) );
        break;

    case SHAPE_T::POLY:
        // polygon has a specific dialog editor. So nothing here
        break;

    default:
        break;
    }

    return true;
}


DIALOG_PAD_PRIMITIVE_POLY_PROPS::DIALOG_PAD_PRIMITIVE_POLY_PROPS( wxWindow* aParent,
                                                                  PCB_BASE_FRAME* aFrame,
                                                                  PCB_SHAPE* aShape ) :
        DIALOG_PAD_PRIMITIVE_POLY_PROPS_BASE( aParent ),
        m_shape( aShape ),
        m_thickness( aFrame, m_thicknessLabel, m_thicknessCtrl, m_thicknessUnits )
{
    if( !m_shape->GetPolyShape().IsEmpty() )
    {
        for( const VECTOR2I& pt : m_shape->GetPolyShape().Outline( 0 ).CPoints() )
            m_currPoints.emplace_back( pt );
    }

    m_addButton->SetBitmap( KiBitmap( BITMAPS::small_plus ) );
    m_deleteButton->SetBitmap( KiBitmap( BITMAPS::small_trash ) );
    m_warningIcon->SetBitmap( KiBitmap( BITMAPS::dialog_warning ) );

    m_gridCornersList->SetUnitsProvider( aFrame );
    m_gridCornersList->SetAutoEvalCols( { 0, 1 } );

    m_gridCornersList->SetDefaultRowSize( m_gridCornersList->GetDefaultRowSize() + 4 );

    // Test for acceptable polygon (more than 2 corners, and not self-intersecting) and
    // remove any redundant corners.  A warning message is displayed if not OK.
    doValidate( true );

    TransferDataToWindow();

    SetupStandardButtons();

	m_gridCornersList->Connect( wxEVT_GRID_CELL_CHANGING,
                                wxGridEventHandler( DIALOG_PAD_PRIMITIVE_POLY_PROPS::onCellChanging ),
                                nullptr, this );

    // Now all widgets have the size fixed, call FinishDialogSettings
    finishDialogSettings();
}


DIALOG_PAD_PRIMITIVE_POLY_PROPS::~DIALOG_PAD_PRIMITIVE_POLY_PROPS()
{
	m_gridCornersList->Disconnect( wxEVT_GRID_CELL_CHANGING,
                                   wxGridEventHandler( DIALOG_PAD_PRIMITIVE_POLY_PROPS::onCellChanging ),
                                   nullptr, this );
}


bool DIALOG_PAD_PRIMITIVE_POLY_PROPS::TransferDataToWindow()
{
    if( m_shape == nullptr )
        return false;

    m_thickness.SetValue( m_shape->GetWidth() );
    m_filledCtrl->SetValue( m_shape->IsFilled() );

    // Populates the list of corners
    int extra_rows = m_currPoints.size() - m_gridCornersList->GetNumberRows();

    if( extra_rows > 0 )
    {
        m_gridCornersList->AppendRows( extra_rows );
    }
    else if( extra_rows < 0 )
    {
        extra_rows = -extra_rows;
        m_gridCornersList->DeleteRows( 0, extra_rows );
    }

    // enter others corner coordinates
    for( unsigned row = 0; row < m_currPoints.size(); ++row )
    {
        // Row label is "Corner x"
        m_gridCornersList->SetRowLabelValue( row, wxString::Format( _( "Corner %d" ), row+1 ) );

        m_gridCornersList->SetUnitValue( row, 0, m_currPoints[row].x );
        m_gridCornersList->SetUnitValue( row, 1, m_currPoints[row].y );
    }

    return true;
}

bool DIALOG_PAD_PRIMITIVE_POLY_PROPS::TransferDataFromWindow()
{
    if( !Validate() )
        return false;

    m_shape->SetPolyPoints( m_currPoints );

    STROKE_PARAMS stroke = m_shape->GetStroke();
    stroke.SetWidth( m_thickness.GetValue() );
    m_shape->SetStroke( stroke );

    m_shape->SetFilled( m_filledCtrl->GetValue() );

    return true;
}


bool DIALOG_PAD_PRIMITIVE_POLY_PROPS::Validate()
{
    // Don't remove redundant corners while user is editing corner list
    return doValidate( false );
}


bool DIALOG_PAD_PRIMITIVE_POLY_PROPS::doValidate( bool aRemoveRedundantCorners )
{
    if( !m_gridCornersList->CommitPendingChanges() )
        return false;

    if( m_currPoints.size() < 3 )
    {
        m_warningText->SetLabel( _("Polygon must have at least 3 corners" ) );
        m_warningText->Show( true );
        m_warningIcon->Show( true );
        return false;
    }

    bool valid = true;

    SHAPE_LINE_CHAIN polyline( m_currPoints, true );

    // Remove redundant corners:
    polyline.Simplify();

    if(  polyline.PointCount() < 3 )
    {
        m_warningText->SetLabel( _( "Polygon must have at least 3 corners after simplification" ) );
        valid = false;
    }

    if( valid && polyline.SelfIntersecting() )
    {
        m_warningText->SetLabel( _( "Polygon can not be self-intersecting" ) );
        valid = false;
    }

    m_warningIcon->Show( !valid );
    m_warningText->Show( !valid );

    if( aRemoveRedundantCorners )
    {
        if( polyline.PointCount() != (int) m_currPoints.size() )
        {   // Happens after simplification
            m_currPoints.clear();

            for( const VECTOR2I& pt : polyline.CPoints() )
                m_currPoints.emplace_back( pt );

            m_warningIcon->Show( true );
            m_warningText->Show( true );
            m_warningText->SetLabel( _( "Note: redundant corners removed" ) );
        }
    }

    return valid;
}


void DIALOG_PAD_PRIMITIVE_POLY_PROPS::OnButtonAdd( wxCommandEvent& event )
{
    if( !m_gridCornersList->CommitPendingChanges() )
        return;

    // Insert a new corner after the currently selected:
    wxArrayInt selections =	m_gridCornersList->GetSelectedRows();
    int row = -1;

    if( m_gridCornersList->GetNumberRows() == 0 )
        row = 0;
    else if( selections.size() > 0 )
        row = selections[ selections.size() - 1 ] + 1;
    else
        row = m_gridCornersList->GetGridCursorRow() + 1;

    if( row < 0 )
    {
        wxMessageBox( _( "Select a corner to add the new corner after." ) );
        return;
    }

    if( m_currPoints.size() == 0 || row >= (int) m_currPoints.size() )
        m_currPoints.emplace_back( 0, 0 );
    else
        m_currPoints.insert( m_currPoints.begin() + row, wxPoint( 0, 0 ) );

    Validate();
    TransferDataToWindow();

    m_gridCornersList->ForceRefresh();
    // Select the new row
    m_gridCornersList->SelectRow( row, false );

    m_panelPoly->Refresh();
}


void DIALOG_PAD_PRIMITIVE_POLY_PROPS::OnButtonDelete( wxCommandEvent& event )
{
    if( !m_gridCornersList->CommitPendingChanges() )
        return;

    wxArrayInt selections =	m_gridCornersList->GetSelectedRows();

    if( m_gridCornersList->GetNumberRows() == 0 )
        return;

    if( selections.size() == 0 && m_gridCornersList->GetGridCursorRow() >= 0 )
        selections.push_back( m_gridCornersList->GetGridCursorRow() );

    if( selections.size() == 0 )
    {
        wxMessageBox( _( "Select a corner to delete." ) );
        return;
    }

    // remove corners:
    std::sort( selections.begin(), selections.end() );

    for( int ii = selections.size()-1; ii >= 0 ; --ii )
        m_currPoints.erase( m_currPoints.begin() + selections[ii] );

    Validate();
    TransferDataToWindow();

    m_gridCornersList->ForceRefresh();

    // select the row previous to the last deleted row
    m_gridCornersList->SelectRow( std::max( 0, selections[ 0 ] - 1 ) );

    m_panelPoly->Refresh();
}


void DIALOG_PAD_PRIMITIVE_POLY_PROPS::onPaintPolyPanel( wxPaintEvent& event )
{
    wxPaintDC dc( m_panelPoly );
    wxSize dc_size = dc.GetSize();
    dc.SetDeviceOrigin( dc_size.x / 2, dc_size.y / 2 );

    // Calculate a suitable scale to fit the available draw area
    int minsize( pcbIUScale.mmToIU( 0.5 ) );

    for( unsigned ii = 0; ii < m_currPoints.size(); ++ii )
    {
        minsize = std::max( minsize, std::abs( m_currPoints[ii].x ) );
        minsize = std::max( minsize, std::abs( m_currPoints[ii].y ) );
    }

    // The draw origin is the center of the window.
    // Therefore the window size is twice the minsize just calculated
    minsize *= 2;
    minsize += m_thickness.GetValue();

    // Give a margin
    double scale = std::min( double( dc_size.x ) / minsize, double( dc_size.y ) / minsize ) * 0.9;

    GRResetPenAndBrush( &dc );

    // Draw X and Y axis. This is particularly useful to show the
    // reference position of basic shape
    // Axis are drawn before the polygon to avoid masking segments on axis
    GRLine( &dc, -dc_size.x, 0, dc_size.x, 0, 0, LIGHTBLUE );   // X axis
    GRLine( &dc, 0, -dc_size.y, 0, dc_size.y, 0, LIGHTBLUE );   // Y axis

    // Draw polygon.
    // The selected edge(s) are shown in selectcolor, the others in normalcolor.
    EDA_COLOR_T normalcolor = WHITE;
    EDA_COLOR_T selectcolor = RED;

    for( unsigned ii = 0; ii < m_currPoints.size(); ++ii )
    {
        EDA_COLOR_T color = normalcolor;

        if( m_gridCornersList->IsInSelection (ii, 0) ||
            m_gridCornersList->IsInSelection (ii, 1) ||
            m_gridCornersList->GetGridCursorRow() == (int)ii )
            color = selectcolor;

        unsigned jj = ii + 1;

        if( jj >= m_currPoints.size() )
            jj = 0;

        GRLine( &dc, m_currPoints[ii].x * scale, m_currPoints[ii].y * scale,
                m_currPoints[jj].x * scale, m_currPoints[jj].y * scale,
                m_thickness.GetValue() * scale, color );
    }

    event.Skip();
}


void DIALOG_PAD_PRIMITIVE_POLY_PROPS::onPolyPanelResize( wxSizeEvent& event )
{
    m_panelPoly->Refresh();
    event.Skip();
}


void DIALOG_PAD_PRIMITIVE_POLY_PROPS::onGridSelect( wxGridRangeSelectEvent& event )
{
    m_panelPoly->Refresh();
}


void DIALOG_PAD_PRIMITIVE_POLY_PROPS::onCellChanging( wxGridEvent& event )
{
    int row = event.GetRow();
    int col = event.GetCol();

    CallAfter(
            [this, row, col]()
            {
                if( col == 0 )  // Set the X value
                    m_currPoints[row].x = m_gridCornersList->GetUnitValue( row, col );
                else            // Set the Y value
                    m_currPoints[row].y = m_gridCornersList->GetUnitValue( row, col );

                Validate();

                m_panelPoly->Refresh();
            } );
}


DIALOG_PAD_PRIMITIVES_TRANSFORM::DIALOG_PAD_PRIMITIVES_TRANSFORM( wxWindow* aParent,
                                                                  PCB_BASE_FRAME* aFrame,
                                                                  std::vector<std::shared_ptr<PCB_SHAPE>>& aList,
                                                                  bool aShowDuplicate ) :
    DIALOG_PAD_PRIMITIVES_TRANSFORM_BASE( aParent ),
    m_list( aList ),
    m_vectorX( aFrame, m_xLabel, m_xCtrl, m_xUnits ),
    m_vectorY( aFrame, m_yLabel, m_yCtrl, m_yUnits ),
    m_rotation( aFrame, m_rotationLabel, m_rotationCtrl, m_rotationUnits )
{
    m_rotation.SetUnits( EDA_UNITS::DEGREES );

    if( !aShowDuplicate )     // means no duplicate transform
    {
		m_staticTextDupCnt->Show( false );
		m_spinCtrlDuplicateCount->Show( false );
    }

    SetupStandardButtons();

    GetSizer()->SetSizeHints( this );
}


void DIALOG_PAD_PRIMITIVES_TRANSFORM::Transform( std::vector<std::shared_ptr<PCB_SHAPE>>* aList,
                                                 int aDuplicateCount )
{
    VECTOR2I  move_vect( m_vectorX.GetValue(), m_vectorY.GetValue() );
    EDA_ANGLE rotation = m_rotation.GetAngleValue();
    double    scale = EDA_UNIT_UTILS::UI::DoubleValueFromString( m_scaleCtrl->GetValue() );

    // Avoid too small / too large scale, which could create issues:
    if( scale < 0.01 )
        scale = 0.01;

    if( scale > 100.0 )
        scale = 100.0;

    // Transform shapes
    // shapes are scaled, then moved then rotated.
    // if aList != NULL, the initial shape will be duplicated, and transform
    // applied to the duplicated shape

    VECTOR2I  currMoveVect = move_vect;
    EDA_ANGLE curr_rotation = rotation;

    do {
        for( unsigned idx = 0; idx < m_list.size(); ++idx )
        {
            std::shared_ptr<PCB_SHAPE> shape;

            if( aList == nullptr )
            {
                shape = m_list[idx];
            }
            else
            {
                aList->emplace_back( std::make_shared<PCB_SHAPE>( *m_list[idx] ) );
                shape = aList->back();
            }

            // Transform parameters common to all shape types (some can be unused)
            STROKE_PARAMS stroke = shape->GetStroke();
            stroke.SetWidth( KiROUND( shape->GetWidth() * scale ) );
            shape->SetStroke( stroke );

            shape->Move( currMoveVect );
            shape->Scale( scale );
            shape->Rotate( VECTOR2I( 0, 0 ), curr_rotation );
        }

        // Prepare new transform on duplication:
        // Each new item is rotated (or moved) by the transform from the last duplication
        curr_rotation += rotation;
        currMoveVect += move_vect;
    } while( aList && --aDuplicateCount > 0 );
}

