/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2017 Wayne Stambaugh <stambaughw@gmail.com>
 * Copyright (C) 2021 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 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 <set>
#include <wx/regex.h>

#include <build_version.h>
#include <common.h>     // For ExpandEnvVarSubstitutions
#include <dialogs/dialog_plugin_options.h>
#include <project.h>
#include <panel_sym_lib_table.h>
#include <lib_id.h>
#include <libraries/library_table.h>
#include <libraries/library_manager.h>
#include <lib_table_grid_tricks.h>
#include <widgets/wx_grid.h>
#include <widgets/grid_readonly_text_helpers.h>
#include <widgets/split_button.h>
#include <widgets/std_bitmap_button.h>
#include <confirm.h>
#include <bitmaps.h>
#include <lib_table_grid_data_model.h>
#include <wildcards_and_files_ext.h>
#include <env_paths.h>
#include <functional>
#include <eeschema_id.h>
#include <env_vars.h>
#include <sch_io/sch_io.h>
#include <symbol_edit_frame.h>
#include <sch_edit_frame.h>
#include <kiway.h>
#include <paths.h>
#include <kiplatform/ui.h>
#include <pgm_base.h>
#include <settings/settings_manager.h>
#include <wx/dir.h>
#include <wx/dirdlg.h>
#include <wx/filedlg.h>
#include <wx/msgdlg.h>
#include <project_sch.h>
#include <libraries/symbol_library_adapter.h>
#include <lib_table_notebook_panel.h>
#include <widgets/wx_aui_art_providers.h>

/**
 * Container that describes file type info for the add a library options
 */
struct SUPPORTED_FILE_TYPE
{
    wxString m_Description;            ///< Description shown in the file picker dialog.
    wxString m_FileFilter;             ///< Filter used for file pickers if m_IsFile is true.

    /// In case of folders it stands for extensions of files stored inside.
    wxString m_FolderSearchExtension;
    bool     m_IsFile;                 ///< Whether the library is a folder or a file.
    SCH_IO_MGR::SCH_FILE_T m_Plugin;
};


/**
 * Special menu ID for folder-based KiCad symbol library format.
 * This ID is offset from SCH_FILE_T values to distinguish folder mode from file mode.
 */
static constexpr int FIRST_MENU_ID = 1000;
// This must not collide with any SCH_FILE_T enum values so we offset it below.
static constexpr int ID_PANEL_SYM_LIB_KICAD_FOLDER = FIRST_MENU_ID - 1;


class SYMBOL_LIB_TABLE_GRID_DATA_MODEL : public LIB_TABLE_GRID_DATA_MODEL
{
public:
    SYMBOL_LIB_TABLE_GRID_DATA_MODEL( DIALOG_SHIM* aParent, WX_GRID* aGrid, const LIBRARY_TABLE& aTableToEdit,
                                      SYMBOL_LIBRARY_ADAPTER* aAdapter, const wxArrayString& aPluginChoices,
                                      wxString* aMRUDirectory, const wxString& aProjectPath ) :
            LIB_TABLE_GRID_DATA_MODEL( aParent, aGrid, aTableToEdit, aAdapter, aPluginChoices, aMRUDirectory,
                                       aProjectPath )
    {
    }

    void SetValue( int aRow, int aCol, const wxString &aValue ) override
    {
        wxCHECK( aRow < (int) size(), /* void */ );

        LIB_TABLE_GRID_DATA_MODEL::SetValue( aRow, aCol, aValue );

        // If setting a filepath, attempt to auto-detect the format
        if( aCol == COL_URI )
        {
            LIBRARY_TABLE_ROW& row = at( static_cast<size_t>( aRow ) );
            wxString uri = LIBRARY_MANAGER::ExpandURI( row.URI(), Pgm().GetSettingsManager().Prj() );
            SCH_IO_MGR::SCH_FILE_T pluginType = SCH_IO_MGR::GuessPluginTypeFromLibPath( uri );

            if( pluginType != SCH_IO_MGR::SCH_FILE_UNKNOWN )
                SetValue( aRow, COL_TYPE, SCH_IO_MGR::ShowType( pluginType ) );
        }
    }

protected:
    wxString getFileTypes( WX_GRID* aGrid, int aRow ) override
    {
        LIB_TABLE_GRID_DATA_MODEL* table = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( aGrid->GetTable() );
        LIBRARY_TABLE_ROW&         tableRow = table->At( aRow );

        if( tableRow.Type() == LIBRARY_TABLE_ROW::TABLE_TYPE_NAME )
        {
            wxString filter = _( "Symbol Library Tables" );
#ifndef __WXOSX__
            filter << wxString::Format( _( " (%s)|%s" ), FILEEXT::SymbolLibraryTableFileName,
                                        FILEEXT::SymbolLibraryTableFileName );
#else
            filter << wxString::Format( _( " (%s)|%s" ), wxFileSelectorDefaultWildcardStr,
                                        wxFileSelectorDefaultWildcardStr );
#endif
            return filter;
        }

        SCH_IO_MGR::SCH_FILE_T pi_type = SCH_IO_MGR::EnumFromStr( tableRow.Type() );
        IO_RELEASER<SCH_IO>    pi( SCH_IO_MGR::FindPlugin( pi_type ) );

        if( pi )
        {
            const IO_BASE::IO_FILE_DESC& desc = pi->GetLibraryDesc();

            if( desc.m_IsFile )
                return desc.FileFilter();
        }

        return wxEmptyString;
    }
};


