/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2019 CERN
 * Copyright The 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 "symbol_editor_edit_tool.h"

#include <tool/picker_tool.h>
#include <tools/sch_selection_tool.h>
#include <tools/sch_tool_utils.h>
#include <tools/symbol_editor_pin_tool.h>
#include <tools/symbol_editor_drawing_tools.h>
#include <tools/symbol_editor_move_tool.h>
#include <clipboard.h>
#include <sch_actions.h>
#include <increment.h>
#include <pin_layout_cache.h>
#include <string_utils.h>
#include <symbol_edit_frame.h>
#include <sch_commit.h>
#include <dialogs/dialog_shape_properties.h>
#include <dialogs/dialog_text_properties.h>
#include <dialogs/dialog_field_properties.h>
#include <dialogs/dialog_lib_symbol_properties.h>
#include <dialogs/dialog_lib_edit_pin_table.h>
#include <dialogs/dialog_update_symbol_fields.h>
#include <view/view_controls.h>
#include <view/view.h>
#include <richio.h>
#include <sch_io/kicad_sexpr/sch_io_kicad_sexpr.h>
#include <sch_textbox.h>
#include <lib_symbol_library_manager.h>
#include <widgets/lib_tree.h>
#include <wx/textdlg.h>     // for wxTextEntryDialog
#include <math/util.h>      // for KiROUND
#include <io/kicad/kicad_io_utils.h>
#include <trace_helpers.h>
#include <plotters/plotters_pslike.h>
#include <sch_painter.h>
#include <sch_plotter.h>
#include <locale_io.h>
#include <gal/gal_print.h>
#include <gal/graphics_abstraction_layer.h>
#include <zoom_defines.h>
#include <wx/ffile.h>
#include <wx/mstream.h>
#include <wx/dcmemory.h>


namespace
{
constexpr int    clipboardMaxBitmapSize = 4096;
constexpr double clipboardBboxInflation = 0.02;


void appendMimeData( std::vector<CLIPBOARD_MIME_DATA>& aMimeData, const wxString& aMimeType,
                     const wxMemoryBuffer& aBuffer )
{
    if( aBuffer.GetDataLen() == 0 )
        return;

    CLIPBOARD_MIME_DATA entry;
    entry.m_mimeType = aMimeType;
    entry.m_data = aBuffer;
    aMimeData.push_back( entry );
}


void appendMimeData( std::vector<CLIPBOARD_MIME_DATA>& aMimeData, const wxString& aMimeType,
                     wxImage&& aImage )
{
    if( !aImage.IsOk() )
        return;

    CLIPBOARD_MIME_DATA entry;
    entry.m_mimeType = aMimeType;
    entry.m_image = std::move( aImage );
    aMimeData.push_back( std::move( entry ) );
}


bool loadFileToBuffer( const wxString& aFileName, wxMemoryBuffer& aBuffer )
{
    wxFFile file( aFileName, wxS( "rb" ) );

    if( !file.IsOpened() )
        return false;

    wxFileOffset size = file.Length();

    if( size <= 0 )
        return false;

    void* data = aBuffer.GetWriteBuf( size );

    if( file.Read( data, size ) != static_cast<size_t>( size ) )
    {
        aBuffer.UngetWriteBuf( 0 );
        return false;
    }

    aBuffer.UngetWriteBuf( size );
    return true;
}


bool plotSymbolToSvg( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol, const BOX2I& aBBox,
                      int aUnit, int aBodyStyle, wxMemoryBuffer& aBuffer )
{
    if( !aSymbol )
        return false;

    SCH_RENDER_SETTINGS renderSettings;
    renderSettings.LoadColors( aFrame->GetColorSettings() );
    renderSettings.SetDefaultPenWidth( aFrame->GetRenderSettings()->GetDefaultPenWidth() );

    std::unique_ptr<SVG_PLOTTER> plotter = std::make_unique<SVG_PLOTTER>();
    plotter->SetRenderSettings( &renderSettings );

    PAGE_INFO pageInfo = aFrame->GetScreen()->GetPageSettings();
    pageInfo.SetWidthMils( schIUScale.IUToMils( aBBox.GetWidth() ) );
    pageInfo.SetHeightMils( schIUScale.IUToMils( aBBox.GetHeight() ) );

    plotter->SetPageSettings( pageInfo );
    plotter->SetColorMode( true );

    VECTOR2I plot_offset = aBBox.GetOrigin();
    plotter->SetViewport( plot_offset, schIUScale.IU_PER_MILS / 10, 1.0, false );
    plotter->SetCreator( wxT( "Eeschema-SVG" ) );

    wxFileName tempFile( wxFileName::CreateTempFileName( wxS( "kicad_symbol_svg" ) ) );

    if( !plotter->OpenFile( tempFile.GetFullPath() ) )
    {
        wxRemoveFile( tempFile.GetFullPath() );
        return false;
    }

    LOCALE_IO     toggle;
    SCH_PLOT_OPTS plotOpts;

    plotter->StartPlot( wxT( "1" ) );

    constexpr bool background = true;
    aSymbol->Plot( plotter.get(), background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );
    aSymbol->Plot( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );
    aSymbol->PlotFields( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false );

    plotter->EndPlot();
    plotter.reset();

    bool ok = loadFileToBuffer( tempFile.GetFullPath(), aBuffer );
    wxRemoveFile( tempFile.GetFullPath() );
    return ok;
}


wxImage renderSymbolToBitmap( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol, const BOX2I& aBBox,
                              int aUnit, int aBodyStyle, int aWidth, int aHeight,
                              double aViewScale, const wxColour& aBgColor )
{
    if( !aSymbol )
        return wxImage();

    wxBitmap bitmap( aWidth, aHeight, 24 );
    wxMemoryDC dc;
    dc.SelectObject( bitmap );
    dc.SetBackground( wxBrush( aBgColor ) );
    dc.Clear();

    KIGFX::GAL_DISPLAY_OPTIONS options;
    options.antialiasing_mode = KIGFX::GAL_ANTIALIASING_MODE::AA_HIGHQUALITY;
    std::unique_ptr<KIGFX::GAL_PRINT> galPrint = KIGFX::GAL_PRINT::Create( options, &dc );

    if( !galPrint )
        return wxImage();

    KIGFX::GAL* gal = galPrint->GetGAL();
    KIGFX::PRINT_CONTEXT* printCtx = galPrint->GetPrintCtx();
    std::unique_ptr<KIGFX::SCH_PAINTER> painter = std::make_unique<KIGFX::SCH_PAINTER>( gal );
    std::unique_ptr<KIGFX::VIEW> view = std::make_unique<KIGFX::VIEW>();

    // For symbol editor, we don't have a full schematic context
    // but SCH_PAINTER can still work for rendering individual items
    view->SetGAL( gal );
    view->SetPainter( painter.get() );
    view->SetScaleLimits( ZOOM_MAX_LIMIT_EESCHEMA, ZOOM_MIN_LIMIT_EESCHEMA );
    view->SetScale( 1.0 );
    gal->SetWorldUnitLength( SCH_WORLD_UNIT );

    // Clone items and add to view
    std::vector<std::unique_ptr<SCH_ITEM>> clonedItems;

    for( SCH_ITEM& item : aSymbol->GetDrawItems() )
    {
        if( aUnit && item.GetUnit() && item.GetUnit() != aUnit )
            continue;

        if( aBodyStyle && item.GetBodyStyle() && item.GetBodyStyle() != aBodyStyle )
            continue;

        SCH_ITEM* clone = static_cast<SCH_ITEM*>( item.Clone() );
        clonedItems.emplace_back( clone );
        view->Add( clone );
    }

    SCH_RENDER_SETTINGS* dstSettings = painter->GetSettings();
    dstSettings->LoadColors( aFrame->GetColorSettings() );
    dstSettings->SetDefaultPenWidth( aFrame->GetRenderSettings()->GetDefaultPenWidth() );
    dstSettings->SetIsPrinting( true );

    COLOR4D bgColor4D( aBgColor.Red() / 255.0, aBgColor.Green() / 255.0,
                       aBgColor.Blue() / 255.0, 1.0 );
    dstSettings->SetBackgroundColor( bgColor4D );

    for( int i = 0; i < KIGFX::VIEW::VIEW_MAX_LAYERS; ++i )
    {
        view->SetLayerVisible( i, true );
        view->SetLayerTarget( i, KIGFX::TARGET_NONCACHED );
    }

    view->SetLayerVisible( LAYER_DRAWINGSHEET, false );

    // Calculate effective output DPI for the print context.
    // On GTK, Cairo uses device scale 72/4800 and SetSheetSize doubles internal resolution.
    // On Windows/macOS, there's no device scale, so effective DPI = native DPI * 2.
#ifdef __WXGTK__
    double ppi = 144.0;
#else
    double ppi = printCtx->GetNativeDPI() * 2.0;
#endif
    double inch2Iu = 1000.0 * schIUScale.IU_PER_MILS;
    VECTOR2D pageSizeIn( (double) aWidth / ppi, (double) aHeight / ppi );

    galPrint->SetSheetSize( pageSizeIn );
    galPrint->SetNativePaperSize( pageSizeIn, printCtx->HasNativeLandscapeRotation() );

    // SetSheetSize creates an internal canvas at 2× the nominal page size for quality.
    // The × 2 multiplier ensures content fills this internal canvas.
    double zoomFactor = 2.0 * aViewScale * inch2Iu / ppi;

    // Set up both the GAL and VIEW to center on the bbox.
    view->SetCenter( aBBox.Centre() );
    view->SetScale( aViewScale * zoomFactor );

    gal->SetLookAtPoint( aBBox.Centre() );
    gal->SetZoomFactor( zoomFactor );
    gal->SetClearColor( bgColor4D );
    gal->ClearScreen();

    view->UseDrawPriority( true );

    {
        KIGFX::GAL_DRAWING_CONTEXT ctx( gal );
        view->Redraw();
    }

    dc.SelectObject( wxNullBitmap );
    return bitmap.ConvertToImage();
}


wxImage renderSymbolToImageWithAlpha( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol,
                                      const BOX2I& aBBox, int aUnit, int aBodyStyle )
{
    if( !aSymbol )
        return wxImage();

    VECTOR2I size = aBBox.GetSize();

    if( size.x <= 0 || size.y <= 0 )
        return wxImage();

    // Use the current view scale to match what the user sees on screen
    double viewScale = aFrame->GetCanvas()->GetView()->GetScale();
    int    bitmapWidth = KiROUND( size.x * viewScale );
    int    bitmapHeight = KiROUND( size.y * viewScale );

    // Clamp to maximum size while preserving aspect ratio
    if( bitmapWidth > clipboardMaxBitmapSize || bitmapHeight > clipboardMaxBitmapSize )
    {
        double scaleDown = (double) clipboardMaxBitmapSize / std::max( bitmapWidth, bitmapHeight );
        bitmapWidth = KiROUND( bitmapWidth * scaleDown );
        bitmapHeight = KiROUND( bitmapHeight * scaleDown );
        viewScale *= scaleDown;
    }

    if( bitmapWidth <= 0 || bitmapHeight <= 0 )
        return wxImage();

    // Render twice with different backgrounds for alpha computation
    wxImage imageOnWhite = renderSymbolToBitmap( aFrame, aSymbol, aBBox, aUnit, aBodyStyle,
                                                  bitmapWidth, bitmapHeight, viewScale, *wxWHITE );
    wxImage imageOnBlack = renderSymbolToBitmap( aFrame, aSymbol, aBBox, aUnit, aBodyStyle,
                                                  bitmapWidth, bitmapHeight, viewScale, *wxBLACK );

    if( !imageOnWhite.IsOk() || !imageOnBlack.IsOk() )
        return wxImage();

    // Create output image with alpha channel
    wxImage result( bitmapWidth, bitmapHeight );
    result.InitAlpha();

    unsigned char* rgbWhite = imageOnWhite.GetData();
    unsigned char* rgbBlack = imageOnBlack.GetData();
    unsigned char* rgbResult = result.GetData();
    unsigned char* alphaResult = result.GetAlpha();

    int pixelCount = bitmapWidth * bitmapHeight;

    for( int i = 0; i < pixelCount; ++i )
    {
        int idx = i * 3;

        int rW = rgbWhite[idx], gW = rgbWhite[idx + 1], bW = rgbWhite[idx + 2];
        int rB = rgbBlack[idx], gB = rgbBlack[idx + 1], bB = rgbBlack[idx + 2];

        // Alpha computation: α = 1 - (white - black) / 255
        int diffR = rW - rB;
        int diffG = gW - gB;
        int diffB = bW - bB;
        int avgDiff = ( diffR + diffG + diffB ) / 3;

        int alpha = 255 - avgDiff;
        alpha = std::max( 0, std::min( 255, alpha ) );
        alphaResult[i] = static_cast<unsigned char>( alpha );

        if( alpha > 0 )
        {
            rgbResult[idx] = static_cast<unsigned char>( std::min( 255, rB * 255 / alpha ) );
            rgbResult[idx + 1] = static_cast<unsigned char>( std::min( 255, gB * 255 / alpha ) );
            rgbResult[idx + 2] = static_cast<unsigned char>( std::min( 255, bB * 255 / alpha ) );
        }
        else
        {
            rgbResult[idx] = 0;
            rgbResult[idx + 1] = 0;
            rgbResult[idx + 2] = 0;
        }
    }

    return result;
}

}  // namespace


