/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or more
 *   contributor license agreements. See the NOTICE file distributed
 *   with this work for additional information regarding copyright
 *   ownership. The ASF licenses this file to you under the Apache
 *   License, Version 2.0 (the "License"); you may not use this file
 *   except in compliance with the License. You may obtain a copy of
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
 */

#include <sal/types.h>
#include <basegfx/matrix/b2dhommatrixtools.hxx>
#include <basegfx/polygon/WaveLine.hxx>
#include <tools/helpers.hxx>
#include <o3tl/hash_combine.hxx>
#include <o3tl/lru_map.hxx>
#include <comphelper/configuration.hxx>
#include <tools/lazydelete.hxx>
#include <vcl/dropcache.hxx>
#include <vcl/metaact.hxx>
#include <vcl/settings.hxx>
#include <vcl/virdev.hxx>
#include <vcl/skia/SkiaHelper.hxx>

#include <drawmode.hxx>
#include <salgdi.hxx>
#include <impglyphitem.hxx>

#include <cassert>

#define UNDERLINE_LAST      LINESTYLE_BOLDWAVE
#define STRIKEOUT_LAST      STRIKEOUT_X

namespace {
    struct WavyLineCache final : public CacheOwner
    {
        WavyLineCache()
#if defined __cpp_lib_memory_resource
            : m_aItems(10, &GetMemoryResource())
#else
            : m_aItems(10)
#endif
        {
        }

        bool find( Color aLineColor, size_t nLineWidth, size_t nWaveHeight, size_t nWordWidth, Bitmap& rOutput )
        {
            Key aKey = { nWaveHeight, sal_uInt32(aLineColor) };
            auto item = m_aItems.find( aKey );
            if ( item == m_aItems.end() )
                return false;
            // needs update
            if ( item->second.m_aLineWidth != nLineWidth || item->second.m_aWordWidth < nWordWidth )
            {
                return false;
            }
            rOutput = item->second.m_Bitmap;
            return true;
        }

        void insert( const Bitmap& aBitmap, const Color& aLineColor, const size_t nLineWidth, const size_t nWaveHeight, const size_t nWordWidth, Bitmap& rOutput )
        {
            Key aKey = { nWaveHeight, sal_uInt32(aLineColor) };
            m_aItems.insert( std::pair< Key, WavyLineCacheItem>( aKey, { nLineWidth, nWordWidth, aBitmap } ) );
            rOutput = aBitmap;
        }

        virtual OUString getCacheName() const override { return "WavyLineCache"; }

        virtual bool dropCaches() override
        {
            m_aItems.clear();
            return true;
        }

        virtual void dumpState(rtl::OStringBuffer& rState) override
        {
            rState.append("\nWavyLineCache:\t");
            rState.append(static_cast<sal_Int32>(m_aItems.size()));
        }

        private:
        struct WavyLineCacheItem
        {
            size_t m_aLineWidth;
            size_t m_aWordWidth;
            Bitmap m_Bitmap;
        };

        struct Key
        {
            size_t m_aFirst;
            size_t m_aSecond;
            bool operator ==( const Key& rOther ) const
            {
                return ( m_aFirst == rOther.m_aFirst && m_aSecond == rOther.m_aSecond );
            }
        };

        struct Hash
        {
            size_t operator() ( const Key& rKey ) const
            {
                size_t aSeed = 0;
                o3tl::hash_combine(aSeed, rKey.m_aFirst);
                o3tl::hash_combine(aSeed, rKey.m_aSecond);
                return aSeed;
            }
        };

        o3tl::lru_map< Key, WavyLineCacheItem, Hash > m_aItems;
    };
}

void OutputDevice::ImplInitTextLineSize()
{
    mpFontInstance->mxFontMetric->ImplInitTextLineSize( this );
}

void OutputDevice::ImplInitAboveTextLineSize()
{
    mpFontInstance->mxFontMetric->ImplInitAboveTextLineSize( this );
}

void OutputDevice::ImplDrawWavePixel( tools::Long nOriginX, tools::Long nOriginY,
                                      tools::Long nCurX, tools::Long nCurY,
                                      tools::Long nWidth,
                                      Degree10 nOrientation,
                                      SalGraphics* pGraphics,
                                      const OutputDevice& rOutDev,
                                      tools::Long nPixWidth, tools::Long nPixHeight )
{
    if (nOrientation)
    {
        Point aPoint( nOriginX, nOriginY );
        aPoint.RotateAround( nCurX, nCurY, nOrientation );
    }

    if (shouldDrawWavePixelAsRect(nWidth))
    {
        pGraphics->DrawRect( nCurX, nCurY, nPixWidth, nPixHeight, rOutDev );
    }
    else
    {
        pGraphics->DrawPixel( nCurX, nCurY, rOutDev );
    }
}

bool OutputDevice::shouldDrawWavePixelAsRect(tools::Long nLineWidth) const
{
    if (nLineWidth > 1)
        return true;

    return false;
}

void OutputDevice::SetWaveLineColors(Color const& rColor, tools::Long nLineWidth)
{
    // On printers that output pixel via DrawRect()
    if (nLineWidth > 1)
    {
        if (mbLineColor || mbInitLineColor)
        {
            mpGraphics->SetLineColor();
            mbInitLineColor = true;
        }

        mpGraphics->SetFillColor( rColor );
        mbInitFillColor = true;
    }
    else
    {
        mpGraphics->SetLineColor( rColor );
        mbInitLineColor = true;
    }
}

