/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2023 Rivos
 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
 *
 * @author Wayne Stambaugh <stambaughw@gmail.com>
 *
 * 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 <wx/log.h>
#include <wx/wupdlock.h>
#include <core/profile.h>
#include <tool/tool_manager.h>
#include <kiface_base.h>
#include <sch_edit_frame.h>
#include <sch_bus_entry.h>
#include <sch_line.h>
#include <sch_junction.h>
#include <sch_no_connect.h>
#include <sch_sheet_pin.h>
#include <string_utils.h>
#include <trace_helpers.h>
#include <connection_graph.h>
#include <widgets/wx_aui_utils.h>
#include <tools/sch_actions.h>


static wxString GetNetNavigatorItemText( const SCH_ITEM* aItem,
                                         const SCH_SHEET_PATH& aSheetPath,
                                         UNITS_PROVIDER* aUnitsProvider )
{
    wxString retv;

    wxCHECK( aItem && aUnitsProvider, retv );

    switch( aItem->Type() )
    {
    case SCH_LINE_T:
    {
        const SCH_LINE* line = static_cast<const SCH_LINE*>( aItem );

        if( aItem->GetLayer() == LAYER_WIRE )
        {
            retv.Printf( _( "Wire from %s, %s to %s, %s" ),
                         aUnitsProvider->MessageTextFromValue( line->GetStartPoint().x ),
                         aUnitsProvider->MessageTextFromValue( line->GetStartPoint().y ),
                         aUnitsProvider->MessageTextFromValue( line->GetEndPoint().x ),
                         aUnitsProvider->MessageTextFromValue( line->GetEndPoint().y ) );
        }
        else if( aItem->GetLayer() == LAYER_BUS )
        {
            retv.Printf( _( "Bus from %s, %s to %s, %s" ),
                         aUnitsProvider->MessageTextFromValue( line->GetStartPoint().x ),
                         aUnitsProvider->MessageTextFromValue( line->GetStartPoint().y ),
                         aUnitsProvider->MessageTextFromValue( line->GetEndPoint().x ),
                         aUnitsProvider->MessageTextFromValue( line->GetEndPoint().y ) );
        }
        else
        {
            retv = _( "Graphic line not connectable" );
        }

        break;
    }
    case SCH_PIN_T:
    {
        const SCH_PIN* pin = static_cast<const SCH_PIN*>( aItem );

        if( const SYMBOL* symbol = pin->GetParentSymbol() )
        {
            retv.Printf( _( "Symbol '%s' pin '%s'" ),
                         symbol->GetRef( &aSheetPath, true ),
                         UnescapeString( pin->GetNumber() ) );

            if( wxString pinName = UnescapeString( pin->GetShownName() ); !pinName.IsEmpty() )
            {
                retv += wxString::Format( " (%s)", pinName );
            }
        }

        break;
    }
    case SCH_SHEET_PIN_T:
    {
        const SCH_SHEET_PIN* pin = static_cast<const SCH_SHEET_PIN*>( aItem );

        if( SCH_SHEET* sheet = pin->GetParent() )
        {
            retv.Printf( _( "Sheet '%s' pin '%s'" ),
                         sheet->GetName(),
                         UnescapeString( pin->GetText() ) );
        }

        break;
    }
    case SCH_LABEL_T:
    {
        const SCH_LABEL* label = static_cast<const SCH_LABEL*>( aItem );

        retv.Printf( _( "Label '%s' at %s, %s" ),
                     UnescapeString( label->GetText() ),
                     aUnitsProvider->MessageTextFromValue( label->GetPosition().x ),
                     aUnitsProvider->MessageTextFromValue( label->GetPosition().y ) );
        break;
    }
    case SCH_GLOBAL_LABEL_T:
    {
        const SCH_GLOBALLABEL* label = static_cast<const SCH_GLOBALLABEL*>( aItem );

        retv.Printf( _( "Global label '%s' at %s, %s" ),
                     UnescapeString( label->GetText() ),
                     aUnitsProvider->MessageTextFromValue( label->GetPosition().x ),
                     aUnitsProvider->MessageTextFromValue( label->GetPosition().y ) );
        break;
    }
    case SCH_HIER_LABEL_T:
    {
        const SCH_HIERLABEL* label = static_cast<const SCH_HIERLABEL*>( aItem );

        retv.Printf( _( "Hierarchical label '%s' at %s, %s" ),
                     UnescapeString( label->GetText() ),
                     aUnitsProvider->MessageTextFromValue( label->GetPosition().x ),
                     aUnitsProvider->MessageTextFromValue( label->GetPosition().y ) );
        break;
    }
    case SCH_JUNCTION_T:
    {
        const SCH_JUNCTION* junction = static_cast<const SCH_JUNCTION*>( aItem );

        retv.Printf( _( "Junction at %s, %s" ),
                     aUnitsProvider->MessageTextFromValue( junction->GetPosition().x ),
                     aUnitsProvider->MessageTextFromValue( junction->GetPosition().y ) );
        break;
    }
    case SCH_NO_CONNECT_T:
    {
        const SCH_NO_CONNECT* nc = static_cast<const SCH_NO_CONNECT*>( aItem );

        retv.Printf( _( "No-Connect at %s, %s" ),
                     aUnitsProvider->MessageTextFromValue( nc->GetPosition().x ),
                     aUnitsProvider->MessageTextFromValue( nc->GetPosition().y ) );
        break;
    }
    case SCH_BUS_WIRE_ENTRY_T:
    {
        const SCH_BUS_WIRE_ENTRY* entry = static_cast<const SCH_BUS_WIRE_ENTRY*>( aItem );

        retv.Printf( _( "Bus to wire entry from %s, %s to %s, %s" ),
                     aUnitsProvider->MessageTextFromValue( entry->GetPosition().x ),
                     aUnitsProvider->MessageTextFromValue( entry->GetPosition().y ),
                     aUnitsProvider->MessageTextFromValue( entry->GetEnd().x ),
                     aUnitsProvider->MessageTextFromValue( entry->GetEnd().y ) );
        break;
    }
    case SCH_BUS_BUS_ENTRY_T:
    {
        const SCH_BUS_BUS_ENTRY* entry = static_cast<const SCH_BUS_BUS_ENTRY*>( aItem );

        retv.Printf( _( "Bus to bus entry from %s, %s to %s, %s" ),
                     aUnitsProvider->MessageTextFromValue( entry->GetPosition().x ),
                     aUnitsProvider->MessageTextFromValue( entry->GetPosition().y ),
                     aUnitsProvider->MessageTextFromValue( entry->GetEnd().x ),
                     aUnitsProvider->MessageTextFromValue( entry->GetEnd().y ) );
        break;
    }
    case SCH_DIRECTIVE_LABEL_T:
    {
        const SCH_DIRECTIVE_LABEL* entry = static_cast<const SCH_DIRECTIVE_LABEL*>( aItem );

        retv.Printf( _( "Netclass label '%s' at %s, %s" ),
                     UnescapeString( entry->GetText() ),
                     aUnitsProvider->MessageTextFromValue( entry->GetPosition().x ),
                     aUnitsProvider->MessageTextFromValue( entry->GetPosition().y ) );
        break;
    }
    default:
        retv.Printf( _( "Unhandled item type %d" ), aItem->Type() );
    }

    return retv;
}


