/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2024 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 <widgets/resettable_panel.h>
#include <widgets/wx_infobar.h>
#include <widgets/wx_panel.h>
#include <widgets/ui_common.h>

#include <wx/button.h>
#include <wx/grid.h>
#include <wx/sizer.h>
#include <wx/treectrl.h>
#include <wx/listctrl.h>
#include <wx/stc/stc.h>
#include <wx/menu.h>
#include <wx/log.h>
#include <wx/wx.h>
#include <wx/dnd.h>
#include <wx/dcclient.h> // Include for wxClientDC if needed
#include <wx/dcbuffer.h> // Include for double-buffered drawing
#include <wx/bitmap.h>

#include <confirm.h>
#include <paths.h>
#include <bitmaps.h>
#include <launch_ext.h>
#include <algorithm>
#include <dialogs/rule_editor_dialog_base.h>
#include <widgets/std_bitmap_button.h>


static wxSearchCtrl* CreateTextFilterBox( wxWindow* aParent, const wxString& aDescriptiveText )
{
    wxSearchCtrl* search_widget = new wxSearchCtrl( aParent, wxID_ANY );

    search_widget->ShowSearchButton( false );
    search_widget->ShowCancelButton( true );

    search_widget->SetDescriptiveText( aDescriptiveText );

#ifdef __WXGTK__
    // wxSearchCtrl vertical height is not calculated correctly on some GTK setups
    // See https://gitlab.com/kicad/code/kicad/-/issues/9019
    search_widget->SetMinSize( wxSize( -1, aParent->GetTextExtent( wxT( "qb" ) ).y + 10 ) );
#endif

    return search_widget;
}


