/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2012 Brian Sidebotham <brian.sidebotham@gmail.com>
 * 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 "dialog_template_selector.h"
#include <bitmaps.h>
#include <kiplatform/ui.h>
#include <pgm_base.h>
#include <settings/kicad_settings.h>
#include <settings/settings_manager.h>
#include <widgets/std_bitmap_button.h>
#include <widgets/ui_common.h>
#include <algorithm>
#include <wx_filename.h>
#include <wx/dir.h>
#include <wx/dirdlg.h>
#include <wx/settings.h>
#include <wx/bitmap.h>
#include <wx/image.h>
#include <wx/math.h>
#include <wx/menu.h>
#include <wx/textdlg.h>
#include <wx/textfile.h>
#include <wx/regex.h>
#include <confirm.h>
#include <project_tree_traverser.h>
#include "template_default_html.h"


static const wxChar* traceTemplateSelector = wxT( "KICAD_TEMPLATE_SELECTOR" );


TEMPLATE_MRU_WIDGET::TEMPLATE_MRU_WIDGET( wxWindow* aParent, DIALOG_TEMPLATE_SELECTOR* aDialog,
                                          const wxString& aPath, const wxString& aTitle,
                                          const wxBitmap& aIcon ) :
        wxPanel( aParent, wxID_ANY ),
        m_dialog( aDialog ),
        m_templatePath( aPath )
{
    SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );

    wxBoxSizer* sizer = new wxBoxSizer( wxHORIZONTAL );

    wxStaticBitmap* icon = new wxStaticBitmap( this, wxID_ANY, aIcon );
    icon->SetMinSize( FromDIP( wxSize( 16, 16 ) ) );
    sizer->Add( icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxTOP | wxBOTTOM, 6 );

    wxStaticText* label = new wxStaticText( this, wxID_ANY, aTitle );
    label->SetFont( KIUI::GetInfoFont( this ) );
    sizer->Add( label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, 6 );

    SetSizer( sizer );
    Layout();

    Bind( wxEVT_LEFT_DOWN, &TEMPLATE_MRU_WIDGET::OnClick, this );
    Bind( wxEVT_LEFT_DCLICK, &TEMPLATE_MRU_WIDGET::OnDoubleClick, this );
    Bind( wxEVT_ENTER_WINDOW, &TEMPLATE_MRU_WIDGET::OnEnter, this );
    Bind( wxEVT_LEAVE_WINDOW, &TEMPLATE_MRU_WIDGET::OnLeave, this );

    icon->Bind( wxEVT_LEFT_DOWN, &TEMPLATE_MRU_WIDGET::OnClick, this );
    icon->Bind( wxEVT_LEFT_DCLICK, &TEMPLATE_MRU_WIDGET::OnDoubleClick, this );
    label->Bind( wxEVT_LEFT_DOWN, &TEMPLATE_MRU_WIDGET::OnClick, this );
    label->Bind( wxEVT_LEFT_DCLICK, &TEMPLATE_MRU_WIDGET::OnDoubleClick, this );
}


void TEMPLATE_MRU_WIDGET::OnClick( wxMouseEvent& event )
{
    // Just select the template in the list without showing the preview pane
    m_dialog->SelectTemplateByPath( m_templatePath, true );
    event.Skip();
}


void TEMPLATE_MRU_WIDGET::OnDoubleClick( wxMouseEvent& event )
{
    m_dialog->SelectTemplateByPath( m_templatePath );
    m_dialog->EndModal( wxID_OK );
    event.Skip();
}


void TEMPLATE_MRU_WIDGET::OnEnter( wxMouseEvent& event )
{
    SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ).ChangeLightness( 150 ) );
    Refresh();
    event.Skip();
}


void TEMPLATE_MRU_WIDGET::OnLeave( wxMouseEvent& event )
{
    SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
    Refresh();
    event.Skip();
}


TEMPLATE_WIDGET::TEMPLATE_WIDGET( wxWindow* aParent, DIALOG_TEMPLATE_SELECTOR* aDialog ) :
        TEMPLATE_WIDGET_BASE( aParent )
{
    m_parent = aParent;
    m_dialog = aDialog;
    m_isUserTemplate = false;

    // Set a small minimum size to allow the dialog to shrink
    // The actual size will be determined by the parent sizer
    SetMinSize( FromDIP( wxSize( 200, -1 ) ) );

    m_bitmapIcon->Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ), nullptr, this );
    m_titleLabel->Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ), nullptr, this );
    m_descLabel->Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ), nullptr, this );

    Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::OnMouse ), nullptr, this );

    m_bitmapIcon->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ), nullptr, this );
    m_titleLabel->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ), nullptr, this );
    m_descLabel->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ), nullptr, this );
    Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( TEMPLATE_WIDGET::onRightClick ), nullptr, this );

    m_bitmapIcon->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ), nullptr, this );
    m_titleLabel->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ), nullptr, this );
    m_descLabel->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ), nullptr, this );
    Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( TEMPLATE_WIDGET::OnDoubleClick ), nullptr, this );

    // Set description text to use a smaller font
    m_descLabel->SetFont( KIUI::GetInfoFont( this ) );