SYMBOL_EDITOR_EDIT_TOOL::SYMBOL_EDITOR_EDIT_TOOL() :
        SCH_TOOL_BASE( "eeschema.SymbolEditTool" )
{
}


const std::vector<KICAD_T> SYMBOL_EDITOR_EDIT_TOOL::SwappableItems = {
    LIB_SYMBOL_T, // Allows swapping the anchor
    SCH_PIN_T,
    SCH_SHAPE_T,
    SCH_TEXT_T,
    SCH_TEXTBOX_T,
    SCH_FIELD_T,
};


bool SYMBOL_EDITOR_EDIT_TOOL::Init()
{
    SCH_TOOL_BASE::Init();

    SYMBOL_EDITOR_DRAWING_TOOLS* drawingTools = m_toolMgr->GetTool<SYMBOL_EDITOR_DRAWING_TOOLS>();
    SYMBOL_EDITOR_MOVE_TOOL*     moveTool = m_toolMgr->GetTool<SYMBOL_EDITOR_MOVE_TOOL>();

    wxASSERT_MSG( drawingTools, "eeschema.SymbolDrawing tool is not available" );

    auto haveSymbolCondition =
            [&]( const SELECTION& sel )
            {
                return m_isSymbolEditor && m_frame->GetCurSymbol();
            };

    auto canEdit =
            [&]( const SELECTION& sel )
            {
                if( !m_frame->IsSymbolEditable() )
                    return false;

                if( m_frame->IsSymbolAlias() )
                {
                    for( EDA_ITEM* item : sel )
                    {
                        if( item->Type() != SCH_FIELD_T )
                            return false;
                    }
                }

                return true;
            };

    auto swapSelectionCondition =
            canEdit && SCH_CONDITIONS::OnlyTypes( SwappableItems ) && SELECTION_CONDITIONS::MoreThan( 1 );

    const auto canCopyText = SCH_CONDITIONS::OnlyTypes( {
            SCH_TEXT_T,
            SCH_TEXTBOX_T,
            SCH_FIELD_T,
            SCH_PIN_T,
            SCH_TABLE_T,
            SCH_TABLECELL_T,
    } );

    const auto canConvertStackedPins =
            [&]( const SELECTION& sel )
            {
                // If multiple pins are selected, check they are all at same location
                if( sel.Size() >= 2 )
                {
                    std::vector<SCH_PIN*> pins;
                    for( EDA_ITEM* item : sel )
                    {
                        if( item->Type() != SCH_PIN_T )
                            return false;
                        pins.push_back( static_cast<SCH_PIN*>( item ) );
                    }

                    // Check that all pins are at the same location
                    VECTOR2I pos = pins[0]->GetPosition();
                    for( size_t i = 1; i < pins.size(); ++i )
                    {
                        if( pins[i]->GetPosition() != pos )
                            return false;
                    }
                    return true;
                }

                // If single pin is selected, check if there are other pins at same location
                if( sel.Size() == 1 && sel.Front()->Type() == SCH_PIN_T )
                {
                    SCH_PIN* selectedPin = static_cast<SCH_PIN*>( sel.Front() );
                    VECTOR2I pos = selectedPin->GetPosition();

                    // Get the symbol and check for other pins at same location
                    LIB_SYMBOL* symbol = m_frame->GetCurSymbol();
                    if( !symbol )
                        return false;

                    int coLocatedCount = 0;

                    for( SCH_PIN* pin : symbol->GetPins() )
                    {
                        if( pin->GetPosition() == pos )
                        {
                            coLocatedCount++;

                            if( coLocatedCount >= 2 )
                                return true;
                        }
                    }
                }

                return false;
            };

    const auto canExplodeStackedPin =
            [&]( const SELECTION& sel )
            {
                if( sel.Size() != 1 || sel.Front()->Type() != SCH_PIN_T )
                    return false;

                SCH_PIN* pin = static_cast<SCH_PIN*>( sel.Front() );
                bool isValid;
                std::vector<wxString> stackedNumbers = pin->GetStackedPinNumbers( &isValid );
                return isValid && stackedNumbers.size() > 1;
            };

    // clang-format off
    // Add edit actions to the move tool menu
    if( moveTool )
    {
        CONDITIONAL_MENU& moveMenu = moveTool->GetToolMenu().GetMenu();

        moveMenu.AddSeparator( 200 );
        moveMenu.AddItem( SCH_ACTIONS::rotateCCW,   canEdit && SCH_CONDITIONS::NotEmpty, 200 );
        moveMenu.AddItem( SCH_ACTIONS::rotateCW,    canEdit && SCH_CONDITIONS::NotEmpty, 200 );
        moveMenu.AddItem( SCH_ACTIONS::mirrorV,     canEdit && SCH_CONDITIONS::NotEmpty, 200 );
        moveMenu.AddItem( SCH_ACTIONS::mirrorH,     canEdit && SCH_CONDITIONS::NotEmpty, 200 );

        moveMenu.AddItem( SCH_ACTIONS::swap,        swapSelectionCondition, 200 );
        moveMenu.AddItem( SCH_ACTIONS::properties,  canEdit && SCH_CONDITIONS::Count( 1 ), 200 );

        moveMenu.AddSeparator( 300 );
        moveMenu.AddItem( ACTIONS::cut,             SCH_CONDITIONS::IdleSelection, 300 );
        moveMenu.AddItem( ACTIONS::copy,            SCH_CONDITIONS::IdleSelection, 300 );
        moveMenu.AddItem( ACTIONS::copyAsText,      canCopyText && SCH_CONDITIONS::IdleSelection, 300 );
        moveMenu.AddItem( ACTIONS::duplicate,       canEdit && SCH_CONDITIONS::NotEmpty, 300 );
        moveMenu.AddItem( ACTIONS::doDelete,        canEdit && SCH_CONDITIONS::NotEmpty, 200 );

        moveMenu.AddSeparator( 400 );
        moveMenu.AddItem( ACTIONS::selectAll,       haveSymbolCondition, 400 );
        moveMenu.AddItem( ACTIONS::unselectAll,     haveSymbolCondition, 400 );
    }

    // Add editing actions to the drawing tool menu
    CONDITIONAL_MENU& drawMenu = drawingTools->GetToolMenu().GetMenu();

    drawMenu.AddSeparator( 200 );
    drawMenu.AddItem( SCH_ACTIONS::rotateCCW,       canEdit && SCH_CONDITIONS::IdleSelection, 200 );
    drawMenu.AddItem( SCH_ACTIONS::rotateCW,        canEdit && SCH_CONDITIONS::IdleSelection, 200 );
    drawMenu.AddItem( SCH_ACTIONS::mirrorV,         canEdit && SCH_CONDITIONS::IdleSelection, 200 );
    drawMenu.AddItem( SCH_ACTIONS::mirrorH,         canEdit && SCH_CONDITIONS::IdleSelection, 200 );

    drawMenu.AddItem( SCH_ACTIONS::properties,      canEdit && SCH_CONDITIONS::Count( 1 ), 200 );

    // Add editing actions to the selection tool menu
    CONDITIONAL_MENU& selToolMenu = m_selectionTool->GetToolMenu().GetMenu();

    selToolMenu.AddItem( SCH_ACTIONS::rotateCCW,    canEdit && SCH_CONDITIONS::NotEmpty, 200 );
    selToolMenu.AddItem( SCH_ACTIONS::rotateCW,     canEdit && SCH_CONDITIONS::NotEmpty, 200 );
    selToolMenu.AddItem( SCH_ACTIONS::mirrorV,      canEdit && SCH_CONDITIONS::NotEmpty, 200 );
    selToolMenu.AddItem( SCH_ACTIONS::mirrorH,      canEdit && SCH_CONDITIONS::NotEmpty, 200 );

    selToolMenu.AddItem( SCH_ACTIONS::swap,         swapSelectionCondition, 200 );
    selToolMenu.AddItem( SCH_ACTIONS::properties,   canEdit && SCH_CONDITIONS::Count( 1 ), 200 );

    selToolMenu.AddSeparator( 250 );
    selToolMenu.AddItem( SCH_ACTIONS::convertStackedPins, canEdit && canConvertStackedPins, 250 );
    selToolMenu.AddItem( SCH_ACTIONS::explodeStackedPin,  canEdit && canExplodeStackedPin, 250 );

    selToolMenu.AddSeparator( 300 );
    selToolMenu.AddItem( ACTIONS::cut,              SCH_CONDITIONS::IdleSelection, 300 );
    selToolMenu.AddItem( ACTIONS::copy,             SCH_CONDITIONS::IdleSelection, 300 );
    selToolMenu.AddItem( ACTIONS::copyAsText,       canCopyText && SCH_CONDITIONS::IdleSelection, 300 );
    selToolMenu.AddItem( ACTIONS::paste,            canEdit && SCH_CONDITIONS::Idle, 300 );
    selToolMenu.AddItem( ACTIONS::duplicate,        canEdit && SCH_CONDITIONS::NotEmpty, 300 );
    selToolMenu.AddItem( ACTIONS::doDelete,         canEdit && SCH_CONDITIONS::NotEmpty, 300 );

    selToolMenu.AddSeparator( 400 );
    selToolMenu.AddItem( ACTIONS::selectAll,        haveSymbolCondition, 400 );
    selToolMenu.AddItem( ACTIONS::unselectAll,      haveSymbolCondition, 400 );
    // clang-format on

    return true;
}


