/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2023 Alex Shvartzkop <dudesuchamazing@gmail.com>
 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, you may find one here:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * or you may search the http://www.gnu.org website for the version 2 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

#include <eda_item.h>
#include <geometry/geometry_utils.h>
#include <pcb_base_edit_frame.h>
#include <router/pns_meander.h>
#include <router/pns_meander_placer_base.h>
#include <router/pns_router.h>
#include <router/pns_routing_settings.h>


namespace KIGFX
{
    class VIEW;
    class RENDER_SETTINGS;
}

enum LENGTH_TUNING_MODE
{
    SINGLE,
    DIFF_PAIR,
    DIFF_PAIR_SKEW
};


class TUNING_STATUS_VIEW_ITEM : public EDA_ITEM
{
public:
    TUNING_STATUS_VIEW_ITEM( PCB_BASE_EDIT_FRAME* aFrame );

    wxString GetClass() const override;

#if defined(DEBUG)
    void Show( int nestLevel, std::ostream& os ) const override;
#endif

    VECTOR2I GetPosition() const override;
    void     SetPosition( const VECTOR2I& aPos ) override;

    void SetMinMax( const double aMin, const double aMax );

    void ClearMinMax();

    void SetCurrent( const double aCurrent, const wxString& aLabel );

    void SetIsTimeDomain( const bool aIsTimeDomain );

    const BOX2I ViewBBox() const override;

    std::vector<int> ViewGetLayers() const override;

    void ViewDraw( int aLayer, KIGFX::VIEW* aView ) const override;

protected:
    EDA_DRAW_FRAME* m_frame;
    VECTOR2I        m_pos;
    double          m_min;
    double          m_max;
    double          m_current;
    wxString        m_currentLabel;
    wxString        m_currentText;
    wxString        m_minText;
    wxString        m_maxText;
    bool            m_isTimeDomain;
};


class PCB_TUNING_PATTERN : public PCB_GENERATOR
{
public:
    static const wxString GENERATOR_TYPE;
    static const wxString DISPLAY_NAME;

    PCB_TUNING_PATTERN( BOARD_ITEM* aParent = nullptr, PCB_LAYER_ID aLayer = F_Cu,
                        LENGTH_TUNING_MODE aMode = LENGTH_TUNING_MODE::SINGLE );

    wxString GetGeneratorType() const override { return wxS( "tuning_pattern" ); }

    wxString GetItemDescription( UNITS_PROVIDER* aUnitsProvider, bool aFull ) const override
    {
        return _( "Tuning Pattern" );
    }

    wxString GetFriendlyName() const override
    {
        return _( "Tuning Pattern" );
    }

    wxString GetPluralName() const override
    {
        return _( "Tuning Patterns" );
    }

    wxString GetCommitMessage() const override
    {
        return _( "Edit Tuning Pattern" );
    }

    BITMAPS GetMenuImage() const override
    {
        switch( m_tuningMode )
        {
        case SINGLE:         return BITMAPS::ps_tune_length;           break;
        case DIFF_PAIR:      return BITMAPS::ps_diff_pair_tune_length; break;
        case DIFF_PAIR_SKEW: return BITMAPS::ps_diff_pair_tune_phase;  break;
        }

        return BITMAPS::unknown;
    }

    static PCB_TUNING_PATTERN* CreateNew( GENERATOR_TOOL* aTool, PCB_BASE_EDIT_FRAME* aFrame,
                                          BOARD_CONNECTED_ITEM* aStartItem,
                                          LENGTH_TUNING_MODE aMode );

    void EditStart( GENERATOR_TOOL* aTool, BOARD* aBoard, BOARD_COMMIT* aCommit ) override;
    bool Update( GENERATOR_TOOL* aTool, BOARD* aBoard, BOARD_COMMIT* aCommit ) override;
    void EditFinish( GENERATOR_TOOL* aTool, BOARD* aBoard, BOARD_COMMIT* aCommit ) override;
    void EditCancel( GENERATOR_TOOL* aTool, BOARD* aBoard, BOARD_COMMIT* aCommit ) override;