#if wxCHECK_VERSION( 3, 3, 2 )
    m_descLabel->SetWindowStyle( wxST_WRAP );
#endif

    // Bind size event for dynamic text wrapping
    Bind( wxEVT_SIZE, &TEMPLATE_WIDGET::OnSize, this );

    Unselect();

    m_currTemplate = nullptr;
}


void TEMPLATE_WIDGET::Select()
{
    m_dialog->SetWidget( this );
    SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
    m_titleLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
    m_descLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
    m_selected = true;
    Refresh();
}


void TEMPLATE_WIDGET::SelectWithoutStateChange()
{
    SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
    m_titleLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
    m_descLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT ) );
    m_selected = true;
    Refresh();
}


void TEMPLATE_WIDGET::Unselect()
{
    SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
    m_titleLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNTEXT ) );
    m_descLabel->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNTEXT ) );
    m_selected = false;
    Refresh();
}


void TEMPLATE_WIDGET::SetTemplate( PROJECT_TEMPLATE* aTemplate )
{
    m_currTemplate = aTemplate;
    m_titleLabel->SetFont( KIUI::GetInfoFont( this ).Bold() );
    m_titleLabel->SetLabel( *aTemplate->GetTitle() );

    // Generate bitmap bundle from template icon bitmap (64x64)
    // so wxStaticBitmap can size itself to the closest match
    const std::vector<int> c_bitmapSizes = { 48, 64, 96, 128 };

    wxBitmapBundle     bundle;
    wxVector<wxBitmap> bitmaps;
    wxBitmap*          icon = aTemplate->GetIcon();

    if( icon && icon->IsOk() )
        bundle = *icon;
    else
        bundle = KiBitmapBundleDef( BITMAPS::icon_kicad, c_bitmapSizes[0] );

    wxSize defSize = bundle.GetDefaultSize();

    for( const int genSize : c_bitmapSizes )
    {
        double scale = std::min( (double) genSize / defSize.GetWidth(),
                                 (double) genSize / defSize.GetHeight() );

        wxSize scaledSize( wxRound( defSize.x * scale ),
                           wxRound( defSize.y * scale ) );

        wxBitmap scaled = bundle.GetBitmap( scaledSize );
        scaled.SetScaleFactor( 1.0 );

        bitmaps.push_back( scaled );
    }

    m_bitmapIcon->SetBitmap( wxBitmapBundle::FromBitmaps( bitmaps ) );
}


void TEMPLATE_WIDGET::SetDescription( const wxString& aDescription )
{
    m_description = aDescription;

    // Truncate long descriptions to approximately 3 lines worth
    wxString displayDesc = aDescription;

    if( displayDesc.Length() > 120 )
        displayDesc = displayDesc.Left( 120 ) + wxS( "..." );

    m_descLabel->SetLabel( displayDesc );
}


void TEMPLATE_WIDGET::OnSize( wxSizeEvent& event )
{
    event.Skip();

    // wx 3.3.2+ can wrap automatically
#if !wxCHECK_VERSION( 3, 3, 2 )
    // Calculate available width for description text (widget width minus icon and padding)
    int wrapWidth = GetClientSize().GetWidth() - 48 - 20;  // 48px icon + 20px padding

    if( wrapWidth > 100 )
    {
        // Recalculate the truncated description from the original stored description
        // to avoid cumulative wrapping artifacts
        wxString displayDesc = m_description;

        if( displayDesc.Length() > 120 )
            displayDesc = displayDesc.Left( 120 ) + wxS( "..." );

        m_descLabel->SetLabel( displayDesc );
        m_descLabel->Wrap( wrapWidth );
        Layout();
    }
#endif
}


void TEMPLATE_WIDGET::OnMouse( wxMouseEvent& event )
{
    Select();
    event.Skip();
}


void TEMPLATE_WIDGET::OnDoubleClick( wxMouseEvent& event )
{
    Select();
    m_dialog->EndModal( wxID_OK );
    event.Skip();
}


void TEMPLATE_WIDGET::onRightClick( wxMouseEvent& event )
{
    if( !m_currTemplate )
    {
        event.Skip();
        return;
    }

    wxMenu menu;
    menu.Append( wxID_EDIT, m_isUserTemplate ? _( "Edit Template" ) : _( "Open Template (Read-Only)" ) );
    menu.Append( wxID_OPEN, _( "Open Template Folder" ) );
    menu.Append( wxID_COPY, _( "Duplicate Template" ) );

    menu.Bind( wxEVT_COMMAND_MENU_SELECTED,
               [this]( wxCommandEvent& evt )
               {
                   if( evt.GetId() == wxID_EDIT )
                       onEditTemplate( evt );
                   if( evt.GetId() == wxID_OPEN )
                       onOpenFolder( evt );
                   else if( evt.GetId() == wxID_COPY )
                       onDuplicateTemplate( evt );
               } );

    PopupMenu( &menu );
}


