/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2018 CERN
 * @author Jon Evans <jon@craftyjon.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 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, see <http://www.gnu.org/licenses/>.
 */

#include <list>
#include <thread>
#include <algorithm>
#include <future>
#include <vector>
#include <unordered_map>
#include <profile.h>

#include <common.h>
#include <erc.h>
#include <sch_bus_entry.h>
#include <sch_component.h>
#include <sch_edit_frame.h>
#include <sch_line.h>
#include <sch_marker.h>
#include <sch_pin.h>
#include <sch_sheet.h>
#include <sch_sheet_path.h>
#include <sch_text.h>

#include <advanced_config.h>
#include <connection_graph.h>
#include <widgets/ui_common.h>

bool CONNECTION_SUBGRAPH::ResolveDrivers( bool aCreateMarkers )
{
    PRIORITY               highest_priority = PRIORITY::INVALID;
    std::vector<SCH_ITEM*> candidates;
    std::vector<SCH_ITEM*> strong_drivers;

    m_driver = nullptr;

    // Hierarchical labels are lower priority than local labels here,
    // because on the first pass we want local labels to drive subgraphs
    // so that we can identify same-sheet neighbors and link them together.
    // Hierarchical labels will end up overriding the final net name if
    // a higher-level sheet has a different name during the hierarchical
    // pass.

    for( SCH_ITEM* item : m_drivers )
    {
        PRIORITY item_priority = GetDriverPriority( item );

        if( item_priority == PRIORITY::PIN
                && !static_cast<SCH_PIN*>( item )->GetParentComponent()->IsInNetlist() )
            continue;

        if( item_priority >= PRIORITY::HIER_LABEL )
            strong_drivers.push_back( item );

        if( item_priority > highest_priority )
        {
            candidates.clear();
            candidates.push_back( item );
            highest_priority = item_priority;
        }
        else if( !candidates.empty() && ( item_priority == highest_priority ) )
        {
            candidates.push_back( item );
        }
    }

    if( highest_priority >= PRIORITY::HIER_LABEL )
        m_strong_driver = true;

    // Power pins are 5, global labels are 6
    m_local_driver = ( highest_priority < PRIORITY::POWER_PIN );

    if( !candidates.empty() )
    {
        if( candidates.size() > 1 )
        {
            if( highest_priority == PRIORITY::SHEET_PIN )
            {
                // We have multiple options, and they are all hierarchical
                // sheet pins.  Let's prefer outputs over inputs.

                for( auto c : candidates )
                {
                    auto p = static_cast<SCH_SHEET_PIN*>( c );

                    if( p->GetShape() == PINSHEETLABEL_SHAPE::PS_OUTPUT )
                    {
                        m_driver = c;
                        break;
                    }
                }
            }
            else
            {
                // For all other driver types, sort by name
                std::sort( candidates.begin(), candidates.end(),
                           [&] ( SCH_ITEM* a, SCH_ITEM* b) -> bool
                           {
                               return GetNameForDriver( a ) < GetNameForDriver( b );
                           } );
            }
        }

        if( !m_driver )
            m_driver = candidates[0];
    }

    if( strong_drivers.size() > 1 )
        m_multiple_drivers = true;

    // Drop weak drivers
    if( m_strong_driver )
        m_drivers = strong_drivers;

    // Cache driver connection
    if( m_driver )
        m_driver_connection = m_driver->Connection( m_sheet );
    else
        m_driver_connection = nullptr;

    if( aCreateMarkers && m_multiple_drivers )
    {
        // First check if all the candidates are actually the same
        bool same = true;
        wxString first = GetNameForDriver( candidates[0] );
        SCH_ITEM* second_item = nullptr;

        for( unsigned i = 1; i < candidates.size(); i++ )
        {
            if( GetNameForDriver( candidates[i] ) != first )
            {
                second_item = candidates[i];
                same = false;
                break;
            }
        }

        if( !same )
        {
            wxPoint pos = ( candidates[0]->Type() == SCH_PIN_T ) ?
                          static_cast<SCH_PIN*>( candidates[0] )->GetTransformedPosition() :
                          candidates[0]->GetPosition();

            auto marker = new SCH_MARKER( MARKER_BASE::MARKER_ERC );
            marker->SetData( m_frame->GetUserUnits(), ERCE_DRIVER_CONFLICT, pos,
                             candidates[0], second_item );
            m_sheet.LastScreen()->Append( marker );

            // If aCreateMarkers is true, then this is part of ERC check, so we
            // should return false even if the driver was assigned
            return false;
        }
    }

    return aCreateMarkers || ( m_driver != nullptr );
}


wxString CONNECTION_SUBGRAPH::GetNetName() const
{
    if( !m_driver || m_dirty )
        return "";

    if( !m_driver->Connection( m_sheet ) )
    {
#ifdef CONNECTIVITY_DEBUG
        wxASSERT_MSG( false, "Tried to get the net name of an item with no connection" );
#endif

        return "";
    }

    return m_driver->Connection( m_sheet )->Name();
}


std::vector<SCH_ITEM*> CONNECTION_SUBGRAPH::GetBusLabels() const
{
    std::vector<SCH_ITEM*> labels;

    for( SCH_ITEM* item : m_drivers )
    {
        switch( item->Type() )
        {
        case SCH_LABEL_T:
        case SCH_GLOBAL_LABEL_T:
        {
            SCH_CONNECTION* label_conn = item->Connection( m_sheet );

            // Only consider bus vectors
            if( label_conn->Type() == CONNECTION_TYPE::BUS )
                labels.push_back( item );
        }
        default: break;
        }
    }

    return labels;
}


wxString CONNECTION_SUBGRAPH::GetNameForDriver( SCH_ITEM* aItem ) const
{
    wxString name;

    switch( aItem->Type() )
    {
    case SCH_PIN_T:
    {
        auto power_object = static_cast<SCH_PIN*>( aItem );
        name = power_object->GetDefaultNetName( m_sheet );
        break;
    }

    case SCH_LABEL_T:
    case SCH_GLOBAL_LABEL_T:
    case SCH_HIER_LABEL_T:
    case SCH_SHEET_PIN_T:
    {
        name = static_cast<SCH_TEXT*>( aItem )->GetText();
        break;
    }

    default:
        break;
    }

    return name;
}


void CONNECTION_SUBGRAPH::Absorb( CONNECTION_SUBGRAPH* aOther )
{
    wxASSERT( m_sheet == aOther->m_sheet );

    for( SCH_ITEM* item : aOther->m_items )
    {
        item->Connection( m_sheet )->SetSubgraphCode( m_code );
        AddItem( item );
    }

    m_bus_neighbors.insert( aOther->m_bus_neighbors.begin(), aOther->m_bus_neighbors.end() );
    m_bus_parents.insert( aOther->m_bus_parents.begin(), aOther->m_bus_parents.end() );

    m_multiple_drivers |= aOther->m_multiple_drivers;

    aOther->m_absorbed = true;
    aOther->m_dirty = false;
    aOther->m_driver = nullptr;
    aOther->m_driver_connection = nullptr;
    aOther->m_absorbed_by = this;
}


void CONNECTION_SUBGRAPH::AddItem( SCH_ITEM* aItem )
{
    m_items.push_back( aItem );

    if( aItem->Connection( m_sheet )->IsDriver() )
        m_drivers.push_back( aItem );

    if( aItem->Type() == SCH_SHEET_PIN_T )
        m_hier_pins.push_back( static_cast<SCH_SHEET_PIN*>( aItem ) );
    else if( aItem->Type() == SCH_HIER_LABEL_T )
        m_hier_ports.push_back( static_cast<SCH_HIERLABEL*>( aItem ) );
}


void CONNECTION_SUBGRAPH::UpdateItemConnections()
{
    if( !m_driver_connection )
        return;

    for( SCH_ITEM* item : m_items )
    {
        SCH_CONNECTION* item_conn = item->Connection( m_sheet );

        if( !item_conn )
            item_conn = item->InitializeConnection( m_sheet );

        if( ( m_driver_connection->IsBus() && item_conn->IsNet() ) ||
            ( m_driver_connection->IsNet() && item_conn->IsBus() ) )
        {
            continue;
        }

        if( item != m_driver )
        {
            item_conn->Clone( *m_driver_connection );
            item_conn->ClearDirty();
        }
    }
}


CONNECTION_SUBGRAPH::PRIORITY CONNECTION_SUBGRAPH::GetDriverPriority( SCH_ITEM* aDriver )
{
    if( !aDriver )
        return PRIORITY::NONE;

    switch( aDriver->Type() )
    {
    case SCH_SHEET_PIN_T:     return PRIORITY::SHEET_PIN;
    case SCH_HIER_LABEL_T:    return PRIORITY::HIER_LABEL;
    case SCH_LABEL_T:         return PRIORITY::LOCAL_LABEL;
    case SCH_GLOBAL_LABEL_T:  return PRIORITY::GLOBAL;
    case SCH_PIN_T:
    {
        auto sch_pin = static_cast<SCH_PIN*>( aDriver );

        if( sch_pin->IsPowerConnection() )
            return PRIORITY::POWER_PIN;
        else
            return PRIORITY::PIN;
    }

    default: return PRIORITY::NONE;
    }
}