int SYMBOL_EDITOR_EDIT_TOOL::Rotate( const TOOL_EVENT& aEvent )
{
    SCH_SELECTION& selection = m_selectionTool->RequestSelection();

    if( selection.GetSize() == 0 )
        return 0;

    VECTOR2I    rotPoint;
    bool        ccw = ( aEvent.Matches( SCH_ACTIONS::rotateCCW.MakeEvent() ) );
    SCH_ITEM*   item = static_cast<SCH_ITEM*>( selection.Front() );
    SCH_COMMIT  localCommit( m_toolMgr );
    SCH_COMMIT* commit = dynamic_cast<SCH_COMMIT*>( aEvent.Commit() );

    if( !commit )
        commit = &localCommit;

    if( !item->IsMoving() )
        commit->Modify( m_frame->GetCurSymbol(), m_frame->GetScreen(), RECURSE_MODE::RECURSE );

    if( selection.GetSize() == 1 )
        rotPoint = item->GetPosition();
    else
        rotPoint = m_frame->GetNearestHalfGridPosition( selection.GetCenter() );

    for( unsigned ii = 0; ii < selection.GetSize(); ii++ )
    {
        item = static_cast<SCH_ITEM*>( selection.GetItem( ii ) );
        item->Rotate( rotPoint, ccw );
        m_frame->UpdateItem( item, false, true );
    }

    if( item->IsMoving() )
    {
        m_toolMgr->RunAction( ACTIONS::refreshPreview );
    }
    else
    {
        if( selection.IsHover() )
            m_toolMgr->RunAction( ACTIONS::selectionClear );

        if( !localCommit.Empty() )
            localCommit.Push( _( "Rotate" ) );
    }

    return 0;
}