RULE_EDITOR_DIALOG_BASE::RULE_EDITOR_DIALOG_BASE( wxWindow* aParent, const wxString& aTitle,
                                                  const wxSize& aInitialSize ) :
        DIALOG_SHIM( aParent, wxID_ANY, aTitle, wxDefaultPosition, aInitialSize,
                     wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER ),
        m_isDragging( false ),
        m_enableMoveUp( false ),
        m_enableMoveDown( false ),
        m_enableAddRule( false ),
        m_enableDuplicateRule( false ),
        m_enableDeleteRule( false ),
        m_modified( false ),
        m_suppressSelectionEvents( false ),
        m_defaultSashPosition( 200 ),
        m_title( aTitle ),
        m_selectedData( nullptr ),
        m_previousId( nullptr ),
        m_draggedItem( nullptr )
{
    wxBoxSizer* mainSizer = new wxBoxSizer( wxVERTICAL );
    SetSizer( mainSizer );

    wxBoxSizer* infoBarSizer = new wxBoxSizer( wxHORIZONTAL );

    m_infoBar = new WX_INFOBAR( this );
    infoBarSizer->Add( m_infoBar, 0, wxEXPAND, 0 );

    // Add the tree panel to the content sizer
    m_splitter = new wxSplitterWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
                                       wxBORDER_NONE | wxSP_LIVE_UPDATE | wxSP_3DSASH );
    m_splitter->SetMinimumPaneSize( 50 ); // Set a minimum pane size

    // Create the tree control panel
    WX_PANEL* treeCtrlPanel = new WX_PANEL( m_splitter, wxID_ANY, wxDefaultPosition, wxDefaultSize,
                                            wxBORDER_NONE | wxTAB_TRAVERSAL );
    treeCtrlPanel->SetBorders( false, false, false, true );
    wxBoxSizer* treeCtrlSizer = new wxBoxSizer( wxVERTICAL );
    treeCtrlPanel->SetSizer( treeCtrlSizer );

    // Add a search text box above the tree control
    m_filterSearch = CreateTextFilterBox( treeCtrlPanel, _( "Type filter text" ) );
    treeCtrlSizer->Add( m_filterSearch, 0, wxEXPAND, 5 );

    // Create the tree control and set its font
    m_ruleTreeCtrl = new wxTreeCtrl( treeCtrlPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize,
                                     wxTR_DEFAULT_STYLE | wxTR_HIDE_ROOT | wxTR_HAS_BUTTONS | wxBORDER_NONE );
    m_ruleTreeCtrl->SetFont( KIUI::GetControlFont( this ) );

    // Add the tree control to its sizer
    treeCtrlSizer->Add( m_ruleTreeCtrl, 1, wxEXPAND | wxBOTTOM, 3 );

    // Create a sizer for the action buttons
    wxBoxSizer* actionButtonsSizer = new wxBoxSizer( wxHORIZONTAL );

    // Create the action buttons
    m_addRuleButton = new STD_BITMAP_BUTTON( treeCtrlPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition,
                                             wxDefaultSize, wxBU_AUTODRAW );
    m_copyRuleButton = new STD_BITMAP_BUTTON( treeCtrlPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition,
                                              wxDefaultSize, wxBU_AUTODRAW );
    m_moveTreeItemUpButton = new STD_BITMAP_BUTTON( treeCtrlPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition,
                                                    wxDefaultSize, wxBU_AUTODRAW );
    m_moveTreeItemDownButton = new STD_BITMAP_BUTTON( treeCtrlPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition,
                                                      wxDefaultSize, wxBU_AUTODRAW );
    m_deleteRuleButton = new STD_BITMAP_BUTTON( treeCtrlPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition,
                                                wxDefaultSize, wxBU_AUTODRAW );

    actionButtonsSizer->Add( m_addRuleButton, 0, wxLEFT, 5 );
    actionButtonsSizer->Add( m_copyRuleButton, 0, wxLEFT, 5 );
    actionButtonsSizer->Add( m_moveTreeItemUpButton, 0, wxLEFT, 5 );
    actionButtonsSizer->Add( m_moveTreeItemDownButton, 0, wxLEFT, 5 );

    // Add a spacer between the "Move" buttons and the "Delete" button
    actionButtonsSizer->AddSpacer( 20 ); // Spacer between the Move and Delete buttons

    // Add the "Delete" button at the end
    actionButtonsSizer->Add( m_deleteRuleButton, 0, wxRIGHT, 5 );

    // Add the action buttons sizer to the tree control sizer
    treeCtrlSizer->Add( actionButtonsSizer, 0, wxBOTTOM | wxEXPAND, 5 );

    treeCtrlPanel->Layout();

    m_addRuleButton->SetBitmap( KiBitmapBundle( BITMAPS::small_plus ) );
    m_copyRuleButton->SetBitmap( KiBitmapBundle( BITMAPS::copy ) );
    m_moveTreeItemUpButton->SetBitmap( KiBitmapBundle( BITMAPS::small_up ) );
    m_moveTreeItemDownButton->SetBitmap( KiBitmapBundle( BITMAPS::small_down ) );
    m_deleteRuleButton->SetBitmap( KiBitmapBundle( BITMAPS::small_trash ) );

    // Create a scrolled window for the dynamic content panel
    m_scrolledContentWin = new wxScrolledWindow( m_splitter, wxID_ANY, wxDefaultPosition, wxDefaultSize,
                                                 wxVSCROLL | wxBORDER_NONE );
    m_scrolledContentWin->SetScrollRate( 0, 10 );
    m_scrolledContentWin->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_FRAMEBK ) );
    m_contentPanel = nullptr;

    // Add the tree control panel to the splitter.
    // Calculate the minimum tree panel width based on the tree control's best size.
    treeCtrlPanel->Layout();
    int minTreeWidth = treeCtrlPanel->GetBestSize().GetWidth();

    // Ensure a reasonable minimum width for the tree panel
    if( minTreeWidth < 240 )
        minTreeWidth = 240;

    m_defaultSashPosition = minTreeWidth;

    m_splitter->SplitVertically( treeCtrlPanel, m_scrolledContentWin, m_defaultSashPosition );

    // Set gravity to 0 so the tree panel maintains its size and the content panel resizes
    m_splitter->SetSashGravity( 0.0 );

    wxBoxSizer* m_sizerButtons = new wxBoxSizer( wxHORIZONTAL );

    wxStdDialogButtonSizer* m_sdbSizer = new wxStdDialogButtonSizer();
    m_cancelRuleButton = new wxButton( this, wxID_CANCEL );
    m_cancelRuleButton->SetLabelText( _( "Close" ) );
    m_sdbSizer->AddButton( m_cancelRuleButton );
    m_saveRuleButton = new wxButton( this, wxID_OK );
    m_saveRuleButton->SetLabelText( _( "Save" ) );
    m_sdbSizer->AddButton( m_saveRuleButton );
    m_sdbSizer->Realize();

    m_sizerButtons->Add( m_sdbSizer, 1, wxEXPAND, 5 );


    // Add the info bar sizer to the main sizer
    mainSizer->Add( infoBarSizer, 0, wxEXPAND, 0 );
    mainSizer->Add( m_splitter, 1, wxEXPAND | wxBOTTOM, 5 );
    mainSizer->Add( m_sizerButtons, 0, wxALL | wxEXPAND, 5 );

    if( aInitialSize != wxDefaultSize )
        SetSize( aInitialSize );

    Layout();

    // Bind the context menu event
    m_filterSearch->Bind( wxEVT_COMMAND_TEXT_UPDATED, &RULE_EDITOR_DIALOG_BASE::onFilterSearch, this );
    m_ruleTreeCtrl->Bind( wxEVT_TREE_ITEM_RIGHT_CLICK, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemRightClick, this );
    m_ruleTreeCtrl->Bind( wxEVT_TREE_SEL_CHANGED, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemSelectionChanged, this );
    m_ruleTreeCtrl->Bind( wxEVT_TREE_ITEM_ACTIVATED, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemActivated, this );
    m_ruleTreeCtrl->Bind( wxEVT_LEFT_DOWN, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftDown, this );
    m_ruleTreeCtrl->Bind( wxEVT_MOTION, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemMouseMotion, this );
    m_ruleTreeCtrl->Bind( wxEVT_LEFT_UP, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftUp, this );

    m_addRuleButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onNewRuleOptionClick, this );
    m_copyRuleButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onDuplicateRuleOptionClick, this );
    m_moveTreeItemUpButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onMoveUpRuleOptionClick, this );
    m_moveTreeItemDownButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onMoveDownRuleOptionClick, this );
    m_deleteRuleButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onDeleteRuleOptionClick, this );
    m_saveRuleButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::OnSave, this );
    m_cancelRuleButton->Bind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::OnCancel, this );

    // Initially disable Save button until modifications are made
    m_saveRuleButton->Enable( false );

    this->Bind( wxEVT_SIZE, &RULE_EDITOR_DIALOG_BASE::onResize, this );

    this->Bind( wxEVT_CLOSE_WINDOW, &RULE_EDITOR_DIALOG_BASE::onClose, this );

    m_scrolledContentWin->Bind( wxEVT_SIZE,
            [this]( wxSizeEvent& evt )
            {
                RefreshContentScrollArea();
                evt.Skip();
            } );
}


void RULE_EDITOR_DIALOG_BASE::finishInitialization()
{
    finishDialogSettings();

    // finishDialogSettings() calls GetSizer()->SetSizeHints() which inflates the dialog
    // to the full unconstrained best size of the scrolled content area. Reset to the
    // caller's initial size and a reasonable minimum so Show() doesn't inherit the
    // inflated values.
    SetMinSize( wxSize( 400, 500 ) );
    SetSize( m_initialSize );

    wxLogTrace( "debug_dlg_size", "finishInitialization: size=%dx%d minSize=%dx%d bestSize=%dx%d",
                GetSize().x, GetSize().y, GetMinSize().x, GetMinSize().y, GetBestSize().x,
                GetBestSize().y );
}