bool CONNECTION_GRAPH::m_allowRealTime = true;


void CONNECTION_GRAPH::Reset()
{
    for( auto& subgraph : m_subgraphs )
        delete subgraph;

    m_items.clear();
    m_subgraphs.clear();
    m_driver_subgraphs.clear();
    m_sheet_to_subgraphs_map.clear();
    m_invisible_power_pins.clear();
    m_bus_alias_cache.clear();
    m_net_name_to_code_map.clear();
    m_bus_name_to_code_map.clear();
    m_net_code_to_subgraphs_map.clear();
    m_net_name_to_subgraphs_map.clear();
    m_local_label_cache.clear();
    m_global_label_cache.clear();
    m_last_net_code = 1;
    m_last_bus_code = 1;
    m_last_subgraph_code = 1;
}


void CONNECTION_GRAPH::Recalculate( const SCH_SHEET_LIST& aSheetList, bool aUnconditional )
{
    PROF_COUNTER recalc_time;
    PROF_COUNTER update_items;

    if( aUnconditional )
        Reset();

    for( const SCH_SHEET_PATH& sheet : aSheetList )
    {
        std::vector<SCH_ITEM*> items;

        for( auto item : sheet.LastScreen()->Items() )
        {
            if( item->IsConnectable() && ( aUnconditional || item->IsConnectivityDirty() ) )
                items.push_back( item );
        }

        updateItemConnectivity( sheet, items );

        // UpdateDanglingState() also adds connected items for SCH_TEXT
        sheet.LastScreen()->TestDanglingEnds( &sheet );
    }

    update_items.Stop();
    wxLogTrace( "CONN_PROFILE", "UpdateItemConnectivity() %0.4f ms", update_items.msecs() );

    PROF_COUNTER build_graph;

    buildConnectionGraph();

    build_graph.Stop();
    wxLogTrace( "CONN_PROFILE", "BuildConnectionGraph() %0.4f ms", build_graph.msecs() );

    recalc_time.Stop();
    wxLogTrace( "CONN_PROFILE", "Recalculate time %0.4f ms", recalc_time.msecs() );

#ifndef DEBUG
    // Pressure relief valve for release builds
    const double max_recalc_time_msecs = 250.;

    if( m_allowRealTime && ADVANCED_CFG::GetCfg().m_realTimeConnectivity &&
        recalc_time.msecs() > max_recalc_time_msecs )
    {
        m_allowRealTime = false;
    }
#endif
}


void CONNECTION_GRAPH::updateItemConnectivity( SCH_SHEET_PATH aSheet,
                                               const std::vector<SCH_ITEM*>& aItemList )
{
    std::unordered_map< wxPoint, std::vector<SCH_ITEM*> > connection_map;

    for( SCH_ITEM* item : aItemList )
    {
        std::vector< wxPoint > points;
        item->GetConnectionPoints( points );
        item->ConnectedItems( aSheet ).clear();

        if( item->Type() == SCH_SHEET_T )
        {
            for( SCH_SHEET_PIN* pin : static_cast<SCH_SHEET*>( item )->GetPins() )
            {
                if( !pin->Connection( aSheet ) )
                    pin->InitializeConnection( aSheet );

                pin->ConnectedItems( aSheet ).clear();
                pin->Connection( aSheet )->Reset();

                connection_map[ pin->GetTextPos() ].push_back( pin );
                m_items.insert( pin );
            }
        }
        else if( item->Type() == SCH_COMPONENT_T )
        {
            SCH_COMPONENT* component = static_cast<SCH_COMPONENT*>( item );
            TRANSFORM t = component->GetTransform();

            // TODO(JE) right now this relies on GetSchPins() returning good SCH_PIN pointers
            // that contain good LIB_PIN pointers.  Since these get invalidated whenever the
            // library component is refreshed, the current solution as of ed025972 is to just
            // rebuild the SCH_PIN list when the component is refreshed, and then re-run the
            // connectivity calculations.  This is slow and should be improved before release.
            // See https://gitlab.com/kicad/code/kicad/issues/3784

            for( SCH_PIN* pin : component->GetSchPins( &aSheet ) )
            {
                pin->InitializeConnection( aSheet );

                wxPoint pos = t.TransformCoordinate( pin->GetPosition() ) +
                              component->GetPosition();

                // because calling the first time is not thread-safe
                pin->GetDefaultNetName( aSheet );
                pin->ConnectedItems( aSheet ).clear();

                // Invisible power pins need to be post-processed later

                if( pin->IsPowerConnection() && !pin->IsVisible() )
                    m_invisible_power_pins.emplace_back( std::make_pair( aSheet, pin ) );

                connection_map[ pos ].push_back( pin );
                m_items.insert( pin );
            }
        }
        else
        {
            m_items.insert( item );
            auto conn = item->InitializeConnection( aSheet );

            // Set bus/net property here so that the propagation code uses it
            switch( item->Type() )
            {
            case SCH_LINE_T:
                conn->SetType( item->GetLayer() == LAYER_BUS ? CONNECTION_TYPE::BUS :
                                                               CONNECTION_TYPE::NET );
                break;

            case SCH_BUS_BUS_ENTRY_T:
                conn->SetType( CONNECTION_TYPE::BUS );
                // clean previous (old) links:
                static_cast<SCH_BUS_BUS_ENTRY*>( item )->m_connected_bus_items[0] = nullptr;
                static_cast<SCH_BUS_BUS_ENTRY*>( item )->m_connected_bus_items[1] = nullptr;
                break;

            case SCH_PIN_T:
                conn->SetType( CONNECTION_TYPE::NET );
                break;

            case SCH_BUS_WIRE_ENTRY_T:
                conn->SetType( CONNECTION_TYPE::NET );
                // clean previous (old) link:
                static_cast<SCH_BUS_WIRE_ENTRY*>( item )->m_connected_bus_item = nullptr;
                break;

            default:
                break;
            }

            for( const wxPoint& point : points )
                connection_map[ point ].push_back( item );
        }

        item->SetConnectivityDirty( false );
    }

    for( const auto& it : connection_map )
    {
        auto connection_vec = it.second;

        for( auto primary_it = connection_vec.begin(); primary_it != connection_vec.end(); primary_it++ )
        {
            SCH_ITEM* connected_item = *primary_it;

            // Bus entries are special: they can have connection points in the
            // middle of a wire segment, because the junction algo doesn't split
            // the segment in two where you place a bus entry.  This means that
            // bus entries that don't land on the end of a line segment need to
            // have "virtual" connection points to the segments they graphically
            // touch.
            if( connected_item->Type() == SCH_BUS_WIRE_ENTRY_T )
            {
                // If this location only has the connection point of the bus
                // entry itself, this means that either the bus entry is not
                // connected to anything graphically, or that it is connected to
                // a segment at some point other than at one of the endpoints.
                if( connection_vec.size() == 1 )
                {
                    SCH_SCREEN* screen = aSheet.LastScreen();
                    SCH_LINE*   bus = screen->GetBus( it.first );

                    if( bus )
                    {
                        auto bus_entry = static_cast<SCH_BUS_WIRE_ENTRY*>( connected_item );
                        bus_entry->m_connected_bus_item = bus;
                    }
                }
            }

            // Bus-to-bus entries are treated just like bus wires
            if( connected_item->Type() == SCH_BUS_BUS_ENTRY_T )
            {
                if( connection_vec.size() < 2 )
                {
                    SCH_SCREEN* screen = aSheet.LastScreen();
                    SCH_LINE*   bus = screen->GetBus( it.first );

                    if( bus )
                    {
                        auto bus_entry = static_cast<SCH_BUS_BUS_ENTRY*>( connected_item );

                        if( it.first == bus_entry->GetPosition() )
                            bus_entry->m_connected_bus_items[0] = bus;
                        else
                            bus_entry->m_connected_bus_items[1] = bus;

                        bus_entry->ConnectedItems( aSheet ).insert( bus );
                        bus->ConnectedItems( aSheet ).insert( bus_entry );
                    }
                }
            }

            for( auto test_it = primary_it + 1; test_it != connection_vec.end(); test_it++ )
            {
                auto test_item = *test_it;

                if( connected_item != test_item &&
                    connected_item->ConnectionPropagatesTo( test_item ) &&
                    test_item->ConnectionPropagatesTo( connected_item ) )
                {
                    connected_item->ConnectedItems( aSheet ).insert( test_item );
                    test_item->ConnectedItems( aSheet ).insert( connected_item );
                }

                // Set up the link between the bus entry net and the bus
                if( connected_item->Type() == SCH_BUS_WIRE_ENTRY_T )
                {
                    if( test_item->Connection( aSheet )->IsBus() )
                    {
                        auto bus_entry = static_cast<SCH_BUS_WIRE_ENTRY*>( connected_item );
                        bus_entry->m_connected_bus_item = test_item;
                    }
                }
            }

            // If we got this far and did not find a connected bus item for a bus entry,
            // we should do a manual scan in case there is a bus item on this connection
            // point but we didn't pick it up earlier because there is *also* a net item here.
            if( connected_item->Type() == SCH_BUS_WIRE_ENTRY_T )
            {
                auto bus_entry = static_cast<SCH_BUS_WIRE_ENTRY*>( connected_item );

                if( !bus_entry->m_connected_bus_item )
                {
                    auto screen = aSheet.LastScreen();
                    auto bus = screen->GetBus( it.first );

                    if( bus )
                        bus_entry->m_connected_bus_item = bus;
                }
            }
        }
    }
}


