/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2017-2023 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 <preview_items/ruler_item.h>
#include <preview_items/preview_utils.h>
#include <gal/graphics_abstraction_layer.h>
#include <layer_ids.h>
#include <gal/painter.h>
#include <view/view.h>
#include <trigo.h>

using namespace KIGFX::PREVIEW;

static const double maxTickDensity = 10.0;       // min pixels between tick marks
static const double midTickLengthFactor = 1.5;
static const double majorTickLengthFactor = 2.5;


/*
 * It would be nice to know why Cairo seems to have an opposite layer order from GAL, but
 * only when drawing RULER_ITEMs (the TWO_POINT_ASSISTANT and ARC_ASSISTANT are immune from
 * this issue).
 *
 * Until then, this egregious hack...
 */
static int getShadowLayer( KIGFX::GAL* aGal )
{
    if( aGal->IsCairoEngine() )
        return LAYER_SELECT_OVERLAY;
    else
        return LAYER_GP_OVERLAY;
}


static void drawCursorStrings( KIGFX::VIEW* aView, const VECTOR2D& aCursor,
                               const VECTOR2D& aRulerVec, const EDA_IU_SCALE& aIuScale, EDA_UNITS aUnits,
                               bool aDrawingDropShadows, bool aFlipX, bool aFlipY )
{
    // draw the cursor labels
    std::vector<wxString> cursorStrings;

    VECTOR2D temp = aRulerVec;

    if( aFlipX )
        temp.x = -temp.x;

    if( aFlipY )
        temp.y = -temp.y;

    cursorStrings.push_back( DimensionLabel( "x", temp.x, aIuScale, aUnits ) );
    cursorStrings.push_back( DimensionLabel( "y", temp.y, aIuScale, aUnits ) );

    cursorStrings.push_back( DimensionLabel( "r", aRulerVec.EuclideanNorm(), aIuScale, aUnits ) );

    EDA_ANGLE angle = -EDA_ANGLE( aRulerVec );
    cursorStrings.push_back( DimensionLabel( wxString::FromUTF8( "θ" ), angle.AsDegrees(), aIuScale,
                                             EDA_UNITS::DEGREES ) );

    temp = aRulerVec;
    DrawTextNextToCursor( aView, aCursor, -temp, cursorStrings, aDrawingDropShadows );
}


static double getTickLineWidth( const TEXT_DIMS& textDims, bool aDrawingDropShadows )
{
    double width = textDims.StrokeWidth * 0.8;

    if( aDrawingDropShadows )
        width += textDims.ShadowWidth;

    return width;
}


/**
 * Description of a "tick format" for a scale factor - how many ticks there are
 * between medium/major ticks and how each scale relates to the last one
 */
struct TICK_FORMAT
{
    double divisionBase;    ///< multiple from the last scale
    int majorStep;          ///< ticks between major ticks
    int midStep;            ///< ticks between medium ticks (0 if no medium ticks)
};


static TICK_FORMAT getTickFormatForScale( double aScale, double& aTickSpace, EDA_UNITS aUnits )
{
    // simple 1/2/5 scales per decade
    static std::vector<TICK_FORMAT> tickFormats =
    {
        { 2,    10,     5 },    // |....:....|
        { 2,     5,     0 },    // |....|
        { 2.5,   2,     0 },    // |.|.|
    };

    // could start at a set number of MM, but that's not available in common
    aTickSpace = 1;

    // Convert to a round (mod-10) number of mils for imperial units
    if( EDA_UNIT_UTILS::IsImperialUnit( aUnits ) )
        aTickSpace *= 2.54;

    int tickFormat = 0;

    while( true )
    {
        const auto pixelSpace = aTickSpace * aScale;

        if( pixelSpace >= maxTickDensity )
            break;

        tickFormat = ( tickFormat + 1 ) % tickFormats.size();
        aTickSpace *= tickFormats[tickFormat].divisionBase;
    }

    return tickFormats[tickFormat];
}


/**
 * Draw labelled ticks on a line. Ticks are spaced according to a
 * maximum density. Minor ticks are not labelled.
 *
 * @param aGal the GAL to draw on
 * @param aOrigin start of line to draw ticks on
 * @param aLine line vector
 * @param aMinorTickLen length of minor ticks in IU
 */
