/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright The 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 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, you may find one here:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * or you may search the http://www.gnu.org website for the version 2 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

#include <board_design_settings.h>
#include <project/net_settings.h>
#include <drc/drc_engine.h>
#include <drc/drc_item.h>
#include <drc/drc_rule.h>
#include <drc/drc_rule_condition.h>
#include <drc/drc_test_provider.h>
#include <collectors.h>
#include <pcb_shape.h>
#include <pad.h>
#include <pcb_track.h>

#include <drawing_sheet/ds_draw_item.h>
#include <drawing_sheet/ds_proxy_view_item.h>

#include <thread_pool.h>

#include <atomic>
#include <limits>
#include <project/project_file.h>
#include <project/tuning_profiles.h>

/*
    Miscellaneous tests:

    - DRCE_DISABLED_LAYER_ITEM,               ///< item on a disabled layer
    - DRCE_INVALID_OUTLINE,                   ///< invalid board outline
    - DRCE_UNRESOLVED_VARIABLE,
    - DRCE_ASSERTION_FAILURE,                 ///< user-defined assertions
    - DRCE_GENERIC_WARNING                    ///< user-defined warnings
    - DRCE_GENERIC_ERROR                      ///< user-defined errors
    - DRCE_MISSING_TUNING_PROFILES            ///< tuning profile for netc lass not defined
*/

static void findClosestOutlineGap( BOARD* aBoard, PCB_SHAPE*& aItemA, PCB_SHAPE*& aItemB,
                                   VECTOR2I& aMidpoint, int& aDistance )
{
    PCB_TYPE_COLLECTOR items;
    items.Collect( aBoard, { PCB_SHAPE_T } );

    std::vector<PCB_SHAPE*> shapes;

    for( int ii = 0; ii < items.GetCount(); ++ii )
    {
        PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( items[ii] );

        if( shape->GetLayer() == Edge_Cuts )
            shapes.push_back( shape );
    }

    int       best = std::numeric_limits<int>::max();
    VECTOR2I  bestA;
    VECTOR2I  bestB;

    for( size_t ii = 0; ii < shapes.size(); ++ii )
    {
        std::shared_ptr<SHAPE> shapeA = shapes[ii]->GetEffectiveShape();

        for( size_t jj = ii + 1; jj < shapes.size(); ++jj )
        {
            std::shared_ptr<SHAPE> shapeB = shapes[jj]->GetEffectiveShape();
            VECTOR2I            ptA;
            VECTOR2I            ptB;

            if( shapeA && shapeB && shapeA->NearestPoints( shapeB.get(), ptA, ptB ) )
            {
                int dist = ( ptA - ptB ).EuclideanNorm();

                if( dist < best )
                {
                    best = dist;
                    bestA = ptA;
                    bestB = ptB;
                    aItemA = shapes[ii];
                    aItemB = shapes[jj];
                }
            }
        }
    }

    if( aItemA && aItemB )
    {
        aDistance = best;
        aMidpoint = ( bestA + bestB ) / 2;
    }
    else
    {
        aDistance = 0;
        aMidpoint = VECTOR2I();
    }
}

class DRC_TEST_PROVIDER_MISC : public DRC_TEST_PROVIDER
{
public:
    DRC_TEST_PROVIDER_MISC() :
        m_board( nullptr )
    {
        m_isRuleDriven = false;
    }

    virtual ~DRC_TEST_PROVIDER_MISC() = default;

    virtual bool Run() override;

    virtual const wxString GetName() const override { return wxT( "miscellaneous" ); };

private:
    void testOutline();
    void testDisabledLayers();
    void testTextVars();
    void testAssertions();
    void testMissingTuningProfiles();

    BOARD* m_board;
};