// TODO(JE) This won't give the same subgraph IDs (and eventually net/graph codes)
// to the same subgraph necessarily if it runs over and over again on the same
// sheet.  We need:
//
//  a) a cache of net/bus codes, like used before
//  b) to persist the CONNECTION_GRAPH globally so the cache is persistent,
//  c) some way of trying to avoid changing net names.  so we should keep track
//     of the previous driver of a net, and if it comes down to choosing between
//     equally-prioritized drivers, choose the one that already exists as a driver
//     on some portion of the items.


void CONNECTION_GRAPH::buildConnectionGraph()
{
    // Recache all bus aliases for later use

    SCH_SHEET_LIST all_sheets( g_RootSheet );

    for( unsigned i = 0; i < all_sheets.size(); i++ )
    {
        for( const auto& alias : all_sheets[i].LastScreen()->GetBusAliases() )
            m_bus_alias_cache[ alias->GetName() ] = alias;
    }

    // Build subgraphs from items (on a per-sheet basis)

    for( SCH_ITEM* item : m_items )
    {
        for( const auto& it : item->m_connection_map )
        {
            const auto sheet = it.first;
            auto connection = it.second;

            if( connection->SubgraphCode() == 0 )
            {
                auto subgraph = new CONNECTION_SUBGRAPH( m_frame );

                subgraph->m_code = m_last_subgraph_code++;
                subgraph->m_sheet = sheet;

                subgraph->AddItem( item );

                connection->SetSubgraphCode( subgraph->m_code );

                std::list<SCH_ITEM*> members;

                auto get_items = [ &sheet ] ( SCH_ITEM* aItem ) -> bool
                    {
                      auto* conn = aItem->Connection( sheet );

                      if( !conn )
                          conn = aItem->InitializeConnection( sheet );

                      return ( conn->SubgraphCode() == 0 );
                    };

                std::copy_if( item->ConnectedItems( sheet ).begin(),
                              item->ConnectedItems( sheet ).end(),
                              std::back_inserter( members ), get_items );

                for( auto connected_item : members )
                {
                    if( connected_item->Type() == SCH_NO_CONNECT_T )
                        subgraph->m_no_connect = connected_item;

                    auto connected_conn = connected_item->Connection( sheet );

                    wxASSERT( connected_conn );

                    if( connected_conn->SubgraphCode() == 0 )
                    {
                        connected_conn->SetSubgraphCode( subgraph->m_code );
                        subgraph->AddItem( connected_item );

                        std::copy_if( connected_item->ConnectedItems( sheet ).begin(),
                                      connected_item->ConnectedItems( sheet ).end(),
                                      std::back_inserter( members ), get_items );
                    }
                }

                subgraph->m_dirty = true;
                m_subgraphs.push_back( subgraph );
            }
        }
    }

    /**
     * TODO(JE)
     *
     * It would be good if net codes were preserved as much as possible when
     * generating netlists, so that unnamed nets don't keep shifting around when
     * you regenerate.
     *
     * Right now, we are clearing out the old connections up in
     * UpdateItemConnectivity(), but that is useful information, so maybe we
     * need to just set the dirty flag or something.
     *
     * That way, ResolveDrivers() can check what the driver of the subgraph was
     * previously, and if it is in the situation of choosing between equal
     * candidates for an auto-generated net name, pick the previous one.
     *
     * N.B. the old algorithm solves this by sorting the possible net names
     * alphabetically, so as long as the same refdes components are involved,
     * the net will be the same.
     */

    // Resolve drivers for subgraphs and propagate connectivity info

    // We don't want to spin up a new thread for fewer than 8 nets (overhead costs)
    size_t parallelThreadCount = std::min<size_t>( std::thread::hardware_concurrency(),
            ( m_subgraphs.size() + 3 ) / 4 );

    std::atomic<size_t> nextSubgraph( 0 );
    std::vector<std::future<size_t>> returns( parallelThreadCount );
    std::vector<CONNECTION_SUBGRAPH*> dirty_graphs;

    std::copy_if( m_subgraphs.begin(), m_subgraphs.end(), std::back_inserter( dirty_graphs ),
                  [&] ( const CONNECTION_SUBGRAPH* candidate )
                  {
                      return candidate->m_dirty;
                  } );

    auto update_lambda = [&nextSubgraph, &dirty_graphs]() -> size_t
    {
        for( size_t subgraphId = nextSubgraph++; subgraphId < dirty_graphs.size(); subgraphId = nextSubgraph++ )
        {
            auto subgraph = dirty_graphs[subgraphId];

            if( !subgraph->m_dirty )
                continue;

            // Special processing for some items
            for( auto item : subgraph->m_items )
            {
                switch( item->Type() )
                {
                case SCH_NO_CONNECT_T:
                    subgraph->m_no_connect = item;
                    break;

                case SCH_BUS_WIRE_ENTRY_T:
                    subgraph->m_bus_entry = item;
                    break;

                case SCH_PIN_T:
                {
                    auto pin = static_cast<SCH_PIN*>( item );

                    if( pin->GetType() == ELECTRICAL_PINTYPE::PT_NC )
                        subgraph->m_no_connect = item;

                    break;
                }

                default:
                    break;
                }
            }

            if( !subgraph->ResolveDrivers() )
            {
                subgraph->m_dirty = false;
            }
            else
            {
                // Now the subgraph has only one driver
                SCH_ITEM* driver = subgraph->m_driver;
                SCH_SHEET_PATH sheet = subgraph->m_sheet;
                SCH_CONNECTION* connection = driver->Connection( sheet );

                // TODO(JE) This should live in SCH_CONNECTION probably
                switch( driver->Type() )
                {
                case SCH_LABEL_T:
                case SCH_GLOBAL_LABEL_T:
                case SCH_HIER_LABEL_T:
                {
                    auto text = static_cast<SCH_TEXT*>( driver );
                    connection->ConfigureFromLabel( text->GetText() );
                    break;
                }
                case SCH_SHEET_PIN_T:
                {
                    auto pin = static_cast<SCH_SHEET_PIN*>( driver );
                    connection->ConfigureFromLabel( pin->GetText() );
                    break;
                }
                case SCH_PIN_T:
                {
                    auto pin = static_cast<SCH_PIN*>( driver );
                    // NOTE(JE) GetDefaultNetName is not thread-safe.
                    connection->ConfigureFromLabel( pin->GetDefaultNetName( sheet ) );

                    break;
                }
                default:
                    wxLogTrace( "CONN", "Driver type unsupported: %s",
                            driver->GetSelectMenuText( EDA_UNITS::MILLIMETRES ) );
                    break;
                }

                connection->SetDriver( driver );
                connection->ClearDirty();

                subgraph->m_dirty = false;
            }
        }

        return 1;
    };

    if( parallelThreadCount == 1 )
        update_lambda();
    else
    {
        for( size_t ii = 0; ii < parallelThreadCount; ++ii )
            returns[ii] = std::async( std::launch::async, update_lambda );

        // Finalize the threads
        for( size_t ii = 0; ii < parallelThreadCount; ++ii )
            returns[ii].wait();
    }

    // Now discard any non-driven subgraphs from further consideration

    std::copy_if( m_subgraphs.begin(), m_subgraphs.end(), std::back_inserter( m_driver_subgraphs ),
                  [&] ( const CONNECTION_SUBGRAPH* candidate ) -> bool
                  {
                      return candidate->m_driver;
                  } );

    // Check for subgraphs with the same net name but only weak drivers.
    // For example, two wires that are both connected to hierarchical
    // sheet pins that happen to have the same name, but are not the same.

    for( auto&& subgraph : m_driver_subgraphs )
    {
        wxString full_name = subgraph->m_driver_connection->Name();
        wxString name = subgraph->m_driver_connection->Name( true );
        m_net_name_to_subgraphs_map[full_name].emplace_back( subgraph );

        subgraph->m_dirty = true;

        if( subgraph->m_strong_driver )
        {
            SCH_ITEM* driver = subgraph->m_driver;
            SCH_SHEET_PATH sheet = subgraph->m_sheet;

            switch( driver->Type() )
            {
            case SCH_LABEL_T:
            case SCH_HIER_LABEL_T:
            {
                m_local_label_cache[std::make_pair( sheet, name )].push_back( subgraph );
                break;
            }
            case SCH_GLOBAL_LABEL_T:
            {
                m_global_label_cache[name].push_back( subgraph );
                break;
            }
            case SCH_PIN_T:
            {
                auto pin = static_cast<SCH_PIN*>( driver );
                wxASSERT( pin->IsPowerConnection() );
                m_global_label_cache[name].push_back( subgraph );
                break;
            }
            default:
                wxLogTrace( "CONN", "Unexpected strong driver %s",
                        driver->GetSelectMenuText( EDA_UNITS::MILLIMETRES ) );
                break;
            }
        }
    }

    // Generate subgraphs for invisible power pins.  These will be merged with other subgraphs
    // on the same sheet in the next loop.

    std::unordered_map<int, CONNECTION_SUBGRAPH*> invisible_pin_subgraphs;

    for( const auto& it : m_invisible_power_pins )
    {
        SCH_SHEET_PATH sheet = it.first;
        SCH_PIN*       pin   = it.second;

        if( !pin->ConnectedItems( sheet ).empty() && !pin->GetLibPin()->GetParent()->IsPower() )
        {
            // ERC will warn about this: user has wired up an invisible pin
            continue;
        }

        SCH_CONNECTION* connection = pin->Connection( sheet );

        if( !connection )
            connection = pin->InitializeConnection( sheet );

        // If this pin already has a subgraph, don't need to process
        if( connection->SubgraphCode() > 0 )
            continue;

        connection->SetName( pin->GetName() );

        int code = assignNewNetCode( *connection );

        connection->SetNetCode( code );

        CONNECTION_SUBGRAPH* subgraph;

        if( invisible_pin_subgraphs.count( code ) )
        {
            subgraph = invisible_pin_subgraphs.at( code );
            subgraph->AddItem( pin );
        }
        else
        {
            subgraph = new CONNECTION_SUBGRAPH( m_frame );

            subgraph->m_code = m_last_subgraph_code++;
            subgraph->m_sheet = sheet;

            subgraph->AddItem( pin );
            subgraph->ResolveDrivers();

            auto key = std::make_pair( subgraph->GetNetName(), code );
            m_net_code_to_subgraphs_map[ key ].push_back( subgraph );
            m_subgraphs.push_back( subgraph );
            m_driver_subgraphs.push_back( subgraph );

            invisible_pin_subgraphs[code] = subgraph;
        }

        connection->SetSubgraphCode( subgraph->m_code );
    }

    for( auto it : invisible_pin_subgraphs )
        it.second->UpdateItemConnections();

    // Here we do all the local (sheet) processing of each subgraph, including assigning net
    // codes, merging subgraphs together that use label connections, etc.

    // Cache remaining valid subgraphs by sheet path
    for( auto subgraph : m_driver_subgraphs )
        m_sheet_to_subgraphs_map[ subgraph->m_sheet ].emplace_back( subgraph );

    std::unordered_set<CONNECTION_SUBGRAPH*> invalidated_subgraphs;

    for( CONNECTION_SUBGRAPH* subgraph : m_driver_subgraphs)
    {
        if( subgraph->m_absorbed )
            continue;

        SCH_CONNECTION* connection = subgraph->m_driver_connection;
        SCH_SHEET_PATH sheet = subgraph->m_sheet;
        wxString name = connection->Name();

        // Test subgraphs with weak drivers for net name conflicts and fix them
        unsigned suffix = 1;

        auto create_new_name = [&] ( SCH_CONNECTION* aConn, wxString aName ) -> wxString
                               {
                                   wxString new_name = wxString::Format( "%s_%u", aName, suffix );
                                   aConn->SetSuffix( wxString::Format( "_%u", suffix ) );
                                   suffix++;
                                   return new_name;
                               };

        if( !subgraph->m_strong_driver )
        {
            auto& vec = m_net_name_to_subgraphs_map.at( name );

            if( vec.size() > 1 )
            {
                wxString new_name = create_new_name( connection, name );

                while( m_net_name_to_subgraphs_map.count( new_name ) )
                    new_name = create_new_name( connection, name );

                wxLogTrace( "CONN", "%ld (%s) is weakly driven and not unique. Changing to %s.",
                            subgraph->m_code, name, new_name );

                vec.erase( std::remove( vec.begin(), vec.end(), subgraph ), vec.end() );

                m_net_name_to_subgraphs_map[new_name].emplace_back( subgraph );

                name = new_name;

                subgraph->UpdateItemConnections();
            }
            else
            {
                // If there is no conflict, promote sheet pins to be strong drivers so that they
                // will be considered below for propagation/merging.

                if( subgraph->m_driver->Type() == SCH_SHEET_PIN_T )
                {
                    wxLogTrace( "CONN", "%ld (%s) weakly driven by unique sheet pin %s, promoting",
                            subgraph->m_code, name,
                            subgraph->m_driver->GetSelectMenuText( EDA_UNITS::MILLIMETRES ) );

                    subgraph->m_strong_driver = true;
                }
            }
        }

        // Assign net codes

        if( connection->IsBus() )
        {
            int code = -1;

            if( m_bus_name_to_code_map.count( name ) )
            {
                code = m_bus_name_to_code_map.at( name );
            }
            else
            {
                code = m_last_bus_code++;
                m_bus_name_to_code_map[ name ] = code;
            }

            connection->SetBusCode( code );
            assignNetCodesToBus( connection );
        }
        else
        {
            assignNewNetCode( *connection );
        }

        subgraph->UpdateItemConnections();

        // Reset the flag for the next loop below
        subgraph->m_dirty = true;

        // Next, we merge together subgraphs that have label connections, and create
        // neighbor links for subgraphs that are part of a bus on the same sheet.
        // For merging, we consider each possible strong driver.

        // If this subgraph doesn't have a strong driver, let's skip it, since there is no
        // way it will be merged with anything.

        if( !subgraph->m_strong_driver )
            continue;

        // candidate_subgraphs will contain each valid, non-bus subgraph on the same sheet
        // as the subgraph we are considering that has a strong driver.
        // Weakly driven subgraphs are not considered since they will never be absorbed or
        // form neighbor links.

        std::vector<CONNECTION_SUBGRAPH*> candidate_subgraphs;
        std::copy_if( m_sheet_to_subgraphs_map[ subgraph->m_sheet ].begin(),
                      m_sheet_to_subgraphs_map[ subgraph->m_sheet ].end(),
                      std::back_inserter( candidate_subgraphs ),
                      [&] ( const CONNECTION_SUBGRAPH* candidate )
                      {
                          return ( !candidate->m_absorbed &&
                                   candidate->m_strong_driver &&
                                   candidate != subgraph );
                      } );

        // This is a list of connections on the current subgraph to compare to the
        // drivers of each candidate subgraph.  If the current subgraph is a bus,
        // we should consider each bus member.
        std::vector< std::shared_ptr<SCH_CONNECTION> > connections_to_check;

        // Also check the main driving connection
        connections_to_check.push_back( std::make_shared<SCH_CONNECTION>( *connection ) );

        auto add_connections_to_check = [&] ( CONNECTION_SUBGRAPH* aSubgraph ) {
            for( SCH_ITEM* possible_driver : aSubgraph->m_items )
            {
                if( possible_driver == aSubgraph->m_driver )
                    continue;

                auto c = getDefaultConnection( possible_driver, aSubgraph->m_sheet );

                if( c )
                {
                    if( c->Type() != aSubgraph->m_driver_connection->Type() )
                        continue;

                    if( c->Name( true ) == aSubgraph->m_driver_connection->Name( true ) )
                        continue;

                    connections_to_check.push_back( c );
                    wxLogTrace( "CONN", "%lu (%s): Adding secondary driver %s", aSubgraph->m_code,
                            aSubgraph->m_driver_connection->Name( true ), c->Name( true ) );
                }
            }
        };

        // Now add other strong drivers
        // The actual connection attached to these items will have been overwritten
        // by the chosen driver of the subgraph, so we need to create a dummy connection
        add_connections_to_check( subgraph );

        for( unsigned i = 0; i < connections_to_check.size(); i++ )
        {
            auto member = connections_to_check[i];

            if( member->IsBus() )
            {
                connections_to_check.insert( connections_to_check.end(),
                                             member->Members().begin(),
                                             member->Members().end() );
            }

            wxString test_name = member->Name( true );

            for( auto candidate : candidate_subgraphs )
            {
                if( candidate->m_absorbed )
                    continue;

                bool match = false;

                if( candidate->m_driver_connection->Name( true ) == test_name )
                {
                    match = true;
                }
                else
                {
                    if( !candidate->m_multiple_drivers )
                        continue;

                    for( SCH_ITEM *driver : candidate->m_drivers )
                    {
                        if( driver == candidate->m_driver )
                            continue;

                        // Sheet pins are not candidates for merging
                        if( driver->Type() == SCH_SHEET_PIN_T )
                            continue;

                        if( driver->Type() == SCH_PIN_T )
                        {
                            auto pin = static_cast<SCH_PIN*>( driver );

                            if( pin->IsPowerConnection() && pin->GetName() == test_name )
                            {
                                match = true;
                                break;
                            }
                        }
                        else
                        {
                            wxASSERT( driver->Type() == SCH_LABEL_T ||
                                      driver->Type() == SCH_GLOBAL_LABEL_T ||
                                      driver->Type() == SCH_HIER_LABEL_T );

                            auto text = static_cast<SCH_TEXT*>( driver );

                            if( text->GetShownText() == test_name )
                            {
                                match = true;
                                break;
                            }
                        }
                    }
                }

                if( match )
                {
                    if( connection->IsBus() && candidate->m_driver_connection->IsNet() )
                    {
                         wxLogTrace( "CONN", "%lu (%s) has bus child %lu (%s)", subgraph->m_code,
                                     connection->Name(), candidate->m_code, member->Name() );

                        subgraph->m_bus_neighbors[member].insert( candidate );
                        candidate->m_bus_parents[member].insert( subgraph );
                    }
                    else
                    {
                        wxLogTrace( "CONN", "%lu (%s) absorbs neighbor %lu (%s)",
                                    subgraph->m_code, connection->Name(),
                                    candidate->m_code, candidate->m_driver_connection->Name() );

                        // Candidate may have other non-chosen drivers we need to follow
                        add_connections_to_check( candidate );

                        subgraph->Absorb( candidate );
                        invalidated_subgraphs.insert( subgraph );
                    }
                }
            }
        }
    }

    // Update any subgraph that was invalidated above
    for( auto subgraph : invalidated_subgraphs )
    {
        if( subgraph->m_absorbed )
            continue;

        subgraph->ResolveDrivers();

        if( subgraph->m_driver_connection->IsBus() )
            assignNetCodesToBus( subgraph->m_driver_connection );
        else
            assignNewNetCode( *subgraph->m_driver_connection );

        subgraph->UpdateItemConnections();

        wxLogTrace( "CONN", "Re-resolving drivers for %lu (%s)", subgraph->m_code,
                    subgraph->m_driver_connection->Name() );
    }

    // Absorbed subgraphs should no longer be considered
    m_driver_subgraphs.erase( std::remove_if( m_driver_subgraphs.begin(), m_driver_subgraphs.end(),
                              [&] ( const CONNECTION_SUBGRAPH* candidate ) -> bool
                              {
                                  return candidate->m_absorbed;
                              } ),
                              m_driver_subgraphs.end() );

    // Store global subgraphs for later reference
    std::vector<CONNECTION_SUBGRAPH*> global_subgraphs;
    std::copy_if( m_driver_subgraphs.begin(), m_driver_subgraphs.end(),
                  std::back_inserter( global_subgraphs ),
                  [&] ( const CONNECTION_SUBGRAPH* candidate ) -> bool
                  {
                      return !candidate->m_local_driver;
                  } );

    // Recache remaining valid subgraphs by sheet path
    m_sheet_to_subgraphs_map.clear();
    for( auto subgraph : m_driver_subgraphs )
        m_sheet_to_subgraphs_map[ subgraph->m_sheet ].emplace_back( subgraph );

    // Next time through the subgraphs, we do some post-processing to handle things like
    // connecting bus members to their neighboring subgraphs, and then propagate connections
    // through the hierarchy

    for( auto subgraph : m_driver_subgraphs )
    {
        if( !subgraph->m_dirty )
            continue;

        // For subgraphs that are driven by a global (power port or label) and have more
        // than one global driver, we need to seek out other subgraphs driven by the
        // same name as the non-chosen driver and update them to match the chosen one.

        if( !subgraph->m_local_driver && subgraph->m_multiple_drivers )
        {
            for( SCH_ITEM* driver : subgraph->m_drivers )
            {
                if( driver == subgraph->m_driver )
                    continue;

                wxString secondary_name = subgraph->GetNameForDriver( driver );

                if( secondary_name == subgraph->m_driver_connection->Name() )
                    continue;

                bool secondary_is_global = CONNECTION_SUBGRAPH::GetDriverPriority( driver )
                                           >= CONNECTION_SUBGRAPH::PRIORITY::POWER_PIN;

                for( CONNECTION_SUBGRAPH* candidate : global_subgraphs )
                {
                    if( candidate == subgraph )
                        continue;

                    if( !secondary_is_global && candidate->m_sheet != subgraph->m_sheet )
                        continue;

                    SCH_CONNECTION* conn = candidate->m_driver_connection;

                    if( conn->Name() == secondary_name )
                    {
                        wxLogTrace( "CONN", "Global %lu (%s) promoted to %s", candidate->m_code,
                                    conn->Name(), subgraph->m_driver_connection->Name() );

                        conn->Clone( *subgraph->m_driver_connection );
                        candidate->UpdateItemConnections();

                        candidate->m_dirty = false;
                    }
                }
            }
        }

        // This call will handle descending the hierarchy and updating child subgraphs
        propagateToNeighbors( subgraph );
    }

    // Handle buses that have been linked together somewhere by member (net) connections.
    // This feels a bit hacky, perhaps this algorithm should be revisited in the future.

    // For net subgraphs that have more than one bus parent, we need to ensure that those
    // buses are linked together in the final netlist.  The final name of each bus might not
    // match the local name that was used to establish the parent-child relationship, because
    // the bus may have been renamed by a hierarchical connection.  So, for each of these cases,
    // we need to identify the appropriate bus members to link together (and their final names),
    // and then update all instances of the old name in the hierarchy.

    for( CONNECTION_SUBGRAPH* subgraph : m_driver_subgraphs )
    {
        if( subgraph->m_bus_parents.size() < 2 )
            continue;

        SCH_CONNECTION* conn = subgraph->m_driver_connection;

        wxLogTrace( "CONN", "%lu (%s) has multiple bus parents", subgraph->m_code, conn->Name() );

        wxASSERT( conn->IsNet() );

        for( const auto& it : subgraph->m_bus_parents )
        {
            SCH_CONNECTION* link_member = it.first.get();

            for( CONNECTION_SUBGRAPH* parent : it.second )
            {
                while( parent->m_absorbed )
                    parent = parent->m_absorbed_by;

                SCH_CONNECTION* match = matchBusMember( parent->m_driver_connection, link_member );

                if( !match )
                {
                    wxLogTrace( "CONN", "Warning: could not match %s inside %lu (%s)", conn->Name(),
                            parent->m_code, parent->m_driver_connection->Name() );
                    continue;
                }

                if( conn->Name() != match->Name() )
                {
                    wxString old_name = match->Name();

                    wxLogTrace( "CONN", "Updating %lu (%s) member %s to %s", parent->m_code,
                            parent->m_driver_connection->Name(), old_name, conn->Name() );

                    match->Clone( *conn );

                    if( !m_net_name_to_subgraphs_map.count( old_name ) )
                        continue;

                    for( CONNECTION_SUBGRAPH* old_sg : m_net_name_to_subgraphs_map.at( old_name ) )
                    {
                        while( old_sg->m_absorbed )
                            old_sg = old_sg->m_absorbed_by;

                        old_sg->m_driver_connection->Clone( *conn );
                        old_sg->UpdateItemConnections();
                    }
                }
            }
        }
    }

    m_net_code_to_subgraphs_map.clear();

    for( CONNECTION_SUBGRAPH* subgraph : m_driver_subgraphs )
    {
        // Every driven subgraph should have been marked by now
        if( subgraph->m_dirty )
        {
            // TODO(JE) this should be caught by hierarchical sheet port/pin ERC, check this
            // Reset to false so no complaints come up later
            subgraph->m_dirty = false;
        }

        if( subgraph->m_driver_connection->IsBus() )
            continue;

        auto key = std::make_pair( subgraph->GetNetName(),
                                   subgraph->m_driver_connection->NetCode() );
        m_net_code_to_subgraphs_map[ key ].push_back( subgraph );
    }

    // Clean up and deallocate stale subgraphs
    m_subgraphs.erase( std::remove_if( m_subgraphs.begin(), m_subgraphs.end(),
            [&]( const CONNECTION_SUBGRAPH* sg )
            {
                if( sg->m_absorbed )
                {
                    delete sg;
                    return true;
                }
                else
                {
                    return false;
                }
            } ),
            m_subgraphs.end() );
}