void TEMPLATE_WIDGET::onEditTemplate( wxCommandEvent& event )
{
    if( !m_currTemplate )
        return;

    wxFileName templatePath = m_currTemplate->GetHtmlFile();
    templatePath.RemoveLastDir();

    wxDir dir( templatePath.GetPath() );

    if( !dir.IsOpened() )
    {
        DisplayErrorMessage( m_dialog, _( "Could not open template directory." ) );
        return;
    }

    wxString filename;
    bool found = dir.GetFirst( &filename, "*.kicad_pro", wxDIR_FILES );

    if( !found )
    {
        DisplayErrorMessage( m_dialog, _( "No project file found in template directory." ) );
        return;
    }

    wxFileName projectFile( templatePath.GetPath(), filename );

    m_dialog->SetProjectToEdit( projectFile.GetFullPath() );

    m_dialog->EndModal( wxID_APPLY );
}


void TEMPLATE_WIDGET::onOpenFolder( wxCommandEvent& event )
{
    if( !m_currTemplate )
        return;

    wxFileName templatePath = m_currTemplate->GetHtmlFile();
    templatePath.RemoveLastDir();

    if( !wxLaunchDefaultApplication( templatePath.GetPath() ) )
        DisplayError( this, wxString::Format( _( "Failed to open '%s'." ), templatePath.GetPath() ) );
}


void TEMPLATE_WIDGET::onDuplicateTemplate( wxCommandEvent& event )
{
    if( !m_currTemplate )
        return;

    wxFileName templatePath = m_currTemplate->GetHtmlFile();
    templatePath.RemoveLastDir();
    wxString srcTemplatePath = templatePath.GetPath();
    wxString srcTemplateName = m_currTemplate->GetPrjDirName();

    wxTextEntryDialog nameDlg( m_dialog, _( "Enter name for the new template:" ), _( "Duplicate Template" ),
                               srcTemplateName + _( "_copy" ) );

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

    wxString newTemplateName = nameDlg.GetValue();

    if( newTemplateName.IsEmpty() )
    {
        DisplayErrorMessage( m_dialog, _( "Template name cannot be empty." ) );
        return;
    }

    wxString userTemplatesPath = m_dialog->GetUserTemplatesPath();

    if( userTemplatesPath.IsEmpty() )
    {
        DisplayErrorMessage( m_dialog, _( "Could not find user templates directory." ) );
        return;
    }

    wxFileName destPath( userTemplatesPath, wxEmptyString );
    destPath.AppendDir( newTemplateName );
    wxString newTemplatePath = destPath.GetPath();

    if( destPath.DirExists() )
    {
        DisplayErrorMessage( m_dialog, wxString::Format( _( "Directory '%s' already exists." ), newTemplatePath ) );
        return;
    }

    if( !destPath.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
    {
        DisplayErrorMessage( m_dialog, wxString::Format( _( "Could not create directory '%s'." ), newTemplatePath ) );
        return;
    }

    wxDir sourceDir( srcTemplatePath );

    if( !sourceDir.IsOpened() )
    {
        DisplayErrorMessage( m_dialog, _( "Could not open source template directory." ) );
        return;
    }

    PROJECT_TREE_TRAVERSER traverser( nullptr, srcTemplatePath, srcTemplateName, newTemplatePath, newTemplateName );

    sourceDir.Traverse( traverser );

    if( !traverser.GetErrors().empty() )
    {
        DisplayErrorMessage( m_dialog, traverser.GetErrors() );
        return;
    }

    wxFileName metaHtmlFile( newTemplatePath, "info.html" );
    metaHtmlFile.AppendDir( "meta" );

    if( metaHtmlFile.FileExists() )
    {
        wxTextFile htmlFile( metaHtmlFile.GetFullPath() );

        if( htmlFile.Open() )
        {
            bool modified = false;

            for( size_t i = 0; i < htmlFile.GetLineCount(); i++ )
            {
                wxString line = htmlFile.GetLine( i );

                if( line.Contains( wxT( "<title>" ) ) && line.Contains( wxT( "</title>" ) ) )
                {
                    int titleStart = line.Find( wxT( "<title>" ) );
                    int titleEnd = line.Find( wxT( "</title>" ) );

                    if( titleStart != wxNOT_FOUND && titleEnd != wxNOT_FOUND && titleEnd > titleStart )
                    {
                        wxString before = line.Left( titleStart + 7 );
                        wxString after = line.Mid( titleEnd );
                        line = before + newTemplateName + after;
                        htmlFile[i] = line;
                        modified = true;
                    }
                }
            }

            if( modified )
                htmlFile.Write();

            htmlFile.Close();
        }
    }

    // The file copy triggers OnFileSystemEvent which starts a refresh timer. If we show a
    // modal info dialog here, the timer fires during the modal event loop and destroys this
    // TEMPLATE_WIDGET while we're still on its call stack. Defer both the message and the
    // refresh to run after the current event handler returns.
    DIALOG_TEMPLATE_SELECTOR* dlg = m_dialog;

    dlg->CallAfter(
            [dlg, newTemplatePath]()
            {
                DisplayInfoMessage( dlg, wxString::Format( _( "Template duplicated successfully to '%s'." ),
                                                           newTemplatePath ) );
                dlg->RefreshTemplateList();
            } );
}


DIALOG_TEMPLATE_SELECTOR::DIALOG_TEMPLATE_SELECTOR( wxWindow* aParent, const wxPoint& aPos,
                                                    const wxSize& aSize, const wxString& aUserTemplatesPath,
                                                    const wxString& aSystemTemplatesPath,
                                                    const std::vector<wxString>& aRecentTemplates ) :
        DIALOG_TEMPLATE_SELECTOR_BASE( aParent, wxID_ANY, _( "Project Template Selector" ), aPos, aSize ),
        m_state( DialogState::Initial ),
        m_selectedWidget( nullptr ),
        m_selectedTemplate( nullptr ),
        m_userTemplatesPath( aUserTemplatesPath ),
        m_systemTemplatesPath( aSystemTemplatesPath ),
        m_recentTemplates( aRecentTemplates ),
        m_searchTimer( this ),
        m_refreshTimer( this ),
        m_watcher( nullptr ),
        m_webviewPanel( nullptr ),
        m_loadingExternalHtml( false )
{
    // The base class now provides the UI structure via wxFormBuilder.
    // Configure the scrolled windows.
    m_scrolledMRU->SetScrollRate( 0, 25 );
    m_scrolledTemplates->SetScrollRate( 0, 25 );
    m_scrolledTemplates->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );

    // Override minimum sizes to allow dialog shrinking (base class Fit() sets large sizes)
    m_scrolledTemplates->SetMinSize( FromDIP( wxSize( 300, 300 ) ) );
    m_panelTemplates->SetMinSize( FromDIP( wxSize( 500, 500 ) ) );

    // Configure the search control
    m_searchCtrl->ShowSearchButton( true );
    m_searchCtrl->ShowCancelButton( true );

    Bind( wxEVT_TIMER, &DIALOG_TEMPLATE_SELECTOR::OnSearchTimer, this, m_searchTimer.GetId() );
    Bind( wxEVT_TIMER, &DIALOG_TEMPLATE_SELECTOR::OnRefreshTimer, this, m_refreshTimer.GetId() );
    Bind( wxEVT_FSWATCHER, &DIALOG_TEMPLATE_SELECTOR::OnFileSystemEvent, this );
    Bind( wxEVT_SYS_COLOUR_CHANGED, &DIALOG_TEMPLATE_SELECTOR::OnSysColourChanged, this );
    m_scrolledTemplates->Bind( wxEVT_SIZE, &DIALOG_TEMPLATE_SELECTOR::OnScrolledTemplatesSize, this );

    // Set initial filter based on saved setting
    KICAD_SETTINGS* settings = Pgm().GetSettingsManager().GetAppSettings<KICAD_SETTINGS>( "kicad" );

    if( settings )
    {
        int filterChoice = settings->m_TemplateFilterChoice;

        if( filterChoice >= 0 && filterChoice < static_cast<int>( m_filterChoice->GetCount() ) )
            m_filterChoice->SetSelection( filterChoice );
        else
            m_filterChoice->SetSelection( 0 );
    }
    else
    {
        m_filterChoice->SetSelection( 0 );
    }

    BuildMRUList();
    BuildTemplateList();
    SetupFileWatcher();

    // Auto-select the most recently used template if available
    if( !m_recentTemplates.empty() )
        SelectTemplateByPath( m_recentTemplates.front(), true );

    SetState( DialogState::Initial );

    m_sdbSizerOK->SetDefault();

    finishDialogSettings();
}