Size OutputDevice::GetWaveLineSize(tools::Long nLineWidth) const
{
    if (nLineWidth > 1)
        return Size(nLineWidth, ((nLineWidth*mnDPIX)+(mnDPIY/2))/mnDPIY);

    return Size(1, 1);
}

void OutputDevice::ImplDrawWaveLine( tools::Long nBaseX, tools::Long nBaseY,
                                     tools::Long nDistX, tools::Long nDistY,
                                     tools::Long nWidth, tools::Long nHeight,
                                     tools::Long nLineWidth, Degree10 nOrientation,
                                     const Color& rColor )
{
    if ( !nHeight )
        return;

    tools::Long nStartX = nBaseX + nDistX;
    tools::Long nStartY = nBaseY + nDistY;

    // If the height is 1 pixel, it's enough output a line
    if ( (nLineWidth == 1) && (nHeight == 1) )
    {
        mpGraphics->SetLineColor( rColor );
        mbInitLineColor = true;

        tools::Long nEndX = nStartX+nWidth;
        tools::Long nEndY = nStartY;
        if ( nOrientation )
        {
            Point aOriginPt( nBaseX, nBaseY );
            aOriginPt.RotateAround( nStartX, nStartY, nOrientation );
            aOriginPt.RotateAround( nEndX, nEndY, nOrientation );
        }
        mpGraphics->DrawLine( nStartX, nStartY, nEndX, nEndY, *this );
    }
    else
    {
        tools::Long    nCurX = nStartX;
        tools::Long    nCurY = nStartY;
        tools::Long    nDiffX = 2;
        tools::Long    nDiffY = nHeight-1;
        tools::Long    nCount = nWidth;
        tools::Long    nOffY = -1;

        SetWaveLineColors(rColor, nLineWidth);
        Size aSize(GetWaveLineSize(nLineWidth));

        tools::Long nPixWidth = aSize.Width();
        tools::Long nPixHeight = aSize.Height();

        if ( !nDiffY )
        {
            while ( nWidth )
            {
                ImplDrawWavePixel( nBaseX, nBaseY, nCurX, nCurY, nLineWidth, nOrientation,
                                   mpGraphics, *this,
                                   nPixWidth, nPixHeight );
                nCurX++;
                nWidth--;
            }
        }
        else
        {
            nCurY += nDiffY;
            tools::Long nFreq = nCount / (nDiffX+nDiffY);
            while ( nFreq-- )
            {
                for( tools::Long i = nDiffY; i; --i )
                {
                    ImplDrawWavePixel( nBaseX, nBaseY, nCurX, nCurY, nLineWidth, nOrientation,
                                       mpGraphics, *this,
                                       nPixWidth, nPixHeight );
                    nCurX++;
                    nCurY += nOffY;
                }
                for( tools::Long i = nDiffX; i; --i )
                {
                    ImplDrawWavePixel( nBaseX, nBaseY, nCurX, nCurY, nLineWidth, nOrientation,
                                       mpGraphics, *this,
                                       nPixWidth, nPixHeight );
                    nCurX++;
                }
                nOffY = -nOffY;
            }
            nFreq = nCount % (nDiffX+nDiffY);
            if ( nFreq )
            {
                for( tools::Long i = nDiffY; i && nFreq; --i, --nFreq )
                {
                    ImplDrawWavePixel( nBaseX, nBaseY, nCurX, nCurY, nLineWidth, nOrientation,
                                       mpGraphics, *this,
                                       nPixWidth, nPixHeight );
                    nCurX++;
                    nCurY += nOffY;

                }
                for( tools::Long i = nDiffX; i && nFreq; --i, --nFreq )
                {
                    ImplDrawWavePixel( nBaseX, nBaseY, nCurX, nCurY, nLineWidth, nOrientation,
                                       mpGraphics, *this,
                                       nPixWidth, nPixHeight );
                    nCurX++;
                }
            }
        }
    }
}