void SCH_EDIT_FRAME::MakeNetNavigatorNode( const wxString& aNetName, wxTreeItemId aParentId,
                                           const NET_NAVIGATOR_ITEM_DATA* aSelection,
                                           bool aSingleSheetSchematic )
{
    wxCHECK( !aNetName.IsEmpty(), /* void */ );
    wxCHECK( m_schematic, /* void */ );
    wxCHECK( m_netNavigator, /* void */ );

    wxTreeItemId expandId = aParentId;
    CONNECTION_GRAPH* connectionGraph = m_schematic->ConnectionGraph();

    wxCHECK( connectionGraph, /* void */ );

    wxString sheetPathPrefix;
    std::set<CONNECTION_SUBGRAPH*> subgraphs;

    {
        const std::vector<CONNECTION_SUBGRAPH*>& tmp = connectionGraph->GetAllSubgraphs( aNetName );
        subgraphs.insert( tmp.begin(), tmp.end() );
    }

    for( CONNECTION_SUBGRAPH* sg : subgraphs )
    {
        for( const auto& [_, bus_sgs] : sg->GetBusParents() )
        {
            for( const CONNECTION_SUBGRAPH* bus_sg : bus_sgs )
            {
                const std::vector<CONNECTION_SUBGRAPH*>& tmp =
                        connectionGraph->GetAllSubgraphs( bus_sg->GetNetName() );
                subgraphs.insert( tmp.begin(), tmp.end() );
            }
        }
    }

    std::map<wxString, wxTreeItemId> sheetIds;

    for( const CONNECTION_SUBGRAPH* subGraph : subgraphs )
    {
        NET_NAVIGATOR_ITEM_DATA* itemData = nullptr;
        SCH_SHEET_PATH sheetPath = subGraph->GetSheet();

        wxCHECK2( subGraph && sheetPath.Last(), continue );

        if( subGraph->GetItems().empty() )
            continue;

        itemData = new NET_NAVIGATOR_ITEM_DATA( sheetPath, nullptr );

        bool stripTrailingSeparator = !sheetPath.Last()->IsRootSheet();
        wxString txt =  sheetPath.PathHumanReadable( true, stripTrailingSeparator );

        wxTreeItemId sheetId;

        if( auto sheetIdIt = sheetIds.find( txt ); sheetIdIt != sheetIds.end() )
        {
            sheetId = sheetIdIt->second;
        }
        else
        {
            sheetIds[txt] = m_netNavigator->AppendItem( aParentId, txt, -1, -1, itemData );
            sheetId = sheetIds[txt];
        }

        if( aSelection && *aSelection == *itemData )
            m_netNavigator->SelectItem( sheetId );

        // If there is only one sheet in the schematic, always expand the sheet tree.
        if( aSingleSheetSchematic )
            expandId = sheetId;

        for( const SCH_ITEM* item : subGraph->GetItems() )
        {
            if( item->Type() == SCH_LINE_T
                    || item->Type() == SCH_JUNCTION_T
                    || item->Type() == SCH_BUS_WIRE_ENTRY_T
                    || item->Type() == SCH_BUS_BUS_ENTRY_T )
            {
                continue;
            }

            itemData = new NET_NAVIGATOR_ITEM_DATA( sheetPath, item );
            wxTreeItemId id = m_netNavigator->AppendItem( sheetId,
                                                          GetNetNavigatorItemText( item, sheetPath, this ),
                                                          -1, -1, itemData );

            if( aSelection && *aSelection == *itemData )
            {
                expandId = sheetId;
                m_netNavigator->EnsureVisible( id );
                m_netNavigator->SelectItem( id );
            }
        }

        m_netNavigator->SortChildren( sheetId );
    }

    // Sort the items in the tree control alphabetically
    m_netNavigator->SortChildren( aParentId );
    m_netNavigator->Expand( aParentId );
}