int SYMBOL_EDITOR_EDIT_TOOL::Mirror( const TOOL_EVENT& aEvent )
{
    SCH_SELECTION& selection = m_selectionTool->RequestSelection();

    if( selection.GetSize() == 0 )
        return 0;

    VECTOR2I  mirrorPoint;
    bool      xAxis = ( aEvent.Matches( SCH_ACTIONS::mirrorV.MakeEvent() ) );
    SCH_ITEM* item = static_cast<SCH_ITEM*>( selection.Front() );

    if( !item->IsMoving() )
        saveCopyInUndoList( m_frame->GetCurSymbol(), UNDO_REDO::LIBEDIT );

    if( selection.GetSize() == 1 )
    {
        mirrorPoint = item->GetPosition();

        switch( item->Type() )
        {
        case SCH_FIELD_T:
        {
            SCH_FIELD* field = static_cast<SCH_FIELD*>( item );

            if( xAxis )
                field->SetVertJustify( GetFlippedAlignment( field->GetVertJustify() ) );
            else
                field->SetHorizJustify( GetFlippedAlignment( field->GetHorizJustify() ) );

            break;
        }

        default:
            if( xAxis )
                item->MirrorVertically( mirrorPoint.y );
            else
                item->MirrorHorizontally( mirrorPoint.x );

            break;
        }


        m_frame->UpdateItem( item, false, true );
    }
    else
    {
        mirrorPoint = m_frame->GetNearestHalfGridPosition( selection.GetCenter() );

        for( unsigned ii = 0; ii < selection.GetSize(); ii++ )
        {
            item = static_cast<SCH_ITEM*>( selection.GetItem( ii ) );

            if( xAxis )
                item->MirrorVertically( mirrorPoint.y );
            else
                item->MirrorHorizontally( mirrorPoint.x );

            m_frame->UpdateItem( item, false, true );
        }
    }

    if( item->IsMoving() )
    {
        m_toolMgr->RunAction( ACTIONS::refreshPreview );
    }
    else
    {
        if( selection.IsHover() )
            m_toolMgr->RunAction( ACTIONS::selectionClear );

        m_frame->OnModify();
    }

    return 0;
}
int SYMBOL_EDITOR_EDIT_TOOL::Swap( const TOOL_EVENT& aEvent )
{
    SCH_SELECTION&         selection = m_selectionTool->RequestSelection( SwappableItems );
    std::vector<EDA_ITEM*> sorted = selection.GetItemsSortedBySelectionOrder();

    if( selection.Size() < 2 )
        return 0;

    EDA_ITEM* front = selection.Front();
    bool      isMoving = front->IsMoving();

    // Save copy for undo if not in edit (edit command already handle the save copy)
    if( front->GetEditFlags() == 0 )
        saveCopyInUndoList( front->GetParent(), UNDO_REDO::LIBEDIT );

    for( size_t i = 0; i < sorted.size() - 1; i++ )
    {
        SCH_ITEM* a = static_cast<SCH_ITEM*>( sorted[i] );
        SCH_ITEM* b = static_cast<SCH_ITEM*>( sorted[( i + 1 ) % sorted.size()] );

        VECTOR2I aPos = a->GetPosition(), bPos = b->GetPosition();
        std::swap( aPos, bPos );

        a->SetPosition( aPos );
        b->SetPosition( bPos );

        // Special case some common swaps
        if( a->Type() == b->Type() )
        {
            switch( a->Type() )
            {
            case SCH_PIN_T:
            {
                SCH_PIN* aPin = static_cast<SCH_PIN*>( a );
                SCH_PIN* bBpin = static_cast<SCH_PIN*>( b );

                PIN_ORIENTATION aOrient = aPin->GetOrientation();
                PIN_ORIENTATION bOrient = bBpin->GetOrientation();

                aPin->SetOrientation( bOrient );
                bBpin->SetOrientation( aOrient );

                break;
            }
            default: break;
            }
        }

        m_frame->UpdateItem( a, false, true );
        m_frame->UpdateItem( b, false, true );
    }

    // Update R-Tree for modified items
    for( EDA_ITEM* selected : selection )
        updateItem( selected, true );

    if( isMoving )
    {
        m_toolMgr->PostAction( ACTIONS::refreshPreview );
    }
    else
    {
        if( selection.IsHover() )
            m_toolMgr->RunAction( ACTIONS::selectionClear );

        m_frame->OnModify();
    }

    return 0;
}


static std::vector<KICAD_T> nonFields =
{
    LIB_SYMBOL_T,
    SCH_SHAPE_T,
    SCH_TEXT_T,
    SCH_TEXTBOX_T,
    SCH_PIN_T
};


int SYMBOL_EDITOR_EDIT_TOOL::DoDelete( const TOOL_EVENT& aEvent )
{
    LIB_SYMBOL*           symbol = m_frame->GetCurSymbol();
    std::deque<EDA_ITEM*> items = m_selectionTool->RequestSelection().GetItems();
    SCH_COMMIT            commit( m_frame );

    if( items.empty() )
        return 0;

    // Don't leave a freed pointer in the selection
    m_toolMgr->RunAction( ACTIONS::selectionClear );

    commit.Modify( symbol, m_frame->GetScreen() );

    std::set<SCH_ITEM*> toDelete;
    int                 fieldsHidden = 0;
    int                 fieldsAlreadyHidden = 0;

    for( EDA_ITEM* item : items )
    {
        if( item->Type() == SCH_PIN_T )
        {
            SCH_PIN*  curr_pin = static_cast<SCH_PIN*>( item );
            VECTOR2I pos = curr_pin->GetPosition();

            toDelete.insert( curr_pin );

            // when pin editing is synchronized, pins in the same position, with the same name
            // in different units are also removed.  But only one pin per unit (matching)
            if( m_frame->SynchronizePins() )
            {
                std::vector<bool> got_unit( symbol->GetUnitCount() + 1 );

                got_unit[curr_pin->GetUnit()] = true;

                for( SCH_PIN* pin : symbol->GetPins() )
                {
                    if( got_unit[pin->GetUnit()] )
                        continue;

                    if( pin->GetPosition() != pos )
                        continue;

                    if( pin->GetBodyStyle() != curr_pin->GetBodyStyle() )
                        continue;

                    if( pin->GetType() != curr_pin->GetType() )
                        continue;

                    if( pin->GetName() != curr_pin->GetName() )
                        continue;

                    toDelete.insert( pin );
                    got_unit[pin->GetUnit()] = true;
                }
            }
        }
        else if( item->Type() == SCH_FIELD_T )
        {
            SCH_FIELD* field = static_cast<SCH_FIELD*>( item );

            // Hide "deleted" fields
            if( field->IsVisible() )
            {
                field->SetVisible( false );
                fieldsHidden++;
            }
            else
            {
                fieldsAlreadyHidden++;
            }
        }
        else if( SCH_ITEM* schItem = dynamic_cast<SCH_ITEM*>( item ) )
        {
            toDelete.insert( schItem );
        }
    }

    for( SCH_ITEM* item : toDelete )
        symbol->RemoveDrawItem( item );

    if( toDelete.size() == 0 )
    {
        if( fieldsHidden == 1 )
            commit.Push( _( "Hide Field" ) );
        else if( fieldsHidden > 1 )
            commit.Push( _( "Hide Fields" ) );
        else if( fieldsAlreadyHidden > 0 )
            m_frame->ShowInfoBarError( _( "Use the Symbol Properties dialog to remove fields." ) );
    }
    else
    {
        commit.Push( _( "Delete" ) );
    }

    m_frame->RebuildView();
    return 0;
}


int SYMBOL_EDITOR_EDIT_TOOL::Properties( const TOOL_EVENT& aEvent )
{
    SCH_SELECTION& selection = m_selectionTool->RequestSelection();

    if( selection.Empty() || aEvent.IsAction( &SCH_ACTIONS::symbolProperties ) )
    {
        // If called from tree context menu, edit properties without loading into canvas
        if( aEvent.IsAction( &SCH_ACTIONS::symbolProperties ) )
        {
            LIB_ID treeLibId = m_frame->GetTreeLIBID();

            // Check if the selected symbol in tree is different from the currently loaded one
            if( treeLibId.IsValid() &&
                ( !m_frame->GetCurSymbol() || m_frame->GetCurSymbol()->GetLibId() != treeLibId ) )
            {
                // Edit properties directly from library buffer without loading to canvas
                editSymbolPropertiesFromLibrary( treeLibId );
                return 0;
            }
        }

        if( m_frame->GetCurSymbol() )
            editSymbolProperties();
    }
    else if( selection.Size() == 1 )
    {
        SCH_ITEM* item = static_cast<SCH_ITEM*>( selection.Front() );

        // Save copy for undo if not in edit (edit command already handle the save copy)
        if( item->GetEditFlags() == 0 )
            saveCopyInUndoList( item->GetParent(), UNDO_REDO::LIBEDIT );

        switch( item->Type() )
        {
        case SCH_PIN_T:
        {
            SCH_PIN& pin = static_cast<SCH_PIN&>( *item );

            // Mouse, not cursor, as grid points may well not be under any text
            const VECTOR2I&   mousePos = m_toolMgr->GetMousePosition();
            PIN_LAYOUT_CACHE& layout = pin.GetLayoutCache();

            bool mouseOverNumber = false;
            if( OPT_BOX2I numberBox = layout.GetPinNumberBBox() )
            {
                mouseOverNumber = numberBox->Contains( mousePos );
            }

            if( SYMBOL_EDITOR_PIN_TOOL* pinTool = m_toolMgr->GetTool<SYMBOL_EDITOR_PIN_TOOL>() )
                pinTool->EditPinProperties( &pin, mouseOverNumber );

            break;
        }
        case SCH_SHAPE_T:
            editShapeProperties( static_cast<SCH_SHAPE*>( item ) );
            break;

        case SCH_TEXT_T:
            editTextProperties( item );
            break;

        case SCH_TEXTBOX_T:
            editTextBoxProperties( item );
            break;

        case SCH_FIELD_T:
            editFieldProperties( static_cast<SCH_FIELD*>( item ) );
            break;

        default:
            wxFAIL_MSG( wxT( "Unhandled item <" ) + item->GetClass() + wxT( ">" ) );
            break;
        }
    }

    if( selection.IsHover() )
        m_toolMgr->RunAction( ACTIONS::selectionClear );

    return 0;
}