void OutputDevice::ImplDrawWaveTextLine( tools::Long nBaseX, tools::Long nBaseY,
                                         tools::Long nDistX, tools::Long nDistY,
                                         tools::Long nWidth, tools::Long nLayoutWidth,
                                         FontLineStyle eTextLine,
                                         Color aColor,
                                         bool bIsAbove )
{
    static bool bFuzzing = comphelper::IsFuzzing();
    if (bFuzzing && nLayoutWidth > 10000)
    {
        SAL_WARN("vcl.gdi", "drawLine, skipping suspicious WaveTextLine of length: "
                                << nLayoutWidth << " for fuzzing performance");
        return;
    }

    LogicalFontInstance* pFontInstance = mpFontInstance.get();
    tools::Long            nLineHeight;
    tools::Long            nLinePos;

    if ( bIsAbove )
    {
        nLineHeight = pFontInstance->mxFontMetric->GetAboveWavelineUnderlineSize();
        nLinePos = pFontInstance->mxFontMetric->GetAboveWavelineUnderlineOffset();
    }
    else
    {
        nLineHeight = pFontInstance->mxFontMetric->GetWavelineUnderlineSize();
        nLinePos = pFontInstance->mxFontMetric->GetWavelineUnderlineOffset();
    }
    if ( (eTextLine == LINESTYLE_SMALLWAVE) && (nLineHeight > 3) )
        nLineHeight = 3;

    tools::Long nLineWidth = mnDPIX / 300;
    if ( !nLineWidth )
        nLineWidth = 1;

    if ( eTextLine == LINESTYLE_BOLDWAVE )
        nLineWidth *= 2;

    nLinePos += nDistY - (nLineHeight / 2);

    tools::Long nLineWidthHeight = ((nLineWidth * mnDPIX) + (mnDPIY / 2)) / mnDPIY;
    if ( eTextLine == LINESTYLE_DOUBLEWAVE )
    {
        tools::Long nOrgLineHeight = nLineHeight;
        nLineHeight /= 3;
        if ( nLineHeight < 2 )
        {
            if ( nOrgLineHeight > 1 )
                nLineHeight = 2;
            else
                nLineHeight = 1;
        }

        tools::Long nLineDY = nOrgLineHeight-(nLineHeight*2);
        if ( nLineDY < nLineWidthHeight )
            nLineDY = nLineWidthHeight;

        tools::Long nLineDY2 = nLineDY/2;
        if ( !nLineDY2 )
            nLineDY2 = 1;

        nLinePos -= nLineWidthHeight-nLineDY2;
        ImplDrawWaveLine( nBaseX, nBaseY, nDistX, nLinePos, nWidth, nLineHeight,
                          nLineWidth, mpFontInstance->mnOrientation, aColor );
        nLinePos += nLineWidthHeight+nLineDY;
        ImplDrawWaveLine( nBaseX, nBaseY, nDistX, nLinePos, nWidth, nLineHeight,
                          nLineWidth, mpFontInstance->mnOrientation, aColor );
    }
    else
    {
        nLinePos -= nLineWidthHeight/2;
        ImplDrawWaveLine( nBaseX, nBaseY, nDistX, nLinePos, nWidth, nLineHeight,
                          nLineWidth, mpFontInstance->mnOrientation, aColor );
    }
}