void DRC_TEST_PROVIDER_MISC::testOutline()
{
    SHAPE_POLY_SET dummyOutline;
    bool           errorHandled = false;

    OUTLINE_ERROR_HANDLER errorHandler =
            [&]( const wxString& msg, BOARD_ITEM* itemA, BOARD_ITEM* itemB, const VECTOR2I& pt )
    {
        errorHandled = true;

        if( m_drcEngine->IsErrorLimitExceeded( DRCE_INVALID_OUTLINE ) )
            return;

        if( !itemA )
            std::swap( itemA, itemB );

        VECTOR2I   markerPos = pt;
        int        gap = 0;
        PCB_SHAPE* shapeA = nullptr;
        PCB_SHAPE* shapeB = nullptr;
        bool       usedGap = false;

        if( itemA && itemB && itemA->Type() == PCB_SHAPE_T && itemB->Type() == PCB_SHAPE_T )
        {
            shapeA = static_cast<PCB_SHAPE*>( itemA );
            shapeB = static_cast<PCB_SHAPE*>( itemB );
        }
        else
        {
            findClosestOutlineGap( m_board, shapeA, shapeB, markerPos, gap );
            itemA = shapeA;
            itemB = shapeB;
            usedGap = shapeA && shapeB;
        }

        if( shapeA && shapeB )
        {
            std::shared_ptr<SHAPE> effectiveShapeA = shapeA->GetEffectiveShape();
            std::shared_ptr<SHAPE> effectiveShapeB = shapeB->GetEffectiveShape();

            if( effectiveShapeA && effectiveShapeB )
            {
                BOX2I bboxA = effectiveShapeA->BBox();
                BOX2I bboxB = effectiveShapeB->BBox();
                BOX2I overlap = bboxA.Intersect( bboxB );

                if( overlap.GetWidth() > 0 && overlap.GetHeight() > 0 )
                {
                    markerPos = overlap.Centre();
                    usedGap = false;
                }
                else
                {
                    VECTOR2I ptA, ptB;

                    if( effectiveShapeA->NearestPoints( effectiveShapeB.get(), ptA, ptB ) )
                    {
                        gap = ( ptA - ptB ).EuclideanNorm();
                        markerPos = ( ptA + ptB ) / 2;
                        usedGap = true;
                    }
                }
            }
        }

        std::shared_ptr<DRC_ITEM> drcItem = DRC_ITEM::Create( DRCE_INVALID_OUTLINE );

        if( itemA && itemB && usedGap )
        {
            drcItem->SetErrorDetail( wxString::Format( _( "%s (gap %s)" ), msg, MessageTextFromValue( gap ) ) );
        }
        else
        {
            drcItem->SetErrorDetail( msg );
        }

        drcItem->SetItems( itemA, itemB );

        reportViolation( drcItem, markerPos, Edge_Cuts );
    };

    // Test for very small graphic items (a few nm size) that can create issues
    // when trying to build the board outlines, and they are not easy to locate on screen.
    const int minSizeForValideGraphics =  pcbIUScale.mmToIU( 0.001 );

    if( !TestBoardOutlinesGraphicItems(m_board, minSizeForValideGraphics, &errorHandler ) )
    {
        if( errorHandled )
        {
            // if there are invalid items on Edge.Cuts, they are already reported
        }
        else
        {
            std::shared_ptr<DRC_ITEM> drcItem = DRC_ITEM::Create( DRCE_INVALID_OUTLINE );
            drcItem->SetErrorDetail( _( "(Suspicious items found on Edge.Cuts layer)" ) );
            drcItem->SetItems( m_board );

            reportViolation( drcItem, m_board->GetBoundingBox().Centre(), Edge_Cuts );
        }
    }


    // Use the standard chaining epsilon here so that we report errors that might affect
    // other tools (such as 3D viewer).
    int chainingEpsilon = m_board->GetOutlinesChainingEpsilon();

    // Arc to segment approximation error (not critical here: we do not use the outline shape):
    int maxError = pcbIUScale.mmToIU( 0.05 );

    if( !BuildBoardPolygonOutlines( m_board, dummyOutline, maxError, chainingEpsilon, true, &errorHandler ) )
    {
        if( errorHandled )
        {
            // if there is an invalid outline, then there must be an outline
        }
        else
        {
            std::shared_ptr<DRC_ITEM> drcItem = DRC_ITEM::Create( DRCE_INVALID_OUTLINE );
            drcItem->SetErrorDetail( _( "(no edges found on Edge.Cuts layer)" ) );
            drcItem->SetItems( m_board );

            reportViolation( drcItem, m_board->GetBoundingBox().Centre(), Edge_Cuts );
        }
    }
}