void SYMBOL_EDITOR_EDIT_TOOL::editShapeProperties( SCH_SHAPE* aShape )
{
    DIALOG_SHAPE_PROPERTIES dlg( m_frame, aShape );

    if( dlg.ShowModal() != wxID_OK )
        return;

    updateItem( aShape, true );
    m_frame->GetCanvas()->Refresh();
    m_frame->OnModify();

    SYMBOL_EDITOR_DRAWING_TOOLS* drawingTools = m_toolMgr->GetTool<SYMBOL_EDITOR_DRAWING_TOOLS>();
    drawingTools->SetDrawSpecificBodyStyle( !dlg.GetApplyToAllConversions() );
    drawingTools->SetDrawSpecificUnit( !dlg.GetApplyToAllUnits() );

    std::vector<MSG_PANEL_ITEM> items;
    aShape->GetMsgPanelInfo( m_frame, items );
    m_frame->SetMsgPanel( items );
}


void SYMBOL_EDITOR_EDIT_TOOL::editTextProperties( SCH_ITEM* aItem )
{
    if ( aItem->Type() != SCH_TEXT_T )
        return;

    DIALOG_TEXT_PROPERTIES dlg( m_frame, static_cast<SCH_TEXT*>( aItem ) );

    if( dlg.ShowModal() != wxID_OK )
        return;

    updateItem( aItem, true );
    m_frame->GetCanvas()->Refresh();
    m_frame->OnModify( );
}


void SYMBOL_EDITOR_EDIT_TOOL::editTextBoxProperties( SCH_ITEM* aItem )
{
    if ( aItem->Type() != SCH_TEXTBOX_T )
        return;

    DIALOG_TEXT_PROPERTIES dlg( m_frame, static_cast<SCH_TEXTBOX*>( aItem ) );

    if( dlg.ShowModal() != wxID_OK )
        return;

    updateItem( aItem, true );
    m_frame->GetCanvas()->Refresh();
    m_frame->OnModify( );
}


void SYMBOL_EDITOR_EDIT_TOOL::editFieldProperties( SCH_FIELD* aField )
{
    if( aField == nullptr )
        return;

    wxString caption;

    if( aField->IsMandatory() )
        caption.Printf( _( "Edit %s Field" ), TitleCaps( aField->GetName() ) );
    else
        caption.Printf( _( "Edit '%s' Field" ), aField->GetName() );

    DIALOG_FIELD_PROPERTIES dlg( m_frame, caption, aField );

    // The dialog may invoke a kiway player for footprint fields
    // so we must use a quasimodal dialog.
    if( dlg.ShowQuasiModal() != wxID_OK )
        return;

    SCH_COMMIT commit( m_toolMgr );
    commit.Modify( aField, m_frame->GetScreen() );

    dlg.UpdateField( aField );

    commit.Push( caption );

    m_frame->GetCanvas()->Refresh();
    m_frame->UpdateSymbolMsgPanelInfo();
}


void SYMBOL_EDITOR_EDIT_TOOL::editSymbolPropertiesFromLibrary( const LIB_ID& aLibId )
{
    LIB_SYMBOL_LIBRARY_MANAGER& libMgr = m_frame->GetLibManager();
    wxString libName = aLibId.GetLibNickname();
    wxString symbolName = aLibId.GetLibItemName();

    // Get the symbol from the library buffer (without loading it into the editor)
    LIB_SYMBOL* bufferedSymbol = libMgr.GetBufferedSymbol( symbolName, libName );

    if( !bufferedSymbol )
        return;

    // Create a copy to work with
    LIB_SYMBOL tempSymbol( *bufferedSymbol );

    m_toolMgr->RunAction( ACTIONS::cancelInteractive );
    m_toolMgr->RunAction( ACTIONS::selectionClear );

    DIALOG_LIB_SYMBOL_PROPERTIES dlg( m_frame, &tempSymbol );

    // This dialog itself subsequently can invoke a KIWAY_PLAYER as a quasimodal
    // frame. Therefore this dialog as a modal frame parent, MUST be run under
    // quasimodal mode for the quasimodal frame support to work.  So don't use
    // the QUASIMODAL macros here.
    if( dlg.ShowQuasiModal() != wxID_OK )
        return;

    wxString newName = tempSymbol.GetName();

    // Update the buffered symbol with the changes.  A rename has to be keyed on the old name so
    // the existing buffer is renamed in place rather than a duplicate being created under the new
    // name.
    if( newName != symbolName )
        libMgr.UpdateSymbolAfterRename( &tempSymbol, symbolName, libName );
    else
        libMgr.UpdateSymbol( &tempSymbol, libName );

    // Mark the library as modified
    libMgr.SetSymbolModified( newName, libName );

    // Update the tree view
    LIB_ID         newLibId( libName, newName );
    wxDataViewItem treeItem = libMgr.GetAdapter()->FindItem( newLibId );
    m_frame->UpdateLibraryTree( treeItem, &tempSymbol );
    m_frame->GetLibTree()->SelectLibId( newLibId );
}


void SYMBOL_EDITOR_EDIT_TOOL::editSymbolProperties()
{
    LIB_SYMBOL* symbol = m_frame->GetCurSymbol();
    bool        partLocked = symbol->UnitsLocked();

    m_toolMgr->RunAction( ACTIONS::cancelInteractive );
    m_toolMgr->RunAction( ACTIONS::selectionClear );

    DIALOG_LIB_SYMBOL_PROPERTIES dlg( m_frame, symbol );

    // This dialog itself subsequently can invoke a KIWAY_PLAYER as a quasimodal
    // frame. Therefore this dialog as a modal frame parent, MUST be run under
    // quasimodal mode for the quasimodal frame support to work.  So don't use
    // the QUASIMODAL macros here.
    if( dlg.ShowQuasiModal() != wxID_OK )
        return;

    m_frame->RebuildSymbolUnitAndBodyStyleLists();
    m_frame->OnModify();

    // Update the library tree node so the description and other metadata reflect the changes
    // immediately without requiring the editor to be reopened.
    LIB_SYMBOL_LIBRARY_MANAGER& libMgr = m_frame->GetLibManager();
    wxDataViewItem treeItem = libMgr.GetAdapter()->FindItem( symbol->GetLibId() );
    m_frame->UpdateLibraryTree( treeItem, symbol );

    // if m_UnitSelectionLocked has changed, set some edit options or defaults
    // to the best value
    if( partLocked != symbol->UnitsLocked() )
    {
        SYMBOL_EDITOR_DRAWING_TOOLS* tools = m_toolMgr->GetTool<SYMBOL_EDITOR_DRAWING_TOOLS>();

        // Enable synchronized pin edit mode for symbols with interchangeable units
        m_frame->m_SyncPinEdit = !symbol->UnitsLocked();

        // also set default edit options to the better value
        // Usually if units are locked, graphic items are specific to each unit
        // and if units are interchangeable, graphic items are common to units
        tools->SetDrawSpecificUnit( symbol->UnitsLocked() );
    }
}