void OutputDevice::ImplDrawStraightTextLine( tools::Long nBaseX, tools::Long nBaseY,
                                             tools::Long nDistX, tools::Long nDistY, tools::Long nWidth,
                                             FontLineStyle eTextLine,
                                             Color aColor,
                                             bool bIsAbove )
{
    static bool bFuzzing = comphelper::IsFuzzing();
    if (bFuzzing && nWidth > 25000)
    {
        SAL_WARN("vcl.gdi", "drawLine, skipping suspicious TextLine of length: "
                                << nWidth << " for fuzzing performance");
        return;
    }

    LogicalFontInstance*  pFontInstance = mpFontInstance.get();
    tools::Long            nLineHeight = 0;
    tools::Long            nLinePos  = 0;
    tools::Long            nLinePos2 = 0;

    const tools::Long nY = nDistY;

    if ( eTextLine > UNDERLINE_LAST )
        eTextLine = LINESTYLE_SINGLE;

    switch ( eTextLine )
    {
    case LINESTYLE_SINGLE:
    case LINESTYLE_DOTTED:
    case LINESTYLE_DASH:
    case LINESTYLE_LONGDASH:
    case LINESTYLE_DASHDOT:
    case LINESTYLE_DASHDOTDOT:
        if ( bIsAbove )
        {
            nLineHeight = pFontInstance->mxFontMetric->GetAboveUnderlineSize();
            nLinePos    = nY + pFontInstance->mxFontMetric->GetAboveUnderlineOffset();
        }
        else
        {
            nLineHeight = pFontInstance->mxFontMetric->GetUnderlineSize();
            nLinePos    = nY + pFontInstance->mxFontMetric->GetUnderlineOffset();
        }
        break;
    case LINESTYLE_BOLD:
    case LINESTYLE_BOLDDOTTED:
    case LINESTYLE_BOLDDASH:
    case LINESTYLE_BOLDLONGDASH:
    case LINESTYLE_BOLDDASHDOT:
    case LINESTYLE_BOLDDASHDOTDOT:
        if ( bIsAbove )
        {
            nLineHeight = pFontInstance->mxFontMetric->GetAboveBoldUnderlineSize();
            nLinePos    = nY + pFontInstance->mxFontMetric->GetAboveBoldUnderlineOffset();
        }
        else
        {
            nLineHeight = pFontInstance->mxFontMetric->GetBoldUnderlineSize();
            nLinePos    = nY + pFontInstance->mxFontMetric->GetBoldUnderlineOffset();
        }
        break;
    case LINESTYLE_DOUBLE:
        if ( bIsAbove )
        {
            nLineHeight = pFontInstance->mxFontMetric->GetAboveDoubleUnderlineSize();
            nLinePos    = nY + pFontInstance->mxFontMetric->GetAboveDoubleUnderlineOffset1();
            nLinePos2   = nY + pFontInstance->mxFontMetric->GetAboveDoubleUnderlineOffset2();
        }
        else
        {
            nLineHeight = pFontInstance->mxFontMetric->GetDoubleUnderlineSize();
            nLinePos    = nY + pFontInstance->mxFontMetric->GetDoubleUnderlineOffset1();
            nLinePos2   = nY + pFontInstance->mxFontMetric->GetDoubleUnderlineOffset2();
        }
        break;
    default:
        break;
    }

    if ( !nLineHeight )
        return;

    if ( mbLineColor || mbInitLineColor )
    {
        mpGraphics->SetLineColor();
        mbInitLineColor = true;
    }
    mpGraphics->SetFillColor( aColor );
    mbInitFillColor = true;

    tools::Long nLeft = nDistX;

    switch ( eTextLine )
    {
    case LINESTYLE_SINGLE:
    case LINESTYLE_BOLD:
        ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos, nWidth, nLineHeight );
        break;
    case LINESTYLE_DOUBLE:
        ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos,  nWidth, nLineHeight );
        ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos2, nWidth, nLineHeight );
        break;
    case LINESTYLE_DOTTED:
    case LINESTYLE_BOLDDOTTED:
        {
            tools::Long nDotWidth = nLineHeight*mnDPIY;
            nDotWidth += mnDPIY/2;
            nDotWidth /= mnDPIY;

            tools::Long nTempWidth = nDotWidth;
            tools::Long nEnd = nLeft+nWidth;
            while ( nLeft < nEnd )
            {
                if ( nLeft+nTempWidth > nEnd )
                    nTempWidth = nEnd-nLeft;

                ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos, nTempWidth, nLineHeight );
                nLeft += nDotWidth*2;
            }
        }
        break;
    case LINESTYLE_DASH:
    case LINESTYLE_LONGDASH:
    case LINESTYLE_BOLDDASH:
    case LINESTYLE_BOLDLONGDASH:
        {
            tools::Long nDotWidth = nLineHeight*mnDPIY;
            nDotWidth += mnDPIY/2;
            nDotWidth /= mnDPIY;

            tools::Long nMinDashWidth;
            tools::Long nMinSpaceWidth;
            tools::Long nSpaceWidth;
            tools::Long nDashWidth;
            if ( (eTextLine == LINESTYLE_LONGDASH) ||
                 (eTextLine == LINESTYLE_BOLDLONGDASH) )
            {
                nMinDashWidth = nDotWidth*6;
                nMinSpaceWidth = nDotWidth*2;
                nDashWidth = 200;
                nSpaceWidth = 100;
            }
            else
            {
                nMinDashWidth = nDotWidth*4;
                nMinSpaceWidth = (nDotWidth*150)/100;
                nDashWidth = 100;
                nSpaceWidth = 50;
            }
            nDashWidth = o3tl::convert(nDashWidth * mnDPIX, o3tl::Length::mm100, o3tl::Length::in);
            nSpaceWidth = o3tl::convert(nSpaceWidth * mnDPIX, o3tl::Length::mm100, o3tl::Length::in);
            // DashWidth will be increased if the line is getting too thick
            // in proportion to the line's length
            if ( nDashWidth < nMinDashWidth )
                nDashWidth = nMinDashWidth;
            if ( nSpaceWidth < nMinSpaceWidth )
                nSpaceWidth = nMinSpaceWidth;

            tools::Long nTempWidth = nDashWidth;
            tools::Long nEnd = nLeft+nWidth;
            while ( nLeft < nEnd )
            {
                if ( nLeft+nTempWidth > nEnd )
                    nTempWidth = nEnd-nLeft;
                ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos, nTempWidth, nLineHeight );
                nLeft += nDashWidth+nSpaceWidth;
            }
        }
        break;
    case LINESTYLE_DASHDOT:
    case LINESTYLE_BOLDDASHDOT:
        {
            tools::Long nDotWidth = nLineHeight*mnDPIY;
            nDotWidth += mnDPIY/2;
            nDotWidth /= mnDPIY;

            tools::Long nDashWidth = o3tl::convert(100 * mnDPIX, o3tl::Length::mm100, o3tl::Length::in);
            tools::Long nMinDashWidth = nDotWidth*4;
            // DashWidth will be increased if the line is getting too thick
            // in proportion to the line's length
            if ( nDashWidth < nMinDashWidth )
                nDashWidth = nMinDashWidth;

            tools::Long nTempDotWidth = nDotWidth;
            tools::Long nTempDashWidth = nDashWidth;
            tools::Long nEnd = nLeft+nWidth;
            while ( nLeft < nEnd )
            {
                if ( nLeft+nTempDotWidth > nEnd )
                    nTempDotWidth = nEnd-nLeft;

                ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos, nTempDotWidth, nLineHeight );
                nLeft += nDotWidth*2;
                if ( nLeft > nEnd )
                    break;

                if ( nLeft+nTempDashWidth > nEnd )
                    nTempDashWidth = nEnd-nLeft;

                ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos, nTempDashWidth, nLineHeight );
                nLeft += nDashWidth+nDotWidth;
            }
        }
        break;
    case LINESTYLE_DASHDOTDOT:
    case LINESTYLE_BOLDDASHDOTDOT:
        {
            tools::Long nDotWidth = nLineHeight*mnDPIY;
            nDotWidth += mnDPIY/2;
            nDotWidth /= mnDPIY;

            tools::Long nDashWidth = o3tl::convert(100 * mnDPIX, o3tl::Length::mm100, o3tl::Length::in);
            tools::Long nMinDashWidth = nDotWidth*4;
            // DashWidth will be increased if the line is getting too thick
            // in proportion to the line's length
            if ( nDashWidth < nMinDashWidth )
                nDashWidth = nMinDashWidth;

            tools::Long nTempDotWidth = nDotWidth;
            tools::Long nTempDashWidth = nDashWidth;
            tools::Long nEnd = nLeft+nWidth;
            while ( nLeft < nEnd )
            {
                if ( nLeft+nTempDotWidth > nEnd )
                    nTempDotWidth = nEnd-nLeft;

                ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos, nTempDotWidth, nLineHeight );
                nLeft += nDotWidth*2;
                if ( nLeft > nEnd )
                    break;

                if ( nLeft+nTempDotWidth > nEnd )
                    nTempDotWidth = nEnd-nLeft;

                ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos, nTempDotWidth, nLineHeight );
                nLeft += nDotWidth*2;
                if ( nLeft > nEnd )
                    break;

                if ( nLeft+nTempDashWidth > nEnd )
                    nTempDashWidth = nEnd-nLeft;

                ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos, nTempDashWidth, nLineHeight );
                nLeft += nDashWidth+nDotWidth;
            }
        }
        break;
    default:
        break;
    }
}