int CONNECTION_GRAPH::assignNewNetCode( SCH_CONNECTION& aConnection )
{
    int code;

    if( m_net_name_to_code_map.count( aConnection.Name() ) )
    {
        code = m_net_name_to_code_map.at( aConnection.Name() );
    }
    else
    {
        code = m_last_net_code++;
        m_net_name_to_code_map[ aConnection.Name() ] = code;
    }

    aConnection.SetNetCode( code );

    return code;
}


void CONNECTION_GRAPH::assignNetCodesToBus( SCH_CONNECTION* aConnection )
{
    auto connections_to_check( aConnection->Members() );

    for( unsigned i = 0; i < connections_to_check.size(); i++ )
    {
        auto member = connections_to_check[i];

        if( member->IsBus() )
        {
            connections_to_check.insert( connections_to_check.end(),
                                         member->Members().begin(),
                                         member->Members().end() );
            continue;
        }

        assignNewNetCode( *member );
    }
}


void CONNECTION_GRAPH::propagateToNeighbors( CONNECTION_SUBGRAPH* aSubgraph )
{
    SCH_CONNECTION* conn = aSubgraph->m_driver_connection;
    std::vector<CONNECTION_SUBGRAPH*> search_list;
    std::unordered_set<CONNECTION_SUBGRAPH*> visited;
    std::vector<SCH_CONNECTION*> stale_bus_members;

    auto visit = [&] ( CONNECTION_SUBGRAPH* aParent ) {
        for( SCH_SHEET_PIN* pin : aParent->m_hier_pins )
        {
            SCH_SHEET_PATH path = aParent->m_sheet;
            path.push_back( pin->GetParent() );

            if( !m_sheet_to_subgraphs_map.count( path ) )
                continue;

            for( auto candidate : m_sheet_to_subgraphs_map.at( path ) )
            {
                if( !candidate->m_strong_driver ||
                    candidate->m_hier_ports.empty() ||
                    visited.count( candidate ) )
                    continue;

                for( SCH_HIERLABEL* label : candidate->m_hier_ports )
                {
                    if( label->GetShownText() == pin->GetShownText() )
                    {
                        wxLogTrace( "CONN", "%lu: found child %lu (%s)", aParent->m_code,
                                    candidate->m_code, candidate->m_driver_connection->Name() );

                        candidate->m_hier_parent = aParent;

                        search_list.push_back( candidate );
                        break;
                    }
                }
            }
        }

        for( SCH_HIERLABEL* label : aParent->m_hier_ports )
        {
            SCH_SHEET_PATH path = aParent->m_sheet;
            path.pop_back();

            if( !m_sheet_to_subgraphs_map.count( path ) )
                continue;

            for( auto candidate : m_sheet_to_subgraphs_map.at( path ) )
            {
                if( candidate->m_hier_pins.empty() ||
                    visited.count( candidate ) ||
                    ( candidate->m_driver_connection->Type() !=
                      aParent->m_driver_connection->Type() ) )
                    continue;

                for( SCH_SHEET_PIN* pin : candidate->m_hier_pins )
                {
                    SCH_SHEET_PATH pin_path = path;
                    pin_path.push_back( pin->GetParent() );

                    if( pin_path != aParent->m_sheet )
                        continue;

                    if( label->GetShownText() == pin->GetShownText() )
                    {
                        wxLogTrace( "CONN", "%lu: found additional parent %lu (%s)",
                                    aParent->m_code, candidate->m_code,
                                    candidate->m_driver_connection->Name() );

                        search_list.push_back( candidate );
                        break;
                    }
                }
            }
      }
    };

    auto propagate_bus_neighbors = [&]( CONNECTION_SUBGRAPH* aParentGraph ) {
        for( const auto& kv : aParentGraph->m_bus_neighbors )
        {
            for( CONNECTION_SUBGRAPH* neighbor : kv.second )
            {
                // May have been absorbed but won't have been deleted
                while( neighbor->m_absorbed )
                    neighbor = neighbor->m_absorbed_by;

                SCH_CONNECTION* parent = aParentGraph->m_driver_connection;

                // Now member may be out of date, since we just cloned the
                // connection from higher up in the hierarchy.  We need to
                // figure out what the actual new connection is.
                SCH_CONNECTION* member = matchBusMember( parent, kv.first.get() );

                if( !member )
                {
                    // Try harder: we might match on a secondary driver
                    for( CONNECTION_SUBGRAPH* sg : kv.second )
                    {
                        if( sg->m_multiple_drivers )
                        {
                            SCH_SHEET_PATH sheet = sg->m_sheet;

                            for( SCH_ITEM* driver : sg->m_drivers )
                            {
                                auto c = getDefaultConnection( driver, sheet );
                                member = matchBusMember( parent, c.get() );

                                if( member )
                                    break;
                            }
                        }

                        if( member )
                            break;
                    }
                }

                // This is bad, probably an ERC error
                if( !member )
                {
                    wxLogTrace( "CONN", "Could not match bus member %s in %s",
                                kv.first->Name(), parent->Name() );
                    continue;
                }

                auto neighbor_conn = neighbor->m_driver_connection;
                auto neighbor_name = neighbor_conn->Name();

                // Matching name: no update needed
                if( neighbor_name == member->Name() )
                    continue;

                // Safety check against infinite recursion
                wxASSERT( neighbor_conn->IsNet() );

                wxLogTrace( "CONN", "%lu (%s) connected to bus member %s (local %s)",
                        neighbor->m_code, neighbor_name, member->Name(), member->LocalName() );

                // Take whichever name is higher priority
                if( CONNECTION_SUBGRAPH::GetDriverPriority( neighbor->m_driver )
                    >= CONNECTION_SUBGRAPH::PRIORITY::POWER_PIN )
                {
                    member->Clone( *neighbor_conn );
                    stale_bus_members.push_back( member );
                }
                else
                {
                    neighbor_conn->Clone( *member );
                    neighbor->UpdateItemConnections();

                    recacheSubgraphName( neighbor, neighbor_name );

                    // Recurse onto this neighbor in case it needs to re-propagate
                    neighbor->m_dirty = true;
                    propagateToNeighbors( neighbor );
                }
            }
        }
    };

    // If we are a bus, we must propagate to local neighbors and then the hierarchy
    if( conn->IsBus() )
        propagate_bus_neighbors( aSubgraph );

    // If we don't have any hier pins (i.e. no children), nothing to do
    if( aSubgraph->m_hier_pins.empty() )
    {
        // If we also don't have any parents, we'll never be visited again
        if( aSubgraph->m_hier_ports.empty() )
            aSubgraph->m_dirty = false;

        return;
    }

    // If we do have hier ports, skip this subgraph as it will be visited by a parent
    // TODO(JE) this will leave the subgraph dirty if there is no matching parent subgraph,
    // which should be flagged as an ERC error
    if( !aSubgraph->m_hier_ports.empty() )
        return;

    visited.insert( aSubgraph );

    wxLogTrace( "CONN", "Propagating %lu (%s) to subsheets",
                aSubgraph->m_code, aSubgraph->m_driver_connection->Name() );

    visit( aSubgraph );

    for( unsigned i = 0; i < search_list.size(); i++ )
    {
        auto child = search_list[i];

        visited.insert( child );

        visit( child );

        child->m_dirty = false;
    }

    // Now, find the best driver for this chain of subgraphs
    CONNECTION_SUBGRAPH* driver = aSubgraph;
    CONNECTION_SUBGRAPH::PRIORITY highest =
            CONNECTION_SUBGRAPH::GetDriverPriority( aSubgraph->m_driver );

    // Check if a subsheet has a higher-priority connection to the same net
    if( highest < CONNECTION_SUBGRAPH::PRIORITY::POWER_PIN )
    {
        for( CONNECTION_SUBGRAPH* subgraph : visited )
        {
            CONNECTION_SUBGRAPH::PRIORITY priority =
                    CONNECTION_SUBGRAPH::GetDriverPriority( subgraph->m_driver );

            // Upgrade driver to be this subgraph if this subgraph has a power pin or global
            // Also upgrade if we found something with a shorter sheet path (higher in hierarchy)
            // but with an equivalent priority

            if( ( priority >= CONNECTION_SUBGRAPH::PRIORITY::POWER_PIN ) ||
                ( priority >= highest && subgraph->m_sheet.size() < aSubgraph->m_sheet.size() ) )
                driver = subgraph;
        }
    }

    if( driver != aSubgraph )
    {
        wxLogTrace( "CONN", "%lu (%s) overridden by new driver %lu (%s)",
                    aSubgraph->m_code, aSubgraph->m_driver_connection->Name(),
                    driver->m_code, driver->m_driver_connection->Name() );
    }

    conn = driver->m_driver_connection;

    for( CONNECTION_SUBGRAPH* subgraph : visited )
    {
        wxString old_name = subgraph->m_driver_connection->Name();

        subgraph->m_driver_connection->Clone( *conn );
        subgraph->UpdateItemConnections();

        if( old_name != conn->Name() )
            recacheSubgraphName( subgraph, old_name );

        if( conn->IsBus() )
            propagate_bus_neighbors( subgraph );
    }

    // Somewhere along the way, a bus member may have been upgraded to a global or power label.
    // Because this can happen anywhere, we need a second pass to update all instances of that bus
    // member to have the correct connection info
    if( conn->IsBus() && !stale_bus_members.empty() )
    {
        for( auto stale_member : stale_bus_members )
        {
            for( CONNECTION_SUBGRAPH* subgraph : visited )
            {
                SCH_CONNECTION* member = matchBusMember( subgraph->m_driver_connection,
                                                         stale_member );
                wxASSERT( member );

                wxLogTrace( "CONN", "Updating %lu (%s) member %s to %s", subgraph->m_code,
                            subgraph->m_driver_connection->Name(), member->LocalName(),
                            stale_member->Name() );

                member->Clone( *stale_member );

                propagate_bus_neighbors( subgraph );
            }
        }
    }

    aSubgraph->m_dirty = false;
}