    void Remove( GENERATOR_TOOL* aTool, BOARD* aBoard, BOARD_COMMIT* aCommit ) override;

    bool MakeEditPoints( EDIT_POINTS& points ) const override;

    bool UpdateFromEditPoints( EDIT_POINTS& aEditPoints ) override;

    bool UpdateEditPoints( EDIT_POINTS& aEditPoints ) override;

    void Move( const VECTOR2I& aMoveVector ) override
    {
        m_origin += aMoveVector;
        m_end += aMoveVector;

        if( !this->HasFlag( IN_EDIT ) )
        {
            PCB_GROUP::Move( aMoveVector );

            if( m_baseLine )
                m_baseLine->Move( aMoveVector );

            if( m_baseLineCoupled )
                m_baseLineCoupled->Move( aMoveVector );
        }
    }

    void Rotate( const VECTOR2I& aRotCentre, const EDA_ANGLE& aAngle ) override
    {
        if( !this->HasFlag( IN_EDIT ) )
        {
            PCB_GENERATOR::Rotate( aRotCentre, aAngle );
            RotatePoint( m_end, aRotCentre, aAngle );

            if( m_baseLine )
                m_baseLine->Rotate( aAngle, aRotCentre );

            if( m_baseLineCoupled )
                m_baseLineCoupled->Rotate( aAngle, aRotCentre );
        }
    }

    void Flip( const VECTOR2I& aCentre, FLIP_DIRECTION aFlipDirection ) override
    {
        if( !this->HasFlag( IN_EDIT ) )
        {
            PCB_GENERATOR::Flip( aCentre, aFlipDirection );

            baseMirror( aCentre, aFlipDirection );
        }
    }

    void Mirror( const VECTOR2I& aCentre, FLIP_DIRECTION aFlipDirection ) override
    {
        if( !this->HasFlag( IN_EDIT ) )
        {
            PCB_GENERATOR::Mirror( aCentre, aFlipDirection );

            baseMirror( aCentre, aFlipDirection );
        }
    }

    void SetLayer( PCB_LAYER_ID aLayer ) override
    {
        PCB_GENERATOR::SetLayer( aLayer );

        for( BOARD_ITEM* item : GetBoardItems() )
            item->SetLayer( aLayer );
    }

    PCB_LAYER_ID GetLayer() const override
    {
        for( BOARD_ITEM* item : GetBoardItems() )
        {
            if( PCB_TRACK* track = dynamic_cast<PCB_TRACK*>( item ) )
                return track->GetLayer();
        }

        return PCB_GENERATOR::GetLayer();
    }

    const BOX2I GetBoundingBox() const override
    {
        return getOutline().BBox();
    }

    std::vector<int> ViewGetLayers() const override
    {
        return { LAYER_ANCHOR, GetLayer() };
    }

    bool HitTest( const VECTOR2I& aPosition, int aAccuracy = 0 ) const override
    {
        return getOutline().Collide( aPosition, aAccuracy );
    }

    bool HitTest( const BOX2I& aRect, bool aContained, int aAccuracy ) const override
    {
        bool anyItems = false;

        for( BOARD_ITEM* item : GetBoardItems() )
        {
            anyItems = true;

            if( aContained )
            {
                if( !item->HitTest( aRect, true, aAccuracy ) )
                    return false;
            }
            else if( item->HitTest( aRect, false, aAccuracy ) )
            {
                return true;
            }
        }

        if( anyItems )
            return aContained;

        BOX2I sel = aRect;

        if( aAccuracy )
            sel.Inflate( aAccuracy );

        // Convert selection box to polygon for accurate hit testing against the actual outline
        SHAPE_LINE_CHAIN selPoly(
                {
                    sel.GetOrigin(),
                    VECTOR2I( sel.GetRight(), sel.GetTop() ),
                    sel.GetEnd(),
                    VECTOR2I( sel.GetLeft(), sel.GetBottom() )
                },
                true );

        return KIGEOM::ShapeHitTest( selPoly, getOutline(), aContained );
    }

    bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override
    {
        bool anyItems = false;

        for( BOARD_ITEM* item : GetBoardItems() )
        {
            anyItems = true;

            if( aContained )
            {
                if( !item->HitTest( aPoly, true ) )
                    return false;
            }
            else if( item->HitTest( aPoly, false ) )
            {
                return true;
            }
        }

        if( anyItems )
            return aContained;

        return KIGEOM::ShapeHitTest( aPoly, getOutline(), aContained );
    }

    const BOX2I ViewBBox() const override { return GetBoundingBox(); }

    EDA_ITEM* Clone() const override { return new PCB_TUNING_PATTERN( *this ); }

    void ViewDraw( int aLayer, KIGFX::VIEW* aView ) const override final;

    const VECTOR2I& GetEnd() const { return m_end; }
    void            SetEnd( const VECTOR2I& aValue ) { m_end = aValue; }

    int  GetEndX() const { return m_end.x; }
    void SetEndX( int aValue ) { m_end.x = aValue; }

    int  GetEndY() const { return m_end.y; }
    void SetEndY( int aValue ) { m_end.y = aValue; }

    int GetWidth() const
    {
        for( BOARD_ITEM* item : GetBoardItems() )
            if( PCB_TRACK* track = dynamic_cast<PCB_TRACK*>( item ) )
                return track->GetWidth();

        return m_trackWidth;
    }

    void SetWidth( int aValue )
    {
        m_trackWidth = aValue;

        for( BOARD_ITEM* item : GetBoardItems() )
            if( PCB_TRACK* track = dynamic_cast<PCB_TRACK*>( item ) )
                track->SetWidth( aValue );
    }

    int GetNetCode() const
    {
        for( BOARD_ITEM* item : GetBoardItems() )
            if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( item ) )
                return bci->GetNetCode();