void DRC_TEST_PROVIDER_MISC::testDisabledLayers()
{
    const int progressDelta = 2000;
    int       ii = 0;
    int       items = 0;

    auto countItems =
            [&]( BOARD_ITEM* item ) -> bool
            {
                ++items;
                return true;
            };

    LSET disabledLayers = LSET( m_board->GetEnabledLayers() ).flip();

    // Perform the test only for copper layers
    disabledLayers &= LSET::AllCuMask();

    auto checkDisabledLayers =
            [&]( BOARD_ITEM* item ) -> bool
            {
                if( m_drcEngine->IsErrorLimitExceeded( DRCE_DISABLED_LAYER_ITEM ) )
                    return false;

                if( !reportProgress( ii++, items, progressDelta ) )
                    return false;

                PCB_LAYER_ID badLayer = UNDEFINED_LAYER;

                if( item->Type() == PCB_PAD_T )
                {
                    PAD* pad = static_cast<PAD*>( item );

                    if( pad->GetAttribute() == PAD_ATTRIB::SMD
                            || pad->GetAttribute() == PAD_ATTRIB::CONN )
                    {
                        if( disabledLayers.test( pad->GetPrincipalLayer() ) )
                            badLayer = item->GetLayer();
                    }
                    else
                    {
                        // Through hole pad pierces all physical layers.
                    }
                }
                else if( item->Type() == PCB_VIA_T )
                {
                    PCB_VIA* via = static_cast<PCB_VIA*>( item );
                    PCB_LAYER_ID top;
                    PCB_LAYER_ID bottom;

                    via->LayerPair( &top, &bottom );

                    if( disabledLayers.test( top ) )
                        badLayer = top;
                    else if( disabledLayers.test( bottom ) )
                        badLayer = bottom;
                }
                else if( item->Type() == PCB_ZONE_T )
                {
                    // Footprint zones just get a top/bottom/inner setting, so they're on
                    // whatever inner layers there are.
                }
                else
                {
                    LSET badLayers = disabledLayers & item->GetLayerSet();

                    if( badLayers.any() )
                        badLayer = badLayers.Seq().front();
                }

                if( badLayer != UNDEFINED_LAYER )
                {
                    std::shared_ptr<DRC_ITEM> drcItem = DRC_ITEM::Create( DRCE_DISABLED_LAYER_ITEM );
                    drcItem->SetErrorDetail( wxString::Format( _( "(layer %s)" ), LayerName( badLayer ) ) );
                    drcItem->SetItems( item );

                    reportViolation( drcItem, item->GetPosition(), UNDEFINED_LAYER );
                }

                return true;
            };

    forEachGeometryItem( s_allBasicItems, LSET::AllLayersMask(), countItems );
    forEachGeometryItem( s_allBasicItems, LSET::AllLayersMask(), checkDisabledLayers );
}


void DRC_TEST_PROVIDER_MISC::testAssertions()
{
    if( !m_drcEngine->HasRulesForConstraintType( ASSERTION_CONSTRAINT ) )
        return;

    std::vector<BOARD_ITEM*> allItems;

    forEachGeometryItem( {}, LSET::AllLayersMask(),
            [&]( BOARD_ITEM* item ) -> bool
            {
                allItems.push_back( item );
                return true;
            } );

    std::atomic<size_t> itemsDone( 0 );
    size_t              itemCount = allItems.size();

    auto checkItem =
            [&]( size_t idx )
            {
                if( !m_drcEngine->IsErrorLimitExceeded( DRCE_ASSERTION_FAILURE )
                    && !m_drcEngine->IsCancelled() )
                {
                    BOARD_ITEM* item = allItems[idx];

                    m_drcEngine->ProcessAssertions( item,
                            [&]( const DRC_CONSTRAINT* c )
                            {
                                std::shared_ptr<DRC_ITEM> drcItem = DRC_ITEM::Create( DRCE_ASSERTION_FAILURE );
                                drcItem->SetErrorDetail( wxString::Format( wxS( "(%s)" ), c->GetName() ) );
                                drcItem->SetItems( item );
                                drcItem->SetViolatingRule( c->GetParentRule() );

                                reportViolation( drcItem, item->GetPosition(), item->GetLayer() );
                            } );
                }

                itemsDone.fetch_add( 1 );
            };

    thread_pool& tp = GetKiCadThreadPool();
    auto         itemFutures = tp.submit_loop( 0, itemCount, checkItem, itemCount );

    while( itemsDone < itemCount )
    {
        reportProgress( itemsDone, itemCount );

        if( m_drcEngine->IsCancelled() )
            break;

        itemFutures.wait_for( std::chrono::milliseconds( 250 ) );
    }

    // Join every worker before the captured locals go out of scope, whether we finished or
    // were cancelled.
    itemFutures.wait();
}