std::shared_ptr<SCH_CONNECTION> CONNECTION_GRAPH::getDefaultConnection( SCH_ITEM* aItem,
                                                                        SCH_SHEET_PATH aSheet )
{
    auto c = std::shared_ptr<SCH_CONNECTION>( nullptr );

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

        if( pin->IsPowerConnection() )
        {
            c = std::make_shared<SCH_CONNECTION>( aItem, aSheet );
            c->ConfigureFromLabel( pin->GetName() );
        }
        break;
    }

    case SCH_GLOBAL_LABEL_T:
    case SCH_HIER_LABEL_T:
    case SCH_LABEL_T:
    {
        auto text = static_cast<SCH_TEXT*>( aItem );

        c = std::make_shared<SCH_CONNECTION>( aItem, aSheet );
        c->ConfigureFromLabel( text->GetText() );
        break;
    }

    default:
        break;
    }

    return c;
}


SCH_CONNECTION* CONNECTION_GRAPH::matchBusMember( SCH_CONNECTION* aBusConnection,
                                                  SCH_CONNECTION* aSearch )
{
    wxASSERT( aBusConnection->IsBus() );

    SCH_CONNECTION* match = nullptr;

    if( aBusConnection->Type() == CONNECTION_TYPE::BUS )
    {
        // Vector bus: compare against index, because we allow the name
        // to be different

        for( const auto& bus_member : aBusConnection->Members() )
        {
            if( bus_member->VectorIndex() == aSearch->VectorIndex() )
            {
                match = bus_member.get();
                break;
            }
        }
    }
    else
    {
        // Group bus
        for( const auto& c : aBusConnection->Members() )
        {
            // Vector inside group: compare names, because for bus groups
            // we expect the naming to be consistent across all usages
            // TODO(JE) explain this in the docs
            if( c->Type() == CONNECTION_TYPE::BUS )
            {
                for( const auto& bus_member : c->Members() )
                {
                    if( bus_member->LocalName() == aSearch->LocalName() )
                    {
                        match = bus_member.get();
                        break;
                    }
                }
            }
            else if( c->LocalName() == aSearch->LocalName() )
            {
                match = c.get();
                break;
            }
        }
    }

    return match;
}