DIALOG_TEMPLATE_SELECTOR::~DIALOG_TEMPLATE_SELECTOR()
{
    m_searchTimer.Stop();
    m_refreshTimer.Stop();

    if( m_watcher )
    {
        m_watcher->RemoveAll();
        m_watcher->SetOwner( nullptr );
        delete m_watcher;
        m_watcher = nullptr;
    }
}


void DIALOG_TEMPLATE_SELECTOR::SetState( DialogState aState )
{
    m_state = aState;

    switch( aState )
    {
    case DialogState::Initial:
        m_panelMRU->Show();
        m_panelPreview->Hide();
        m_btnBack->Enable( false );
        break;

    case DialogState::Preview:
        m_panelMRU->Hide();
        m_panelPreview->Show();
        m_btnBack->Enable( true );
        break;

    case DialogState::MRUWithPreview:
        m_panelMRU->Show();
        m_panelPreview->Show();
        m_btnBack->Enable( true );
        break;
    }

    Layout();
}


void DIALOG_TEMPLATE_SELECTOR::BuildMRUList()
{
    // Clear existing MRU widgets
    for( TEMPLATE_MRU_WIDGET* widget : m_mruWidgets )
    {
        m_sizerMRU->Detach( widget );
        widget->Destroy();
    }

    m_mruWidgets.clear();

    for( const wxString& path : m_recentTemplates )
    {
        wxFileName templateDir;
        templateDir.AssignDir( path );

        if( !templateDir.DirExists() )
            continue;

        PROJECT_TEMPLATE templ( path );

        wxString* title = templ.GetTitle();
        wxBitmap* icon = templ.GetIcon();

        wxBitmap scaledIcon;

        if( icon && icon->IsOk() )
        {
            wxImage img = icon->ConvertToImage();
            img.Rescale( 16, 16, wxIMAGE_QUALITY_HIGH );
            scaledIcon = wxBitmap( img );
        }
        else
        {
            scaledIcon = KiBitmap( BITMAPS::icon_kicad );
            wxImage img = scaledIcon.ConvertToImage();
            img.Rescale( 16, 16, wxIMAGE_QUALITY_HIGH );
            scaledIcon = wxBitmap( img );
        }

        wxString displayTitle = title ? *title : templateDir.GetDirs().Last();

        TEMPLATE_MRU_WIDGET* mruWidget = new TEMPLATE_MRU_WIDGET( m_scrolledMRU, this, path, displayTitle,
                                                                  scaledIcon );
        m_sizerMRU->Add( mruWidget, 0, wxEXPAND | wxBOTTOM, 2 );
        m_mruWidgets.push_back( mruWidget );
    }

    m_scrolledMRU->FitInside();
    m_scrolledMRU->Layout();
    m_panelMRU->Layout();
}