void SCH_EDIT_FRAME::RefreshNetNavigator( const NET_NAVIGATOR_ITEM_DATA* aSelection )
{
    wxCHECK( m_netNavigator, /* void */ );

    if( !m_netNavigator->IsShown() )
        return;

    bool   singleSheetSchematic = m_schematic->Hierarchy().size() == 1;
    size_t nodeCnt = 0;

    wxWindowUpdateLocker updateLock( m_netNavigator );
    PROF_TIMER           timer;

    if( m_highlightedConn.IsEmpty() )
    {
        m_netNavigator->DeleteAllItems();

        // Create a tree of all nets in the schematic.
        wxTreeItemId rootId = m_netNavigator->AddRoot( _( "Nets" ), 0 );

        const NET_MAP& netMap = m_schematic->ConnectionGraph()->GetNetMap();

        for( const auto& net : netMap )
        {
            // Skip bus member subgraphs for the moment.
            if( net.first.Name.IsEmpty() )
                continue;

            nodeCnt++;
            wxTreeItemId netId = m_netNavigator->AppendItem( rootId, UnescapeString( net.first.Name ) );
            MakeNetNavigatorNode( net.first.Name, netId, aSelection, singleSheetSchematic );
        }

        m_netNavigator->Expand( rootId );
    }
    else if( !m_netNavigator->IsEmpty() )
    {
        const wxString shownNetName = m_netNavigator->GetItemText( m_netNavigator->GetRootItem() );

        if( shownNetName != m_highlightedConn )
        {
            m_netNavigator->DeleteAllItems();

            nodeCnt++;

            wxTreeItemId rootId = m_netNavigator->AddRoot( UnescapeString( m_highlightedConn ), 0 );

            MakeNetNavigatorNode( m_highlightedConn, rootId, aSelection, singleSheetSchematic );
        }
        else
        {
            NET_NAVIGATOR_ITEM_DATA* itemData = nullptr;

            wxTreeItemId selection = m_netNavigator->GetSelection();

            if( selection.IsOk() )
                itemData = dynamic_cast<NET_NAVIGATOR_ITEM_DATA*>( m_netNavigator->GetItemData( selection ) );

            m_netNavigator->DeleteAllItems();
            nodeCnt++;

            wxTreeItemId rootId = m_netNavigator->AddRoot( UnescapeString( m_highlightedConn ), 0 );

            MakeNetNavigatorNode( m_highlightedConn, rootId, itemData, singleSheetSchematic );
        }
    }
    else
    {
        nodeCnt++;

        wxTreeItemId rootId = m_netNavigator->AddRoot( UnescapeString( m_highlightedConn ), 0 );

        MakeNetNavigatorNode( m_highlightedConn, rootId, aSelection, singleSheetSchematic );
    }

    timer.Stop();

    wxLogTrace( traceUiProfile, wxS( "Adding %zu nodes to net navigator took %s." ),
                nodeCnt, timer.to_string() );
}