void DRC_TEST_PROVIDER_MISC::testTextVars()
{
    const int progressDelta = 2000;
    int       ii = 0;
    int       items = 0;

    static const std::vector<KICAD_T> itemTypes = {
        PCB_FIELD_T,
        PCB_TEXT_T, PCB_TEXTBOX_T, PCB_TABLECELL_T,
        PCB_DIMENSION_T
    };

    auto testAssertion =
            [&]( BOARD_ITEM* item, const wxString& text, const VECTOR2I& pos, int layer )
            {
                // Match anywhere in the text so users can embed ${DRC_ERROR ...}
                // or ${DRC_WARNING ...} inside placeholder strings rather than
                // only at the start of the field.  The leading "(^|[^\\\\])"
                // group requires the marker to start the string or follow a
                // non-backslash, so `\${DRC_ERROR ...}` stays inert; the
                // captured message is group 2.
                static wxRegEx warningExpr( wxS( "(^|[^\\\\])\\$\\{DRC_WARNING\\s*([^}]*)\\}" ) );
                static wxRegEx errorExpr( wxS( "(^|[^\\\\])\\$\\{DRC_ERROR\\s*([^}]*)\\}" ) );

                auto reportEach =
                        [&]( wxRegEx& aExpr, int aErrorCode )
                        {
                            // Return true if any token of this kind was *matched*, even when
                            // the per-code error limit had already been exceeded.  The caller
                            // uses the return value to suppress an unrelated
                            // DRCE_UNRESOLVED_VARIABLE marker; if we let the limit-exceeded
                            // case fall through to false we'd raise the wrong error.
                            bool     found = false;
                            wxString remaining = text;

                            while( aExpr.Matches( remaining ) )
                            {
                                found = true;

                                if( !m_drcEngine->IsErrorLimitExceeded( aErrorCode ) )
                                {
                                    wxString drcText = aExpr.GetMatch( remaining, 2 );

                                    std::shared_ptr<DRC_ITEM> drcItem =
                                            DRC_ITEM::Create( aErrorCode );

                                    if( item )
                                        drcItem->SetItems( item );
                                    else
                                        drcText += _( " (in drawing sheet)" );

                                    drcItem->SetErrorMessage( drcText );

                                    reportViolation( drcItem, pos, layer );
                                }

                                size_t start = 0;
                                size_t len = 0;

                                if( !aExpr.GetMatch( &start, &len, 0 ) || len == 0 )
                                    break;

                                remaining = remaining.Mid( start + len );
                            }

                            return found;
                        };

                bool foundWarning = reportEach( warningExpr, DRCE_GENERIC_WARNING );
                bool foundError = reportEach( errorExpr, DRCE_GENERIC_ERROR );

                return foundWarning || foundError;
            };

    forEachGeometryItem( itemTypes, LSET::AllLayersMask(),
            [&]( BOARD_ITEM* item ) -> bool
            {
                ++items;
                return true;
            } );

    forEachGeometryItem( itemTypes, LSET::AllLayersMask(),
            [&]( BOARD_ITEM* item ) -> bool
            {
                if( m_drcEngine->IsErrorLimitExceeded( DRCE_UNRESOLVED_VARIABLE ) )
                    return false;

                if( !reportProgress( ii++, items, progressDelta ) )
                    return false;

                if( EDA_TEXT* textItem = dynamic_cast<EDA_TEXT*>( item ) )
                {
                    // A matched ${DRC_ERROR}/${DRC_WARNING} is the intended signal; the resolved
                    // text still carries a literal ${...}, so flag the unresolved variable only
                    // when no marker fired (matching the drawing-sheet path below).
                    if( testAssertion( item, textItem->GetText(), item->GetPosition(),
                                       item->GetLayer() ) )
                    {
                        // Don't run unresolved test
                    }
                    else if( ExpandEnvVarSubstitutions( textItem->GetShownText( true ),
                                                        nullptr /*project already done*/ )
                                     .Matches( wxT( "*${*}*" ) ) )
                    {
                        auto drcItem = DRC_ITEM::Create( DRCE_UNRESOLVED_VARIABLE );
                        drcItem->SetItems( item );

                        reportViolation( drcItem, item->GetPosition(), item->GetLayer() );
                    }
                }

                return true;
            } );

    DS_PROXY_VIEW_ITEM* drawingSheet = m_drcEngine->GetDrawingSheet();
    DS_DRAW_ITEM_LIST   drawItems( pcbIUScale, FOR_ERC_DRC );

    if( !drawingSheet || m_drcEngine->IsErrorLimitExceeded( DRCE_UNRESOLVED_VARIABLE ) )
        return;

    drawItems.SetPageNumber( wxT( "1" ) );
    drawItems.SetSheetCount( 1 );
    drawItems.SetFileName( wxT( "dummyFilename" ) );
    drawItems.SetSheetName( wxT( "dummySheet" ) );
    drawItems.SetSheetLayer( wxT( "dummyLayer" ) );
    drawItems.SetProject( m_board->GetProject() );
    drawItems.BuildDrawItemsList( drawingSheet->GetPageInfo(), drawingSheet->GetTitleBlock() );

    for( DS_DRAW_ITEM_BASE* item = drawItems.GetFirst(); item; item = drawItems.GetNext() )
    {
        if( m_drcEngine->IsErrorLimitExceeded( DRCE_UNRESOLVED_VARIABLE ) )
            break;

        if( m_drcEngine->IsCancelled() )
            return;

        if( DS_DRAW_ITEM_TEXT* text = dynamic_cast<DS_DRAW_ITEM_TEXT*>( item ) )
        {
            if( testAssertion( nullptr, text->GetText(), text->GetPosition(), LAYER_DRAWINGSHEET ) )
            {
                // Don't run unresolved test
            }
            else if( text->GetShownText( true ).Matches( wxT( "*${*}*" ) ) )
            {
                std::shared_ptr<DRC_ITEM> drcItem = DRC_ITEM::Create( DRCE_UNRESOLVED_VARIABLE );
                drcItem->SetItems( drawingSheet );

                reportViolation( drcItem, text->GetPosition(), LAYER_DRAWINGSHEET );
            }
        }
    }
}