void DIALOG_TEMPLATE_SELECTOR::BuildTemplateList()
{
    wxLogTrace( traceTemplateSelector, "BuildTemplateList() called" );

    // Clear existing template widgets
    for( TEMPLATE_WIDGET* widget : m_templateWidgets )
    {
        m_sizerTemplateList->Detach( widget );
        widget->Destroy();
    }

    m_templateWidgets.clear();
    m_templates.clear();

    auto scanDirectory =
            [this]( const wxString& aPath, bool aIsUser )
            {
                if( aPath.IsEmpty() )
                    return;

                wxDir dir;

                if( !dir.Open( aPath ) )
                    return;

                wxLogTrace( traceTemplateSelector, "Scanning directory: %s (user=%d)", aPath, aIsUser );

                if( dir.HasSubDirs( "meta" ) )
                {
                    auto templ = std::make_unique<PROJECT_TEMPLATE>( aPath );
                    wxFileName htmlFile = templ->GetHtmlFile();
                    wxString description = ExtractDescription( htmlFile );

                    TEMPLATE_WIDGET* widget = new TEMPLATE_WIDGET( m_scrolledTemplates, this );
                    widget->SetTemplate( templ.get() );
                    widget->SetDescription( description );
                    widget->SetIsUserTemplate( aIsUser );

                    m_templates.push_back( std::move( templ ) );
                    m_templateWidgets.push_back( widget );
                }
                else
                {
                    wxString subName;
                    bool cont = dir.GetFirst( &subName, wxEmptyString, wxDIR_DIRS );

                    while( cont )
                    {
                        wxString subFull = aPath + wxFileName::GetPathSeparator() + subName;

                        // Only treat subdirectories that contain a meta directory as templates.
                        // Directories without meta (e.g. regular project directories the user
                        // placed in the template path) are silently skipped.
                        wxFileName metaTest;
                        metaTest.AssignDir( subFull );
                        metaTest.AppendDir( METADIR );

                        if( metaTest.DirExists() )
                        {
                            auto templ = std::make_unique<PROJECT_TEMPLATE>( subFull );
                            wxFileName htmlFile = templ->GetHtmlFile();
                            wxString description = templ->GetError().IsEmpty() ? ExtractDescription( htmlFile ) : templ->GetError();

                            TEMPLATE_WIDGET* widget =
                                    new TEMPLATE_WIDGET( m_scrolledTemplates, this );
                            widget->SetTemplate( templ.get() );
                            widget->SetDescription( description );
                            widget->SetIsUserTemplate( aIsUser );

                            m_templates.push_back( std::move( templ ) );
                            m_templateWidgets.push_back( widget );
                        }

                        cont = dir.GetNext( &subName );
                    }
                }
            };

    scanDirectory( m_userTemplatesPath, true );
    scanDirectory( m_systemTemplatesPath, false );

    // Sort alphabetically with "Default" first
    std::sort( m_templateWidgets.begin(), m_templateWidgets.end(),
               []( TEMPLATE_WIDGET* a, TEMPLATE_WIDGET* b )
               {
                   wxString titleA = *a->GetTemplate()->GetTitle();
                   wxString titleB = *b->GetTemplate()->GetTitle();
                   int      cmp = titleA.CmpNoCase( titleB );

                   if( cmp == 0 )
                       return false;

                   if( titleA.CmpNoCase( "default" ) == 0 )
                       return true;
                   else if( titleB.CmpNoCase( "default" ) == 0 )
                       return false;

                   return cmp < 0;
               } );

    for( TEMPLATE_WIDGET* widget : m_templateWidgets )
    {
        m_sizerTemplateList->Add( widget, 0, wxEXPAND | wxBOTTOM, 8 );
    }

    ApplyFilter();

    // Force initial sizing of widgets after layout
    CallAfter(
            [this]()
            {
                wxSizeEvent evt( m_scrolledTemplates->GetSize() );
                OnScrolledTemplatesSize( evt );
            } );

    wxLogTrace( traceTemplateSelector, "BuildTemplateList() found %zu templates", m_templateWidgets.size() );
}