const SCH_ITEM* SCH_EDIT_FRAME::SelectNextPrevNetNavigatorItem( bool aNext )
{
    wxCHECK( m_netNavigator, nullptr );

    wxTreeItemId id = m_netNavigator->GetSelection();

    if( !id.IsOk() )
        return nullptr;

    wxTreeItemId nextId;
    wxTreeItemId netNode = m_netNavigator->GetRootItem();

    std::vector<wxTreeItemId> netItems;
    std::list<wxTreeItemId> itemList;
    itemList.push_back( netNode );

    while( !itemList.empty() )
    {
        wxTreeItemId current = itemList.front();
        itemList.pop_front();

        wxTreeItemIdValue cookie;
        wxTreeItemId      child = m_netNavigator->GetFirstChild( current, cookie );

        while( child.IsOk() )
        {
            if( m_netNavigator->ItemHasChildren( child ) )
                itemList.push_back( child );
            else
                netItems.push_back( child );

            child = m_netNavigator->GetNextSibling( child );
        }
    }

    // Locate current item and move forward or backward with wrap
    auto it = std::find( netItems.begin(), netItems.end(), id );

    if( it != netItems.end() )
    {
        if( aNext )
        {
            ++it;
            if( it == netItems.end() )
                it = netItems.begin();
        }
        else
        {
            if( it == netItems.begin() )
                it = netItems.end();
            --it;
        }
        nextId = *it;
    }

    if( nextId.IsOk() )
    {
        if( !m_netNavigator->IsVisible( nextId ) )
        {
            m_netNavigator->CollapseAll();
            m_netNavigator->EnsureVisible( nextId );
        }

        m_netNavigator->UnselectAll();
        m_netNavigator->SelectItem( nextId );

        NET_NAVIGATOR_ITEM_DATA* data = static_cast<NET_NAVIGATOR_ITEM_DATA*>(
            m_netNavigator->GetItemData( nextId ) );

        if( data && data->GetItem() )
            return data->GetItem();
    }

    return nullptr;
}