void OutputDevice::ImplDrawStrikeoutLine( tools::Long nBaseX, tools::Long nBaseY,
                                          tools::Long nDistX, tools::Long nDistY, tools::Long nWidth,
                                          FontStrikeout eStrikeout,
                                          Color aColor )
{
    LogicalFontInstance*  pFontInstance = mpFontInstance.get();
    tools::Long            nLineHeight = 0;
    tools::Long            nLinePos  = 0;
    tools::Long            nLinePos2 = 0;

    tools::Long nY = nDistY;

    if ( eStrikeout > STRIKEOUT_LAST )
        eStrikeout = STRIKEOUT_SINGLE;

    switch ( eStrikeout )
    {
    case STRIKEOUT_SINGLE:
        nLineHeight = pFontInstance->mxFontMetric->GetStrikeoutSize();
        nLinePos    = nY + pFontInstance->mxFontMetric->GetStrikeoutOffset();
        break;
    case STRIKEOUT_BOLD:
        nLineHeight = pFontInstance->mxFontMetric->GetBoldStrikeoutSize();
        nLinePos    = nY + pFontInstance->mxFontMetric->GetBoldStrikeoutOffset();
        break;
    case STRIKEOUT_DOUBLE:
        nLineHeight = pFontInstance->mxFontMetric->GetDoubleStrikeoutSize();
        nLinePos    = nY + pFontInstance->mxFontMetric->GetDoubleStrikeoutOffset1();
        nLinePos2   = nY + pFontInstance->mxFontMetric->GetDoubleStrikeoutOffset2();
        break;
    default:
        break;
    }

    if ( !nLineHeight )
        return;

    if ( mbLineColor || mbInitLineColor )
    {
        mpGraphics->SetLineColor();
        mbInitLineColor = true;
    }
    mpGraphics->SetFillColor( aColor );
    mbInitFillColor = true;

    const tools::Long& nLeft = nDistX;

    switch ( eStrikeout )
    {
    case STRIKEOUT_SINGLE:
    case STRIKEOUT_BOLD:
        ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos, nWidth, nLineHeight );
        break;
    case STRIKEOUT_DOUBLE:
        ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos, nWidth, nLineHeight );
        ImplDrawTextRect( nBaseX, nBaseY, nLeft, nLinePos2, nWidth, nLineHeight );
        break;
    default:
        break;
    }
}

void OutputDevice::ImplDrawStrikeoutChar( tools::Long nBaseX, tools::Long nBaseY,
                                          tools::Long nDistX, tools::Long nDistY, tools::Long nWidth,
                                          FontStrikeout eStrikeout,
                                          Color aColor )
{
    // See qadevOOo/testdocs/StrikeThrough.odt for examples if you need
    // to tweak this
    if (!nWidth)
        return;

    // prepare string for strikeout measurement
    const char cStrikeoutChar =  eStrikeout == STRIKEOUT_SLASH ? '/' : 'X';
    static const int nTestStrLen = 4;
    static const int nMaxStrikeStrLen = 2048;
    sal_Unicode aChars[nMaxStrikeStrLen+1]; // +1 for valgrind...

    for( int i = 0; i < nTestStrLen; ++i)
        aChars[i] = cStrikeoutChar;

    const OUString aStrikeoutTest(aChars, nTestStrLen);

    // calculate approximation of strikeout atom size
    tools::Long nStrikeoutWidth = 0;
    std::unique_ptr<SalLayout> pLayout = ImplLayout( aStrikeoutTest, 0, nTestStrLen );
    if( pLayout )
    {
        nStrikeoutWidth = pLayout->GetTextWidth() / nTestStrLen;
    }
    if( nStrikeoutWidth <= 0 ) // sanity check
        return;

    int nStrikeStrLen = (nWidth+(nStrikeoutWidth-1)) / nStrikeoutWidth;
    if( nStrikeStrLen > nMaxStrikeStrLen )
        nStrikeStrLen = nMaxStrikeStrLen;
    else if (nStrikeStrLen < 0)
        nStrikeStrLen = 0;

    // build the strikeout string
    for( int i = nTestStrLen; i < nStrikeStrLen; ++i)
        aChars[i] = cStrikeoutChar;

    const OUString aStrikeoutText(aChars, nStrikeStrLen);

    if( mpFontInstance->mnOrientation )
    {
        Point aOriginPt(0, 0);
        aOriginPt.RotateAround( nDistX, nDistY, mpFontInstance->mnOrientation );
    }

    nBaseX += nDistX;
    nBaseY += nDistY;

    // strikeout text has to be left aligned
    vcl::text::ComplexTextLayoutFlags nOrigTLM = mnTextLayoutMode;
    mnTextLayoutMode = vcl::text::ComplexTextLayoutFlags::BiDiStrong;
    pLayout = ImplLayout( aStrikeoutText, 0, aStrikeoutText.getLength() );
    mnTextLayoutMode = nOrigTLM;

    if( !pLayout )
        return;

    // draw the strikeout text
    const Color aOldColor = GetTextColor();
    SetTextColor( aColor );
    ImplInitTextColor();

    pLayout->DrawBase() = basegfx::B2DPoint(nBaseX + mnTextOffX, nBaseY + mnTextOffY);

    tools::Rectangle aPixelRect;
    aPixelRect.SetLeft( nBaseX+mnTextOffX );
    aPixelRect.SetRight( aPixelRect.Left()+nWidth );
    aPixelRect.SetBottom( nBaseY+mpFontInstance->mxFontMetric->GetDescent() );
    aPixelRect.SetTop( nBaseY-mpFontInstance->mxFontMetric->GetAscent() );

    if (mpFontInstance->mnOrientation)
    {
        tools::Polygon aPoly( aPixelRect );
        aPoly.Rotate( Point(nBaseX+mnTextOffX, nBaseY+mnTextOffY), mpFontInstance->mnOrientation);
        aPixelRect = aPoly.GetBoundRect();
    }

    Push( vcl::PushFlags::CLIPREGION );
    IntersectClipRegion( PixelToLogic(aPixelRect) );
    if( mbInitClipRegion )
        InitClipRegion();

    pLayout->DrawText( *mpGraphics );

    Pop();

    SetTextColor( aOldColor );
    ImplInitTextColor();
}