void DIALOG_TEMPLATE_SELECTOR::ApplyFilter()
{
    int filterChoice = m_filterChoice->GetSelection();
    wxString searchText = m_searchCtrl->GetValue().Lower();

    for( TEMPLATE_WIDGET* widget : m_templateWidgets )
    {
        bool matchesFilter = true;

        if( filterChoice == 1 && !widget->IsUserTemplate() )
            matchesFilter = false;
        else if( filterChoice == 2 && widget->IsUserTemplate() )
            matchesFilter = false;

        bool matchesSearch = true;

        if( !searchText.IsEmpty() )
        {
            wxString title = widget->GetTemplate()->GetTitle()->Lower();
            wxString description = widget->GetDescription().Lower();

            matchesSearch = title.Contains( searchText ) || description.Contains( searchText );
        }

        widget->Show( matchesFilter && matchesSearch );
    }

    m_scrolledTemplates->FitInside();
    m_scrolledTemplates->Layout();
    Layout();
}


void DIALOG_TEMPLATE_SELECTOR::OnSearchCtrl( wxCommandEvent& event )
{
    m_searchTimer.Stop();
    m_searchTimer.StartOnce( 200 );
}


void DIALOG_TEMPLATE_SELECTOR::OnSearchCtrlCancel( wxCommandEvent& event )
{
    m_searchCtrl->Clear();
    ApplyFilter();
}


void DIALOG_TEMPLATE_SELECTOR::OnFilterChanged( wxCommandEvent& event )
{
    KICAD_SETTINGS* settings = Pgm().GetSettingsManager().GetAppSettings<KICAD_SETTINGS>( "kicad" );

    if( settings )
        settings->m_TemplateFilterChoice = m_filterChoice->GetSelection();

    ApplyFilter();
}


void DIALOG_TEMPLATE_SELECTOR::OnSearchTimer( wxTimerEvent& event )
{
    ApplyFilter();
}


void DIALOG_TEMPLATE_SELECTOR::OnRefreshTimer( wxTimerEvent& event )
{
    wxString selectedPath;

    if( m_selectedWidget && m_selectedWidget->GetTemplate() )
    {
        wxFileName htmlFile = m_selectedWidget->GetTemplate()->GetHtmlFile();
        htmlFile.RemoveLastDir();
        selectedPath = htmlFile.GetPath();
    }

    // Clear pointers before destroying widgets to avoid dangling pointers
    m_selectedWidget = nullptr;
    m_selectedTemplate = nullptr;

    BuildTemplateList();

    if( !selectedPath.IsEmpty() )
        SelectTemplateByPath( selectedPath );
}


void DIALOG_TEMPLATE_SELECTOR::OnBackClicked( wxCommandEvent& event )
{
    if( m_selectedWidget )
        m_selectedWidget->Unselect();

    m_selectedWidget = nullptr;
    m_selectedTemplate = nullptr;

    SetState( DialogState::Initial );
    ShowWelcomeHtml();
}


void DIALOG_TEMPLATE_SELECTOR::SetWidget( TEMPLATE_WIDGET* aWidget )
{
    if( m_selectedWidget != nullptr && m_selectedWidget != aWidget )
        m_selectedWidget->Unselect();

    m_selectedWidget = aWidget;
    m_selectedTemplate = aWidget ? aWidget->GetTemplate() : nullptr;

    SetState( DialogState::Preview );

    if( m_selectedTemplate )
        LoadTemplatePreview( m_selectedTemplate );
}


void DIALOG_TEMPLATE_SELECTOR::SelectTemplateByPath( const wxString& aPath )
{
    SelectTemplateByPath( aPath, false );
}


void DIALOG_TEMPLATE_SELECTOR::SelectTemplateByPath( const wxString& aPath, bool aKeepMRUVisible )
{
    for( TEMPLATE_WIDGET* widget : m_templateWidgets )
    {
        if( widget->GetTemplate() )
        {
            wxFileName htmlFile = widget->GetTemplate()->GetHtmlFile();
            htmlFile.RemoveLastDir();

            if( htmlFile.GetPath() == aPath )
            {
                if( aKeepMRUVisible )
                {
                    // Select the widget but keep MRU visible, don't show preview
                    if( m_selectedWidget != nullptr && m_selectedWidget != widget )
                        m_selectedWidget->Unselect();

                    m_selectedWidget = widget;
                    m_selectedTemplate = widget->GetTemplate();

                    widget->SelectWithoutStateChange();

                    // Scroll the widget into view
                    int y = 0;
                    int scrollRate = 0;
                    widget->GetPosition( nullptr, &y );
                    m_scrolledTemplates->GetScrollPixelsPerUnit( nullptr, &scrollRate );

                    if( scrollRate > 0 )
                        m_scrolledTemplates->Scroll( -1, y / scrollRate );
                }
                else
                {
                    widget->Select();
                }

                return;
            }
        }
    }

    wxLogTrace( traceTemplateSelector, "SelectTemplateByPath: template not found at %s", aPath );
}