void drawTicksAlongLine( KIGFX::VIEW* aView, const VECTOR2D& aOrigin, const VECTOR2D& aLine,
                         double aMinorTickLen, const EDA_IU_SCALE& aIuScale, EDA_UNITS aUnits, bool aDrawingDropShadows )
{
    KIGFX::GAL*   gal = aView->GetGAL();
    KIFONT::FONT* font = KIFONT::FONT::GetFont();
    double        tickSpace;
    TICK_FORMAT   tickFormat = getTickFormatForScale( gal->GetWorldScale(), tickSpace, aUnits );
    double        majorTickLen = aMinorTickLen * ( majorTickLengthFactor + 1 );
    VECTOR2D      tickLine = aLine;

    RotatePoint( tickLine, ANGLE_90 );

    // number of ticks in whole ruler
    int           numTicks = (int) std::ceil( aLine.EuclideanNorm() / tickSpace );

    // work out which way up the tick labels go
    TEXT_DIMS     labelDims = GetConstantGlyphHeight( gal, -1 );
    EDA_ANGLE     labelAngle = - EDA_ANGLE( tickLine );
    VECTOR2I      labelOffset = tickLine.Resize( majorTickLen );

    // text is left (or right) aligned, so shadow text need a small offset to be draw
    // around the basic text
    int shadowXoffset = 0;

    if( aDrawingDropShadows )
    {
        labelDims.StrokeWidth += 2 * labelDims.ShadowWidth;
        shadowXoffset = labelDims.ShadowWidth;
        // Due to the fact a shadow text is drawn left or right aligned,
        // it needs an offset = shadowXoffset to be drawn at the same place as normal text
        // But for some reason we need to slightly modify this offset
        // for a better look for KiCad font (better alignment of shadow shape)
        const float adjust = 1.2f;      // Value chosen after tests
        shadowXoffset *= adjust;
    }

    if( aView->IsMirroredX() )
    {
        labelOffset = -labelOffset;
        shadowXoffset = -shadowXoffset;
    }

    TEXT_ATTRIBUTES labelAttrs;
    labelAttrs.m_Size = labelDims.GlyphSize;
    labelAttrs.m_StrokeWidth = labelDims.StrokeWidth;
    labelAttrs.m_Mirrored = aView->IsMirroredX();   // Prevent text mirrored when view is mirrored

    if( EDA_ANGLE( aLine ) > ANGLE_0 )
    {
        labelAttrs.m_Halign = GR_TEXT_H_ALIGN_LEFT;
        labelAttrs.m_Angle = labelAngle;

        // Adjust the text position of the shadow shape:
        labelOffset.x -= shadowXoffset * labelAttrs.m_Angle.Cos();;
        labelOffset.y += shadowXoffset * labelAttrs.m_Angle.Sin();;
    }
    else
    {
        labelAttrs.m_Halign = GR_TEXT_H_ALIGN_RIGHT;
        labelAttrs.m_Angle = labelAngle + ANGLE_180;

        // Adjust the text position of the shadow shape:
        labelOffset.x += shadowXoffset * labelAttrs.m_Angle.Cos();;
        labelOffset.y -= shadowXoffset * labelAttrs.m_Angle.Sin();;
    }

    BOX2D viewportD = aView->GetViewport();
    BOX2I viewport( VECTOR2I( viewportD.GetPosition() ), VECTOR2I( viewportD.GetSize() ) );

    viewport.Inflate( majorTickLen * 2 );   // Doesn't have to be accurate, just big enough not
                                            // to exclude anything that should be partially drawn

    int isign = aView->IsMirroredX() ? -1 : 1;

    for( int i = 0; i < numTicks; ++i )
    {
        const VECTOR2D tickPos = aOrigin + aLine.Resize( tickSpace * i );

        if( !viewport.Contains( tickPos ) )
            continue;

        double length = aMinorTickLen;
        bool   drawLabel = false;

        if( i % tickFormat.majorStep == 0 )
        {
            drawLabel = true;
            length *= majorTickLengthFactor;
        }
        else if( tickFormat.midStep && i % tickFormat.midStep == 0 )
        {
            drawLabel = true;
            length *= midTickLengthFactor;
        }

        gal->SetLineWidth( labelAttrs.m_StrokeWidth / 2 );
        gal->DrawLine( tickPos, tickPos + tickLine.Resize( length*isign ) );

        if( drawLabel )
        {
            wxString label = DimensionLabel( "", tickSpace * i, aIuScale, aUnits, false );
            font->Draw( gal, label, tickPos + labelOffset, labelAttrs, KIFONT::METRICS::Default() );
        }
    }
}


/**
 * Draw simple ticks on the back of a line such that the line is
 * divided into n parts.
 *
 * @param aGal the GAL to draw on
 * @param aOrigin start of line to draw ticks on
 * @param aLine line vector
 * @param aTickLen length of ticks in IU
 * @param aNumDivisions number of parts to divide the line into
 */