void OutputDevice::ImplDrawTextLine( tools::Long nX, tools::Long nY,
                                     tools::Long nDistX, double nWidth,
                                     double nLayoutWidth,
                                     FontStrikeout eStrikeout,
                                     FontLineStyle eUnderline,
                                     FontLineStyle eOverline,
                                     bool bUnderlineAbove )
{
    if ( !nWidth )
        return;

    Color aStrikeoutColor = GetTextColor();
    Color aUnderlineColor = GetTextLineColor();
    Color aOverlineColor  = GetOverlineColor();
    bool bStrikeoutDone = false;
    bool bUnderlineDone = false;
    bool bOverlineDone  = false;

    if ( IsRTLEnabled() )
    {
        tools::Long nXAdd = nWidth - nDistX;
        if( mpFontInstance->mnOrientation )
            nXAdd = basegfx::fround<tools::Long>( nXAdd * cos( toRadians(mpFontInstance->mnOrientation) ) );

        nX += nXAdd - 1;
    }

    if ( !IsTextLineColor() )
        aUnderlineColor = GetTextColor();

    if ( !IsOverlineColor() )
        aOverlineColor = GetTextColor();

    if ( (eUnderline == LINESTYLE_SMALLWAVE) ||
         (eUnderline == LINESTYLE_WAVE) ||
         (eUnderline == LINESTYLE_DOUBLEWAVE) ||
         (eUnderline == LINESTYLE_BOLDWAVE) )
    {
        ImplDrawWaveTextLine( nX, nY, nDistX, 0, nWidth, nLayoutWidth, eUnderline, aUnderlineColor, bUnderlineAbove );
        bUnderlineDone = true;
    }
    if ( (eOverline == LINESTYLE_SMALLWAVE) ||
         (eOverline == LINESTYLE_WAVE) ||
         (eOverline == LINESTYLE_DOUBLEWAVE) ||
         (eOverline == LINESTYLE_BOLDWAVE) )
    {
        ImplDrawWaveTextLine( nX, nY, nDistX, 0, nWidth, nLayoutWidth, eOverline, aOverlineColor, true );
        bOverlineDone = true;
    }

    if ( (eStrikeout == STRIKEOUT_SLASH) ||
         (eStrikeout == STRIKEOUT_X) )
    {
        ImplDrawStrikeoutChar( nX, nY, nDistX, 0, nWidth, eStrikeout, aStrikeoutColor );
        bStrikeoutDone = true;
    }

    if ( !bUnderlineDone )
        ImplDrawStraightTextLine( nX, nY, nDistX, 0, nWidth, eUnderline, aUnderlineColor, bUnderlineAbove );

    if ( !bOverlineDone )
        ImplDrawStraightTextLine( nX, nY, nDistX, 0, nWidth, eOverline, aOverlineColor, true );

    if ( !bStrikeoutDone )
        ImplDrawStrikeoutLine( nX, nY, nDistX, 0, nWidth, eStrikeout, aStrikeoutColor );
}