void CONNECTION_GRAPH::recacheSubgraphName(
        CONNECTION_SUBGRAPH* aSubgraph, const wxString& aOldName )
{
    if( m_net_name_to_subgraphs_map.count( aOldName ) )
    {
        auto& vec = m_net_name_to_subgraphs_map.at( aOldName );
        vec.erase( std::remove( vec.begin(), vec.end(), aSubgraph ), vec.end() );
    }

    wxLogTrace( "CONN", "recacheSubgraphName: %s => %s", aOldName,
        aSubgraph->m_driver_connection->Name() );

    m_net_name_to_subgraphs_map[aSubgraph->m_driver_connection->Name()].push_back( aSubgraph );
}


std::shared_ptr<BUS_ALIAS> CONNECTION_GRAPH::GetBusAlias( const wxString& aName )
{
    if( m_bus_alias_cache.count( aName ) )
        return m_bus_alias_cache.at( aName );

    return nullptr;
}


std::vector<const CONNECTION_SUBGRAPH*> CONNECTION_GRAPH::GetBusesNeedingMigration()
{
    std::vector<const CONNECTION_SUBGRAPH*> ret;

    for( auto&& subgraph : m_subgraphs )
    {
        // Graph is supposed to be up-to-date before calling this
        wxASSERT( !subgraph->m_dirty );

        if( !subgraph->m_driver )
            continue;

        auto sheet = subgraph->m_sheet;
        auto connection = subgraph->m_driver->Connection( sheet );

        if( !connection->IsBus() )
            continue;

        auto labels = subgraph->GetBusLabels();

        if( labels.size() > 1 )
        {
            bool different = false;
            wxString first = static_cast<SCH_TEXT*>( labels.at( 0 ) )->GetText();

            for( unsigned i = 1; i < labels.size(); ++i )
            {
                if( static_cast<SCH_TEXT*>( labels.at( i ) )->GetText() != first )
                {
                    different = true;
                    break;
                }
            }

            if( !different )
                continue;

            wxLogTrace( "CONN", "SG %ld (%s) has multiple bus labels", subgraph->m_code,
                        connection->Name() );

            ret.push_back( subgraph );
        }
    }

    return ret;
}