void drawBacksideTicks( KIGFX::VIEW* aView, const VECTOR2D& aOrigin, const VECTOR2D& aLine,
                        double aTickLen, int aNumDivisions, bool aDrawingDropShadows )
{
    KIGFX::GAL*  gal = aView->GetGAL();
    TEXT_DIMS    textDims = GetConstantGlyphHeight( gal, -1 );
    const double backTickSpace = aLine.EuclideanNorm() / aNumDivisions;
    VECTOR2D     backTickVec = aLine;
    int isign                = aView->IsMirroredX() ? -1 : 1;

    RotatePoint( backTickVec, -ANGLE_90 );
    backTickVec = backTickVec.Resize( aTickLen * isign );

    BOX2D viewportD = aView->GetViewport();
    BOX2I viewport( VECTOR2I( viewportD.GetPosition() ), VECTOR2I( viewportD.GetSize() ) );

    viewport.Inflate( aTickLen * 4 );   // Doesn't have to be accurate, just big enough not to
                                        // exclude anything that should be partially drawn

    for( int i = 0; i < aNumDivisions + 1; ++i )
    {
        const VECTOR2D backTickPos = aOrigin + aLine.Resize( backTickSpace * i );

        if( !viewport.Contains( backTickPos ) )
            continue;

        gal->SetLineWidth( getTickLineWidth( textDims, aDrawingDropShadows ) );
        gal->DrawLine( backTickPos, backTickPos + backTickVec );
    }
}


RULER_ITEM::RULER_ITEM( const TWO_POINT_GEOMETRY_MANAGER& aGeomMgr, const EDA_IU_SCALE& aIuScale, EDA_UNITS userUnits,
        bool aFlipX, bool aFlipY )
        : EDA_ITEM( NOT_USED ), // Never added to anything - just a preview
          m_geomMgr( aGeomMgr ),
          m_userUnits( userUnits ),
          m_iuScale( aIuScale ),
          m_flipX( aFlipX ),
          m_flipY( aFlipY )
{
}


const BOX2I RULER_ITEM::ViewBBox() const
{
    BOX2I tmp;

    if( m_geomMgr.GetOrigin() == m_geomMgr.GetEnd() )
        return tmp;

    // this is an edit-time artefact; no reason to try and be smart with the bounding box
    // (besides, we can't tell the text extents without a view to know what the scale is)
    tmp.SetMaximum();
    return tmp;
}


void RULER_ITEM::ViewGetLayers( int aLayers[], int& aCount ) const
{
    aLayers[0] = LAYER_SELECT_OVERLAY;
    aLayers[1] = LAYER_GP_OVERLAY;
    aCount = 2;
}


void RULER_ITEM::ViewDraw( int aLayer, KIGFX::VIEW* aView ) const
{
    KIGFX::GAL*      gal = aView->GetGAL();
    RENDER_SETTINGS* rs = aView->GetPainter()->GetSettings();
    bool             drawingDropShadows = ( aLayer == getShadowLayer( gal ) );

    gal->PushDepth();
    gal->SetLayerDepth( gal->GetMinDepth() );

    VECTOR2D origin = m_geomMgr.GetOrigin();
    VECTOR2D end = m_geomMgr.GetEnd();

    gal->SetIsStroke( true );
    gal->SetIsFill( false );

    gal->SetTextMirrored( false );
    gal->SetStrokeColor( rs->GetLayerColor( LAYER_AUX_ITEMS ) );

    if( drawingDropShadows )
        gal->SetStrokeColor( GetShadowColor( gal->GetStrokeColor() ) );

    gal->ResetTextAttributes();
    TEXT_DIMS textDims = GetConstantGlyphHeight( gal );

    // draw the main line from the origin to cursor
    gal->SetLineWidth( getTickLineWidth( textDims, drawingDropShadows ) );
    gal->DrawLine( origin, end );

    VECTOR2D rulerVec( end - origin );

    drawCursorStrings( aView, end, rulerVec, m_iuScale, m_userUnits, drawingDropShadows, m_flipX,
                       m_flipY );

    // basic tick size
    const double minorTickLen = 5.0 / gal->GetWorldScale();
    const double majorTickLen = minorTickLen * majorTickLengthFactor;

    drawTicksAlongLine( aView, origin, rulerVec, minorTickLen, m_iuScale, m_userUnits, drawingDropShadows );

    drawBacksideTicks( aView, origin, rulerVec, majorTickLen, 2, drawingDropShadows );

    // draw the back of the origin "crosshair"
    gal->DrawLine( origin, origin + rulerVec.Resize( -minorTickLen * midTickLengthFactor ) );
    gal->PopDepth();
}