void DIALOG_TEMPLATE_SELECTOR::EnsureWebViewCreated()
{
    if( m_webviewPanel )
        return;

    // Create the WEBVIEW_PANEL lazily to avoid WebKit JavaScript VM initialization issues
    // when the dialog is opened from a coroutine context.
    m_webviewPanel = new WEBVIEW_PANEL( m_webviewPlaceholder, wxID_ANY, wxDefaultPosition, wxDefaultSize,
                                        wxTAB_TRAVERSAL );

    wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL );
    sizer->Add( m_webviewPanel, 1, wxEXPAND );
    m_webviewPlaceholder->SetSizer( sizer );
    m_webviewPlaceholder->Layout();

    m_webviewPanel->BindLoadedEvent();

    if( m_webviewPanel->GetWebView() )
    {
        Bind( wxEVT_WEBVIEW_LOADED, &DIALOG_TEMPLATE_SELECTOR::OnWebViewLoaded, this,
              m_webviewPanel->GetWebView()->GetId() );
    }
}


void DIALOG_TEMPLATE_SELECTOR::LoadTemplatePreview( PROJECT_TEMPLATE* aTemplate )
{
    if( !aTemplate )
        return;

    EnsureWebViewCreated();

    wxFileName htmlFile = aTemplate->GetHtmlFile();

    if( htmlFile.FileExists() && htmlFile.IsFileReadable() )
    {
        m_loadingExternalHtml = true;
        wxString url = wxFileName::FileNameToURL( htmlFile );
        m_webviewPanel->LoadURL( url );
    }
    else
    {
        m_loadingExternalHtml = false;
        wxString html = GetTemplateInfoHtml( *aTemplate->GetTitle(), KIPLATFORM::UI::IsDarkTheme() );
        m_webviewPanel->SetPage( html );
    }
}


void DIALOG_TEMPLATE_SELECTOR::ShowWelcomeHtml()
{
    EnsureWebViewCreated();
    m_loadingExternalHtml = false;
    m_webviewPanel->SetPage( GetWelcomeHtml( KIPLATFORM::UI::IsDarkTheme() ) );
}


void DIALOG_TEMPLATE_SELECTOR::SetupFileWatcher()
{
    if( m_watcher )
    {
        m_watcher->RemoveAll();
        delete m_watcher;
        m_watcher = nullptr;
    }

    m_watcher = new wxFileSystemWatcher();
    m_watcher->SetOwner( this );

    wxLogNull logNo;

    if( !m_userTemplatesPath.IsEmpty() )
    {
        wxFileName userDir;
        userDir.AssignDir( m_userTemplatesPath );

        if( userDir.DirExists() )
        {
            m_watcher->Add( userDir );
            wxLogTrace( traceTemplateSelector, "Watching user templates: %s", m_userTemplatesPath );
        }
    }

    if( !m_systemTemplatesPath.IsEmpty() )
    {
        wxFileName systemDir;
        systemDir.AssignDir( m_systemTemplatesPath );

        if( systemDir.DirExists() )
        {
            m_watcher->Add( systemDir );
            wxLogTrace( traceTemplateSelector, "Watching system templates: %s", m_systemTemplatesPath );
        }
    }
}


void DIALOG_TEMPLATE_SELECTOR::OnFileSystemEvent( wxFileSystemWatcherEvent& event )
{
    if( !m_watcher )
        return;

    wxLogTrace( traceTemplateSelector, "File system event detected" );

    // wxFileSystemWatcher may fire events from a worker thread on some platforms.
    // Use CallAfter to marshal timer operations to the main thread.
    CallAfter(
            [this]()
            {
                m_refreshTimer.Stop();
                m_refreshTimer.StartOnce( 500 );
            } );
}


void DIALOG_TEMPLATE_SELECTOR::OnSysColourChanged( wxSysColourChangedEvent& event )
{
    event.Skip();

    // Update background colors for the templates panel
    m_scrolledTemplates->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );

    // Update all template widgets
    for( TEMPLATE_WIDGET* widget : m_templateWidgets )
    {
        if( widget->IsSelected() )
            widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT ) );
        else
            widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );

        widget->Refresh();
    }

    // Update all MRU widgets
    for( TEMPLATE_MRU_WIDGET* widget : m_mruWidgets )
    {
        widget->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
        widget->Refresh();
    }

    m_scrolledTemplates->Refresh();
    m_scrolledMRU->Refresh();
}


void DIALOG_TEMPLATE_SELECTOR::OnScrolledTemplatesSize( wxSizeEvent& event )
{
    event.Skip();

    // Get the client width of the scrolled window
    int clientWidth = m_scrolledTemplates->GetClientSize().GetWidth();

    if( clientWidth <= 0 )
        return;

    // Force each widget to rewrap its text at the new width
    for( TEMPLATE_WIDGET* widget : m_templateWidgets )
    {
        wxSizeEvent sizeEvt( wxSize( clientWidth, -1 ) );
        widget->GetEventHandler()->ProcessEvent( sizeEvt );
    }

    m_scrolledTemplates->FitInside();
}