void OutputDevice::ImplDrawTextLines( SalLayout& rSalLayout, FontStrikeout eStrikeout,
                                      FontLineStyle eUnderline, FontLineStyle eOverline,
                                      bool bWordLine, bool bUnderlineAbove )
{
    double nLayoutWidth = rSalLayout.GetTextWidth();
    if( bWordLine )
    {
        // draw everything relative to the layout base point
        const basegfx::B2DPoint aStartPt = rSalLayout.DrawBase();

        // calculate distance of each word from the base point
        basegfx::B2DPoint aPos;
        double nDist = 0;
        double nWidth = 0;
        const GlyphItem* pGlyph;
        int nStart = 0;
        while (rSalLayout.GetNextGlyph(&pGlyph, aPos, nStart))
        {
            // calculate the boundaries of each word
            if (!pGlyph->IsSpacing())
            {
                if( !nWidth )
                {
                    // get the distance to the base point (as projected to baseline)
                    nDist = aPos.getX() - aStartPt.getX();
                    if( mpFontInstance->mnOrientation )
                    {
                        const double nDY = aPos.getY() - aStartPt.getY();
                        const double fRad = toRadians(mpFontInstance->mnOrientation);
                        nDist = basegfx::fround<tools::Long>(nDist * cos(fRad) - nDY * sin(fRad));
                    }
                }

                // update the length of the textline
                nWidth += pGlyph->newWidth();
            }
            else if( nWidth > 0 )
            {
                // draw the textline for each word
                ImplDrawTextLine( aStartPt.getX(), aStartPt.getY(), nDist, nWidth, nLayoutWidth,
                                  eStrikeout, eUnderline, eOverline, bUnderlineAbove );
                nWidth = 0;
            }
        }

        // draw textline for the last word
        if( nWidth > 0 )
        {
            ImplDrawTextLine( aStartPt.getX(), aStartPt.getY(), nDist, nWidth, nLayoutWidth,
                              eStrikeout, eUnderline, eOverline, bUnderlineAbove );
        }
    }
    else
    {
        basegfx::B2DPoint aStartPt = rSalLayout.GetDrawPosition();
        ImplDrawTextLine( aStartPt.getX(), aStartPt.getY(), 0,
                          nLayoutWidth, nLayoutWidth,
                          eStrikeout, eUnderline, eOverline, bUnderlineAbove );
    }
}

void OutputDevice::ImplDrawMnemonicLine( tools::Long nX, tools::Long nY, tools::Long nWidth )
{
    tools::Long nBaseX = nX;
    if( /*HasMirroredGraphics() &&*/ IsRTLEnabled() )
    {
        // revert the hack that will be done later in ImplDrawTextLine
        nX = nBaseX - nWidth - (nX - nBaseX - 1);
    }

    ImplDrawTextLine( nX, nY, 0, nWidth, nWidth, STRIKEOUT_NONE, LINESTYLE_SINGLE, LINESTYLE_NONE, false );
}

void OutputDevice::SetTextLineColor()
{
    if ( mpMetaFile )
        mpMetaFile->AddAction( new MetaTextLineColorAction( Color(), false ) );

    maTextLineColor = COL_TRANSPARENT;
}

void OutputDevice::SetTextLineColor( const Color& rColor )
{
    Color aColor(vcl::drawmode::GetTextColor(rColor, GetDrawMode(), GetSettings().GetStyleSettings()));

    if ( mpMetaFile )
        mpMetaFile->AddAction( new MetaTextLineColorAction( aColor, true ) );

    maTextLineColor = aColor;
}

void OutputDevice::SetOverlineColor()
{
    if ( mpMetaFile )
        mpMetaFile->AddAction( new MetaOverlineColorAction( Color(), false ) );

    maOverlineColor = COL_TRANSPARENT;
}

void OutputDevice::SetOverlineColor( const Color& rColor )
{
    Color aColor(vcl::drawmode::GetTextColor(rColor, GetDrawMode(), GetSettings().GetStyleSettings()));

    if ( mpMetaFile )
        mpMetaFile->AddAction( new MetaOverlineColorAction( aColor, true ) );

    maOverlineColor = aColor;
}

void OutputDevice::DrawTextLine( const Point& rPos, tools::Long nWidth,
                                 FontStrikeout eStrikeout,
                                 FontLineStyle eUnderline,
                                 FontLineStyle eOverline )
{
    assert(!is_double_buffered_window());

    if ( mpMetaFile )
        mpMetaFile->AddAction( new MetaTextLineAction( rPos, nWidth, eStrikeout, eUnderline, eOverline ) );

    if ( ((eUnderline == LINESTYLE_NONE) || (eUnderline == LINESTYLE_DONTKNOW)) &&
         ((eOverline  == LINESTYLE_NONE) || (eOverline  == LINESTYLE_DONTKNOW)) &&
         ((eStrikeout == STRIKEOUT_NONE) || (eStrikeout == STRIKEOUT_DONTKNOW)) )
    {
        return;
    }
    if ( !IsDeviceOutputNecessary() || ImplIsRecordLayout() )
        return;

    if( mbInitClipRegion )
        InitClipRegion();

    if( mbOutputClipped )
        return;

    // initialize font if needed to get text offsets
    // TODO: only needed for mnTextOff!=(0,0)
    if (!InitFont())
        return;

    Point aPos = ImplLogicToDevicePixel( rPos );
    double fWidth = ImplLogicWidthToDeviceSubPixel(nWidth);
    aPos += Point( mnTextOffX, mnTextOffY );
    ImplDrawTextLine( aPos.X(), aPos.X(), 0, fWidth, fWidth, eStrikeout, eUnderline, eOverline, /*bUnderlineAbove*/false );
}