bool RULE_EDITOR_DIALOG_BASE::Show( bool show )
{
    bool ret = DIALOG_SHIM::Show( show );

    if( show )
    {
        // DIALOG_SHIM::Show() sets SetMinSize(GetBestSize()) which includes the unconstrained
        // height of the scrolled content area. Cap the minimum to a usable value so the user
        // can resize the dialog smaller.
        wxSize minSize = GetMinSize();

        if( minSize.GetHeight() > 500 )
            SetMinSize( wxSize( minSize.GetWidth(), 500 ) );

        wxLogTrace( "debug_dlg_size", "Show: size=%dx%d minSize=%dx%d",
                    GetSize().x, GetSize().y, GetMinSize().x, GetMinSize().y );
    }

    return ret;
}


RULE_EDITOR_DIALOG_BASE::~RULE_EDITOR_DIALOG_BASE()
{
    m_filterSearch->Unbind( wxEVT_COMMAND_TEXT_UPDATED, &RULE_EDITOR_DIALOG_BASE::onFilterSearch, this );
    m_ruleTreeCtrl->Unbind( wxEVT_TREE_ITEM_RIGHT_CLICK, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemRightClick, this );
    m_ruleTreeCtrl->Unbind( wxEVT_TREE_SEL_CHANGED, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemSelectionChanged, this );
    m_ruleTreeCtrl->Unbind( wxEVT_TREE_ITEM_ACTIVATED, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemActivated, this );
    m_ruleTreeCtrl->Unbind( wxEVT_LEFT_DOWN, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftDown, this );
    m_ruleTreeCtrl->Unbind( wxEVT_MOTION, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemMouseMotion, this );
    m_ruleTreeCtrl->Unbind( wxEVT_LEFT_UP, &RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftUp, this );

    m_addRuleButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onNewRuleOptionClick, this );
    m_copyRuleButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onDuplicateRuleOptionClick, this );
    m_moveTreeItemUpButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onMoveUpRuleOptionClick, this );
    m_moveTreeItemDownButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onMoveDownRuleOptionClick, this );
    m_deleteRuleButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::onDeleteRuleOptionClick, this );
    m_saveRuleButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::OnSave, this );
    m_cancelRuleButton->Unbind( wxEVT_BUTTON, &RULE_EDITOR_DIALOG_BASE::OnCancel, this );

    this->Unbind( wxEVT_SIZE, &RULE_EDITOR_DIALOG_BASE::onResize, this );
    this->Unbind( wxEVT_CLOSE_WINDOW, &RULE_EDITOR_DIALOG_BASE::onClose, this );

    m_selectedData = nullptr;
    m_previousId = nullptr;
}


bool RULE_EDITOR_DIALOG_BASE::TransferDataToWindow()
{
    finishInitialization();

    // Call TransferDataToWindow() only once:
    // this is enough on wxWidgets 3.1
    if( !DIALOG_SHIM::TransferDataToWindow() )
        return false;

    return true;
}


bool RULE_EDITOR_DIALOG_BASE::TransferDataFromWindow()
{
    bool ret = true;

    // Call TransferDataFromWindow() only once:
    // this is enough on wxWidgets 3.1
    if( !DIALOG_SHIM::TransferDataFromWindow() )
        ret = false;

    return ret;
}


void RULE_EDITOR_DIALOG_BASE::OnCharHook( wxKeyEvent& aEvt )
{
    if( aEvt.GetKeyCode() == 'S' && aEvt.ControlDown() && !aEvt.ShiftDown() && !aEvt.AltDown() )
    {
        wxCommandEvent evt;
        OnSave( evt );
        return;
    }

    DIALOG_SHIM::OnCharHook( aEvt );
}


RULE_EDITOR_DIALOG_BASE* RULE_EDITOR_DIALOG_BASE::GetDialog( wxWindow* aParent )
{
    while( aParent )
    {
        if( RULE_EDITOR_DIALOG_BASE* currentDialog = dynamic_cast<RULE_EDITOR_DIALOG_BASE*>( aParent ) )
            return currentDialog;

        aParent = aParent->GetParent();
    }

    return nullptr;
}


void RULE_EDITOR_DIALOG_BASE::InitRuleTreeItems( const std::vector<RULE_TREE_NODE>& aNodes )
{
    if( aNodes.empty() )
        return;

    m_defaultTreeItems = aNodes;

    m_ruleTreeCtrl->Freeze();
    m_suppressSelectionEvents = true;

    wxTreeItemId rootId = m_ruleTreeCtrl->AddRoot( aNodes[0].m_nodeName );

    RULE_TREE_ITEM_DATA* itemData = new RULE_TREE_ITEM_DATA( aNodes[0].m_nodeId, nullptr, rootId );
    m_ruleTreeCtrl->SetItemData( rootId, itemData );

    std::vector<RULE_TREE_NODE> childNodes;
    getRuleTreeChildNodes( aNodes, aNodes[0].m_nodeId, childNodes );

    for( const auto& child : childNodes )
    {
        populateRuleTreeCtrl( aNodes, child, rootId );
    }

    m_treeHistoryData.clear();
    saveRuleTreeState( m_ruleTreeCtrl->GetRootItem(), m_defaultTreeItems[0].m_nodeId );

    m_ruleTreeCtrl->CollapseAll();

    m_suppressSelectionEvents = false;
    m_ruleTreeCtrl->Thaw();

    m_ruleTreeCtrl->SelectItem( m_ruleTreeCtrl->GetFirstVisibleItem() );
}