int SYMBOL_EDITOR_EDIT_TOOL::PinTable( const TOOL_EVENT& aEvent )
{
    SCH_COMMIT  commit( m_frame );
    LIB_SYMBOL* symbol = m_frame->GetCurSymbol();

    if( !symbol )
        return 0;

    commit.Modify( symbol, m_frame->GetScreen() );

    SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool<SCH_SELECTION_TOOL>();
    wxCHECK( selTool, -1 );

    std::vector<SCH_PIN*> selectedPins;

    SCH_SELECTION& selection = selTool->GetSelection();

    for( EDA_ITEM* item : selection )
    {
        if( item->Type() == SCH_PIN_T )
        {
            SCH_PIN* pinItem = static_cast<SCH_PIN*>( item );
            selectedPins.push_back( pinItem );
        }
    }

    // And now clear the selection so if we change the pins we don't have dangling pointers
    // in the selection.
    m_toolMgr->RunAction( ACTIONS::selectionClear );

    DIALOG_LIB_EDIT_PIN_TABLE dlg( m_frame, symbol, selectedPins );

    if( dlg.ShowModal() == wxID_CANCEL )
        return -1;

    commit.Push( _( "Edit Pins" ) );
    m_frame->RebuildView();

    return 0;
}


int SYMBOL_EDITOR_EDIT_TOOL::ConvertStackedPins( const TOOL_EVENT& aEvent )
{
    SCH_COMMIT  commit( m_frame );
    LIB_SYMBOL* symbol = m_frame->GetCurSymbol();

    if( !symbol )
        return 0;

    SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool<SCH_SELECTION_TOOL>();
    wxCHECK( selTool, -1 );

    SCH_SELECTION& selection = selTool->GetSelection();

    // Collect pins to convert - accept pins with any number format
    std::vector<SCH_PIN*> pinsToConvert;

    if( selection.Size() == 1 && selection.Front()->Type() == SCH_PIN_T )
    {
        // Single pin selected - find all pins at the same location
        SCH_PIN* selectedPin = static_cast<SCH_PIN*>( selection.Front() );
        VECTOR2I pos = selectedPin->GetPosition();

        for( SCH_PIN* pin : symbol->GetPins() )
        {
            if( pin->GetPosition() == pos )
                pinsToConvert.push_back( pin );
        }
    }
    else
    {
        // Multiple pins selected - use them directly, accepting any pin numbers
        for( EDA_ITEM* item : selection )
        {
            if( item->Type() == SCH_PIN_T )
                pinsToConvert.push_back( static_cast<SCH_PIN*>( item ) );
        }
    }

    if( pinsToConvert.size() < 2 )
    {
        m_frame->ShowInfoBarError( _( "At least two pins are needed to convert to stacked pins" ) );
        return 0;
    }

    // Check that all pins are at the same location
    VECTOR2I pos = pinsToConvert[0]->GetPosition();
    for( size_t i = 1; i < pinsToConvert.size(); ++i )
    {
        if( pinsToConvert[i]->GetPosition() != pos )
        {
            m_frame->ShowInfoBarError( _( "All pins must be at the same location" ) );
            return 0;
        }
    }

    commit.Modify( symbol, m_frame->GetScreen() );

    // Clear selection before modifying pins, like the Delete command does
    m_toolMgr->RunAction( ACTIONS::selectionClear );

    // Sort pins for consistent ordering - handle arbitrary pin number formats
    std::sort( pinsToConvert.begin(), pinsToConvert.end(),
        []( SCH_PIN* a, SCH_PIN* b )
        {
            wxString numA = a->GetNumber();
            wxString numB = b->GetNumber();

            // Try to convert to integers for proper numeric sorting
            long longA, longB;
            bool aIsNumeric = numA.ToLong( &longA );
            bool bIsNumeric = numB.ToLong( &longB );

            // Both are purely numeric - sort numerically
            if( aIsNumeric && bIsNumeric )
                return longA < longB;

            // Mixed numeric/non-numeric - numeric pins come first
            if( aIsNumeric && !bIsNumeric )
                return true;
            if( !aIsNumeric && bIsNumeric )
                return false;

            // Both non-numeric or mixed alphanumeric - use lexicographic sorting
            return numA < numB;
        });

    // Build the stacked notation string with range collapsing
    wxString stackedNotation = wxT("[");

    // Helper function to collapse consecutive numbers into ranges - handles arbitrary pin formats
    auto collapseRanges = [&]() -> wxString
    {
        if( pinsToConvert.empty() )
            return wxT("");

        wxString result;

        // Group pins by their alphanumeric prefix for range collapsing
        std::map<wxString, std::vector<long>> prefixGroups;
        std::vector<wxString> nonNumericPins;

        // Parse each pin number to separate prefix from numeric suffix
        for( SCH_PIN* pin : pinsToConvert )
        {
            wxString pinNumber = pin->GetNumber();

            // Skip empty pin numbers (shouldn't happen, but be defensive)
            if( pinNumber.IsEmpty() )
            {
                nonNumericPins.push_back( wxT("(empty)") );
                continue;
            }

            wxString prefix;
            wxString numericPart;

            // Find where numeric part starts (scan from end)
            size_t numStart = pinNumber.length();
            for( int i = pinNumber.length() - 1; i >= 0; i-- )
            {
                if( !wxIsdigit( pinNumber[i] ) )
                {
                    numStart = i + 1;
                    break;
                }
                if( i == 0 )  // All digits
                    numStart = 0;
            }

            if( numStart < pinNumber.length() )  // Has numeric suffix
            {
                prefix = pinNumber.Left( numStart );
                numericPart = pinNumber.Mid( numStart );

                long numValue;
                if( numericPart.ToLong( &numValue ) && numValue >= 0 )  // Valid non-negative number
                {
                    prefixGroups[prefix].push_back( numValue );
                }
                else
                {
                    // Numeric part couldn't be parsed or is negative - treat as non-numeric
                    nonNumericPins.push_back( pinNumber );
                }
            }
            else  // No numeric suffix - consolidate as individual value
            {
                nonNumericPins.push_back( pinNumber );
            }
        }

        // Process each prefix group
        for( auto& [prefix, numbers] : prefixGroups )
        {
            if( !result.IsEmpty() )
                result += wxT(",");

            // Sort numeric values for this prefix
            std::sort( numbers.begin(), numbers.end() );

            // Collapse consecutive ranges within this prefix
            size_t i = 0;
            while( i < numbers.size() )
            {
                if( i > 0 )  // Not first number in this prefix group
                    result += wxT(",");

                long start = numbers[i];
                long end = start;

                // Find the end of consecutive sequence
                while( i + 1 < numbers.size() && numbers[i + 1] == numbers[i] + 1 )
                {
                    i++;
                    end = numbers[i];
                }

                // Add range or single number with prefix
                if( end > start + 1 )  // Range of 3+ numbers
                    result += wxString::Format( wxT("%s%ld-%s%ld"), prefix, start, prefix, end );
                else if( end == start + 1 )  // Two consecutive numbers
                    result += wxString::Format( wxT("%s%ld,%s%ld"), prefix, start, prefix, end );
                else  // Single number
                    result += wxString::Format( wxT("%s%ld"), prefix, start );

                i++;
            }
        }

        // Add non-numeric pin numbers as individual comma-separated values
        for( const wxString& nonNum : nonNumericPins )
        {
            if( !result.IsEmpty() )
                result += wxT(",");
            result += nonNum;
        }

        return result;
    };

    stackedNotation += collapseRanges();
    stackedNotation += wxT("]");

    // Keep the first pin and give it the stacked notation
    SCH_PIN* masterPin = pinsToConvert[0];
    masterPin->SetNumber( stackedNotation );

    // Log information about pins being removed before we remove them
    wxLogTrace( traceStackedPins,
               wxString::Format( "Converting %zu pins to stacked notation '%s'",
                               pinsToConvert.size(), stackedNotation ) );

    // Remove all other pins from the symbol that were consolidated into the stacked notation
    // Collect pins to remove first, then remove them all at once like the Delete command
    std::vector<SCH_PIN*> pinsToRemove;
    for( size_t i = 1; i < pinsToConvert.size(); ++i )
    {
        SCH_PIN* pinToRemove = pinsToConvert[i];

        // Log the pin before removing it
    wxLogTrace( traceStackedPins,
           wxString::Format( "Will remove pin '%s' at position (%d, %d)",
                   pinToRemove->GetNumber(),
                   pinToRemove->GetPosition().x,
                   pinToRemove->GetPosition().y ) );

        pinsToRemove.push_back( pinToRemove );
    }

    // Remove all pins at once, like the Delete command does
    for( SCH_PIN* pin : pinsToRemove )
    {
        symbol->RemoveDrawItem( pin );
    }

    commit.Push( wxString::Format( _( "Convert %zu Stacked Pins to '%s'" ),
                                  pinsToConvert.size(), stackedNotation ) );
    m_frame->RebuildView();
    return 0;
}