class SYMBOL_GRID_TRICKS : public LIB_TABLE_GRID_TRICKS
{
public:
    SYMBOL_GRID_TRICKS( PANEL_SYM_LIB_TABLE* aPanel, WX_GRID* aGrid,
                        std::function<void( wxCommandEvent& )> aAddHandler ) :
            LIB_TABLE_GRID_TRICKS( aGrid, aAddHandler ),
            m_panel( aPanel )
    {
        SetTooltipEnable( COL_STATUS );
    }

    static bool SupportsVisibilityColumn()
    {
        return true;
    }

protected:
    void optionsEditor( int aRow ) override
    {
        LIB_TABLE_GRID_DATA_MODEL* tbl = static_cast<LIB_TABLE_GRID_DATA_MODEL*>( m_grid->GetTable() );

        if( tbl->GetNumberRows() > aRow )
        {
            LIBRARY_TABLE_ROW& row = tbl->At( static_cast<size_t>( aRow ) );
            const wxString&    options = row.Options();
            wxString           result = options;
            std::map<std::string, UTF8> choices;

            SCH_IO_MGR::SCH_FILE_T pi_type = SCH_IO_MGR::EnumFromStr( row.Type() );
            IO_RELEASER<SCH_IO> pi( SCH_IO_MGR::FindPlugin( pi_type ) );
            pi->GetLibraryOptions( &choices );

            DIALOG_PLUGIN_OPTIONS dlg( wxGetTopLevelParent( m_grid ), row.Nickname(), choices, options, &result );
            dlg.ShowModal();

            if( options != result )
            {
                row.SetOptions( result );
                m_grid->Refresh();
            }
        }
    }

    void openTable( const LIBRARY_TABLE_ROW& aRow ) override
    {
        wxFileName fn( LIBRARY_MANAGER::ExpandURI( aRow.URI(), Pgm().GetSettingsManager().Prj() ) );
        std::shared_ptr<LIBRARY_TABLE> child = std::make_shared<LIBRARY_TABLE>( fn, LIBRARY_TABLE_SCOPE::GLOBAL, LIBRARY_TABLE_TYPE::SYMBOL );

        if( !child->IsOk() )
        {
            wxMessageBox( _( "Unable to load library table." ) );
        }
        else
        {
            Pgm().GetLibraryManager().ApplyLibOverrides( *child );

            m_panel->OpenTable( child, aRow.Nickname() );
        }
    }

    wxString getTablePreamble() override
    {
        return wxT( "(sym_lib_table" );
    }

    bool supportsVisibilityColumn() override
    {
        return SYMBOL_GRID_TRICKS::SupportsVisibilityColumn();
    }

protected:
    PANEL_SYM_LIB_TABLE* m_panel;
};


void PANEL_SYM_LIB_TABLE::OpenTable( const std::shared_ptr<LIBRARY_TABLE>& aTable, const wxString& aTitle )
{
    wxString tabTitle = aTitle;

    if( aTable->IsReadOnly() )
        tabTitle += wxS( " " ) + _( "(read-only)" );

    for( int ii = 2; ii < (int) m_notebook->GetPageCount(); ++ii )
    {
        if( m_notebook->GetPageText( ii ) == tabTitle )
        {
            // Something is pretty fishy with wxAuiNotebook::ChangeSelection(); on Mac at least it
            // results in a re-entrant call where the second call is one page behind.
            for( int attempts = 0; attempts < 3; ++attempts )
                m_notebook->ChangeSelection( ii );

            return;
        }
    }

    m_nestedTables.push_back( aTable );
    AddTable( aTable.get(), tabTitle, true );

    // Something is pretty fishy with wxAuiNotebook::ChangeSelection(); on Mac at least it
    // results in a re-entrant call where the second call is one page behind.
    for( int attempts = 0; attempts < 3; ++attempts )
        m_notebook->ChangeSelection( m_notebook->GetPageCount() - 1 );
}