void RULE_EDITOR_DIALOG_BASE::SetContentPanel( wxPanel* aContentPanel )
{
    m_scrolledContentWin->Freeze();

    if( m_contentPanel )
    {
        unregisterUnitBinders( m_contentPanel );
        m_contentPanel->Destroy();
        m_contentPanel = nullptr;
    }

    m_contentPanel = aContentPanel;
    m_scrolledContentWin->SetBackgroundColour( m_contentPanel->GetBackgroundColour() );

    resetUndoRedoForNewContent( m_contentPanel->GetChildren() );

    Layout();
    m_scrolledContentWin->Thaw();

    CallAfter(
            [this]()
            {
                RefreshContentScrollArea();
            } );
}


void RULE_EDITOR_DIALOG_BASE::RefreshContentScrollArea()
{
    if( !m_contentPanel || !m_scrolledContentWin )
        return;

    m_contentPanel->Layout();

    wxSize clientSize = m_scrolledContentWin->GetClientSize();
    wxSize bestSize = m_contentPanel->GetBestSize();
    int    width = clientSize.GetWidth();
    int    height = std::max( clientSize.GetHeight(), bestSize.GetHeight() );

    m_contentPanel->SetSize( width, height );
    m_scrolledContentWin->SetVirtualSize( width, height );
    m_scrolledContentWin->Refresh();
}


void RULE_EDITOR_DIALOG_BASE::AppendNewRuleTreeItem( const RULE_TREE_NODE& aNode,
                                                     wxTreeItemId aParent )
{
    wxTreeItemId currentTreeItemId = appendRuleTreeItem( aNode, aParent );

    auto it = m_treeHistoryData.find( aNode.m_nodeData->GetParentId() );

    if( it != m_treeHistoryData.end() )
    {
        std::vector<int>& existingChildren = std::get<1>( it->second );
        existingChildren.push_back( aNode.m_nodeId );

        m_treeHistoryData[aNode.m_nodeId] = { aNode.m_nodeName, {}, currentTreeItemId };
    }

    if( !m_suppressSelectionEvents )
        m_ruleTreeCtrl->SelectItem( currentTreeItemId );
}


void RULE_EDITOR_DIALOG_BASE::UpdateRuleTreeItemText( wxTreeItemId aItemId, wxString aItemText )
{
    m_ruleTreeCtrl->SetItemText( aItemId, aItemText );
}


void RULE_EDITOR_DIALOG_BASE::SetControlsEnabled( bool aEnable )
{
    m_ruleTreeCtrl->Enable( aEnable );
    m_filterSearch->Enable( aEnable );

    updateRuleTreeActionButtonsState( m_selectedData );

    if( m_cancelRuleButton )
    {
        if( !aEnable )
        {
            m_cancelRuleButton->SetLabelText( _( "Cancel" ) );
        }
        else if( m_modified )
        {
            m_cancelRuleButton->SetLabelText( _( "Discard" ) );
        }
        else
        {
            m_cancelRuleButton->SetLabelText( _( "Close" ) );
        }
    }
}


void RULE_EDITOR_DIALOG_BASE::SetModified()
{
    m_modified = true;

    if( m_saveRuleButton )
        m_saveRuleButton->Enable( true );

    if( m_cancelRuleButton && m_ruleTreeCtrl->IsEnabled() )
        m_cancelRuleButton->SetLabelText( _( "Discard" ) );

    if( m_selectedData )
    {
        wxTreeItemId itemId = m_selectedData->GetTreeItemId();
        if( itemId.IsOk() )
        {
            wxString currentText = m_ruleTreeCtrl->GetItemText( itemId );

            if( !currentText.EndsWith( wxS( " *" ) ) )
            {
                m_ruleTreeCtrl->SetItemText( itemId, currentText + wxS( " *" ) );
            }
        }
    }
}


void RULE_EDITOR_DIALOG_BASE::ClearModified()
{
    m_modified = false;

    if( m_saveRuleButton )
        m_saveRuleButton->Enable( false );

    if( m_cancelRuleButton && m_ruleTreeCtrl->IsEnabled() )
        m_cancelRuleButton->SetLabelText( _( "Close" ) );
}


void RULE_EDITOR_DIALOG_BASE::DeleteRuleTreeItem( wxTreeItemId aItemId, const int& aNodeId )
{
    if( m_selectedData && m_selectedData->GetNodeId() == aNodeId )
        m_selectedData = nullptr;

    m_ruleTreeCtrl->Delete( aItemId );
    m_treeHistoryData.erase( aNodeId );
}


void RULE_EDITOR_DIALOG_BASE::populateRuleTreeCtrl( const std::vector<RULE_TREE_NODE>& aNodes,
                                                    const RULE_TREE_NODE& aNode, wxTreeItemId aParent )
{
    wxTreeItemId currentId = appendRuleTreeItem( aNode, aParent );

    std::vector<RULE_TREE_NODE> childNodes;
    getRuleTreeChildNodes( aNodes, aNode.m_nodeId, childNodes );

    for( const auto& child : childNodes )
    {
        populateRuleTreeCtrl( aNodes, child, currentId );
    }

    m_ruleTreeCtrl->Expand( currentId );
}