        return 0;
    }

    void SetNetCode( int aNetCode )
    {
        if( BOARD* board = GetBoard() )
        {
            if( NETINFO_ITEM* net = board->FindNet( aNetCode ) )
                m_lastNetName = net->GetNetname();
            else
                m_lastNetName.clear();
        }

        for( BOARD_ITEM* item : GetBoardItems() )
            if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( item ) )
                bci->SetNetCode( aNetCode );
    }

    bool HasSolderMask() const
    {
        for( BOARD_ITEM* item : GetBoardItems() )
            if( PCB_TRACK* track = dynamic_cast<PCB_TRACK*>( item ) )
                return track->HasSolderMask();

        return true;
    }

    void SetHasSolderMask( bool aVal )
    {
        for( BOARD_ITEM* item : GetBoardItems() )
            if( PCB_TRACK* track = dynamic_cast<PCB_TRACK*>( item ) )
                track->SetHasSolderMask( aVal );
    }

    std::optional<int> GetLocalSolderMaskMargin() const
    {
        for( BOARD_ITEM* item : GetBoardItems() )
            if( PCB_TRACK* track = dynamic_cast<PCB_TRACK*>( item ) )
                return track->GetLocalSolderMaskMargin();

        return std::optional<int>();
    }

    void SetLocalSolderMaskMargin( std::optional<int> aMargin )
    {
        for( BOARD_ITEM* item : GetBoardItems() )
            if( PCB_TRACK* track = dynamic_cast<PCB_TRACK*>( item ) )
                track->SetLocalSolderMaskMargin( aMargin );
    }

    LENGTH_TUNING_MODE GetTuningMode() const { return m_tuningMode; }

    PNS::ROUTER_MODE GetPNSMode()
    {
        switch( m_tuningMode )
        {
        case LENGTH_TUNING_MODE::SINGLE:         return PNS::PNS_MODE_TUNE_SINGLE;
        case LENGTH_TUNING_MODE::DIFF_PAIR:      return PNS::PNS_MODE_TUNE_DIFF_PAIR;
        case LENGTH_TUNING_MODE::DIFF_PAIR_SKEW: return PNS::PNS_MODE_TUNE_DIFF_PAIR_SKEW;
        default:                                 return PNS::PNS_MODE_TUNE_SINGLE;
        }
    }

    PNS::MEANDER_SETTINGS& GetSettings() { return m_settings; }

    int  GetMinAmplitude() const { return m_settings.m_minAmplitude; }
    void SetMinAmplitude( int aValue )
    {
        aValue = std::max( aValue, 0 );

        m_settings.m_minAmplitude = aValue;

        if( m_settings.m_maxAmplitude < m_settings.m_minAmplitude )
            m_settings.m_maxAmplitude = m_settings.m_minAmplitude;
    }

    int  GetMaxAmplitude() const { return m_settings.m_maxAmplitude; }
    void SetMaxAmplitude( int aValue )
    {
        aValue = std::max( aValue, 0 );

        m_settings.m_maxAmplitude = aValue;

        if( m_settings.m_maxAmplitude < m_settings.m_minAmplitude )
            m_settings.m_minAmplitude = m_settings.m_maxAmplitude;
    }

    // Update the initial side one time at EditStart based on m_end.
    void UpdateSideFromEnd() { m_updateSideFromEnd = true; }

    PNS::MEANDER_SIDE GetInitialSide() const { return m_settings.m_initialSide; }
    void              SetInitialSide( PNS::MEANDER_SIDE aValue ) { m_settings.m_initialSide = aValue; }

    int  GetSpacing() const { return m_settings.m_spacing; }
    void SetSpacing( int aValue ) { m_settings.m_spacing = aValue; }

    std::optional<int> GetTargetLength() const
    {
        if( m_settings.m_targetLength.Opt() == PNS::MEANDER_SETTINGS::LENGTH_UNCONSTRAINED )
            return std::optional<int>();
        else
            return m_settings.m_targetLength.Opt();
    }

    void SetTargetLength( std::optional<int> aValue )
    {
        m_settings.m_isTimeDomain = false;

        if( aValue.has_value() )
            m_settings.SetTargetLength( aValue.value() );
        else
            m_settings.SetTargetLength( PNS::MEANDER_SETTINGS::LENGTH_UNCONSTRAINED );
    }

    std::optional<int> GetTargetDelay() const
    {
        if( m_settings.m_targetLengthDelay.Opt() == PNS::MEANDER_SETTINGS::DELAY_UNCONSTRAINED )
            return std::optional<int>();
        else
            return m_settings.m_targetLengthDelay.Opt();
    }

    void SetTargetDelay( std::optional<int> aValue )
    {
        m_settings.m_isTimeDomain = true;

        if( aValue.has_value() )
            m_settings.SetTargetLengthDelay( aValue.value() );
        else
            m_settings.SetTargetLengthDelay( PNS::MEANDER_SETTINGS::DELAY_UNCONSTRAINED );
    }

    int  GetTargetSkew() const { return m_settings.m_targetSkew.Opt(); }
    void SetTargetSkew( int aValue ) { m_settings.SetTargetSkew( aValue ); }

    int  GetTargetSkewDelay() const { return m_settings.m_targetSkewDelay.Opt(); }
    void SetTargetSkewDelay( int aValue ) { m_settings.SetTargetSkewDelay( aValue ); }

    bool GetOverrideCustomRules() const { return m_settings.m_overrideCustomRules; }
    void SetOverrideCustomRules( bool aOverride ) { m_settings.m_overrideCustomRules = aOverride; }

    int  GetCornerRadiusPercentage() const { return m_settings.m_cornerRadiusPercentage; }
    void SetCornerRadiusPercentage( int aValue ) { m_settings.m_cornerRadiusPercentage = aValue; }

    bool IsSingleSided() const { return m_settings.m_singleSided; }
    void SetSingleSided( bool aValue ) { m_settings.m_singleSided = aValue; }

    bool IsRounded() const { return m_settings.m_cornerStyle == PNS::MEANDER_STYLE_ROUND; }
    void SetRounded( bool aFlag ) { m_settings.m_cornerStyle = aFlag ? PNS::MEANDER_STYLE_ROUND
                                                                     : PNS::MEANDER_STYLE_CHAMFER; }

    std::vector<std::pair<wxString, wxVariant>> GetRowData() override
    {
        std::vector<std::pair<wxString, wxVariant>> data = PCB_GENERATOR::GetRowData();
        data.emplace_back( _HKI( "Net" ), m_lastNetName );
        data.emplace_back( _HKI( "Tuning" ), m_tuningInfo );
        return data;
    }

    const STRING_ANY_MAP GetProperties() const override;
    void SetProperties( const STRING_ANY_MAP& aProps ) override;

    void ShowPropertiesDialog( PCB_BASE_EDIT_FRAME* aEditFrame ) override;

    std::vector<EDA_ITEM*> GetPreviewItems( GENERATOR_TOOL* aTool, PCB_BASE_EDIT_FRAME* aFrame,
                                            bool aStatusItemsOnly = false ) override;

    void GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector<MSG_PANEL_ITEM>& aList ) override;