int CONNECTION_GRAPH::RunERC()
{
    int error_count = 0;

    for( auto&& subgraph : m_subgraphs )
    {
        // Graph is supposed to be up-to-date before calling RunERC()
        wxASSERT( !subgraph->m_dirty );

        /**
         * NOTE:
         *
         * We could check that labels attached to bus subgraphs follow the
         * proper format (i.e. actually define a bus).
         *
         * This check doesn't need to be here right now because labels
         * won't actually be connected to bus wires if they aren't in the right
         * format due to their TestDanglingEnds() implementation.
         */

        if( g_ErcSettings->IsTestEnabled( ERCE_DRIVER_CONFLICT ) && !subgraph->ResolveDrivers() )
            error_count++;

        if( g_ErcSettings->IsTestEnabled( ERCE_BUS_TO_NET_CONFLICT )
                && !ercCheckBusToNetConflicts( subgraph ) )
            error_count++;

        if( g_ErcSettings->IsTestEnabled( ERCE_BUS_ENTRY_CONFLICT )
                && !ercCheckBusToBusEntryConflicts( subgraph ) )
            error_count++;

        if( g_ErcSettings->IsTestEnabled( ERCE_BUS_TO_BUS_CONFLICT )
                && !ercCheckBusToBusConflicts( subgraph ) )
            error_count++;

        // The following checks are always performed since they don't currently
        // have an option exposed to the user

        if( !ercCheckNoConnects( subgraph ) )
            error_count++;

        if( ( g_ErcSettings->IsTestEnabled( ERCE_LABEL_NOT_CONNECTED )
                || g_ErcSettings->IsTestEnabled( ERCE_GLOBLABEL ) ) && !ercCheckLabels( subgraph ) )
            error_count++;
    }

    return error_count;
}


bool CONNECTION_GRAPH::ercCheckBusToNetConflicts( const CONNECTION_SUBGRAPH* aSubgraph )
{
    auto sheet = aSubgraph->m_sheet;
    auto screen = sheet.LastScreen();

    SCH_ITEM* net_item = nullptr;
    SCH_ITEM* bus_item = nullptr;
    SCH_CONNECTION conn;

    for( auto item : aSubgraph->m_items )
    {
        switch( item->Type() )
        {
        case SCH_LINE_T:
        {
            if( item->GetLayer() == LAYER_BUS )
                bus_item = ( !bus_item ) ? item : bus_item;
            else
                net_item = ( !net_item ) ? item : net_item;
            break;
        }

        case SCH_GLOBAL_LABEL_T:
        case SCH_SHEET_PIN_T:
        case SCH_HIER_LABEL_T:
        {
            auto text = static_cast<SCH_TEXT*>( item )->GetShownText();
            conn.ConfigureFromLabel( text );

            if( conn.IsBus() )
                bus_item = ( !bus_item ) ? item : bus_item;
            else
                net_item = ( !net_item ) ? item : net_item;
            break;
        }

        default:
            break;
        }
    }

    if( net_item && bus_item )
    {
        auto marker = new SCH_MARKER( MARKER_BASE::MARKER_ERC );
        marker->SetData( m_frame->GetUserUnits(), ERCE_BUS_TO_NET_CONFLICT,
                         net_item->GetPosition(), net_item, bus_item );
        screen->Append( marker );

        return false;
    }

    return true;
}


bool CONNECTION_GRAPH::ercCheckBusToBusConflicts( const CONNECTION_SUBGRAPH* aSubgraph )
{
    wxString msg;
    auto sheet = aSubgraph->m_sheet;
    auto screen = sheet.LastScreen();

    SCH_ITEM* label = nullptr;
    SCH_ITEM* port = nullptr;

    for( auto item : aSubgraph->m_items )
    {
        switch( item->Type() )
        {
        case SCH_TEXT_T:
        case SCH_GLOBAL_LABEL_T:
        {
            if( !label && item->Connection( sheet )->IsBus() )
                label = item;
            break;
        }

        case SCH_SHEET_PIN_T:
        case SCH_HIER_LABEL_T:
        {
            if( !port && item->Connection( sheet )->IsBus() )
                port = item;
            break;
        }

        default:
            break;
        }
    }

    if( label && port )
    {
        bool match = false;

        for( const auto& member : label->Connection( sheet )->Members() )
        {
            for( const auto& test : port->Connection( sheet )->Members() )
            {
                if( test != member && member->Name() == test->Name() )
                {
                    match = true;
                    break;
                }
            }

            if( match )
                break;
        }

        if( !match )
        {
            auto marker = new SCH_MARKER( MARKER_BASE::MARKER_ERC );
            marker->SetData( m_frame->GetUserUnits(), ERCE_BUS_TO_BUS_CONFLICT,
                             label->GetPosition(), label, port );
            screen->Append( marker );

            return false;
        }
    }

    return true;
}