void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemRightClick( wxTreeEvent& aEvent )
{
    wxTreeItemId item = aEvent.GetItem();

    if( !item.IsOk() )
        return;

    m_ruleTreeCtrl->SelectItem( item );

    RULE_TREE_ITEM_DATA* ruleTreeItemData = dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( item ) );

    if( !ruleTreeItemData )
        return;

    updateRuleTreeActionButtonsState( ruleTreeItemData );

    wxMenu menu;

    if( m_enableAddRule )
        KIUI::AddMenuItem( &menu, ID_NEWRULE, _( "New Rule" ), KiBitmap( BITMAPS::small_plus ) );

    if( m_enableAddRule )
        KIUI::AddMenuItem( &menu, ID_COPYRULE, _( "Copy Rule" ), KiBitmap( BITMAPS::copy ) );

    if( m_enableDeleteRule )
        KIUI::AddMenuItem( &menu, ID_DELETERULE, _( "Delete Rule" ), KiBitmap( BITMAPS::small_trash ) );

    if( isEnabled( ruleTreeItemData, RULE_EDITOR_TREE_CONTEXT_OPT::MOVE_UP ) && m_enableMoveUp )
        KIUI::AddMenuItem( &menu, ID_MOVEUP, _( "Move Up" ), KiBitmap( BITMAPS::small_up ) );

    if( isEnabled( ruleTreeItemData, RULE_EDITOR_TREE_CONTEXT_OPT::MOVE_DOWN ) && m_enableMoveDown )
        KIUI::AddMenuItem( &menu, ID_MOVEDOWN, _( "Move Down" ), KiBitmap( BITMAPS::small_down ) );

    menu.Bind( wxEVT_COMMAND_MENU_SELECTED,
               [&]( wxCommandEvent& aCmd )
               {
                   switch( aCmd.GetId() )
                   {
                   case ID_NEWRULE: onNewRuleOptionClick( aCmd ); break;

                   case ID_COPYRULE: onDuplicateRuleOptionClick( aCmd ); break;

                   case ID_DELETERULE: onDeleteRuleOptionClick( aCmd ); break;

                   case ID_MOVEUP: onMoveUpRuleOptionClick( aCmd ); break;

                   case ID_MOVEDOWN: onMoveDownRuleOptionClick( aCmd ); break;
                   default: aCmd.Skip();
                   }
               } );

    PopupMenu( &menu );
}


void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemSelectionChanged( wxTreeEvent& aEvent )
{
    if( m_suppressSelectionEvents )
        return;

    wxTreeItemId selectedItem = aEvent.GetItem();

    RULE_TREE_ITEM_DATA* selectedItemData =
            dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( selectedItem ) );

    if( !selectedItemData )
        return;

    if( m_selectedData )
    {
        if( m_selectedData->GetNodeId() == selectedItemData->GetNodeId() )
        {
            m_selectedData = selectedItemData;
            updateRuleTreeActionButtonsState( selectedItemData );
            return;
        }

        m_previousId = m_selectedData->GetTreeItemId();
    }

    m_selectedData = selectedItemData;

    RuleTreeItemSelectionChanged( m_selectedData );

    updateRuleTreeActionButtonsState( selectedItemData );
}

void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemActivated( wxTreeEvent& aEvent )
{
    RULE_TREE_ITEM_DATA* itemData =
            dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( aEvent.GetItem() ) );

    if( itemData &&
        isEnabled( itemData, RULE_EDITOR_TREE_CONTEXT_OPT::DUPLICATE_RULE ) )
    {
        DuplicateRule( itemData );
    }
    else if( itemData &&
            isEnabled( itemData, RULE_EDITOR_TREE_CONTEXT_OPT::ADD_RULE ) )
    {
        AddNewRule( itemData );
    }
}


void RULE_EDITOR_DIALOG_BASE::onNewRuleOptionClick( wxCommandEvent& aEvent )
{
    AddNewRule( m_selectedData );
}


void RULE_EDITOR_DIALOG_BASE::onDuplicateRuleOptionClick( wxCommandEvent& aEvent )
{
    DuplicateRule( m_selectedData );
}


void RULE_EDITOR_DIALOG_BASE::onDeleteRuleOptionClick( wxCommandEvent& aEvent )
{
    RemoveRule( m_selectedData->GetNodeId() );
}


void RULE_EDITOR_DIALOG_BASE::onMoveUpRuleOptionClick( wxCommandEvent& aEvent )
{
    if( !m_enableMoveUp )
        return;

    wxTreeItemId selectedItem = m_ruleTreeCtrl->GetSelection();

    if( !selectedItem.IsOk() )
        return;

    wxTreeItemId parent = m_ruleTreeCtrl->GetItemParent( selectedItem );

    if( !parent.IsOk() )
        return;

    wxTreeItemIdValue cookie;
    wxTreeItemId      current = m_ruleTreeCtrl->GetFirstChild( parent, cookie );
    wxTreeItemId      previous;
    wxTreeItemId      beforePrevious;

    while( current.IsOk() && current != selectedItem )
    {
        beforePrevious = previous;
        previous = current;
        current = m_ruleTreeCtrl->GetNextChild( parent, cookie );
    }

    if( !previous.IsOk() )
    {
        return;
    }

    wxTreeItemId insertPosition = beforePrevious.IsOk() ? beforePrevious : wxTreeItemId();

    wxTreeItemId newItem = m_ruleTreeCtrl->InsertItem( parent, insertPosition,
                                                       m_ruleTreeCtrl->GetItemText( selectedItem ) );

    moveRuleTreeItemChildrensTooOnDrag( selectedItem, newItem );

    wxTreeItemData* itemData = m_ruleTreeCtrl->GetItemData( selectedItem );

    if( itemData )
    {
        RULE_TREE_ITEM_DATA* ruleTreeItemData = dynamic_cast<RULE_TREE_ITEM_DATA*>( itemData );
        ruleTreeItemData->SetTreeItemId( newItem );

        m_ruleTreeCtrl->SetItemData( newItem, itemData );
        m_ruleTreeCtrl->SetItemData( selectedItem, nullptr ); // Detach data from the old item

        saveRuleTreeState( newItem, ruleTreeItemData->GetNodeId() );

        m_ruleTreeCtrl->SelectItem( newItem );

        m_ruleTreeCtrl->Expand( newItem );
        m_ruleTreeCtrl->ExpandAllChildren( newItem );
    }

    m_ruleTreeCtrl->DeleteChildren( selectedItem );
    m_ruleTreeCtrl->Delete( selectedItem );
}