int SYMBOL_EDITOR_EDIT_TOOL::ExplodeStackedPin( const TOOL_EVENT& aEvent )
{
    SCH_COMMIT  commit( m_frame );
    LIB_SYMBOL* symbol = m_frame->GetCurSymbol();

    if( !symbol )
        return 0;

    SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool<SCH_SELECTION_TOOL>();
    wxCHECK( selTool, -1 );

    SCH_SELECTION& selection = selTool->GetSelection();

    if( selection.GetSize() != 1 || selection.Front()->Type() != SCH_PIN_T )
    {
        m_frame->ShowInfoBarError( _( "Select a single pin with stacked notation to explode" ) );
        return 0;
    }

    SCH_PIN* pin = static_cast<SCH_PIN*>( selection.Front() );

    // Check if the pin has stacked notation
    bool isValid;
    std::vector<wxString> stackedNumbers = pin->GetStackedPinNumbers( &isValid );

    if( !isValid || stackedNumbers.size() <= 1 )
    {
        m_frame->ShowInfoBarError( _( "Selected pin does not have valid stacked notation" ) );
        return 0;
    }

    commit.Modify( symbol, m_frame->GetScreen() );

    // Clear selection before modifying pins
    m_toolMgr->RunAction( ACTIONS::selectionClear );

    // Sort the stacked numbers to find the smallest one
    std::sort( stackedNumbers.begin(), stackedNumbers.end(),
        []( const wxString& a, const wxString& b )
        {
            // Try to convert to integers for proper numeric sorting
            long numA, numB;
            if( a.ToLong( &numA ) && b.ToLong( &numB ) )
                return numA < numB;

            // Fall back to string comparison if not numeric
            return a < b;
        });

    // Change the original pin to use the first (smallest) number and make it visible
    pin->SetNumber( stackedNumbers[0] );
    pin->SetVisible( true );

    // Create additional pins for the remaining numbers and make them invisible
    for( size_t i = 1; i < stackedNumbers.size(); ++i )
    {
        SCH_PIN* newPin = new SCH_PIN( symbol );

        // Copy all properties from the original pin
        newPin->SetPosition( pin->GetPosition() );
        newPin->SetOrientation( pin->GetOrientation() );
        newPin->SetShape( pin->GetShape() );
        newPin->SetLength( pin->GetLength() );
        // Hidden power input pins act as global labels, so demote them to passive
        if( pin->GetType() == ELECTRICAL_PINTYPE::PT_POWER_IN )
            newPin->SetType( ELECTRICAL_PINTYPE::PT_PASSIVE );
        else
            newPin->SetType( pin->GetType() );

        newPin->SetName( pin->GetName() );
        newPin->SetNumber( stackedNumbers[i] );
        newPin->SetNameTextSize( pin->GetNameTextSize() );
        newPin->SetNumberTextSize( pin->GetNumberTextSize() );
        newPin->SetUnit( pin->GetUnit() );
        newPin->SetBodyStyle( pin->GetBodyStyle() );
        newPin->SetVisible( false );

        // Add the new pin to the symbol
        symbol->AddDrawItem( newPin );
    }

    commit.Push( _( "Explode Stacked Pin" ) );
    m_frame->RebuildView();
    return 0;
}


int SYMBOL_EDITOR_EDIT_TOOL::UpdateSymbolFields( const TOOL_EVENT& aEvent )
{
    LIB_SYMBOL* symbol = m_frame->GetCurSymbol();

    if( !symbol )
        return 0;

    if( !symbol->CanUpdateFieldsFromParent() )
    {
        m_frame->ShowInfoBarError( _( "Symbol is not derived from another symbol." ) );
    }
    else
    {
        DIALOG_UPDATE_SYMBOL_FIELDS dlg( m_frame, symbol );

        if( dlg.ShowModal() == wxID_CANCEL )
            return -1;
    }

    return 0;
}


int SYMBOL_EDITOR_EDIT_TOOL::Undo( const TOOL_EVENT& aEvent )
{
    SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool<SCH_SELECTION_TOOL>();

    // Nuke the selection for later rebuilding.  This does *not* clear the flags on any items;
    // it just clears the SELECTION's reference to them.
    selTool->GetSelection().Clear();
    {
        m_frame->GetSymbolFromUndoList();
    }
    selTool->RebuildSelection();

    return 0;
}


int SYMBOL_EDITOR_EDIT_TOOL::Redo( const TOOL_EVENT& aEvent )
{
    SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool<SCH_SELECTION_TOOL>();

    // Nuke the selection for later rebuilding.  This does *not* clear the flags on any items;
    // it just clears the SELECTION's reference to them.
    selTool->GetSelection().Clear();
    {
        m_frame->GetSymbolFromRedoList();
    }
    selTool->RebuildSelection();

    return 0;
}


int SYMBOL_EDITOR_EDIT_TOOL::Cut( const TOOL_EVENT& aEvent )
{
    int retVal = Copy( aEvent );

    if( retVal == 0 )
        retVal = DoDelete( aEvent );

    return retVal;
}


int SYMBOL_EDITOR_EDIT_TOOL::Copy( const TOOL_EVENT& aEvent )
{
    LIB_SYMBOL*    symbol = m_frame->GetCurSymbol();
    SCH_SELECTION& selection = m_selectionTool->RequestSelection( nonFields );

    if( !symbol || !selection.GetSize() )
        return 0;

    for( SCH_ITEM& item : symbol->GetDrawItems() )
    {
        if( item.Type() == SCH_FIELD_T )
            continue;

        wxASSERT( !item.HasFlag( STRUCT_DELETED ) );

        if( !item.IsSelected() )
            item.SetFlags( STRUCT_DELETED );
    }

    LIB_SYMBOL* partCopy = new LIB_SYMBOL( *symbol );

    STRING_FORMATTER  formatter;
    SCH_IO_KICAD_SEXPR::FormatLibSymbol( partCopy, formatter );

    delete partCopy;

    for( SCH_ITEM& item : symbol->GetDrawItems() )
        item.ClearFlags( STRUCT_DELETED );

    std::string prettyData = formatter.GetString();
    KICAD_FORMAT::Prettify( prettyData, KICAD_FORMAT::FORMAT_MODE::COMPACT_TEXT_PROPERTIES );

    // Generate SVG and PNG for multi-format clipboard
    std::vector<CLIPBOARD_MIME_DATA> mimeData;

    // Get the bounding box for just the selected items
    BOX2I bbox;

    for( EDA_ITEM* item : selection )
    {
        SCH_ITEM* schItem = static_cast<SCH_ITEM*>( item );
        if( bbox.GetWidth() == 0 && bbox.GetHeight() == 0 )
            bbox = schItem->GetBoundingBox();
        else
            bbox.Merge( schItem->GetBoundingBox() );
    }

    if( bbox.GetWidth() > 0 && bbox.GetHeight() > 0 )
    {
        bbox.Inflate( bbox.GetWidth() * clipboardBboxInflation,
                      bbox.GetHeight() * clipboardBboxInflation );

        // Create a temporary symbol with just the selected items for plotting
        LIB_SYMBOL* plotSymbol = new LIB_SYMBOL( *symbol );

        // Mark unselected items as deleted in the plot copy
        for( SCH_ITEM& item : plotSymbol->GetDrawItems() )
        {
            if( item.Type() == SCH_FIELD_T )
                continue;

            // Find matching item in selection by position/type
            bool found = false;

            for( EDA_ITEM* selItem : selection )
            {
                SCH_ITEM* selSchItem = static_cast<SCH_ITEM*>( selItem );

                if( selSchItem->Type() == item.Type()
                    && selSchItem->GetPosition() == item.GetPosition() )
                {
                    found = true;
                    break;
                }
            }

            if( !found )
                item.SetFlags( STRUCT_DELETED );
        }

        // Now copy only the non-deleted items to a clean symbol for plotting
        LIB_SYMBOL* cleanSymbol = new LIB_SYMBOL( *plotSymbol );
        delete plotSymbol;

        int unit = m_frame->GetUnit();
        int bodyStyle = m_frame->GetBodyStyle();

        wxMemoryBuffer svgBuffer;

        if( plotSymbolToSvg( m_frame, cleanSymbol, bbox, unit, bodyStyle, svgBuffer ) )
            appendMimeData( mimeData, wxS( "image/svg+xml" ), svgBuffer );

        wxImage pngImage = renderSymbolToImageWithAlpha( m_frame, cleanSymbol, bbox, unit, bodyStyle );

        if( pngImage.IsOk() )
            appendMimeData( mimeData, wxS( "image/png" ), std::move( pngImage ) );

        delete cleanSymbol;
    }

    if( SaveClipboard( prettyData, mimeData ) )
        return 0;
    else
        return -1;
}