void OutputDevice::DrawWaveLine(const Point& rStartPos, const Point& rEndPos, tools::Long nLineWidth, tools::Long nWaveHeight)
{
    assert(!is_double_buffered_window());

    if ( !IsDeviceOutputNecessary() || ImplIsRecordLayout() )
        return;

    // we need a graphics
    if( !mpGraphics && !AcquireGraphics() )
        return;
    assert(mpGraphics);

    if ( mbInitClipRegion )
        InitClipRegion();

    if ( mbOutputClipped )
        return;

    if (!InitFont())
        return;

    Point aStartPt = ImplLogicToDevicePixel(rStartPos);
    Point aEndPt = ImplLogicToDevicePixel(rEndPos);

    tools::Long nStartX = aStartPt.X();
    tools::Long nStartY = aStartPt.Y();
    tools::Long nEndX = aEndPt.X();
    tools::Long nEndY = aEndPt.Y();
    double fOrientation = 0.0;

    // handle rotation
    if (nStartY != nEndY || nStartX > nEndX)
    {
        fOrientation = basegfx::rad2deg(std::atan2(nStartY - nEndY, nEndX - nStartX));
        // un-rotate the end point
        aStartPt.RotateAround(nEndX, nEndY, Degree10(static_cast<sal_Int16>(-fOrientation * 10.0)));
    }

    // Handle HiDPI
    float fScaleFactor = GetDPIScaleFactor();
    if (fScaleFactor > 1.0f)
    {
        nWaveHeight *= fScaleFactor;

        nStartY += fScaleFactor - 1; // Shift down additional pixel(s) to create more visual separation.

        // odd heights look better than even
        if (nWaveHeight % 2 == 0)
        {
            nWaveHeight--;
        }
    }

    // #109280# make sure the waveline does not exceed the descent to avoid paint problems
    LogicalFontInstance* pFontInstance = mpFontInstance.get();
    if (nWaveHeight > pFontInstance->mxFontMetric->GetWavelineUnderlineSize()
    // tdf#153223 polyline with lineheight >0 not drawn when skia is off
#ifdef MACOSX
        || !SkiaHelper::isVCLSkiaEnabled()
#endif
       )
    {
        nWaveHeight = pFontInstance->mxFontMetric->GetWavelineUnderlineSize();
        // tdf#124848 hairline
        nLineWidth = 0;
    }

    if ( fOrientation == 0.0 )
    {
        static tools::DeleteOnDeinit< WavyLineCache > snLineCache {};
        if ( !snLineCache.get() )
            return;
        WavyLineCache& rLineCache = *snLineCache.get();
        Bitmap aWavylinebmp;
        if ( !rLineCache.find( GetLineColor(), nLineWidth, nWaveHeight, nEndX - nStartX, aWavylinebmp ) )
        {
            size_t nWordLength = nEndX - nStartX;
            // start with something big to avoid updating it frequently
            nWordLength = nWordLength < 1024 ? 1024 : nWordLength;
            ScopedVclPtrInstance< VirtualDevice > pVirtDev( *this, DeviceFormat::WITH_ALPHA );
            pVirtDev->SetOutputSizePixel( Size( nWordLength, nWaveHeight * 2 ), false );
            pVirtDev->SetLineColor( GetLineColor() );
            pVirtDev->SetBackground( Wallpaper( COL_TRANSPARENT ) );
            pVirtDev->Erase();
            pVirtDev->SetAntialiasing( AntialiasingFlags::Enable );
            pVirtDev->ImplDrawWaveLineBezier( 0, 0, nWordLength, 0, nWaveHeight, fOrientation, nLineWidth );
            Bitmap aBitmap(pVirtDev->GetBitmap(Point(0, 0), pVirtDev->GetOutputSize()));

            rLineCache.insert( aBitmap, GetLineColor(), nLineWidth, nWaveHeight, nWordLength, aWavylinebmp );
        }
        if ( aWavylinebmp.ImplGetSalBitmap() != nullptr )
        {
            Size _size( nEndX - nStartX, aWavylinebmp.GetSizePixel().Height() );
            DrawBitmap(Point( rStartPos.X(), rStartPos.Y() ), PixelToLogic( _size ), Point(), _size, aWavylinebmp);
        }
        return;
    }

    ImplDrawWaveLineBezier( nStartX, nStartY, nEndX, nEndY, nWaveHeight, fOrientation, nLineWidth );
}

void OutputDevice::ImplDrawWaveLineBezier(tools::Long nStartX, tools::Long nStartY, tools::Long nEndX, tools::Long nEndY, tools::Long nWaveHeight, double fOrientation, tools::Long nLineWidth)
{
    // we need a graphics
    if( !mpGraphics && !AcquireGraphics() )
        return;
    assert(mpGraphics);

    if ( mbInitClipRegion )
        InitClipRegion();

    if ( mbOutputClipped )
        return;

    if (!InitFont())
        return;

    const basegfx::B2DRectangle aWaveLineRectangle(nStartX, nStartY, nEndX, nEndY + nWaveHeight);
    const basegfx::B2DPolygon aWaveLinePolygon = basegfx::createWaveLinePolygon(aWaveLineRectangle);
    const basegfx::B2DHomMatrix aRotationMatrix = basegfx::utils::createRotateAroundPoint(nStartX, nStartY, basegfx::deg2rad(-fOrientation));
    const bool bPixelSnapHairline(mnAntialiasing & AntialiasingFlags::PixelSnapHairline);

    mpGraphics->SetLineColor(GetLineColor());
    mpGraphics->DrawPolyLine(
            aRotationMatrix,
            aWaveLinePolygon,
            0.0,
            nLineWidth,
            nullptr, // MM01
            basegfx::B2DLineJoin::NONE,
            css::drawing::LineCap_BUTT,
            basegfx::deg2rad(15.0),
            bPixelSnapHairline,
            *this);
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