void RULE_EDITOR_DIALOG_BASE::onMoveDownRuleOptionClick( wxCommandEvent& aEvent )
{
    if( !m_enableMoveDown )
        return;

    wxTreeItemId selectedItem = m_ruleTreeCtrl->GetSelection();

    if( !selectedItem.IsOk() )
        return;

    wxTreeItemId      parent = m_ruleTreeCtrl->GetItemParent( selectedItem );
    wxTreeItemIdValue cookie;
    wxTreeItemId      current = m_ruleTreeCtrl->GetFirstChild( parent, cookie );

    while( current.IsOk() && current != selectedItem )
    {
        current = m_ruleTreeCtrl->GetNextChild( parent, cookie );
    }

    wxTreeItemId next = m_ruleTreeCtrl->GetNextChild( parent, cookie );

    if( !next.IsOk() )
    {
        return;
    }

    wxString        itemText = m_ruleTreeCtrl->GetItemText( selectedItem );
    wxTreeItemData* itemData = m_ruleTreeCtrl->GetItemData( selectedItem );

    wxTreeItemId newItem = m_ruleTreeCtrl->InsertItem( parent, next, itemText );

    moveRuleTreeItemChildrensTooOnDrag( selectedItem, newItem );

    if( itemData )
    {
        RULE_TREE_ITEM_DATA* ruleTreeItemData = dynamic_cast<RULE_TREE_ITEM_DATA*>( itemData );
        ruleTreeItemData->SetTreeItemId( newItem );

        m_ruleTreeCtrl->SetItemData( newItem, itemData );
        m_ruleTreeCtrl->SetItemData( selectedItem, nullptr ); // Detach data

        saveRuleTreeState( newItem, ruleTreeItemData->GetNodeId() );

        m_ruleTreeCtrl->SelectItem( newItem );

        m_ruleTreeCtrl->Expand( newItem );
    }

    m_ruleTreeCtrl->DeleteChildren( selectedItem );
    m_ruleTreeCtrl->Delete( selectedItem );
}


void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftDown( wxMouseEvent& aEvent )
{
    wxPoint      pt = aEvent.GetPosition();
    int          flags;
    wxTreeItemId item = m_ruleTreeCtrl->HitTest( pt, flags );

    // Check if click is on the expand/collapse button
    if( flags & wxTREE_HITTEST_ONITEMBUTTON )
    {
        // Prevent wxEVT_LEFT_DOWN from propagating further
        aEvent.Skip();
        return;
    }

    if( item.IsOk() )
    {
        m_draggedItem = item;
        m_isDragging = false;
    }

    aEvent.Skip();
}