void DIALOG_TEMPLATE_SELECTOR::OnWebViewLoaded( wxWebViewEvent& event )
{
    event.Skip();

    // Safety check - ensure webview panel is valid
    if( !m_webviewPanel || !m_webviewPanel->GetWebView() )
        return;

    // Add style to external documets
    if( m_loadingExternalHtml )
    {
        wxString script = wxString::Format( wxS( R"(
( function()
{
    var style = document.createElement( 'style' );
    style.textContent = `
%s
    `;
    document.head.appendChild( style );
} )();
                                                 )" ), GetCommonStyles() );

#if !defined( __MINGW32__ )     // RunScriptAsync() is not supported on MINGW build
        if( m_webviewPanel->GetBackend() != wxWebViewBackendIE )
            m_webviewPanel->RunScriptAsync( script );
#endif
    }
}


void DIALOG_TEMPLATE_SELECTOR::RefreshTemplateList()
{
    wxString selectedPath;

    if( m_selectedWidget && m_selectedWidget->GetTemplate() )
    {
        wxFileName htmlFile = m_selectedWidget->GetTemplate()->GetHtmlFile();
        htmlFile.RemoveLastDir();
        selectedPath = htmlFile.GetPath();
    }

    // Clear pointers before destroying widgets to avoid dangling pointers
    m_selectedWidget = nullptr;
    m_selectedTemplate = nullptr;

    BuildTemplateList();

    if( !selectedPath.IsEmpty() )
        SelectTemplateByPath( selectedPath );
}


wxString DIALOG_TEMPLATE_SELECTOR::ExtractDescription( const wxFileName& aHtmlFile )
{
    if( !aHtmlFile.FileExists() || !aHtmlFile.IsFileReadable() )
        return wxEmptyString;

    wxTextFile file( aHtmlFile.GetFullPath() );

    if( !file.Open() )
        return wxEmptyString;

    wxString content;

    for( wxString line = file.GetFirstLine(); !file.Eof(); line = file.GetNextLine() )
    {
        content += line + wxT( " " );
    }

    file.Close();

    // Try to extract text from the meta description tag
    wxRegEx reMetaDesc( wxT( "<meta[^>]*name=[\"']description[\"'][^>]*content=[\"']([^\"']*)[\"']" ),
                        wxRE_ICASE );

    if( reMetaDesc.Matches( content ) )
        return reMetaDesc.GetMatch( content, 1 );

    // Fallback to first paragraph tag (allowing nested HTML tags)
    wxRegEx reParagraph( wxT( "<p[^>]*>(.*?)</p>" ), wxRE_ICASE );

    if( reParagraph.Matches( content ) )
    {
        wxString desc = reParagraph.GetMatch( content, 1 );

        // Strip nested HTML tags
        wxRegEx reTags( wxT( "<[^>]*>" ) );
        reTags.ReplaceAll( &desc, wxT( " " ) );

        // Decode common HTML entities
        desc.Replace( wxT( "&nbsp;" ), wxT( " " ) );
        desc.Replace( wxT( "&amp;" ), wxT( "&" ) );
        desc.Replace( wxT( "&lt;" ), wxT( "<" ) );
        desc.Replace( wxT( "&gt;" ), wxT( ">" ) );
        desc.Replace( wxT( "&quot;" ), wxT( "\"" ) );

        // Normalize whitespace
        wxRegEx reWhitespace( wxT( "\\s+" ) );
        reWhitespace.ReplaceAll( &desc, wxT( " " ) );

        desc = desc.Trim().Trim( false );

        if( !desc.IsEmpty() )
            return desc;
    }

    // Final fallback: extract text content from body, stripping all HTML tags
    wxRegEx reBody( wxT( "<body[^>]*>(.*)</body>" ), wxRE_ICASE );

    if( reBody.Matches( content ) )
    {
        wxString body = reBody.GetMatch( content, 1 );

        // Strip all HTML tags
        wxRegEx reTags( wxT( "<[^>]*>" ) );
        reTags.ReplaceAll( &body, wxT( " " ) );

        // Decode common HTML entities
        body.Replace( wxT( "&nbsp;" ), wxT( " " ) );
        body.Replace( wxT( "&amp;" ), wxT( "&" ) );
        body.Replace( wxT( "&lt;" ), wxT( "<" ) );
        body.Replace( wxT( "&gt;" ), wxT( ">" ) );
        body.Replace( wxT( "&quot;" ), wxT( "\"" ) );

        // Normalize whitespace
        wxRegEx reWhitespace( wxT( "\\s+" ) );
        reWhitespace.ReplaceAll( &body, wxT( " " ) );

        body = body.Trim().Trim( false );

        // Return up to 250 characters
        if( body.Length() > 250 )
            return body.Left( 250 ) + wxS( "..." );

        return body;
    }

    return wxEmptyString;
}


PROJECT_TEMPLATE* DIALOG_TEMPLATE_SELECTOR::GetSelectedTemplate()
{
    return m_selectedTemplate;
}