int SYMBOL_EDITOR_EDIT_TOOL::CopyAsText( const TOOL_EVENT& aEvent )
{
    SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool<SCH_SELECTION_TOOL>();
    SCH_SELECTION&      selection = selTool->RequestSelection();

    if( selection.Empty() )
        return 0;

    wxString itemsAsText = GetSelectedItemsAsText( selection );

    if( selection.IsHover() )
        m_toolMgr->RunAction( ACTIONS::selectionClear );

    return SaveClipboard( itemsAsText.ToStdString() );
}


int SYMBOL_EDITOR_EDIT_TOOL::Paste( const TOOL_EVENT& aEvent )
{
    LIB_SYMBOL* symbol  = m_frame->GetCurSymbol();
    LIB_SYMBOL* newPart = nullptr;

    if( !symbol || symbol->IsDerived() )
        return 0;

    std::string clipboardData = GetClipboardUTF8();

    try
    {
        std::vector<LIB_SYMBOL*> newParts = SCH_IO_KICAD_SEXPR::ParseLibSymbols( clipboardData, "Clipboard" );

        if( newParts.empty() || !newParts[0] )
            return -1;

        newPart = newParts[0];
    }
    catch( IO_ERROR& )
    {
        // If it's not a symbol then paste as text
        newPart = new LIB_SYMBOL( "dummy_part" );

        wxString pasteText( clipboardData );

        // Limit of 5000 is totally arbitrary.  Without a limit, pasting a bitmap image from
        // eeschema makes KiCad appear to hang.
        if( pasteText.Length() > 5000 )
            pasteText = pasteText.Left( 5000 ) + wxT( "..." );

        SCH_TEXT* newText = new SCH_TEXT( { 0, 0 }, pasteText, LAYER_DEVICE );
        newPart->AddDrawItem( newText );
    }

    SCH_COMMIT commit( m_toolMgr );

    commit.Modify( symbol, m_frame->GetScreen() );
    m_selectionTool->ClearSelection();

    for( SCH_ITEM& item : symbol->GetDrawItems() )
        item.ClearFlags( IS_NEW | IS_PASTED | SELECTED );

    for( SCH_ITEM& item : newPart->GetDrawItems() )
    {
        if( item.Type() == SCH_FIELD_T )
            continue;

        SCH_ITEM* newItem = item.Duplicate( true, &commit );
        newItem->SetParent( symbol );
        newItem->SetFlags( IS_NEW | IS_PASTED | SELECTED );

        newItem->SetUnit( newItem->GetUnit() ? m_frame->GetUnit() : 0 );
        newItem->SetBodyStyle( newItem->GetBodyStyle() ? m_frame->GetBodyStyle() : 0 );

        symbol->AddDrawItem( newItem );
        getView()->Add( newItem );
    }

    delete newPart;

    m_selectionTool->RebuildSelection();

    SCH_SELECTION& selection = m_selectionTool->GetSelection();

    if( !selection.Empty() )
    {
        selection.SetReferencePoint( getViewControls()->GetCursorPosition( true ) );

        if( m_toolMgr->RunSynchronousAction( SCH_ACTIONS::move, &commit ) )
            commit.Push( _( "Paste" ) );
        else
            commit.Revert();
    }

    return 0;
}


int SYMBOL_EDITOR_EDIT_TOOL::Duplicate( const TOOL_EVENT& aEvent )
{
    LIB_SYMBOL*    symbol = m_frame->GetCurSymbol();
    SCH_SELECTION& selection = m_selectionTool->RequestSelection( nonFields );
    SCH_COMMIT     commit( m_toolMgr );

    if( selection.GetSize() == 0 )
        return 0;

    commit.Modify( symbol, m_frame->GetScreen() );

    std::vector<EDA_ITEM*> oldItems;
    std::vector<EDA_ITEM*> newItems;

    std::copy( selection.begin(), selection.end(), std::back_inserter( oldItems ) );
    std::sort( oldItems.begin(), oldItems.end(), []( EDA_ITEM* a, EDA_ITEM* b )
    {
        int cmp;

        if( a->Type() != b->Type() )
            return a->Type() < b->Type();

        // Create the new pins in the same order as the old pins
        if( a->Type() == SCH_PIN_T )
        {
            const wxString& aNum = static_cast<SCH_PIN*>( a )->GetNumber();
            const wxString& bNum = static_cast<SCH_PIN*>( b )->GetNumber();

            cmp = StrNumCmp( aNum, bNum );

            // If the pin numbers are not numeric, then just number them by their position
            // on the screen.
            if( aNum.IsNumber() && bNum.IsNumber() && cmp != 0 )
                return cmp < 0;
        }

        cmp = LexicographicalCompare( a->GetPosition(), b->GetPosition() );

        if( cmp != 0 )
            return cmp < 0;

        return a->m_Uuid < b->m_Uuid;
    } );

    for( EDA_ITEM* item : oldItems )
    {
        SCH_ITEM* oldItem = static_cast<SCH_ITEM*>( item );
        SCH_ITEM* newItem = oldItem->Duplicate( true, &commit );

        if( newItem->Type() == SCH_PIN_T )
        {
            SCH_PIN* newPin = static_cast<SCH_PIN*>( newItem );

            if( !newPin->GetNumber().IsEmpty() )
                newPin->SetNumber( wxString::Format( wxT( "%i" ), symbol->GetMaxPinNumber() + 1 ) );
        }

        oldItem->ClearFlags( IS_NEW | IS_PASTED | SELECTED );
        newItem->SetFlags( IS_NEW | IS_PASTED | SELECTED );
        newItem->SetParent( symbol );
        newItems.push_back( newItem );

        symbol->AddDrawItem( newItem );
        getView()->Add( newItem );
    }

    m_toolMgr->RunAction( ACTIONS::selectionClear );
    m_toolMgr->RunAction<EDA_ITEMS*>( ACTIONS::selectItems, &newItems );

    selection.SetReferencePoint( getViewControls()->GetCursorPosition( true ) );

    if( m_toolMgr->RunSynchronousAction( SCH_ACTIONS::move, &commit ) )
        commit.Push( _( "Duplicate" ) );
    else
        commit.Revert();

    return 0;
}


void SYMBOL_EDITOR_EDIT_TOOL::setTransitions()
{
    // clang-format off
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Undo,               ACTIONS::undo.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Redo,               ACTIONS::redo.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Cut,                ACTIONS::cut.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Copy,               ACTIONS::copy.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::CopyAsText,         ACTIONS::copyAsText.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Paste,              ACTIONS::paste.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Duplicate,          ACTIONS::duplicate.MakeEvent() );

    Go( &SYMBOL_EDITOR_EDIT_TOOL::Rotate,             SCH_ACTIONS::rotateCW.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Rotate,             SCH_ACTIONS::rotateCCW.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Mirror,             SCH_ACTIONS::mirrorV.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Mirror,             SCH_ACTIONS::mirrorH.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Swap,               SCH_ACTIONS::swap.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::DoDelete,           ACTIONS::doDelete.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::InteractiveDelete,  ACTIONS::deleteTool.MakeEvent() );

    Go( &SYMBOL_EDITOR_EDIT_TOOL::Increment,          ACTIONS::increment.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Increment,          ACTIONS::incrementPrimary.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Increment,          ACTIONS::decrementPrimary.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Increment,          ACTIONS::incrementSecondary.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Increment,          ACTIONS::decrementSecondary.MakeEvent() );

    Go( &SYMBOL_EDITOR_EDIT_TOOL::Properties,         SCH_ACTIONS::properties.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::Properties,         SCH_ACTIONS::symbolProperties.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::PinTable,           SCH_ACTIONS::pinTable.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::ConvertStackedPins, SCH_ACTIONS::convertStackedPins.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::ExplodeStackedPin,  SCH_ACTIONS::explodeStackedPin.MakeEvent() );
    Go( &SYMBOL_EDITOR_EDIT_TOOL::UpdateSymbolFields, SCH_ACTIONS::updateSymbolFields.MakeEvent() );
    // clang-format on
}