void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemMouseMotion( wxMouseEvent& aEvent )
{
    if( aEvent.Dragging() && m_draggedItem.IsOk() )
    {
        m_isDragging = true;

        // Clear previous highlight
        if( m_dropTargetItem.IsOk() )
        {
            m_ruleTreeCtrl->SetItemBackgroundColour( m_dropTargetItem, wxNullColour );
            m_dropTargetItem = wxTreeItemId();
        }

        // Highlight item under cursor
        int          flags;
        wxTreeItemId hitItem = m_ruleTreeCtrl->HitTest( aEvent.GetPosition(), flags );

        if( hitItem.IsOk() && hitItem != m_draggedItem )
        {
            wxTreeItemId hitParent = m_ruleTreeCtrl->GetItemParent( hitItem );
            wxTreeItemId dragParent = m_ruleTreeCtrl->GetItemParent( m_draggedItem );

            if( hitParent == dragParent )
            {
                m_ruleTreeCtrl->SetItemBackgroundColour( hitItem,
                                                         wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
                m_dropTargetItem = hitItem;
            }
        }
    }

    aEvent.Skip();
}

void RULE_EDITOR_DIALOG_BASE::onRuleTreeItemLeftUp( wxMouseEvent& aEvent )
{
    if( m_draggedItem.IsOk() && m_isDragging )
    {
        m_isDragging = false;

        if( m_dropTargetItem.IsOk() )
        {
            m_ruleTreeCtrl->SetItemBackgroundColour( m_dropTargetItem, wxNullColour );
            m_dropTargetItem = wxTreeItemId();
        }

        wxPoint      pos = aEvent.GetPosition();
        int          flags;
        wxTreeItemId targetItem = m_ruleTreeCtrl->HitTest( pos, flags );

        if( !targetItem.IsOk() )
            return;

        wxTreeItemId draggedParent = m_ruleTreeCtrl->GetItemParent( m_draggedItem );
        wxTreeItemId targetParent = m_ruleTreeCtrl->GetItemParent( targetItem );

        if( draggedParent != targetParent )
            return;

        if( draggedParent == targetParent )
        {
            wxTreeItemId      newItem;
            wxTreeItemIdValue cookie;

            if( flags & wxTREE_HITTEST_ONITEMLABEL
                && targetItem == m_ruleTreeCtrl->GetFirstChild( targetParent, cookie ) )
            {
                // Prepend item as the first child of target parent
                newItem = m_ruleTreeCtrl->PrependItem( targetParent,
                                                       m_ruleTreeCtrl->GetItemText( m_draggedItem ) );
            }
            else
            {
                wxTreeItemId current = m_ruleTreeCtrl->GetFirstChild( targetParent, cookie );
                wxTreeItemId previous;

                // Traverse through siblings to find the position of the target item
                while( current.IsOk() && current != targetItem )
                {
                    previous = current;
                    current = m_ruleTreeCtrl->GetNextChild( targetParent, cookie );
                }

                if( !previous.IsOk() )
                {
                    // If no previous item, insert as the first child
                    newItem = m_ruleTreeCtrl->PrependItem( targetParent,
                                                           m_ruleTreeCtrl->GetItemText( m_draggedItem ) );
                }
                else
                {
                    wxTreeItemId nextSibling = m_ruleTreeCtrl->GetNextChild( targetParent, cookie );

                    if( nextSibling.IsOk() )
                    {
                        // If there is a next sibling, insert after the previous item
                        newItem = m_ruleTreeCtrl->InsertItem( targetParent, previous,
                                                              m_ruleTreeCtrl->GetItemText( m_draggedItem ) );
                    }
                    else
                    {
                        // If no next sibling, append the item as the last child
                        newItem = m_ruleTreeCtrl->AppendItem( targetParent,
                                                              m_ruleTreeCtrl->GetItemText( m_draggedItem ) );
                    }
                }
            }

            moveRuleTreeItemChildrensTooOnDrag( m_draggedItem, newItem );

            wxTreeItemData* itemData = m_ruleTreeCtrl->GetItemData( m_draggedItem );

            if( itemData )
            {
                RULE_TREE_ITEM_DATA* ruleTreeItemData =
                        dynamic_cast<RULE_TREE_ITEM_DATA*>( itemData );
                ruleTreeItemData->SetTreeItemId( newItem );

                m_ruleTreeCtrl->SetItemData( newItem, ruleTreeItemData );
                m_ruleTreeCtrl->SetItemData( m_draggedItem, nullptr );

                saveRuleTreeState( newItem, ruleTreeItemData->GetNodeId() );

                m_ruleTreeCtrl->SelectItem( newItem );

                m_ruleTreeCtrl->Expand( newItem );
            }

            m_ruleTreeCtrl->DeleteChildren( m_draggedItem );
            m_ruleTreeCtrl->Delete( m_draggedItem );
        }

        m_draggedItem = nullptr;
    }

    aEvent.Skip();
}


void RULE_EDITOR_DIALOG_BASE::onFilterSearch( wxCommandEvent& aEvent )
{
    const auto searchStr = aEvent.GetString().Lower();

    m_ruleTreeCtrl->Freeze();
    m_suppressSelectionEvents = true;

    m_ruleTreeCtrl->DeleteAllItems();

    restoreRuleTree( nullptr, m_defaultTreeItems[0].m_nodeId );

    wxTreeItemId root = m_ruleTreeCtrl->GetRootItem();

    if( root.IsOk() )
    {
        filterRuleTree( root, searchStr );
    }

    m_suppressSelectionEvents = false;
    m_ruleTreeCtrl->Thaw();
}


bool RULE_EDITOR_DIALOG_BASE::filterRuleTree( const wxTreeItemId& aItem, const wxString& aFilter )
{
    bool matches = m_ruleTreeCtrl->GetItemText( aItem ).Lower().Contains( aFilter );

    wxTreeItemIdValue cookie;
    wxTreeItemId      child = m_ruleTreeCtrl->GetFirstChild( aItem, cookie );
    bool              hasVisibleChildren = false;

    std::vector<wxTreeItemId> itemsToDelete;

    while( child.IsOk() )
    {
        if( filterRuleTree( child, aFilter ) )
        {
            hasVisibleChildren = true;
        }
        else
        {
            itemsToDelete.push_back( child );
        }

        child = m_ruleTreeCtrl->GetNextChild( aItem, cookie );
    }

    for( const auto& id : itemsToDelete )
    {
        m_ruleTreeCtrl->Delete( id );
    }

    return matches || hasVisibleChildren;
}


void RULE_EDITOR_DIALOG_BASE::saveRuleTreeState( const wxTreeItemId& aItem, const int& aNodeId )
{
    wxString         itemText = m_ruleTreeCtrl->GetItemText( aItem );
    std::vector<int> children;

    wxTreeItemIdValue cookie;
    wxTreeItemId      child = m_ruleTreeCtrl->GetFirstChild( aItem, cookie );

    while( child.IsOk() )
    {
        RULE_TREE_ITEM_DATA* childItemData =
                dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( child ) );
        int childId = childItemData->GetNodeId();

        children.push_back( childId );

        saveRuleTreeState( child, childId );

        child = m_ruleTreeCtrl->GetNextChild( aItem, cookie );
    }

    m_treeHistoryData[aNodeId] = { itemText, children, aItem };
}


void RULE_EDITOR_DIALOG_BASE::restoreRuleTree( const wxTreeItemId& aParent, const int& aNodeId )
{
    auto it = m_treeHistoryData.find( aNodeId );

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

    const auto& [itemText, children, itemId] = it->second;

    wxTreeItemId newItem;

    if( aParent )
        newItem = m_ruleTreeCtrl->AppendItem( aParent, itemText );
    else
        newItem = m_ruleTreeCtrl->AddRoot( itemText );

    if( newItem )
    {
        wxTreeItemId& treeItemId = std::get<2>( it->second );
        treeItemId = newItem;
    }

    RULE_TREE_ITEM_DATA* itemData = new RULE_TREE_ITEM_DATA( aNodeId, aParent, newItem );
    m_ruleTreeCtrl->SetItemData( newItem, itemData );

    for( const auto& childId : children )
    {
        restoreRuleTree( newItem, childId );
    }

    // Don't expand the root item because it is hidden
    if( aParent )
        m_ruleTreeCtrl->Expand( newItem );
}