bool CONNECTION_GRAPH::ercCheckBusToBusEntryConflicts( const CONNECTION_SUBGRAPH* aSubgraph )
{
    bool conflict = false;
    auto sheet = aSubgraph->m_sheet;
    auto screen = sheet.LastScreen();

    SCH_BUS_WIRE_ENTRY* bus_entry = nullptr;
    SCH_ITEM* bus_wire = nullptr;

    for( auto item : aSubgraph->m_items )
    {
        switch( item->Type() )
        {
        case SCH_BUS_WIRE_ENTRY_T:
        {
            if( !bus_entry )
                bus_entry = static_cast<SCH_BUS_WIRE_ENTRY*>( item );
            break;
        }

        default:
            break;
        }
    }

    if( bus_entry && bus_entry->m_connected_bus_item )
    {
        bus_wire = bus_entry->m_connected_bus_item;

        wxASSERT( bus_wire->Type() == SCH_LINE_T );

        // In some cases, the connection list (SCH_CONNECTION*) can be null.
        // Skip null connections.
        if( bus_entry->Connection( sheet ) && bus_wire->Type() == SCH_LINE_T
            && bus_wire->Connection( sheet ) )
        {
            conflict = true;

            auto test_name = bus_entry->Connection( sheet )->Name( true );

            for( const auto& member : bus_wire->Connection( sheet )->Members() )
            {
                if( member->Type() == CONNECTION_TYPE::BUS )
                {
                    for( const auto& sub_member : member->Members() )
                    {
                        if( sub_member->Name( true ) == test_name )
                            conflict = false;
                    }
                }
                else if( member->Name( true ) == test_name )
                {
                    conflict = false;
                }
            }
        }
    }

    // Don't report warnings if this bus member has been overridden by a higher priority power pin
    // or global label
    if( conflict && CONNECTION_SUBGRAPH::GetDriverPriority( aSubgraph->m_driver )
                       >= CONNECTION_SUBGRAPH::PRIORITY::POWER_PIN )
        conflict = false;

    if( conflict )
    {
        auto marker = new SCH_MARKER( MARKER_BASE::MARKER_ERC );
        marker->SetData( m_frame->GetUserUnits(), ERCE_BUS_ENTRY_CONFLICT,
                         bus_entry->GetPosition(), bus_entry, bus_wire );
        screen->Append( marker );

        return false;
    }

    return true;
}


// TODO(JE) Check sheet pins here too?
bool CONNECTION_GRAPH::ercCheckNoConnects( const CONNECTION_SUBGRAPH* aSubgraph )
{
    wxString msg;
    auto sheet = aSubgraph->m_sheet;
    auto screen = sheet.LastScreen();

    if( aSubgraph->m_no_connect != nullptr )
    {
        bool has_invalid_items = false;
        bool has_other_items = false;
        SCH_PIN* pin = nullptr;
        std::vector<SCH_ITEM*> invalid_items;

        // Any subgraph that contains both a pin and a no-connect should not
        // contain any other driving items.

        for( auto item : aSubgraph->m_items )
        {
            switch( item->Type() )
            {
            case SCH_PIN_T:
                pin = static_cast<SCH_PIN*>( item );
                has_other_items = true;
                break;

            case SCH_LINE_T:
            case SCH_JUNCTION_T:
            case SCH_NO_CONNECT_T:
                break;

            default:
                has_invalid_items = true;
                has_other_items = true;
                invalid_items.push_back( item );
            }
        }

        if( pin && has_invalid_items )
        {
            auto marker = new SCH_MARKER( MARKER_BASE::MARKER_ERC );
            marker->SetData( ERCE_NOCONNECT_CONNECTED, pin->GetTransformedPosition(),
                             pin->GetDescription( &aSubgraph->m_sheet ), pin->m_Uuid );
            screen->Append( marker );

            return false;
        }

        if( !has_other_items )
        {
            SCH_MARKER* marker = new SCH_MARKER( MARKER_BASE::MARKER_ERC );
            marker->SetData( m_frame->GetUserUnits(), ERCE_NOCONNECT_NOT_CONNECTED,
                             aSubgraph->m_no_connect->GetPosition(), aSubgraph->m_no_connect );
            screen->Append( marker );

            return false;
        }
    }
    else
    {
        bool has_other_connections = false;
        SCH_PIN* pin = nullptr;

        // Any subgraph that lacks a no-connect and contains a pin should also
        // contain at least one other connectable item.

        for( auto item : aSubgraph->m_items )
        {
            switch( item->Type() )
            {
            case SCH_PIN_T:
                if( !pin )
                    pin = static_cast<SCH_PIN*>( item );
                else
                    has_other_connections = true;

                break;

            default:
                if( item->IsConnectable() )
                    has_other_connections = true;

                break;
            }
        }

        // Check if invisible power pins connect to anything else.
        // Note this won't catch a component with multiple invisible power pins but these don't
        // connect to any other net; maybe that should be added as a further optional ERC check?

        if( pin && !has_other_connections && pin->IsPowerConnection() && !pin->IsVisible() )
        {
            wxString name = pin->Connection( sheet )->Name();
            wxString local_name = pin->Connection( sheet )->Name( true );

            if( m_global_label_cache.count( name )  ||
                ( m_local_label_cache.count( std::make_pair( sheet, local_name ) ) ) )
            {
                has_other_connections = true;
            }
        }

        if( pin && !has_other_connections && pin->GetType() != ELECTRICAL_PINTYPE::PT_NC )
        {
            SCH_MARKER* marker = new SCH_MARKER( MARKER_BASE::MARKER_ERC );
            marker->SetData( ERCE_PIN_NOT_CONNECTED, pin->GetTransformedPosition(),
                             pin->GetDescription( &aSubgraph->m_sheet ), pin->m_Uuid );
            screen->Append( marker );

            return false;
        }
    }

    return true;
}


bool CONNECTION_GRAPH::ercCheckLabels( const CONNECTION_SUBGRAPH* aSubgraph )
{
    // Label connection rules:
    // Local labels are flagged if they don't connect to any pins and don't have a no-connect
    // Global labels are flagged if they appear only once, don't connect to any local labels,
    // and don't have a no-connect marker

    // So, if there is a no-connect, we will never generate a warning here
    if( aSubgraph->m_no_connect )
        return true;

    SCH_TEXT* text = nullptr;
    bool has_other_connections = false;

    for( auto item : aSubgraph->m_items )
    {
        switch( item->Type() )
        {
        case SCH_LABEL_T:
        case SCH_GLOBAL_LABEL_T:
        case SCH_HIER_LABEL_T:
            text = static_cast<SCH_TEXT*>( item );
            break;

        case SCH_PIN_T:
        case SCH_SHEET_PIN_T:
            has_other_connections = true;
            break;

        default:
            break;
        }
    }

    if( !text )
        return true;

    bool is_global = text->Type() == SCH_GLOBAL_LABEL_T;

    // Global label check can be disabled independently
    if( !g_ErcSettings->IsTestEnabled( ERCE_GLOBLABEL ) && is_global )
        return true;

    wxString name = text->GetShownText();

    if( is_global )
    {
        // This will be set to true if the global is connected to a pin above, but we
        // want to reset this to false so that globals get flagged if they only have a
        // single instance
        has_other_connections = false;

        if( m_net_name_to_subgraphs_map.count( name )
                && m_net_name_to_subgraphs_map.at( name ).size() > 1 )
            has_other_connections = true;
    }
    else if( text->Type() == SCH_HIER_LABEL_T )
    {
        // For a hier label, check if the parent pin is connected
        if( aSubgraph->m_hier_parent &&
            ( aSubgraph->m_hier_parent->m_strong_driver ||
                aSubgraph->m_hier_parent->m_drivers.size() > 1))
        {
            // For now, a simple check: if there is more than one driver, the parent is probably
            // connected elsewhere (because at least one driver will be the hier pin itself)
            has_other_connections = true;
        }
    }
    else
    {
        auto pair = std::make_pair( aSubgraph->m_sheet, name );

        if( m_local_label_cache.count( pair ) && m_local_label_cache.at( pair ).size() > 1 )
            has_other_connections = true;
    }

    if( !has_other_connections )
    {
        SCH_MARKER* marker = new SCH_MARKER( MARKER_BASE::MARKER_ERC );
        marker->SetData( m_frame->GetUserUnits(),
                         is_global ? ERCE_GLOBLABEL : ERCE_LABEL_NOT_CONNECTED,
                         text->GetPosition(), text );
        aSubgraph->m_sheet.LastScreen()->Append( marker );

        return false;
    }

    return true;
}