protected:
    void swapData( BOARD_ITEM* aImage ) override
    {
        wxASSERT( aImage->Type() == PCB_GENERATOR_T );

        std::swap( *this, *static_cast<PCB_TUNING_PATTERN*>( aImage ) );
    }

    bool recoverBaseline( PNS::ROUTER* aRouter );

    bool baselineValid();

    bool initBaseLine( PNS::ROUTER* aRouter, int aPNSLayer, BOARD* aBoard, VECTOR2I& aStart,
                       VECTOR2I& aEnd, NETINFO_ITEM* aNet,
                       std::optional<SHAPE_LINE_CHAIN>& aBaseLine );

    bool initBaseLines( PNS::ROUTER* aRouter, int aPNSLayer, BOARD* aBoard );

    bool removeToBaseline( PNS::ROUTER* aRouter, int aPNSLayer, SHAPE_LINE_CHAIN& aBaseLine );

    bool resetToBaseline( GENERATOR_TOOL* aTool, int aPNSLayer, SHAPE_LINE_CHAIN& aBaseLine,
                          bool aPrimary );

    SHAPE_LINE_CHAIN getOutline() const;

    void baseMirror( const VECTOR2I& aCentre, FLIP_DIRECTION aFlipDirection )
    {
        PCB_GENERATOR::baseMirror( aCentre, aFlipDirection );

        if( m_baseLine )
        {
            m_baseLine->Mirror( aCentre, aFlipDirection );
            m_origin = m_baseLine->CPoint( 0 );
            m_end = m_baseLine->CLastPoint();
        }

        if( m_baseLineCoupled )
            m_baseLineCoupled->Mirror( aCentre, aFlipDirection );

        if( m_settings.m_initialSide == PNS::MEANDER_SIDE_RIGHT )
            m_settings.m_initialSide = PNS::MEANDER_SIDE_LEFT;
        else
            m_settings.m_initialSide = PNS::MEANDER_SIDE_RIGHT;
    }

protected:
    VECTOR2I              m_end;

    PNS::MEANDER_SETTINGS m_settings;

    std::optional<SHAPE_LINE_CHAIN> m_baseLine;
    std::optional<SHAPE_LINE_CHAIN> m_baseLineCoupled;

    int                   m_trackWidth;
    int                   m_diffPairGap;

    LENGTH_TUNING_MODE    m_tuningMode;

    wxString              m_lastNetName;
    wxString              m_tuningInfo;
    long long             m_tuningLength;

    PNS::MEANDER_PLACER_BASE::TUNING_STATUS m_tuningStatus;

    bool                  m_updateSideFromEnd;
};