void DRC_TEST_PROVIDER_MISC::testMissingTuningProfiles()
{
    if( !m_board->GetProject() )
        return;

    std::shared_ptr<NET_SETTINGS>          netSettings = m_board->GetProject()->GetProjectFile().NetSettings();
    const std::shared_ptr<TUNING_PROFILES> tuningProfiles =
            m_board->GetProject()->GetProjectFile().TuningProfileParameters();

    std::set<wxString> profileNames;
    std::ranges::for_each( tuningProfiles->GetTuningProfiles(),
                           [&profileNames]( const TUNING_PROFILE& tuningProfile )
                           {
                               if( const wxString name = tuningProfile.m_ProfileName; name != wxEmptyString )
                                   profileNames.insert( name );
                           } );

    for( const auto& [name, netclass] : netSettings->GetNetclasses() )
    {
        if( m_drcEngine->IsErrorLimitExceeded( DRCE_MISSING_TUNING_PROFILE ) )
            return;

        const wxString profileName = netclass->GetTuningProfile();

        if( netclass->HasTuningProfile() && !profileNames.contains( profileName ) )
        {
            std::shared_ptr<DRC_ITEM> drcItem = DRC_ITEM::Create( DRCE_MISSING_TUNING_PROFILE );
            drcItem->SetErrorDetail( wxString::Format( "(Net Class: %s, Tuning Profile: %s)",
                                                       name,
                                                       profileName ) );

            reportViolation( drcItem, VECTOR2I(), UNDEFINED_LAYER );
        }
    }
}


bool DRC_TEST_PROVIDER_MISC::Run()
{
    m_board = m_drcEngine->GetBoard();

    if( !m_drcEngine->IsErrorLimitExceeded( DRCE_INVALID_OUTLINE ) )
    {
        if( !reportPhase( _( "Checking board outline..." ) ) )
            return false;   // DRC cancelled

        testOutline();
    }

    if( !m_drcEngine->IsErrorLimitExceeded( DRCE_DISABLED_LAYER_ITEM ) )
    {
        if( !reportPhase( _( "Checking disabled layers..." ) ) )
            return false;   // DRC cancelled

        testDisabledLayers();
    }

    if( !m_drcEngine->IsErrorLimitExceeded( DRCE_UNRESOLVED_VARIABLE ) )
    {
        if( !reportPhase( _( "Checking text variables..." ) ) )
            return false;   // DRC cancelled

        testTextVars();
    }

    if( !m_drcEngine->IsErrorLimitExceeded( DRCE_ASSERTION_FAILURE )
            || !m_drcEngine->IsErrorLimitExceeded( DRCE_GENERIC_WARNING )
            || !m_drcEngine->IsErrorLimitExceeded( DRCE_GENERIC_ERROR ) )
    {
        if( !reportPhase( _( "Checking assertions..." ) ) )
            return false;   // DRC cancelled

        testAssertions();
    }

    if( !m_drcEngine->IsErrorLimitExceeded( DRCE_MISSING_TUNING_PROFILE ) )
    {
        if( !reportPhase( _( "Checking for missing tuning profiles..." ) ) )
            return false; // DRC cancelled

        testMissingTuningProfiles();
    }

    return !m_drcEngine->IsCancelled();
}


namespace detail
{
static DRC_REGISTER_TEST_PROVIDER<DRC_TEST_PROVIDER_MISC> dummy;
}
