/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2004-2022 KiCad Developers.
 *
 * 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 <cstdio>
#include <memory>
#include <reporter.h>
#include <board.h>
#include <string_utils.h>

#include <pcbexpr_evaluator.h>

#include <connectivity/connectivity_data.h>
#include <connectivity/connectivity_algo.h>

#include <connectivity/from_to_cache.h>

void FROM_TO_CACHE::buildEndpointList( )
{
    m_ftEndpoints.clear();

    for( FOOTPRINT* footprint : m_board->Footprints() )
    {
        for( PAD* pad : footprint->Pads() )
        {
            FT_ENDPOINT ent;
            ent.name = footprint->GetReference() + wxT( "-" ) + pad->GetNumber();
            ent.parent = pad;
            m_ftEndpoints.push_back( ent );
            ent.name = footprint->GetReference();
            ent.parent = pad;
            m_ftEndpoints.push_back( ent );
        }
    }
}


enum PATH_STATUS {
    PS_OK = 0,
    PS_MULTIPLE_PATHS = -1,
    PS_NO_PATH = -2
};


static bool isVertexVisited( CN_ITEM* v, const std::vector<CN_ITEM*>& path )
{
    for( CN_ITEM* u : path )
    {
        if ( u == v )
            return true;
    }

    return false;
}


static PATH_STATUS uniquePathBetweenNodes( CN_ITEM* u, CN_ITEM* v, std::vector<CN_ITEM*>& outPath )
{
    using Path = std::vector<CN_ITEM*>;
    std::deque<Path> Q;

    Path pInit;
    bool pathFound = false;
    pInit.push_back( u );
    Q.push_back( pInit );

    while( Q.size() )
    {
        Path path = Q.front();
        Q.pop_front();
        CN_ITEM* last = path.back();

        if( last == v )
        {
            outPath = path;

            if( pathFound )
                return PS_MULTIPLE_PATHS;

            pathFound = true;
        }

        for( CN_ITEM* ci : last->ConnectedItems() )
        {
            bool vertexVisited = isVertexVisited( ci, path );

            for( std::vector<CN_ITEM*>& p : Q )
                if( isVertexVisited( ci, p ) )
                {
                    vertexVisited = true;
                    break;
                }

            if( !vertexVisited )
            {
                Path newpath( path );
                newpath.push_back( ci );
                Q.push_back( newpath );
            }
        }
    }

    return pathFound ? PS_OK : PS_NO_PATH;
};


int FROM_TO_CACHE::cacheFromToPaths( const wxString& aFrom, const wxString& aTo )
{
    std::vector<FT_PATH>                  paths;
    std::shared_ptr<CONNECTIVITY_DATA>    connectivity = m_board->GetConnectivity();
    std::shared_ptr<CN_CONNECTIVITY_ALGO> cnAlgo = connectivity->GetConnectivityAlgo();

    for( FT_ENDPOINT& endpoint : m_ftEndpoints )
    {
        if( WildCompareString( aFrom, endpoint.name, false ) )
        {
            FT_PATH p;
            p.net = endpoint.parent->GetNetCode();
            p.from = endpoint.parent;
            p.to = nullptr;
            paths.push_back(p);
        }
    }

    for( FT_PATH& path : paths )
    {
        int count = 0;

        wxString fromName = path.from->GetParentFootprint()->GetReference()
                                + wxT( "-" ) + path.from->GetNumber();

        auto padCandidates = connectivity->GetConnectedItems( path.from,
                                                              { PCB_PAD_T, PCB_ARC_T, PCB_VIA_T,
                                                                PCB_TRACE_T } );
        PAD* toPad = nullptr;

        for( BOARD_CONNECTED_ITEM* pitem : padCandidates )
        {
            if( pitem == path.from )
                continue;

            if( pitem->Type() != PCB_PAD_T )
                continue;

            const PAD *pad = static_cast<const PAD*>( pitem );

            wxString toName = pad->GetParentFootprint()->GetReference()
                                    + wxT( "-" ) + pad->GetNumber();

            for( const FT_ENDPOINT& endpoint : m_ftEndpoints )
            {
                if( pad == endpoint.parent )
                {
                    if( WildCompareString( aTo, endpoint.name, false ) )
                    {
                        count++;
                        toPad = endpoint.parent;

                        path.to = toPad;
                        path.fromName = fromName;
                        path.toName = toName;
                        path.fromWildcard = aFrom;
                        path.toWildcard = aTo;

                        if( count >= 2 )
                        {
                            // fixme: report this somewhere?
                            path.to = nullptr;
                        }
                    }
                }
            }
        }
    }

    int newPaths = 0;

    for( FT_PATH& path : paths )
    {
        if( !path.from || !path.to )
            continue;

        CN_ITEM*              cnFrom = cnAlgo->ItemEntry( path.from ).GetItems().front();
        CN_ITEM*              cnTo = cnAlgo->ItemEntry( path.to ).GetItems().front();
        std::vector<CN_ITEM*> upath;

        auto result = uniquePathBetweenNodes( cnFrom, cnTo, upath );

        if( result == PS_OK )
            path.isUnique = true;
        else
            path.isUnique = false;

        if( result == PS_NO_PATH )
            continue;

        for( const auto item : upath )
        {
            path.pathItems.insert( item->Parent() );
        }

        m_ftPaths.push_back(path);
        newPaths++;
    }

    // reportAux( _("Cached %d paths\n"), newPaths );

    return newPaths;
}

bool  FROM_TO_CACHE::IsOnFromToPath( BOARD_CONNECTED_ITEM* aItem, const wxString& aFrom, const wxString& aTo )
{
    int nFromTosFound = 0;

    if( !m_board )
        return false;

    for( int attempt = 0; attempt < 2; attempt++ )
    {
        // item already belongs to path
        for( FT_PATH& ftPath : m_ftPaths )
        {
            if( aFrom == ftPath.fromWildcard && aTo == ftPath.toWildcard )
            {
                nFromTosFound++;

                if( ftPath.pathItems.count( aItem ) )
                    return true;
            }
        }

        if( !nFromTosFound )
            cacheFromToPaths( aFrom, aTo );
        else
            return false;
    }

    return false;
}


void FROM_TO_CACHE::Rebuild( BOARD* aBoard )
{
    m_board = aBoard;
    buildEndpointList();
    m_ftPaths.clear();
}


FROM_TO_CACHE::FT_PATH* FROM_TO_CACHE::QueryFromToPath( const std::set<BOARD_CONNECTED_ITEM*>& aItems )
{
    for( FT_PATH& ftPath : m_ftPaths )
    {
        if ( ftPath.pathItems == aItems )
            return &ftPath;
    }

    return nullptr;
}