void PANEL_SYM_LIB_TABLE::AddTable( LIBRARY_TABLE* table, const wxString& aTitle, bool aClosable )
{
    SYMBOL_LIBRARY_ADAPTER* adapter = PROJECT_SCH::SymbolLibAdapter( m_project );
    wxString                projectPath = m_project->GetProjectPath();

    LIB_TABLE_NOTEBOOK_PANEL::AddTable( m_notebook, aTitle, aClosable );

    WX_GRID* grid = get_grid( (int) m_notebook->GetPageCount() - 1 );

    if( table->Path().StartsWith( projectPath ) )
    {
        grid->SetTable( new SYMBOL_LIB_TABLE_GRID_DATA_MODEL( m_parent, grid, *table, adapter, m_pluginChoices,
                                                              &m_lastProjectLibDir, projectPath ),
                        true /* take ownership */ );
    }
    else
    {
        wxString* lastGlobalLibDir = nullptr;

        if( EESCHEMA_SETTINGS* cfg = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
        {
            if( cfg->m_lastSymbolLibDir.IsEmpty() )
                cfg->m_lastSymbolLibDir = PATHS::GetDefaultUserSymbolsPath();

            lastGlobalLibDir = &cfg->m_lastSymbolLibDir;
        }

        grid->SetTable( new SYMBOL_LIB_TABLE_GRID_DATA_MODEL( m_parent, grid, *table, adapter, m_pluginChoices,
                                                              lastGlobalLibDir, wxEmptyString ),
                        true /* take ownership */ );
    }

    static_cast<LIB_TABLE_GRID_DATA_MODEL*>( grid->GetTable() )->RecheckRows();

    LIB_TABLE_NOTEBOOK_PANEL* notebookPanel =
            static_cast<LIB_TABLE_NOTEBOOK_PANEL*>( m_notebook->GetPage( m_notebook->GetPageCount() - 1 ) );

    static_cast<LIB_TABLE_GRID_DATA_MODEL*>( grid->GetTable() )
            ->SetChangeCallback(
                    [notebookPanel]()
                    {
                        notebookPanel->MarkDirty();
                    } );

    // add Cut, Copy, and Paste to wxGrids
    grid->PushEventHandler( new SYMBOL_GRID_TRICKS( this, grid,
            [this]( wxCommandEvent& event )
            {
                appendRowHandler( event );
            } ) );

    auto autoSizeCol =
            [&]( int aCol )
            {
                int prevWidth = grid->GetColSize( aCol );

                grid->AutoSizeColumn( aCol, false );
                grid->SetColSize( aCol, std::max( prevWidth, grid->GetColSize( aCol ) ) );
            };

    // all but COL_OPTIONS, which is edited with Option Editor anyways.
    autoSizeCol( COL_NICKNAME );
    autoSizeCol( COL_TYPE );
    autoSizeCol( COL_URI );
    autoSizeCol( COL_DESCR );

    if( grid->GetNumberRows() > 0 )
    {
        grid->SetGridCursor( 0, COL_NICKNAME );
        grid->SelectRow( 0 );
    }
}


PANEL_SYM_LIB_TABLE::PANEL_SYM_LIB_TABLE( DIALOG_EDIT_LIBRARY_TABLES* aParent, PROJECT* aProject ) :
        PANEL_SYM_LIB_TABLE_BASE( aParent ),
        m_project( aProject ),
        m_parent( aParent ),
        m_suppressNotebookPageEvents( false )
{
    m_notebook->SetArtProvider( new WX_AUI_TAB_ART() );

    m_lastProjectLibDir = m_project->GetProjectPath();

    populatePluginList();

    for( const SCH_IO_MGR::SCH_FILE_T& type : SCH_IO_MGR::SCH_FILE_T_vector )
    {
        if( type == SCH_IO_MGR::SCH_NESTED_TABLE )
        {
            m_pluginChoices.Add( SCH_IO_MGR::ShowType( type ) );
            continue;
        }

        IO_RELEASER<SCH_IO> pi( SCH_IO_MGR::FindPlugin( type ) );

        if( pi )
            m_pluginChoices.Add( SCH_IO_MGR::ShowType( type ) );
    }

    std::optional<LIBRARY_TABLE*> table = Pgm().GetLibraryManager().Table( LIBRARY_TABLE_TYPE::SYMBOL,
                                                                           LIBRARY_TABLE_SCOPE::GLOBAL );
    wxASSERT( table.has_value() );

    AddTable( table.value(), _( "Global Libraries" ), false /* closable */ );

    std::optional<LIBRARY_TABLE*> projectTable = Pgm().GetLibraryManager().Table( LIBRARY_TABLE_TYPE::SYMBOL,
                                                                                  LIBRARY_TABLE_SCOPE::PROJECT );

    if( projectTable.has_value() )
        AddTable( projectTable.value(), _( "Project Specific Libraries" ), false /* closable */ );

    // add Cut, Copy, and Paste to wxGrids
    m_path_subs_grid->PushEventHandler( new GRID_TRICKS( m_path_subs_grid ) );

    populateEnvironReadOnlyTable();

    m_path_subs_grid->SetColLabelValue( 0, _( "Name" ) );
    m_path_subs_grid->SetColLabelValue( 1, _( "Value" ) );

    // Configure button logos
    m_append_button->SetBitmap( KiBitmapBundle( BITMAPS::small_plus ) );
    m_delete_button->SetBitmap( KiBitmapBundle( BITMAPS::small_trash ) );
    m_move_up_button->SetBitmap( KiBitmapBundle( BITMAPS::small_up ) );
    m_move_down_button->SetBitmap( KiBitmapBundle( BITMAPS::small_down ) );
    m_browseButton->SetBitmap( KiBitmapBundle( BITMAPS::small_folder ) );

    // For aesthetic reasons, we must set the size of m_browseButton to match the other bitmaps
    Layout();
    wxSize buttonSize = m_append_button->GetSize();

    m_browseButton->SetWidthPadding( 4 );
    m_browseButton->SetMinSize( buttonSize );

    // Populate the browse library options
    wxMenu* browseMenu = m_browseButton->GetSplitButtonMenu();

    auto joinExtensions =
            []( const std::vector<std::string>& aExts ) -> wxString
            {
                wxString result;

                for( const std::string& ext : aExts )
                {
                    if( !result.IsEmpty() )
                        result << wxT( ", " );

                    result << wxT( "." ) << ext;
                }

                return result;
            };

    for( auto& [type, desc] : m_supportedSymFiles )
    {
        wxString entryStr = SCH_IO_MGR::ShowType( type );

        if( !desc.m_FileExtensions.empty() )
            entryStr << wxString::Format( wxS( " (%s)" ), joinExtensions( desc.m_FileExtensions ) );

        browseMenu->Append( type + FIRST_MENU_ID, entryStr );
        browseMenu->Bind( wxEVT_COMMAND_MENU_SELECTED, &PANEL_SYM_LIB_TABLE::browseLibrariesHandler, this,
                          type + FIRST_MENU_ID );

        // Add folder-based entry right after KiCad file-based entry
        if( type == SCH_IO_MGR::SCH_KICAD )
        {
            wxString folderEntry = SCH_IO_MGR::ShowType( SCH_IO_MGR::SCH_KICAD );
            folderEntry << wxString::Format( wxS( " (%s)" ), _( "folder with .kicad_sym files" ) );
            browseMenu->Append( ID_PANEL_SYM_LIB_KICAD_FOLDER, folderEntry );
            browseMenu->Bind( wxEVT_COMMAND_MENU_SELECTED, &PANEL_SYM_LIB_TABLE::browseLibrariesHandler, this,
                              ID_PANEL_SYM_LIB_KICAD_FOLDER );
        }
    }

    Layout();

    m_notebook->Bind( wxEVT_AUINOTEBOOK_PAGE_CLOSE, &PANEL_SYM_LIB_TABLE::onNotebookPageCloseRequest, this );
    m_notebook->Bind( wxEVT_AUINOTEBOOK_PAGE_CHANGING, &PANEL_SYM_LIB_TABLE::onNotebookPageChangeRequest, this );
    m_browseButton->Bind( wxEVT_BUTTON, &PANEL_SYM_LIB_TABLE::browseLibrariesHandler, this );
}


PANEL_SYM_LIB_TABLE::~PANEL_SYM_LIB_TABLE()
{
    wxMenu* browseMenu = m_browseButton->GetSplitButtonMenu();

    for( auto& [type, desc] : m_supportedSymFiles )
        browseMenu->Unbind( wxEVT_COMMAND_MENU_SELECTED, &PANEL_SYM_LIB_TABLE::browseLibrariesHandler, this, type );

    browseMenu->Unbind( wxEVT_COMMAND_MENU_SELECTED, &PANEL_SYM_LIB_TABLE::browseLibrariesHandler,
                        this, ID_PANEL_SYM_LIB_KICAD_FOLDER );
    m_browseButton->Unbind( wxEVT_BUTTON, &PANEL_SYM_LIB_TABLE::browseLibrariesHandler, this );

    // Delete the GRID_TRICKS.
    // (Notebook page GRID_TRICKS are deleted by LIB_TABLE_NOTEBOOK_PANEL.)
    m_path_subs_grid->PopEventHandler( true );
}


void PANEL_SYM_LIB_TABLE::populatePluginList()
{
    for( const SCH_IO_MGR::SCH_FILE_T& type : SCH_IO_MGR::SCH_FILE_T_vector )
    {
        IO_RELEASER<SCH_IO> pi( SCH_IO_MGR::FindPlugin( type ) );

        if( !pi )
            continue;

        if( const IO_BASE::IO_FILE_DESC& desc = pi->GetLibraryDesc() )
        {
            if( !desc.m_FileExtensions.empty() )
                m_supportedSymFiles.emplace( type, desc );
        }
    }

    m_supportedSymFiles.emplace( SCH_IO_MGR::SCH_NESTED_TABLE,
                                 IO_BASE::IO_FILE_DESC( _( "Table (nested library table)" ), {} ) );
}


SYMBOL_LIB_TABLE_GRID_DATA_MODEL* PANEL_SYM_LIB_TABLE::get_model( int aPage ) const
{
    return static_cast<SYMBOL_LIB_TABLE_GRID_DATA_MODEL*>( get_grid( aPage )->GetTable() );
}


WX_GRID* PANEL_SYM_LIB_TABLE::get_grid( int aPage ) const
{
    return static_cast<LIB_TABLE_NOTEBOOK_PANEL*>( m_notebook->GetPage( aPage ) )->GetGrid();
}


bool PANEL_SYM_LIB_TABLE::TransferDataToWindow()
{
    // for ALT+A handling, we want the initial focus to be on the first selected grid.
    m_parent->SetInitialFocus( cur_grid() );

    return true;
}


bool PANEL_SYM_LIB_TABLE::verifyTables()
{
    for( int page = 0 ; page < (int) m_notebook->GetPageCount(); ++page )
    {
        WX_GRID* grid = get_grid( page );

        if( !SYMBOL_GRID_TRICKS::VerifyTable( grid, SYMBOL_GRID_TRICKS::SupportsVisibilityColumn(),
                [&]( int aRow, int aCol )
                {
                    // show the tabbed panel holding the grid we have flunked:
                    if( m_notebook->GetSelection() != page )
                        m_notebook->SetSelection( page );

                    grid->MakeCellVisible( aRow, 0 );
                    grid->SetGridCursor( aRow, aCol );
                } ) )
        {
            return false;
        }
    }

    return true;
}


void PANEL_SYM_LIB_TABLE::browseLibrariesHandler( wxCommandEvent& event )
{
    if( !cur_grid()->CommitPendingChanges() )
        return;

    SCH_IO_MGR::SCH_FILE_T fileType = SCH_IO_MGR::SCH_FILE_UNKNOWN;
    bool                   selectingFolder = false;

    // We are bound both to the menu and button with this one handler
    if( event.GetEventType() == wxEVT_BUTTON )
    {
        // Default to KiCad file format when clicking the button directly
        fileType = SCH_IO_MGR::SCH_KICAD;
    }
    else if( event.GetId() == ID_PANEL_SYM_LIB_KICAD_FOLDER )
    {
        // Special case for folder-based KiCad library
        fileType = SCH_IO_MGR::SCH_KICAD;
        selectingFolder = true;
    }
    else
    {
        fileType = static_cast<SCH_IO_MGR::SCH_FILE_T>( event.GetId() - FIRST_MENU_ID );
    }

    if( fileType == SCH_IO_MGR::SCH_FILE_UNKNOWN )
        return;

    const ENV_VAR_MAP& envVars = Pgm().GetLocalEnvVariables();

    EESCHEMA_SETTINGS* cfg = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
    wxString           dummy;
    wxString*          lastDir;

    if( m_notebook->GetSelection() == 0 )
        lastDir = cfg ? &cfg->m_lastSymbolLibDir : &dummy;
    else
        lastDir = &m_lastProjectLibDir;

    wxString       title = wxString::Format( _( "Select %s Library" ), SCH_IO_MGR::ShowType( fileType ) );
    wxWindow*      topLevelParent = wxGetTopLevelParent( this );
    wxArrayString  files;

    if( selectingFolder )
    {
        wxDirDialog dlg( topLevelParent, title, *lastDir, wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST | wxDD_MULTIPLE );

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

        dlg.GetPaths( files );

        if( !files.IsEmpty() )
        {
            wxFileName first( files.front() );
            *lastDir = first.GetPath();
        }
    }
    else
    {
        auto it = m_supportedSymFiles.find( fileType );

        if( it == m_supportedSymFiles.end() )
            return;

        const IO_BASE::IO_FILE_DESC& fileDesc = it->second;

        wxFileDialog dlg( topLevelParent, title, *lastDir, wxEmptyString, fileDesc.FileFilter(),
                          wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_MULTIPLE );

        KIPLATFORM::UI::AllowNetworkFileSystems( &dlg );

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

        dlg.GetPaths( files );
        *lastDir = dlg.GetDirectory();
    }

    bool     addDuplicates = false;
    bool     applyToAll    = false;
    wxString warning       = _( "Warning: Duplicate Nicknames" );
    wxString msg           = _( "An item nicknamed '%s' already exists." );
    wxString detailedMsg   = _( "One of the nicknames will need to be changed." );

    for( const wxString& filePath : files )
    {
        wxFileName fn( filePath );
        wxString   nickname = LIB_ID::FixIllegalChars( fn.GetName(), true );
        bool       doAdd    = true;

        if( cur_model()->ContainsNickname( nickname ) )
        {
            if( !applyToAll )
            {
                addDuplicates = OKOrCancelDialog( topLevelParent, warning,
                                                  wxString::Format( msg, nickname ), detailedMsg,
                                                  _( "Skip" ), _( "Add Anyway" ),
                                                  &applyToAll ) == wxID_CANCEL;
            }

            doAdd = addDuplicates;
        }

        if( doAdd && cur_grid()->AppendRows( 1 ) )
        {
            int last_row = cur_grid()->GetNumberRows() - 1;

            cur_grid()->SetCellValue( last_row, COL_NICKNAME, nickname );
            cur_grid()->SetCellValue( last_row, COL_TYPE, SCH_IO_MGR::ShowType( fileType ) );

            // try to use path normalized to an environmental variable or project path
            wxString path = NormalizePath( filePath, &envVars, m_project->GetProjectPath() );

            // Do not use the project path in the global library table.  This will almost
            // assuredly be wrong for a different project.
            if( m_notebook->GetSelection() == 0 && path.Contains( wxT( "${KIPRJMOD}" ) ) )
                path = fn.GetFullPath();

            cur_grid()->SetCellValue( last_row, COL_URI, path );
        }
    }

    if( !files.IsEmpty() )
    {
        cur_grid()->MakeCellVisible( cur_grid()->GetNumberRows() - 1, COL_ENABLED );
        cur_grid()->SetGridCursor( cur_grid()->GetNumberRows() - 1, COL_NICKNAME );
    }
}


void PANEL_SYM_LIB_TABLE::appendRowHandler( wxCommandEvent& event )
{
    LIB_TABLE_GRID_TRICKS::AppendRowHandler( cur_grid() );
}


void PANEL_SYM_LIB_TABLE::deleteRowHandler( wxCommandEvent& event )
{
    LIB_TABLE_GRID_TRICKS::DeleteRowHandler( cur_grid() );
}


void PANEL_SYM_LIB_TABLE::moveUpHandler( wxCommandEvent& event )
{
    LIB_TABLE_GRID_TRICKS::MoveUpHandler( cur_grid() );
}


void PANEL_SYM_LIB_TABLE::moveDownHandler( wxCommandEvent& event )
{
    LIB_TABLE_GRID_TRICKS::MoveDownHandler( cur_grid() );
}


void PANEL_SYM_LIB_TABLE::onReset( wxCommandEvent& event )
{
    if( !cur_grid()->CommitPendingChanges() )
        return;

    WX_GRID* grid = get_grid( 0 );

    // No need to prompt to preserve an empty table
    if( grid->GetNumberRows() > 0 && !IsOK( this, wxString::Format( _( "This action will reset your global library "
                                                                       "table on disk and cannot be undone." ) ) ) )
    {
        return;
    }

    wxString* lastGlobalLibDir = nullptr;

    if( EESCHEMA_SETTINGS* cfg = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
    {
        if( cfg->m_lastSymbolLibDir.IsEmpty() )
            cfg->m_lastSymbolLibDir = PATHS::GetDefaultUserSymbolsPath();

        lastGlobalLibDir = &cfg->m_lastSymbolLibDir;
    }

    LIBRARY_MANAGER::CreateGlobalTable( LIBRARY_TABLE_TYPE::SYMBOL, true );

    // Go ahead and reload here because this action takes place even if the dialog is canceled
    Pgm().GetLibraryManager().LoadGlobalTables( { LIBRARY_TABLE_TYPE::SYMBOL } );

    if( KIFACE *schface = m_parent->Kiway().KiFACE( KIWAY::FACE_SCH ) )
        schface->PreloadLibraries( &m_parent->Kiway() );

    grid->Freeze();

    wxGridTableBase* table = grid->GetTable();
    grid->DestroyTable( table );

    std::optional<LIBRARY_TABLE*> newTable = Pgm().GetLibraryManager().Table( LIBRARY_TABLE_TYPE::SYMBOL,
                                                                              LIBRARY_TABLE_SCOPE::GLOBAL );
    wxASSERT( newTable );

    SYMBOL_LIBRARY_ADAPTER* adapter = PROJECT_SCH::SymbolLibAdapter( &m_parent->Kiway().Prj() );

    grid->SetTable( new SYMBOL_LIB_TABLE_GRID_DATA_MODEL( m_parent, grid, *newTable.value(), adapter, m_pluginChoices,
                                                          lastGlobalLibDir, wxEmptyString ),
                    true /* take ownership */ );

    LIB_TABLE_NOTEBOOK_PANEL* panel0 =
            static_cast<LIB_TABLE_NOTEBOOK_PANEL*>( m_notebook->GetPage( 0 ) );
    panel0->ClearDirty();
    static_cast<LIB_TABLE_GRID_DATA_MODEL*>( grid->GetTable() )->SetChangeCallback(
            [panel0]() { panel0->MarkDirty(); } );

    m_parent->m_GlobalTableChanged = true;

    grid->Thaw();

    if( grid->GetNumberRows() > 0 )
    {
        grid->SetGridCursor( 0, COL_NICKNAME );
        grid->SelectRow( 0 );
    }
}


void PANEL_SYM_LIB_TABLE::onNotebookPageChangeRequest( wxAuiNotebookEvent& aEvent )
{
    if( m_suppressNotebookPageEvents )
        aEvent.Veto();
    else
        aEvent.Skip();
}


void PANEL_SYM_LIB_TABLE::onPageChange( wxAuiNotebookEvent& event )
{
    m_resetGlobal->Enable( m_notebook->GetSelection() == 0 );
}


void PANEL_SYM_LIB_TABLE::onNotebookPageCloseRequest( wxAuiNotebookEvent& aEvent )
{
    wxAuiNotebook* notebook = (wxAuiNotebook*) aEvent.GetEventObject();
    wxWindow*      page = notebook->GetPage( aEvent.GetSelection() );

    if( LIB_TABLE_NOTEBOOK_PANEL* panel = dynamic_cast<LIB_TABLE_NOTEBOOK_PANEL*>( page ) )
    {
        if( panel->GetClosable() )
        {
            if( !panel->GetCanClose() )
                aEvent.Veto();
        }
        else
        {
            aEvent.Veto();
        }
    }
}


void PANEL_SYM_LIB_TABLE::onConvertLegacyLibraries( wxCommandEvent& event )
{
    if( !cur_grid()->CommitPendingChanges() )
        return;

    wxArrayInt selectedRows = cur_grid()->GetSelectedRows();

    if( selectedRows.empty() && cur_grid()->GetGridCursorRow() >= 0 )
        selectedRows.push_back( cur_grid()->GetGridCursorRow() );

    wxArrayInt legacyRows;
    wxString   databaseType = SCH_IO_MGR::ShowType( SCH_IO_MGR::SCH_DATABASE );
    wxString   httpType = SCH_IO_MGR::ShowType( SCH_IO_MGR::SCH_HTTP );
    wxString   kicadType = SCH_IO_MGR::ShowType( SCH_IO_MGR::SCH_KICAD );
    wxString   nestedTableType = LIBRARY_TABLE_ROW::TABLE_TYPE_NAME;
    wxString   msg;

    // HTTP and Database libraries are live, dynamic backends that are not file-based.
    // Migrating them to a static .kicad_sym snapshot is not meaningful and would silently
    // produce an empty library, destroying the original table entry.  Nested library tables
    // are not symbol libraries at all and likewise cannot be migrated.
    for( int row : selectedRows )
    {
        const wxString& type = cur_grid()->GetCellValue( row, COL_TYPE );

        if( type != databaseType && type != httpType && type != kicadType && type != nestedTableType )
            legacyRows.push_back( row );
    }

    if( legacyRows.size() <= 0 )
    {
        wxMessageBox( _( "Select one or more rows containing libraries to save as current KiCad format." ) );
        return;
    }
    else
    {
        if( legacyRows.size() == 1 )
        {
            msg.Printf( _( "Save '%s' as current KiCad format (*.kicad_sym) and replace legacy entry in table?" ),
                        cur_grid()->GetCellValue( legacyRows[0], COL_NICKNAME ) );
        }
        else
        {
            msg.Printf( _( "Save %d libraries as current KiCad format (*.kicad_sym) and replace legacy entries "
                           "in table?" ),
                        (int) legacyRows.size() );
        }

        if( !IsOK( m_parent, msg ) )
            return;
    }

    for( int row : legacyRows )
    {
        wxString   relPath = cur_grid()->GetCellValue( row, COL_URI );
        wxString   resolvedPath = ExpandEnvVarSubstitutions( relPath, m_project );
        wxFileName legacyLib( resolvedPath );

        if( !legacyLib.Exists() )
        {
            DisplayErrorMessage( m_parent, wxString::Format( _( "Library '%s' not found." ), relPath ) );
            continue;
        }

        wxFileName newLib( resolvedPath );
        newLib.SetExt( "kicad_sym" );

        if( newLib.Exists() )
        {
            msg.Printf( _( "File '%s' already exists. Do you want overwrite this file?" ), newLib.GetFullPath() );

            switch( wxMessageBox( msg, _( "Migrate Library" ), wxYES_NO | wxCANCEL | wxICON_QUESTION, m_parent ) )
            {
            case wxYES:    break;
            case wxNO:     continue;
            case wxCANCEL: return;
            }
        }

        wxString                    options = cur_grid()->GetCellValue( row, COL_OPTIONS );
        std::map<std::string, UTF8> props( LIBRARY_TABLE::ParseOptions( options.ToStdString() ) );

        if( SCH_IO_MGR::ConvertLibrary( &props, legacyLib.GetFullPath(), newLib.GetFullPath() ) )
        {
            relPath = NormalizePath( newLib.GetFullPath(), &Pgm().GetLocalEnvVariables(), m_project );

            cur_grid()->SetCellValue( row, COL_URI, relPath );
            cur_grid()->SetCellValue( row, COL_TYPE, kicadType );
            cur_grid()->SetCellValue( row, COL_OPTIONS, wxEmptyString );
        }
        else
        {
            DisplayErrorMessage( m_parent, wxString::Format( _( "Failed to save symbol library file '%s'." ),
                                                             newLib.GetFullPath() ) );
        }
    }
}


bool PANEL_SYM_LIB_TABLE::TransferDataFromWindow()
{
    if( !cur_grid()->CommitPendingChanges() )
        return false;

    if( !verifyTables() )
        return false;

    LIBRARY_MANAGER&              manager = Pgm().GetLibraryManager();
    std::optional<LIBRARY_TABLE*> optTable = manager.Table( LIBRARY_TABLE_TYPE::SYMBOL, LIBRARY_TABLE_SCOPE::GLOBAL );
    wxCHECK( optTable, false );
    LIBRARY_TABLE* globalTable = *optTable;

    if( get_model( 0 )->Table() != *globalTable )
    {
        m_parent->m_GlobalTableChanged = true;
        *globalTable = get_model( 0 )->Table();

        globalTable->Save().map_error(
                []( const LIBRARY_ERROR& aError )
                {
                    wxMessageBox( _( "Error saving global library table:\n\n" ) + aError.message,
                                  _( "File Save Error" ), wxOK | wxICON_ERROR );
                } );
    }

    optTable = manager.Table( LIBRARY_TABLE_TYPE::SYMBOL, LIBRARY_TABLE_SCOPE::PROJECT );

    if( optTable.has_value() && get_model( 1 )->Table().Path() == optTable.value()->Path() )
    {
        LIBRARY_TABLE* projectTable = *optTable;

        if( get_model( 1 )->Table() != *projectTable )
        {
            m_parent->m_ProjectTableChanged = true;
            *projectTable = get_model( 1 )->Table();

            projectTable->Save().map_error(
                    []( const LIBRARY_ERROR& aError )
                    {
                        wxMessageBox( _( "Error saving project library table:\n\n" ) + aError.message,
                                      _( "File Save Error" ), wxOK | wxICON_ERROR );
                    } );
        }
    }

    for( int ii = 0; ii < (int) m_notebook->GetPageCount(); ++ii )
    {
        LIB_TABLE_NOTEBOOK_PANEL* panel = static_cast<LIB_TABLE_NOTEBOOK_PANEL*>( m_notebook->GetPage( ii ) );

        if( panel->GetClosable() && panel->TableModified() )
        {
            panel->SaveTable();
            m_parent->m_GlobalTableChanged = true;
            m_parent->m_ProjectTableChanged = true;
        }
    }

    m_suppressNotebookPageEvents = true;
    return true;
}


void PANEL_SYM_LIB_TABLE::populateEnvironReadOnlyTable()
{
    wxRegEx re( ".*?(\\$\\{(.+?)\\})|(\\$\\((.+?)\\)).*?", wxRE_ADVANCED );
    wxASSERT( re.IsValid() );   // wxRE_ADVANCED is required.

    std::set< wxString > unique;

    // clear the table
    m_path_subs_grid->ClearRows();

    for( int page = 0 ; page < (int) m_notebook->GetPageCount(); ++page )
    {
        LIB_TABLE_GRID_DATA_MODEL* model = get_model( page );

        for( int row = 0; row < model->GetNumberRows(); ++row )
        {
            wxString uri = model->GetValue( row, COL_URI );

            while( re.Matches( uri ) )
            {
                wxString envvar = re.GetMatch( uri, 2 );

                // if not ${...} form then must be $(...)
                if( envvar.IsEmpty() )
                    envvar = re.GetMatch( uri, 4 );

                // ignore duplicates
                unique.insert( envvar );

                // delete the last match and search again
                uri.Replace( re.GetMatch( uri, 0 ), wxEmptyString );
            }
        }
    }

    // Make sure this special environment variable shows up even if it was
    // not used yet.  It is automatically set by KiCad to the directory holding
    // the current project.
    unique.insert( PROJECT_VAR_NAME );
    unique.insert( ENV_VAR::GetVersionedEnvVarName( wxS( "SYMBOL_DIR" ) ) );

    for( const wxString& evName : unique )
    {
        int row = m_path_subs_grid->GetNumberRows();
        m_path_subs_grid->AppendRows( 1 );

        m_path_subs_grid->SetCellValue( row, 0, wxT( "${" ) + evName + wxT( "}" ) );
        m_path_subs_grid->SetCellEditor( row, 0, new GRID_CELL_READONLY_TEXT_EDITOR() );

        wxString evValue;
        wxGetEnv( evName, &evValue );
        m_path_subs_grid->SetCellValue( row, 1, evValue );
        m_path_subs_grid->SetCellEditor( row, 1, new GRID_CELL_READONLY_TEXT_EDITOR() );
    }

    adjustPathSubsGridColumns( m_path_subs_grid->GetRect().GetWidth() );
}


void PANEL_SYM_LIB_TABLE::adjustPathSubsGridColumns( int aWidth )
{
    // Account for scroll bars
    aWidth -= ( m_path_subs_grid->GetSize().x - m_path_subs_grid->GetClientSize().x );

    m_path_subs_grid->AutoSizeColumn( 0 );
    m_path_subs_grid->SetColSize( 0, std::max( 72, m_path_subs_grid->GetColSize( 0 ) ) );
    m_path_subs_grid->SetColSize( 1, std::max( 120, aWidth - m_path_subs_grid->GetColSize( 0 ) ) );
}


void PANEL_SYM_LIB_TABLE::onSizeGrid( wxSizeEvent& event )
{
    adjustPathSubsGridColumns( event.GetSize().GetX() );

    event.Skip();
}


void InvokeSchEditSymbolLibTable( KIWAY* aKiway, wxWindow *aParent )
{
    auto symbolEditor = static_cast<SYMBOL_EDIT_FRAME*>( aKiway->Player( FRAME_SCH_SYMBOL_EDITOR, false ) );
    wxString msg;

    // Refuse to open the dialog re-entrantly while a library sync is running.  A
    // sync can yield the event loop (via the progress dialog), which dispatches any
    // pending UI events — including clicks that accumulated while the app was busy.
    // Opening the dialog mid-sync corrupts the library tree.  Reschedule instead.
    if( symbolEditor && symbolEditor->IsSyncLibrariesInProgress() )
    {
        symbolEditor->CallAfter( [aKiway, aParent]()
        {
            InvokeSchEditSymbolLibTable( aKiway, aParent );
        } );
        return;
    }

    if( symbolEditor )
    {
        // This prevents an ugly crash on OSX (https://bugs.launchpad.net/kicad/+bug/1765286)
        symbolEditor->FreezeLibraryTree();

        if( symbolEditor->HasLibModifications() )
        {
            msg = _( "Modifications have been made to one or more symbol libraries.\n"
                     "Changes must be saved or discarded before the symbol library table can be modified." );

            switch( UnsavedChangesDialog( aParent, msg ) )
            {
            case wxID_YES:    symbolEditor->SaveAll();         break;
            case wxID_NO:     symbolEditor->RevertAll();       break;
            default:
            case wxID_CANCEL: symbolEditor->ThawLibraryTree(); return;
            }
        }
    }

    DIALOG_EDIT_LIBRARY_TABLES dlg( aParent, _( "Symbol Libraries" ) );
    dlg.SetKiway( &dlg, aKiway );

    dlg.InstallPanel( new PANEL_SYM_LIB_TABLE( &dlg, &aKiway->Prj() ) );

    if( dlg.ShowModal() == wxID_CANCEL )
    {
        if( symbolEditor )
            symbolEditor->ThawLibraryTree();

        return;
    }

    if( dlg.m_GlobalTableChanged )
        Pgm().GetLibraryManager().LoadGlobalTables( { LIBRARY_TABLE_TYPE::SYMBOL } );

    if( dlg.m_ProjectTableChanged )
    {
        // Trigger a reload of the table and cancel an in-progress background load
        Pgm().GetLibraryManager().ProjectChanged();
    }

    // Trigger a reload in case any libraries have been added or removed
    if( KIFACE *schface = aKiway->KiFACE( KIWAY::FACE_SCH ) )
        schface->PreloadLibraries( aKiway );

    if( symbolEditor )
        symbolEditor->ThawLibraryTree();
}