void SCH_EDIT_FRAME::SelectNetNavigatorItem( const NET_NAVIGATOR_ITEM_DATA* aSelection )
{
    wxCHECK( m_netNavigator && !m_netNavigator->IsFrozen(), /* void */ );

    // Maybe in the future we can do something like collapse the tree for an empty selection.
    // For now, leave the tree selection in its current state.
    if( !aSelection )
        return;

    wxTreeItemId rootId = m_netNavigator->GetRootItem();

    if( !rootId.IsOk() )
        return;

    wxTreeItemIdValue        sheetCookie;
    NET_NAVIGATOR_ITEM_DATA* itemData = nullptr;
    wxTreeItemId             sheetId = m_netNavigator->GetFirstChild( rootId, sheetCookie );

    while( sheetId.IsOk() )
    {
        if( m_netNavigator->ItemHasChildren( sheetId ) )
        {
            wxTreeItemIdValue itemCookie;
            wxTreeItemId itemId = m_netNavigator->GetFirstChild( sheetId, itemCookie );

            while( itemId.IsOk() )
            {
                itemData = dynamic_cast<NET_NAVIGATOR_ITEM_DATA*>( m_netNavigator->GetItemData( itemId ) );

                wxCHECK2( itemData, continue );

                if( *itemData == *aSelection )
                {
                    if( !m_netNavigator->IsVisible( itemId ) )
                    {
                        m_netNavigator->CollapseAll();
                        m_netNavigator->EnsureVisible( itemId );
                    }

                    m_netNavigator->SelectItem( itemId );
                    return;
                }

                itemId = m_netNavigator->GetNextSibling( itemId );
            }

            sheetId = m_netNavigator->GetNextSibling( sheetId );
        }
    }
}


const SCH_ITEM* SCH_EDIT_FRAME::GetSelectedNetNavigatorItem() const
{
    if( !m_netNavigator || m_netNavigator->IsFrozen() )
        return nullptr;

    wxTreeItemId id = m_netNavigator->GetSelection();

    if( !id.IsOk() || ( id == m_netNavigator->GetRootItem() ) )
        return nullptr;

    auto* itemData = dynamic_cast<NET_NAVIGATOR_ITEM_DATA*>( m_netNavigator->GetItemData( id ) );

    wxCHECK( itemData, nullptr );

    return itemData->GetItem();
}


void SCH_EDIT_FRAME::onNetNavigatorSelection( wxTreeEvent& aEvent )
{
    if( !m_netNavigator || m_netNavigator->IsFrozen() )
        return;

    wxTreeItemId id = aEvent.GetItem();

    // Clicking on the root item (net name ) does nothing.
    if( id == m_netNavigator->GetRootItem() )
        return;

    auto* itemData = dynamic_cast<NET_NAVIGATOR_ITEM_DATA*>( m_netNavigator->GetItemData( id ) );

    // Just a net name when we have all nets displayed.
    if( !itemData )
        return;

    if( GetCurrentSheet() != itemData->GetSheetPath() )
    {
        GetToolManager()->RunAction<SCH_SHEET_PATH*>( SCH_ACTIONS::changeSheet,
                                                      &itemData->GetSheetPath() );
    }

    // Do not focus on item when a sheet tree node is selected.
    if( m_netNavigator->GetItemParent( id ) != m_netNavigator->GetRootItem()
            && itemData->GetItem() )
    {
        // Make sure we didn't remove the item and/or the screen it resides on before we access it.
        const SCH_ITEM* item = itemData->GetItem();

        // Don't search for child items in screen r-tree.
        item = ( item->Type() == SCH_SHEET_PIN_T || item->Type() == SCH_PIN_T )
                                                            ? static_cast<const SCH_ITEM*>( item->GetParent() )
                                                            : item;

        const SCH_SCREEN* screen = itemData->GetSheetPath().LastScreen();

        wxCHECK( screen, /* void */ );
        wxCHECK( screen->Items().contains( item, true ), /* void */ );

        FocusOnLocation( itemData->GetItem()->GetBoundingBox().Centre() );
    }

    GetCanvas()->Refresh();
}