wxTreeItemId RULE_EDITOR_DIALOG_BASE::appendRuleTreeItem( const RULE_TREE_NODE& aNode, wxTreeItemId aParent )
{
    wxString displayName = aNode.m_nodeName;

    if( aNode.m_nodeData && aNode.m_nodeData->IsNew() )
        displayName += wxS( " *" );

    wxTreeItemId currentTreeItemId = m_ruleTreeCtrl->AppendItem( aParent, displayName );

    RULE_TREE_ITEM_DATA* itemData = new RULE_TREE_ITEM_DATA( aNode.m_nodeId, aParent, currentTreeItemId );
    m_ruleTreeCtrl->SetItemData( currentTreeItemId, itemData );

    m_ruleTreeCtrl->Expand( currentTreeItemId );

    return currentTreeItemId;
}


void RULE_EDITOR_DIALOG_BASE::getRuleTreeChildNodes( const std::vector<RULE_TREE_NODE>& aNodes,
                                                     int aParentId,
                                                     std::vector<RULE_TREE_NODE>& aResult )
{
    std::vector<RULE_TREE_NODE> filteredNodes;

    std::copy_if( aNodes.begin(), aNodes.end(), std::back_inserter( filteredNodes ),
                  [&aParentId]( const RULE_TREE_NODE& node )
                  {
                      return node.m_nodeData->GetParentId() == aParentId;
                  } );

    if( filteredNodes.size() > 0 )
    {
        aResult.insert( aResult.end(), filteredNodes.begin(), filteredNodes.end() );
    }
}

void RULE_EDITOR_DIALOG_BASE::moveRuleTreeItemChildrensTooOnDrag( wxTreeItemId aSrc,
                                                                  wxTreeItemId aDest )
{
    wxTreeItemIdValue cookie;
    wxTreeItemId      child = m_ruleTreeCtrl->GetFirstChild( aSrc, cookie );

    while( child.IsOk() )
    {
        wxTreeItemId newChild = m_ruleTreeCtrl->AppendItem( aDest, m_ruleTreeCtrl->GetItemText( child ) );

        RULE_TREE_ITEM_DATA* ruleTreeItemData =
                dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( child ) );
        ruleTreeItemData->SetParentTreeItemId( aDest );
        ruleTreeItemData->SetTreeItemId( newChild );

        m_ruleTreeCtrl->SetItemData( newChild, ruleTreeItemData );
        m_ruleTreeCtrl->SetItemData( child, nullptr );

        moveRuleTreeItemChildrensTooOnDrag( child, newChild );

        child = m_ruleTreeCtrl->GetNextChild( aSrc, cookie );
    }
}


void RULE_EDITOR_DIALOG_BASE::updateRuleTreeItemMoveOptionState()
{
    wxTreeItemId selectedItem = m_ruleTreeCtrl->GetSelection();

    m_enableMoveUp = false;
    m_enableMoveDown = false;

    if( !selectedItem.IsOk() )
        return;

    if( selectedItem == m_ruleTreeCtrl->GetRootItem() )
        return;

    RULE_TREE_ITEM_DATA* itemData =
            dynamic_cast<RULE_TREE_ITEM_DATA*>( m_ruleTreeCtrl->GetItemData( selectedItem ) );

    if( !itemData ||
        !isEnabled( itemData, RULE_EDITOR_TREE_CONTEXT_OPT::MOVE_UP ) ||
        !isEnabled( itemData, RULE_EDITOR_TREE_CONTEXT_OPT::MOVE_DOWN ) )
    {
        return;
    }

    wxTreeItemId      parent = m_ruleTreeCtrl->GetItemParent( selectedItem );
    wxTreeItemIdValue cookie;
    wxTreeItemId      firstChild = m_ruleTreeCtrl->GetFirstChild( parent, cookie );
    wxTreeItemId      lastChild = firstChild;

    while( lastChild.IsOk() )
    {
        wxTreeItemId next = m_ruleTreeCtrl->GetNextChild( parent, cookie );

        if( !next.IsOk() )
            break;

        lastChild = next;
    }

    m_enableMoveUp = ( selectedItem != firstChild );
    m_enableMoveDown = ( selectedItem != lastChild );
}


void RULE_EDITOR_DIALOG_BASE::updateRuleTreeActionButtonsState( RULE_TREE_ITEM_DATA* aData )
{
    wxTreeItemId selectedItem = m_ruleTreeCtrl->GetSelection();

    if( !selectedItem.IsOk() || !m_ruleTreeCtrl->IsEnabled() )
    {
        m_addRuleButton->Enable( false );
        m_copyRuleButton->Enable( false );
        m_moveTreeItemUpButton->Enable( false );
        m_moveTreeItemDownButton->Enable( false );
        m_deleteRuleButton->Enable( false );

        return;
    }

    m_enableAddRule = false;
    m_enableDuplicateRule = false;
    m_enableDeleteRule = false;

    if( isEnabled( aData, RULE_EDITOR_TREE_CONTEXT_OPT::ADD_RULE ) )
        m_enableAddRule = true;

    if( isEnabled( aData, RULE_EDITOR_TREE_CONTEXT_OPT::DUPLICATE_RULE ) )
        m_enableDuplicateRule = true;

    if( isEnabled( aData, RULE_EDITOR_TREE_CONTEXT_OPT::DELETE_RULE ) )
        m_enableDeleteRule = true;

    updateRuleTreeItemMoveOptionState();

    m_addRuleButton->Enable( m_enableAddRule );
    m_copyRuleButton->Enable( m_enableDuplicateRule );
    m_moveTreeItemUpButton->Enable( m_enableMoveUp );
    m_moveTreeItemDownButton->Enable( m_enableMoveDown );
    m_deleteRuleButton->Enable( m_enableDeleteRule );
}


void RULE_EDITOR_DIALOG_BASE::onResize( wxSizeEvent& event )
{
    Layout();

    event.Skip();
}


void RULE_EDITOR_DIALOG_BASE::onClose( wxCloseEvent& aEvt )
{
    wxPostEvent( this, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_CANCEL ) );
}