void SCH_EDIT_FRAME::onNetNavigatorSelChanging( wxTreeEvent& aEvent )
{
    if( !m_netNavigator || m_netNavigator->IsFrozen() )
        return;

    aEvent.Skip();
}


void SCH_EDIT_FRAME::ToggleNetNavigator()
{
    EESCHEMA_SETTINGS* cfg = eeconfig();

    wxCHECK( cfg, /* void */ );

    wxAuiPaneInfo& netNavigatorPane = m_auimgr.GetPane( NetNavigatorPaneName() );

    netNavigatorPane.Show( !netNavigatorPane.IsShown() );
    updateSelectionFilterVisbility();

    cfg->m_AuiPanels.show_net_nav_panel = netNavigatorPane.IsShown();

    if( netNavigatorPane.IsShown() )
    {
        if( netNavigatorPane.IsFloating() )
        {
            netNavigatorPane.FloatingSize( cfg->m_AuiPanels.net_nav_panel_float_size );
            m_auimgr.Update();
        }
        else if( cfg->m_AuiPanels.net_nav_panel_docked_size.GetWidth() > 0 )
        {
            // SetAuiPaneSize also updates m_auimgr
            SetAuiPaneSize( m_auimgr, netNavigatorPane,
                            cfg->m_AuiPanels.net_nav_panel_docked_size.GetWidth(), -1 );
        }
    }
    else
    {
        if( netNavigatorPane.IsFloating() )
        {
            cfg->m_AuiPanels.net_nav_panel_float_size  = netNavigatorPane.floating_size;
        }
        else
        {
            cfg->m_AuiPanels.net_nav_panel_docked_size = m_netNavigator->GetSize();
        }

        m_auimgr.Update();
    }

    if( netNavigatorPane.IsShown() )
    {
        NET_NAVIGATOR_ITEM_DATA* itemData = nullptr;

        wxTreeItemId selection = m_netNavigator->GetSelection();

        if( selection.IsOk() )
            itemData = dynamic_cast<NET_NAVIGATOR_ITEM_DATA*>( m_netNavigator->GetItemData( selection ) );

        RefreshNetNavigator( itemData );
    }
}


void SCH_EDIT_FRAME::onResizeNetNavigator( wxSizeEvent& aEvent )
{
    aEvent.Skip();

    // Called when resizing the Hierarchy Navigator panel
    // Store the current pane size
    // It allows to retrieve the last defined pane size when switching between
    // docked and floating pane state
    // Note: *DO NOT* call m_auimgr.Update() here: it crashes KiCad at least on Windows

    EESCHEMA_SETTINGS* cfg = dynamic_cast<EESCHEMA_SETTINGS*>( Kiface().KifaceSettings() );

    wxCHECK( cfg, /* void */ );

    wxAuiPaneInfo& netNavigatorPane = m_auimgr.GetPane( NetNavigatorPaneName() );

    if( m_netNavigator->IsShownOnScreen() )
    {
        cfg->m_AuiPanels.net_nav_panel_float_size  = netNavigatorPane.floating_size;

        // initialize net_nav_panel_docked_width and best size only if the netNavigatorPane
        // width is > 0 (i.e. if its size is already set and has meaning)
        // if it is floating, its size is not initialized (only floating_size is initialized)
        // initializing netNavigatorPane.best_size is useful when switching to float pane and
        // after switching to the docked pane, to retrieve the last docked pane width
        if( netNavigatorPane.rect.width > 50 )    // 50 is a good margin
        {
            cfg->m_AuiPanels.net_nav_panel_docked_size.SetWidth( netNavigatorPane.rect.width );
            netNavigatorPane.best_size.x = netNavigatorPane.rect.width;
        }
    }
}
