/* -*- 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 <com/sun/star/i18n/ScriptType.hpp>
#include <com/sun/star/i18n/XBreakIterator.hpp>
#include <com/sun/star/uno/Reference.hxx>
#include <editeng/unolingu.hxx>
#include <i18nlangtag/mslangid.hxx>
#include <breakit.hxx>
#include <hintids.hxx>
#include <EnhancedPDFExportHelper.hxx>
#include <SwPortionHandler.hxx>
#include "porlay.hxx"
#include "inftxt.hxx"
#include "guess.hxx"
#include "porfld.hxx"
#include <pagefrm.hxx>
#include <tgrditem.hxx>
#include <IDocumentSettingAccess.hxx>
#include <IDocumentMarkAccess.hxx>

#include <IMark.hxx>
#include <pam.hxx>
#include <doc.hxx>
#include <o3tl/temporary.hxx>
#include <xmloff/odffields.hxx>
#include <viewopt.hxx>

using namespace ::sw::mark;
using namespace ::com::sun::star;
using namespace ::com::sun::star::i18n::ScriptType;

static TextFrameIndex lcl_AddSpace_Latin(const SwTextSizeInfo& rInf, const OUString* pStr,
                                         const SwLinePortion& rPor, TextFrameIndex nPos,
                                         TextFrameIndex nEnd, const SwScriptInfo* pSI,
                                         sal_uInt8 nScript);

// Returns for how many characters an extra space has to be added
// (for justified alignment).
static TextFrameIndex lcl_AddSpace(const SwTextSizeInfo &rInf,
        const OUString* pStr, const SwLinePortion& rPor)
{
    TextFrameIndex nPos, nEnd;
    const SwScriptInfo* pSI = nullptr;

    if ( pStr )
    {
        // passing a string means we are inside a field
        nPos = TextFrameIndex(0);
        nEnd = TextFrameIndex(pStr->getLength());
    }
    else
    {
        nPos = rInf.GetIdx();
        nEnd = rInf.GetIdx() + rPor.GetLen();
        pStr = &rInf.GetText();
        pSI = &const_cast<SwParaPortion*>(rInf.GetParaPortion())->GetScriptInfo();
    }

    TextFrameIndex nCnt(0);
    sal_uInt8 nScript = 0;

    // If portion consists of Asian characters and language is not
    // Korean, we add extra space to each character.
    // first we get the script type
    if ( pSI )
        nScript = pSI->ScriptType( nPos );
    else
        nScript = static_cast<sal_uInt8>(
            g_pBreakIt->GetBreakIter()->getScriptType(*pStr, sal_Int32(nPos)));

    // Note: rInf.GetIdx() can differ from nPos,
    // e.g., when rPor is a field portion. nPos refers to the string passed
    // to the function, rInf.GetIdx() refers to the original string.

    // We try to find out which justification mode is required. This is done by
    // evaluating the script type and the language attribute set for this portion

    // Asian Justification: Each character get some extra space
    if ( nEnd > nPos && ASIAN == nScript )
    {
        LanguageType aLang =
            rInf.GetTextFrame()->GetLangOfChar(rInf.GetIdx(), nScript);

        if (!MsLangId::isKorean(aLang))
        {
            const SwLinePortion* pPor = rPor.GetNextPortion();
            if ( pPor && ( pPor->IsKernPortion() ||
                           pPor->IsControlCharPortion() ||
                           pPor->IsPostItsPortion() ) )
                pPor = pPor->GetNextPortion();

            nCnt += SwScriptInfo::CountCJKCharacters( *pStr, nPos, nEnd, aLang );

            if ( !pPor || pPor->IsHolePortion() || pPor->InFixMargGrp() ||
                  pPor->IsBreakPortion() )
                --nCnt;

            return nCnt;
        }
    }

    // Kashida Justification: Insert Kashidas
    if ( nEnd > nPos && pSI && COMPLEX == nScript )
    {
        if (pSI->ParagraphContainsKashidaScript()
            && SwScriptInfo::IsKashidaScriptText(*pStr, nPos, nEnd - nPos))
        {
            // tdf#163105: For kashida justification, also expand whitespace.
            return lcl_AddSpace_Latin(rInf, pStr, rPor, nPos, nEnd, pSI, nScript)
                   + TextFrameIndex{ pSI->CountKashidaPositions(nPos, nEnd) };
        }
    }

    // Thai Justification: Each character cell gets some extra space
    if ( nEnd > nPos && COMPLEX == nScript )
    {
        LanguageType aLang =
            rInf.GetTextFrame()->GetLangOfChar(rInf.GetIdx(), nScript);

        if ( LANGUAGE_THAI == aLang )
        {
            nCnt = SwScriptInfo::ThaiJustify(*pStr, nullptr, nPos, nEnd - nPos);

            const SwLinePortion* pPor = rPor.GetNextPortion();
            if ( pPor && ( pPor->IsKernPortion() ||
                           pPor->IsControlCharPortion() ||
                           pPor->IsPostItsPortion() ) )
                pPor = pPor->GetNextPortion();

            if ( nCnt && ( ! pPor || pPor->IsHolePortion() || pPor->InFixMargGrp() ) )
                --nCnt;

            return nCnt;
        }
    }

    return lcl_AddSpace_Latin(rInf, pStr, rPor, nPos, nEnd, pSI, nScript);
}

static TextFrameIndex lcl_AddSpace_Latin(const SwTextSizeInfo& rInf, const OUString* pStr,
                                         const SwLinePortion& rPor, TextFrameIndex nPos,
                                         TextFrameIndex nEnd, const SwScriptInfo* pSI,
                                         sal_uInt8 nScript)
{
    TextFrameIndex nCnt(0);

    // Here starts the good old "Look for blanks and add space to them" part.
    // Note: We do not want to add space to an isolated latin blank in front
    // of some complex characters in RTL environment
    const bool bDoNotAddSpace =
            LATIN == nScript && (nEnd == nPos + TextFrameIndex(1)) && pSI &&
            ( i18n::ScriptType::COMPLEX ==
              pSI->ScriptType(nPos + TextFrameIndex(1))) &&
            rInf.GetTextFrame() && rInf.GetTextFrame()->IsRightToLeft();

    if ( bDoNotAddSpace )
        return nCnt;

    TextFrameIndex nTextEnd = std::min(nEnd, TextFrameIndex(pStr->getLength()));
    for ( ; nPos < nTextEnd; ++nPos )
    {
        if (CH_BLANK == (*pStr)[ sal_Int32(nPos) ])
            ++nCnt;
    }

    // We still have to examine the next character:
    // If the next character is ASIAN and not KOREAN we have
    // to add an extra space
    // nPos refers to the original string, even if a field string has
    // been passed to this function
    nPos = rInf.GetIdx() + rPor.GetLen();
    if (nPos < TextFrameIndex(rInf.GetText().getLength()))
    {
        sal_uInt8 nNextScript = 0;
        const SwLinePortion* pPor = rPor.GetNextPortion();
        if ( pPor && pPor->IsKernPortion() )
            pPor = pPor->GetNextPortion();

        if (!pPor || pPor->InFixMargGrp())
            return nCnt;

        // next character is inside a field?
        if ( CH_TXTATR_BREAKWORD == rInf.GetChar( nPos ) && pPor->InExpGrp() )
        {
            bool bOldOnWin = rInf.OnWin();
            const_cast<SwTextSizeInfo &>(rInf).SetOnWin( false );

            OUString aStr;
            pPor->GetExpText( rInf, aStr );
            const_cast<SwTextSizeInfo &>(rInf).SetOnWin( bOldOnWin );

            nNextScript = static_cast<sal_uInt8>(g_pBreakIt->GetBreakIter()->getScriptType( aStr, 0 ));
        }
        else
            nNextScript = static_cast<sal_uInt8>(
                g_pBreakIt->GetBreakIter()->getScriptType(rInf.GetText(), sal_Int32(nPos)));

        if( ASIAN == nNextScript )
        {
            LanguageType aLang =
                rInf.GetTextFrame()->GetLangOfChar(nPos, nNextScript);

            if (!MsLangId::isKorean(aLang))
                ++nCnt;
        }
    }

    return nCnt;
}

static void GetLimitedStringPart(const SwTextFormatInfo& rInf, TextFrameIndex nIndex,
                                 TextFrameIndex nLength, sal_uInt16 nComp, SwTwips nOriginalWidth,
                                 SwTwips nMaxWidth, TextFrameIndex& rOutLength, SwTwips& rOutWidth)
{
    assert(nLength >= TextFrameIndex(0));
    const SwScriptInfo& rSI = rInf.GetParaPortion()->GetScriptInfo();
    rOutLength = nLength;
    rOutWidth = nOriginalWidth;
    if (nMaxWidth < 0)
        nMaxWidth = 0;
    while (rOutWidth > nMaxWidth)
    {
        TextFrameIndex nNewOnLineLengthGuess(rOutLength.get() * nMaxWidth / rOutWidth);
        assert(nNewOnLineLengthGuess < rOutLength);
        if (nNewOnLineLengthGuess < (rOutLength - TextFrameIndex(1)))
            ++nNewOnLineLengthGuess; // to avoid too aggressive decrease
        rOutLength = nNewOnLineLengthGuess;
        rInf.GetTextSize(&rSI, nIndex, rOutLength, std::nullopt, nComp, rOutWidth,
                         o3tl::temporary(tools::Long()), o3tl::temporary(SwTwips()),
                         o3tl::temporary(SwTwips()), rInf.GetCachedVclData().get());
    }
}

static bool IsPaintTrailingSpaceDecorations(const SwTextFormatInfo& rInf)
{
    // We underline (and show background of) the trailing whitespace, when either Word-compatible
    // trailing blanks are off, or underline Word-compatible trailing space is on.
    const auto& settings = rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess();
    return !settings.get(DocumentSettingId::MS_WORD_COMP_TRAILING_BLANKS)
           || settings.get(DocumentSettingId::MS_WORD_UL_TRAIL_SPACE);
}

SwTextPortion * SwTextPortion::CopyLinePortion(const SwLinePortion &rPortion)
{
    SwTextPortion *const pNew(new SwTextPortion);
    static_cast<SwLinePortion&>(*pNew) = rPortion;
    pNew->SetWhichPor( PortionType::Text ); // overwrite that!
    return pNew;
}

void SwTextPortion::BreakCut( SwTextFormatInfo &rInf, const SwTextGuess &rGuess )
{
    // The word/char is larger than the line
    // Special case 1: The word is larger than the line
    // We truncate ...
    const SwTwips nLineWidth = rInf.Width() - rInf.X();
    TextFrameIndex nLen = rGuess.CutPos() - rInf.GetIdx();
    if (nLen > TextFrameIndex(0))
    {
        // special case: guess does not always provide the correct
        // width, only in common cases.
        if ( !rGuess.BreakWidth() )
        {
            rInf.SetLen( nLen );
            SetLen( nLen );
            CalcTextSize( rInf );

            // changing these values requires also changing them in
            // guess.cxx
            SwTwips nItalic = 0;
            if( ITALIC_NONE != rInf.GetFont()->GetItalic() && !rInf.NotEOL() )
            {
                nItalic = Height() / 12;
            }
            Width( Width() + nItalic );
        }
        else
        {
            Width( rGuess.BreakWidth() );
            SetLen( nLen );
        }
    }
    // special case: first character does not fit to line
    else if ( rGuess.CutPos() == rInf.GetLineStart() )
    {
        SetLen( TextFrameIndex(1) );
        Width( nLineWidth );
    }
    else
    {
        SetLen( TextFrameIndex(0) );
        Width( 0 );
        ExtraShrunkWidth( 0 );
        ExtraSpaceSize( 0 );
        SetModifiedWidth( false );
    }
}

void SwTextPortion::BreakUnderflow( SwTextFormatInfo &rInf )
{
    Truncate();
    Height( 0 );
    Width( 0 );
    ExtraShrunkWidth( 0 );
    ExtraSpaceSize( 0 );
    SetModifiedWidth( false );
    SetLen( TextFrameIndex(0) );
    SetAscent( 0 );
    rInf.SetUnderflow( this );
}

static bool lcl_HasContent( const SwFieldPortion& rField, SwTextFormatInfo const &rInf )
{
    OUString aText;
    return rField.GetExpText( rInf, aText ) && !aText.isEmpty();
}

sal_uInt16 SwTextPortion::GetMaxComp(const SwTextFormatInfo& rInf) const
{
    const SwScriptInfo& rSI = rInf.GetParaPortion()->GetScriptInfo();
    return (SwFontScript::CJK == rInf.GetFont()->GetActual()) && rSI.CountCompChg()
                   && !rInf.IsMulti() && !InFieldGrp() && !IsDropPortion()
               ? 10000
               : 0;
}

void SwTextPortion::SetSpacing( SwTextFormatInfo &rInf, SwTextGuess const &rGuess,
                const sal_Int32 nSpaces, const sal_Int16 nWidthOf10Spaces )
{
    // TODO allow letter spacing and glyph scaling in single word lines, too
    if ( nSpaces == 0 )
        return;

    // count zero width characters to calculate correct letter spacing
    sal_Int32 nZeroWidthCharCount = 0;

    // sum width of the previous portions to calculate real line width
    SwTwips nStartWidth = 0;
    SwLineLayout *pLay = rInf.GetRoot();
    SwLinePortion *pPos = pLay;
    // first portion isn't redundant, if it's the only portion before the actual one: don't skip it
    if ( !pPos->GetNextPortion() && (
            ( pLay->GetLetterSpacing() && pPos->GetLen() ) ||
            pLay->GetScaleWidth() != 100 ) )
    {
        nStartWidth += pPos->PrtWidth();
    }
    for (pPos = pPos->GetNextPortion(); pPos; pPos = pPos->GetNextPortion())
    {
        if ( pPos->Width() == 0 )
            nZeroWidthCharCount += sal_Int32(pPos->GetLen());
        nStartWidth += pPos->PrtWidth();
    }
    // adjust hyphen: remove hyphen width from the line width, because
    // hyphen mark and optional text of the alternative hyphenation will
    // be in an extra portion
    sal_Int16 nHyphenWidth = 0;
    if( rGuess.HyphWord().is() && rGuess.BreakPos() > rInf.GetLineStart()
            && ( rGuess.BreakPos() > rInf.GetIdx() ||
               ( rInf.GetLast() && ! rInf.GetLast()->IsFlyPortion() ) ) )
    {
        // TODO support custom hyphen mark, also in SwHyphPortion/CreateHyphen()
        static constexpr OUString STR_HYPHEN = u"-"_ustr;
        const uno::Reference<  com::sun::star::linguistic2::XHyphenatedWord >&  xHyphWord = rGuess.HyphWord();
        // first case: hyphenated word has alternative spelling
        if ( xHyphWord->isAlternativeSpelling() )
        {
            SvxAlternativeSpelling aAltSpell = SvxGetAltSpelling( xHyphWord );
            OUString aAltText = aAltSpell.aReplacement;
            nHyphenWidth = rInf.GetTextSize(aAltText + STR_HYPHEN).Width();
        }
        else
            nHyphenWidth = rInf.GetTextSize(STR_HYPHEN).Width();
    }

    SvxAdjustItem aAdjustItem =
        rInf.GetTextFrame()->GetTextNodeForParaProps()->GetSwAttrSet().GetAdjust();
    // width of a single expanded space without letter spacing and glyph scaling,
    // When nStartWidth is non-zero, i.e. the line contains multiple text portions,
    // nStartWidth contains the length of the text portions before the last one,
    // and GetLineWidth() contains the remaining blank space of the line.
    // When nStartWidth is zero, i.e. the line is a single text portion,
    // GetLineWidth() contains the line width with the blank space, and GetBreakwidth()
    // is used for calculating the remaining blank space, except when the last portion
    // fills the remaining space
    SwTwips nBreakWidth = rGuess.BreakWidth();
    float fSpaceNormal = ( nStartWidth + rInf.GetLineWidth() - nHyphenWidth -
                   ( nStartWidth + nBreakWidth - nSpaces * nWidthOf10Spaces/10.0 ) ) / nSpaces;

    // the part to be removed: the previous width minus the maximum allowed space width
    float fExpansionOverMax =
        fSpaceNormal - nWidthOf10Spaces / 10.0 * aAdjustItem.GetPropWordSpacingMaximum() / 100.0;
    ExtraSpaceSize( fExpansionOverMax > 0 ? fExpansionOverMax : 0 );

    sal_Int32 nLetterCount = sal_Int32(rGuess.BreakPos()) -
            sal_Int32(rInf.GetLineStart()) - nZeroWidthCharCount;

    // letter spacing/character to be added or subtracted to get the desired word spacing
    float fLetterSpacingForDesiredWordSpacing =
        nLetterCount > 0 ? (((fSpaceNormal - nWidthOf10Spaces/10.0) * nSpaces) / nLetterCount) : 0;
    // letter spacing/character allowed by maximum letter spacing
    float fMaximumLetterSpacing =
        nWidthOf10Spaces / 10.0 * aAdjustItem.GetPropLetterSpacingMaximum() / 100.0;
    // final letter spacing/character based on the desired word spacing and maximum letter spacing
    // TODO fix resolution applying 1/100 twips instead of 1 twip
    SwTwips nLetterSpacing = std::min( fLetterSpacingForDesiredWordSpacing, fMaximumLetterSpacing );
    // at shrinking by letter spacing, avoid overshrinking
    if ( nLetterSpacing < 0 )
    {
        float fMinimumLetterSpacing =
            nWidthOf10Spaces / 10.0 * aAdjustItem.GetPropLetterSpacingMinimum() / 100.0;
        nLetterSpacing = std::max( fLetterSpacingForDesiredWordSpacing, fMinimumLetterSpacing );
    }

    // don't set letter spacing for multiportion lines, where the last portion
    // does not continue in the next line, because if it is not fit the line e.g. during typing,
    // it could result freezing
    if ( nBreakWidth == 0 )
        nLetterSpacing = 0;

    // full width of the extra (rounded) letter spacing within the line
    // to adjust word spacing in SwTextAdjuster::CalcNewBlock()
    if ( aAdjustItem.GetPropLetterSpacingMinimum() != 100 ||
            aAdjustItem.GetPropLetterSpacingMaximum() != 100 )
        pLay->SetLetterSpacing(nLetterSpacing);
    else
        pLay->SetLetterSpacing(0);

    pLay->SetLetterCount(TextFrameIndex(nLetterCount));
    pLay->SetSpaceCount(nSpaces);

    // limit glyph scaling without optical sizing:
    // apply it only after applying maximum letter spacing
    // TODO: change this after variable font support
    if ( ( aAdjustItem.GetPropScaleWidthMaximum() != 100 ||
            aAdjustItem.GetPropScaleWidthMinimum() != 100 ) && nStartWidth + nBreakWidth > 0 )
    {
        pLay->SetScaleWidth( 100.0 *
            ( nStartWidth + rInf.GetLineWidth() - nHyphenWidth - nLetterSpacing * nLetterCount ) /
                         ( nStartWidth + nBreakWidth ) );
    }
    else
    {
        pLay->SetScaleWidth(100);
    }

    // set scaling limits
    if ( pLay->GetScaleWidth() > aAdjustItem.GetPropScaleWidthMaximum() )
        pLay->SetScaleWidth( aAdjustItem.GetPropScaleWidthMaximum() );

    if ( pLay->GetScaleWidth() < aAdjustItem.GetPropScaleWidthMinimum() )
        pLay->SetScaleWidth( aAdjustItem.GetPropScaleWidthMinimum() );

    if ( pLay->GetLetterSpacing() < 0 && aAdjustItem.GetPropLetterSpacingMinimum() >= 100 )
            pLay->SetLetterSpacing(100);

    // fix calculation problem, when letter spacing removed the available space
    if ( pLay->GetScaleWidth() < 100 && pLay->GetLetterSpacing() > 0 )
        pLay->SetScaleWidth(100);

    // space used by glyph scaling to adjust word spacing
    if (  ( aAdjustItem.GetPropScaleWidthMaximum() != 100 ||
            aAdjustItem.GetPropScaleWidthMinimum() != 100 ) &&
        // no need correction for multiportion lines,
        // where the portions have got corrected width
        nStartWidth == 0 )
    {
        pLay->SetScaleWidthSpacing( nBreakWidth * (pLay->GetScaleWidth() - 100.0) / 100.0 );
    }
    else
        pLay->SetScaleWidthSpacing(0);
}

bool SwTextPortion::Format_( SwTextFormatInfo &rInf )
{
    // restore original portion width without custom letter spacing and scaling
    SwLineLayout *pLay = rInf.GetRoot();
    for (SwLinePortion *pPos = pLay; pPos; pPos = pPos->GetNextPortion())
    {
        if ( pPos->Width() != 0 && pPos->IsModifiedWidth() && (
            ( pLay->GetLetterSpacing() && pPos->GetLen() ) || pLay->GetScaleWidth() != 100) )
        {
            pPos->Width( ( pPos->Width() - pLay->GetLetterSpacing() * sal_Int32(pPos->GetLen()) )
                            * 100.0 / pLay->GetScaleWidth() );
        }
        pPos->SetModifiedWidth(false);
    }

    SetModifiedWidth(false);
    pLay->SetLetterSpacing(0);
    pLay->SetScaleWidth(100);
    pLay->SetScaleWidthSpacing(0);

    // 5744: If only the hyphen does not fit anymore, we still need to wrap
    // the word, or else return true!
    if( rInf.IsUnderflow() && rInf.GetSoftHyphPos() )
    {
        // soft hyphen portion has triggered an underflow event because
        // of an alternative spelling position
        bool bFull = false;
        const bool bHyph = rInf.ChgHyph( true );
        if( rInf.IsHyphenate() )
        {
            SwTextGuess aGuess;
            // check for alternative spelling left from the soft hyphen
            // this should usually be true but
            aGuess.AlternativeSpelling(rInf, rInf.GetSoftHyphPos() - TextFrameIndex(1));
            bFull = CreateHyphen( rInf, aGuess );
            OSL_ENSURE( bFull, "Problem with hyphenation!!!" );
        }
        rInf.ChgHyph( bHyph );
        rInf.SetSoftHyphPos( TextFrameIndex(0) );
        return bFull;
    }

    ExtraShrunkWidth( 0 );
    ExtraSpaceSize( 0 );
    std::optional<SwTextGuess> pGuess(std::in_place);
    bool bFull = !pGuess->Guess( *this, rInf, Height() );

    // tdf#158776 for the last full text portion, call Guess() again to allow more text in the
    // adjusted line by shrinking spaces using the know space count from the first Guess() call
    SvxAdjustItem aAdjustItem = rInf.GetTextFrame()->GetTextNodeForParaProps()->GetSwAttrSet().GetAdjust();
    const SvxAdjust aAdjust = aAdjustItem.GetAdjust();
    bool bFullJustified = bFull && aAdjust == SvxAdjust::Block &&
         pGuess->BreakPos() != TextFrameIndex(COMPLETE_STRING);
    bool bInteropSmartJustify = bFullJustified &&
            rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(
                    DocumentSettingId::JUSTIFY_LINES_WITH_SHRINKING);
    bool bNoWordSpacing = aAdjustItem.GetPropWordSpacing() == 100 &&
                    aAdjustItem.GetPropWordSpacingMinimum() == 100 &&
                    aAdjustItem.GetPropWordSpacingMaximum() == 100 &&
                    aAdjustItem.GetPropScaleWidthMinimum() == 100 &&
                    aAdjustItem.GetPropScaleWidthMaximum() == 100 &&
                    aAdjustItem.GetPropLetterSpacingMinimum() == 0 &&
                    aAdjustItem.GetPropLetterSpacingMaximum() == 0;
    // support old ODT documents, where only JustifyLinesWithShrinking was set
    bool bOldInterop = bInteropSmartJustify && bNoWordSpacing;
    bool bWordSpacing = bFullJustified && (!bNoWordSpacing || bOldInterop);
    bool bWordSpacingMaximum = bWordSpacing && !bOldInterop &&
           aAdjustItem.GetPropWordSpacingMaximum() > aAdjustItem.GetPropWordSpacing();
    bool bWordSpacingMinimum = bWordSpacing && ( bOldInterop ||
           aAdjustItem.GetPropWordSpacingMinimum() < aAdjustItem.GetPropWordSpacing() );

    if ( ( bInteropSmartJustify || bWordSpacing || bWordSpacingMaximum || bWordSpacingMinimum ) &&
         // tdf#164499 no shrinking in tabulated line
         ( !rInf.GetLast() || !rInf.GetLast()->InTabGrp() ) &&
         // tdf#158436 avoid shrinking at underflow, e.g. no-break space after a
         // very short word resulted endless loop
         !rInf.IsUnderflow() )
    {
        sal_Int32 nSpacesInLine = rInf.GetLineSpaceCount( pGuess->BreakPos() );
        sal_Int32 nSpacesInLineOrig = nSpacesInLine;
        SwTextSizeInfo aOrigInf( rInf );

        // call with an extra space: shrinking can result a new word in the line
        // and a new space before that, which is also a shrank space
        // (except if the line was already broken inside a word with hyphenation)
        // TODO: handle the case, if the line contains extra amount of spaces
        if (
             // no automatic hyphenation
             !pGuess->HyphWord().is() &&
             // no hyphenation at soft hyphen
             pGuess->BreakPos() < TextFrameIndex(rInf.GetText().getLength()) &&
             rInf.GetText()[sal_Int32(pGuess->BreakPos())] != CHAR_SOFTHYPHEN )
        {
            ++nSpacesInLine;
        }

        // there are spaces in the line, so it's possible to shrink them
        if ( nSpacesInLineOrig > 0 )
        {
            SwTwips nOldWidth = pGuess->BreakWidth();
            bool bIsPortion = rInf.GetLineWidth() < rInf.GetBreakWidth();

            // measure ten spaces for higher precision
            static constexpr OUStringLiteral STR_BLANK = u"          ";
            sal_Int16 nSpaceWidth = rInf.GetTextSize(STR_BLANK).Width();
            sal_Int32 nRealSpaces = rInf.GetLineSpaceCount( pGuess->BreakPos() );
            float fSpaceNormal = (rInf.GetLineWidth() - (rInf.GetBreakWidth() - nRealSpaces * nSpaceWidth/10.0))/nRealSpaces;

            bool bOrigHyphenated = pGuess->HyphWord().is() &&
                        pGuess->BreakPos() > rInf.GetLineStart();

            // calculate available word spacing for letter spacing, and for the word spacing indicator
            SetSpacing(rInf, *pGuess, nRealSpaces, nSpaceWidth);

            // calculate line breaking with desired word spacing, also
            // if the desired word spacing is 100%, but there is a greater
            // maximum word spacing, and the word is hyphenated at the desired
            // word spacing: to skip hyphenation, if the maximum word spacing allows it
            if ( bWordSpacing || ( bWordSpacingMaximum && bOrigHyphenated ) )
            {
                pGuess.emplace();
                bFull = !pGuess->Guess( *this, rInf, Height(), nSpacesInLine, aAdjustItem.GetPropWordSpacing(), nSpaceWidth );
                sal_Int32 nSpacesInLine2 = rInf.GetLineSpaceCount( pGuess->BreakPos() );
                if ( aAdjustItem.GetPropLetterSpacingMinimum() < 0 || rInf.GetBreakWidth() <= rInf.GetLineWidth() )
                {
                    fSpaceNormal = (rInf.GetLineWidth() - (rInf.GetBreakWidth() - nSpacesInLine2 * nSpaceWidth/10.0))/nSpacesInLine2;
                    SetSpacing(rInf, *pGuess, nSpacesInLine2, nSpaceWidth);
                }
            }

            sal_Int32 nSpacesInLineShrink = 0;
            // TODO if both maximum word spacing or minimum word spacing can disable hyphenation, prefer the last one
            if ( bWordSpacingMinimum )
            {
                std::optional<SwTextGuess> pGuess2(std::in_place);
                SwTwips nOldExtraSpace = rInf.GetExtraSpace();
                // break the line after the hyphenated word, if it's possible
                // (hyphenation is disabled in Guess(), when called with GetPropWordSpacingMinimum())
                sal_uInt16 nMinimum = bOldInterop ? 75 : aAdjustItem.GetPropWordSpacingMinimum();
                bool bFull2 = !pGuess2->Guess( *this, rInf, Height(), nSpacesInLine, nMinimum, nSpaceWidth );
                nSpacesInLineShrink = rInf.GetLineSpaceCount( pGuess2->BreakPos() );
                if ( pGuess2->BreakWidth() > nOldWidth )
                {
                    // instead of the maximum shrinking, break after the word which was hyphenated before
                    sal_Int32 i = sal_Int32(pGuess->BreakPos());
                    sal_Int32 j = sal_Int32(pGuess2->BreakPos());
                    // skip terminal spaces
                    for (; i < j && rInf.GetText()[i] == CH_BLANK; ++i);
                    for (; j > i && rInf.GetText()[i] == CH_BLANK; --j);
                    sal_Int32 nOldBreakTrim = i;
                    sal_Int32 nOldBreak = j - i;
                    for (; i < j; ++i)
                    {
                        sal_Unicode cChar = rInf.GetText()[i];
                        // first space after the hyphenated word, and it's not the chosen one
                        if ( cChar == CH_BLANK )
                        {
                            // using a weighted word spacing, try to break the line after the hyphenated word
                            sal_Int32 nNewBreak = i - nOldBreakTrim;
                            SwTwips nWeightedSpacing = nMinimum * (1.0 * nNewBreak/nOldBreak) +
                                   aAdjustItem.GetPropWordSpacing() * (1.0 * (nOldBreak - nNewBreak)/nOldBreak);
                            std::optional<SwTextGuess> pGuess3(std::in_place);
                            pGuess3->Guess( *this, rInf, Height(), nSpacesInLineShrink-1, nWeightedSpacing, nSpaceWidth );

                            sal_Int32 nSpacesInLineShrink2 = rInf.GetLineSpaceCount( pGuess3->BreakPos() );
                            if ( nSpacesInLineShrink2 == nSpacesInLineShrink )
                            {
                                nNewBreak = i - nOldBreakTrim - 1;
                                nWeightedSpacing = nMinimum * (1.0 * nNewBreak/nOldBreak) +
                                    aAdjustItem.GetPropWordSpacing() * (1.0 * (nOldBreak - nNewBreak)/nOldBreak);
                                pGuess3->Guess( *this, rInf, Height(), nSpacesInLineShrink-1, nWeightedSpacing, nSpaceWidth );
                            }

                            if ( pGuess3->BreakWidth() > nOldWidth )
                            {
                                pGuess2.emplace();
                                pGuess2 = std::move(pGuess3);
                            }
                            break;
                        }
                    }

                    nSpacesInLineShrink = rInf.GetLineSpaceCount( pGuess2->BreakPos() );
                    if ( rInf.GetBreakWidth() > rInf.GetLineWidth() || bIsPortion )
                    {
                        float fExpansionWeight = static_cast<float>(1/1.7);
                        float fSpaceShrunk = nSpacesInLineShrink > 0
                                ? (rInf.GetLineWidth() - (rInf.GetBreakWidth() - nSpacesInLineShrink * nSpaceWidth/10.0))/nSpacesInLineShrink
                                : 1;
                        float z0 = (nSpaceWidth/10.0)/fSpaceShrunk;
                        float z1 = (nSpaceWidth/10.0+((fSpaceNormal-nSpaceWidth/10.0)*fExpansionWeight))/(nSpaceWidth/10.0);
                        // TODO shrink line portions only if needed
                        if ( z1 >= z0 || bIsPortion )
                        {
                            pGuess = std::move(pGuess2);
                            SetSpacing(rInf, *pGuess, nSpacesInLineShrink, nSpaceWidth);
                            bFull = bFull2;
                        }
                    }
                    else if ( bOldInterop )
                    {
                        pGuess = std::move(pGuess2);
                        ExtraSpaceSize(0);
                        bFull = bFull2;
                    }
                }
                else
                    // minimum word spacing is not applicable
                    rInf.SetExtraSpace(nOldExtraSpace);
            }

            // modify non-zero width portions with letter spacing
            SwLinePortion *pPos = pLay;
            // skip redundant first portion
            if ( pPos->GetNextPortion() )
                pPos = static_cast<SwLinePortion*>(pLay)->GetNextPortion();
            while( pPos )
            {
                // Note: GetLen() can be zero at Italics correction
                if ( pPos->Width() && !IsModifiedWidth() &&
                     ( pLay->GetLetterSpacing() || pLay->GetScaleWidth() != 100 ) )
                {
                    pPos->Width( pPos->Width() * pLay->GetScaleWidth()/100.0 +
                            pLay->GetLetterSpacing() * sal_Int32(pPos->GetLen()) );
                    pPos->SetModifiedWidth(true);
                }
                pPos = pPos->GetNextPortion();
            }

            if ( pGuess->BreakWidth() != nOldWidth )
            {
                ExtraShrunkWidth( pGuess->BreakWidth() );
                ExtraSpaceSize( 0 );
            }
        }
    }

    // these are the possible cases:
    // A Portion fits to current line
    // B Portion does not fit to current line but a possible line break
    //   within the portion has been found by the break iterator, 2 subcases
    //   B1 break is hyphen
    //   B2 break is word end
    // C Portion does not fit to current line and no possible line break
    //   has been found by break iterator, 2 subcases:
    //   C1 break iterator found a possible line break in portion before us
    //      ==> this break is used (underflow)
    //   C2 break iterator does not found a possible line break at all:
    //      ==> line break

    // case A: line not yet full
    if ( !bFull )
    {
        Width( pGuess->BreakWidth() );
        ExtraBlankWidth(pGuess->ExtraBlankWidth());
        // Caution!
        if( !InExpGrp() || InFieldGrp() )
            SetLen( rInf.GetLen() );

        short nKern = rInf.GetFont()->CheckKerning();
        if( nKern > 0 && rInf.Width() < rInf.X() + Width() + nKern )
        {
            nKern = static_cast<short>(rInf.Width() - rInf.X() - Width() - 1);
            if( nKern < 0 )
                nKern = 0;
        }
        if( nKern )
            new SwKernPortion( *this, nKern );
    }
    // special case: hanging portion
    else if( pGuess->GetHangingPortion() )
    {
        Width( pGuess->BreakWidth() );
        SetLen( pGuess->BreakPos() - rInf.GetIdx() );
        pGuess->GetHangingPortion()->SetAscent( GetAscent() );
        Insert( pGuess->ReleaseHangingPortion() );
    }
    // breakPos >= index
    else if (pGuess->BreakPos() >= rInf.GetIdx() && pGuess->BreakPos() != TextFrameIndex(COMPLETE_STRING))
    {
        // case B1
        if( pGuess->HyphWord().is() && pGuess->BreakPos() > rInf.GetLineStart()
            && ( pGuess->BreakPos() > rInf.GetIdx() ||
               ( rInf.GetLast() && ! rInf.GetLast()->IsFlyPortion() ) ) )
        {
            CreateHyphen( rInf, *pGuess );
            if ( rInf.GetFly() )
                rInf.GetRoot()->SetMidHyph( true );
            else
                rInf.GetRoot()->SetEndHyph( true );

            // modify width of last portion with letter spacing
            // if it's not a zero-width portion (i.e. a soft hyphen)
            // Note: GetLen() can be zero at Italics correction
            pLay = rInf.GetRoot();
            if ( Width() && !IsModifiedWidth() &&
                            ( pLay->GetLetterSpacing() || pLay->GetScaleWidth() != 100 ) )
            {
                Width( Width() * pLay->GetScaleWidth() / 100.0 +
                                pLay->GetLetterSpacing() * sal_Int32(GetLen()) );

                SetModifiedWidth(true);
            }
        }
        // case C1
        // - Footnote portions with fake line start (i.e., not at beginning of line)
        //   should keep together with the text portion. (Note: no keep together
        //   with only footnote portions.
        // - TabPortions not at beginning of line should keep together with the
        //   text portion, if they are not followed by a blank
        //   (work around different definition of tab stop character - breaking or
        //   non breaking character - in compatibility mode)
        else if ( ( IsFootnotePortion() && rInf.IsFakeLineStart() &&

                    rInf.IsOtherThanFootnoteInside() ) ||
                  ( rInf.GetLast() &&
                    rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::TAB_COMPAT) &&
                    rInf.GetLast()->InTabGrp() &&
                    rInf.GetLineStart() + rInf.GetLast()->GetLen() < rInf.GetIdx() &&
                    pGuess->BreakPos() == rInf.GetIdx()  &&
                    CH_BLANK != rInf.GetChar( rInf.GetIdx() ) &&
                    CH_FULL_BLANK != rInf.GetChar( rInf.GetIdx() ) &&
                    CH_SIX_PER_EM != rInf.GetChar( rInf.GetIdx() ) ) )
            BreakUnderflow( rInf );
        // case B2
        else if( rInf.GetIdx() > rInf.GetLineStart() ||
                 pGuess->BreakPos() > rInf.GetIdx() ||
                 // this is weird: during formatting the follow of a field
                 // the values rInf.GetIdx and rInf.GetLineStart are replaced
                 // IsFakeLineStart indicates GetIdx > GetLineStart
                 rInf.IsFakeLineStart() ||
                 rInf.GetFly() ||
                 rInf.IsFirstMulti() ||
                 ( rInf.GetLast() &&
                    ( rInf.GetLast()->IsFlyPortion() ||
                        ( rInf.GetLast()->InFieldGrp() &&
                          ! rInf.GetLast()->InNumberGrp() &&
                          ! rInf.GetLast()->IsErgoSumPortion() &&
                          lcl_HasContent(*static_cast<SwFieldPortion*>(rInf.GetLast()),rInf ) ) ) ) )
        {
            Width( pGuess->BreakWidth() );

            SetLen( pGuess->BreakPos() - rInf.GetIdx() );

            // Clamp layout context to the end of the line
            if(auto stClampedContext = GetLayoutContext(); stClampedContext.has_value()) {
                stClampedContext->m_nEnd = pGuess->BreakPos().get();
                SetLayoutContext(stClampedContext);
            }

            OSL_ENSURE( pGuess->BreakStart() >= pGuess->FieldDiff(),
                    "Trouble with expanded field portions during line break" );
            TextFrameIndex const nRealStart = pGuess->BreakStart() - pGuess->FieldDiff();
            if( pGuess->BreakPos() < nRealStart && !InExpGrp() )
            {
                TextFrameIndex nTotalExtraLen(nRealStart - pGuess->BreakPos());
                TextFrameIndex nExtraLen(nTotalExtraLen);
                TextFrameIndex nExtraLenOutOfLine(0);
                SwTwips nTotalExtraWidth(pGuess->ExtraBlankWidth());
                SwTwips nExtraWidth(nTotalExtraWidth);
                SwTwips nExtraWidthOutOfLine(0);
                SwTwips nAvailableLineWidth(rInf.GetLineWidth() - Width());
                bool bPaintSpaceDecorations(IsPaintTrailingSpaceDecorations(rInf));
                if (nExtraWidth > nAvailableLineWidth && bPaintSpaceDecorations)
                {
                    GetLimitedStringPart(rInf, pGuess->BreakPos(), nTotalExtraLen, GetMaxComp(rInf),
                                         nTotalExtraWidth, nAvailableLineWidth, nExtraLen,
                                         nExtraWidth);
                    if (nExtraLen)
                    {
                        nExtraLenOutOfLine = nTotalExtraLen - nExtraLen;
                        nExtraWidthOutOfLine = nTotalExtraWidth - nExtraWidth;
                    }
                    else
                    {
                        // Not a single space fits to the available width - don't create decorated
                        // hole portion
                        nExtraLen = nTotalExtraLen;
                        nExtraWidth = nTotalExtraWidth;
                        bPaintSpaceDecorations = false;
                    }
                }

                SwHolePortion* pNew = new SwHolePortion(*this, bPaintSpaceDecorations);
                pNew->SetLen(nExtraLen);
                pNew->ExtraBlankWidth(nExtraWidth);
                Insert( pNew );

                if (nExtraWidthOutOfLine)
                {
                    // Out-of-line hole portion - will not show underline
                    SwHolePortion* pNewOutOfLine = new SwHolePortion(*this, false);
                    pNewOutOfLine->SetLen(nExtraLenOutOfLine);
                    pNewOutOfLine->ExtraBlankWidth(nExtraWidthOutOfLine);
                    pNew->Insert(pNewOutOfLine);
                }

                // UAX #14 Unicode Line Breaking Algorithm Non-tailorable Line breaking rule LB6:
                // https://www.unicode.org/reports/tr14/#LB6 Do not break before hard line breaks
                if (auto ch = rInf.GetChar(pGuess->BreakStart()); !ch || ch == CH_BREAK)
                    bFull = false; // Keep following SwBreakPortion / para break in the same line
            }

            // modify width of last portion with letter spacing
            // if it's not a zero-width portion (i.e. a soft hyphen)
            // Note: GetLen() can be zero at Italics correction
            pLay = rInf.GetRoot();
            if ( Width() && !IsModifiedWidth() &&
                            ( pLay->GetLetterSpacing() || pLay->GetScaleWidth() != 100 ) )
            {
                Width( Width() * pLay->GetScaleWidth() / 100.0 +
                                        pLay->GetLetterSpacing() * sal_Int32(GetLen()) );

                SetModifiedWidth(true);
            }
        }
        else    // case C2, last exit
            BreakCut( rInf, *pGuess );
    }
    // breakPos < index or no breakpos at all
    else
    {
        bool bFirstPor = rInf.GetLineStart() == rInf.GetIdx();
        if (pGuess->BreakPos() != TextFrameIndex(COMPLETE_STRING) &&
            pGuess->BreakPos() != rInf.GetLineStart() &&
            ( !bFirstPor || rInf.GetFly() || rInf.GetLast()->IsFlyPortion() ||
              rInf.IsFirstMulti() ) &&
            ( !rInf.GetLast()->IsBlankPortion() ||
              SwBlankPortion::MayUnderflow(rInf, rInf.GetIdx() - TextFrameIndex(1), true)))
        {       // case C1 (former BreakUnderflow())
            BreakUnderflow( rInf );
        }
        else
             // case C2, last exit
            BreakCut(rInf, *pGuess);
    }

    return bFull;
}

bool SwTextPortion::Format( SwTextFormatInfo &rInf )
{
    // GetLineWidth() takes care of DocumentSettingId::TAB_OVER_MARGIN.
    if( rInf.GetLineWidth() + rInf.GetExtraSpace() < 0 || (!GetLen() && !InExpGrp()) )
    {
        Height( 0 );
        Width( 0 );
        ExtraShrunkWidth( 0 );
        ExtraSpaceSize( 0 );
        SetLen( TextFrameIndex(0) );
        SetAscent( 0 );
        SetNextPortion( nullptr );  // ????
        return true;
    }

    OSL_ENSURE( rInf.RealWidth() || (rInf.X() == rInf.Width()),
        "SwTextPortion::Format: missing real width" );
    OSL_ENSURE( Height(), "SwTextPortion::Format: missing height" );

    return Format_( rInf );
}

// Format end of line
// 5083: We can have awkward cases e.g.:
// "from {Santa}"
// Santa wraps, "from " turns into "from" and " " in a justified
// paragraph, in which the glue gets expanded instead of merged
// with the MarginPortion.

// rInf.nIdx points to the next word, nIdx-1 is the portion's last char
void SwTextPortion::FormatEOL( SwTextFormatInfo &rInf )
{
    if( ( GetNextPortion() &&
          ( !GetNextPortion()->IsKernPortion() || GetNextPortion()->GetNextPortion() ) ) ||
        !GetLen() ||
        rInf.GetIdx() >= TextFrameIndex(rInf.GetText().getLength()) ||
        TextFrameIndex(1) >= rInf.GetIdx() ||
        ' ' != rInf.GetChar(rInf.GetIdx() - TextFrameIndex(1)) ||
        rInf.GetLast()->IsHolePortion() )
        return;

    // calculate number of blanks
    TextFrameIndex nX(rInf.GetIdx() - TextFrameIndex(1));
    TextFrameIndex nHoleLen(1);
    while( nX && nHoleLen < GetLen() && CH_BLANK == rInf.GetChar( --nX ) )
        nHoleLen++;

    // First set ourselves and the insert, because there could be
    // a SwLineLayout
    SwTwips nBlankSize;
    if( nHoleLen == GetLen() )
        nBlankSize = Width();
    else
        nBlankSize = sal_Int32(nHoleLen) * rInf.GetTextSize(OUString(' ')).Width();
    Width( Width() - nBlankSize );
    rInf.X( rInf.X() - nBlankSize );
    SetLen( GetLen() - nHoleLen );
    SwHolePortion* pHole = new SwHolePortion(*this);
    pHole->SetBlankWidth(nBlankSize);
    pHole->SetLen(nHoleLen);
    Insert( pHole );

}

TextFrameIndex SwTextPortion::GetModelPositionForViewPoint(const SwTwips nOfst) const
{
    OSL_ENSURE( false, "SwTextPortion::GetModelPositionForViewPoint: don't use this method!" );
    return SwLinePortion::GetModelPositionForViewPoint( nOfst );
}

// The GetTextSize() assumes that the own length is correct
SwPositiveSize SwTextPortion::GetTextSize( const SwTextSizeInfo &rInf ) const
{
    SwPositiveSize aSize = rInf.GetTextSize(GetLayoutContext());
    if( !GetJoinBorderWithPrev() )
        aSize.Width(aSize.Width() + rInf.GetFont()->GetLeftBorderSpace() );
    if( !GetJoinBorderWithNext() )
        aSize.Width(aSize.Width() + rInf.GetFont()->GetRightBorderSpace() );

    aSize.Height(aSize.Height() +
        rInf.GetFont()->GetTopBorderSpace() +
        rInf.GetFont()->GetBottomBorderSpace() );

    return aSize;
}

void SwTextPortion::Paint( const SwTextPaintInfo &rInf ) const
{
    if (rInf.OnWin() && TextFrameIndex(1) == rInf.GetLen()
        && CH_TXT_ATR_FIELDEND == rInf.GetText()[sal_Int32(rInf.GetIdx())])
    {
        assert(false); // this is some debugging only code
        rInf.DrawBackBrush( *this );
        const OUString aText(CH_TXT_ATR_SUBST_FIELDEND);
        rInf.DrawText(aText, *this, TextFrameIndex(0), TextFrameIndex(aText.getLength()));
    }
    else if (rInf.OnWin() && TextFrameIndex(1) == rInf.GetLen()
        && CH_TXT_ATR_FIELDSTART == rInf.GetText()[sal_Int32(rInf.GetIdx())])
    {
        assert(false); // this is some debugging only code
        rInf.DrawBackBrush( *this );
        const OUString aText(CH_TXT_ATR_SUBST_FIELDSTART);
        rInf.DrawText(aText, *this, TextFrameIndex(0), TextFrameIndex(aText.getLength()));
    }
    else if( GetLen() )
    {
        rInf.DrawBackBrush( *this );
        rInf.DrawBorder( *this );

        rInf.DrawCSDFHighlighting(*this);

        // do we have to repaint a post it portion?
        if( rInf.OnWin() && mpNextPortion && !mpNextPortion->Width() )
            mpNextPortion->PrePaint( rInf, this );

        auto const* pWrongList = rInf.GetpWrongList();
        auto const* pGrammarCheckList = rInf.GetGrammarCheckList();
        auto const* pSmarttags = rInf.GetSmartTags();

        const bool bWrong = nullptr != pWrongList;
        const bool bGrammarCheck = nullptr != pGrammarCheckList;
        const bool bSmartTags = nullptr != pSmarttags;

        if ( bWrong || bSmartTags || bGrammarCheck )
            rInf.DrawMarkedText( *this, rInf.GetLen(), bWrong, bSmartTags, bGrammarCheck );
        else
            rInf.DrawText( *this, rInf.GetLen() );
    }
}

bool SwTextPortion::GetExpText( const SwTextSizeInfo &, OUString & ) const
{
    return false;
}

// Responsible for the justified paragraph. They calculate the blank
// count and the resulting added space.
TextFrameIndex SwTextPortion::GetSpaceCnt(const SwTextSizeInfo &rInf,
                                          TextFrameIndex& rCharCnt) const
{
    TextFrameIndex nCnt(0);
    TextFrameIndex nPos(0);

    if ( rInf.SnapToGrid() )
    {
        SwTextGridItem const*const pGrid(GetGridItem(rInf.GetTextFrame()->FindPageFrame()));
        if (pGrid && SwTextGrid::LinesAndChars == pGrid->GetGridType() && pGrid->IsSnapToChars())
            return TextFrameIndex(0);
    }

    if ( InExpGrp() || PortionType::InputField == GetWhichPor() )
    {
        if (OUString ExpOut;
            (!IsBlankPortion()
             || (GetExpText(rInf, ExpOut) && OUStringChar(CH_BLANK) == ExpOut))
            && !InNumberGrp() && !IsCombinedPortion())
        {
            // OnWin() likes to return a blank instead of an empty string from
            // time to time. We cannot use that here at all, however.
            bool bOldOnWin = rInf.OnWin();
            const_cast<SwTextSizeInfo &>(rInf).SetOnWin( false );

            OUString aStr;
            GetExpText( rInf, aStr );
            const_cast<SwTextSizeInfo &>(rInf).SetOnWin( bOldOnWin );

            nCnt = nCnt + lcl_AddSpace( rInf, &aStr, *this );
            nPos = TextFrameIndex(aStr.getLength());
        }
    }
    else if( !IsDropPortion() )
    {
        nCnt = nCnt + lcl_AddSpace( rInf, nullptr, *this );
        nPos = GetLen();
    }
    rCharCnt = rCharCnt + nPos;
    return nCnt;
}

SwTwips SwTextPortion::CalcSpacing( tools::Long nSpaceAdd, const SwTextSizeInfo &rInf ) const
{
    TextFrameIndex nCnt(0);

    if ( rInf.SnapToGrid() )
    {
        SwTextGridItem const*const pGrid(GetGridItem(rInf.GetTextFrame()->FindPageFrame()));
        if (pGrid && SwTextGrid::LinesAndChars == pGrid->GetGridType() && pGrid->IsSnapToChars())
            return 0;
    }

    if ( InExpGrp() || PortionType::InputField == GetWhichPor() )
    {
        if (OUString ExpOut;
            (!IsBlankPortion()
             || (GetExpText(rInf, ExpOut) && OUStringChar(CH_BLANK) == ExpOut))
            && !InNumberGrp() && !IsCombinedPortion())
        {
            // OnWin() likes to return a blank instead of an empty string from
            // time to time. We cannot use that here at all, however.
            bool bOldOnWin = rInf.OnWin();
            const_cast<SwTextSizeInfo &>(rInf).SetOnWin( false );

            OUString aStr;
            GetExpText( rInf, aStr );
            const_cast<SwTextSizeInfo &>(rInf).SetOnWin( bOldOnWin );
            if( nSpaceAdd > 0 )
                nCnt = nCnt + lcl_AddSpace( rInf, &aStr, *this );
            else
            {
                nSpaceAdd = -nSpaceAdd;
                nCnt = TextFrameIndex(aStr.getLength());
            }
        }
    }
    else if( !IsDropPortion() )
    {
        if( nSpaceAdd > 0 )
            nCnt = nCnt + lcl_AddSpace( rInf, nullptr, *this );
        else
        {
            nSpaceAdd = -nSpaceAdd;
            nCnt = GetLen();
            SwLinePortion* pPor = GetNextPortion();

            // we do not want an extra space in front of margin portions
            if ( nCnt )
            {
                while ( pPor && !pPor->Width() && ! pPor->IsHolePortion() )
                    pPor = pPor->GetNextPortion();

                if ( !pPor || pPor->InFixMargGrp() || pPor->IsHolePortion() )
                    --nCnt;
            }
        }
    }

    return sal_Int32(nCnt) * (nSpaceAdd > LONG_MAX/2 ? LONG_MAX/2 - nSpaceAdd : nSpaceAdd)
             / SPACING_PRECISION_FACTOR;
}

void SwTextPortion::HandlePortion( SwPortionHandler& rPH ) const
{
    rPH.Text( GetLen(), GetWhichPor() );
}

SwTextInputFieldPortion::SwTextInputFieldPortion()
{
    SetWhichPor( PortionType::InputField );
}

bool SwTextInputFieldPortion::Format(SwTextFormatInfo &rTextFormatInfo)
{
    return SwTextPortion::Format(rTextFormatInfo);
}

void SwTextInputFieldPortion::Paint( const SwTextPaintInfo &rInf ) const
{
    if ( Width() )
    {
        rInf.DrawViewOpt( *this, PortionType::InputField );
        SwTextSlot aPaintText( &rInf, this, true, true, OUString() );
        SwTextPortion::Paint( rInf );
    }
    else
    {
        // highlight empty input field, elsewhere they are completely invisible for the user
        SwRect aIntersect;
        rInf.CalcRect(*this, &aIntersect);
        const SwTwips aAreaWidth = rInf.GetTextSize(OUString(' ')).Width();
        aIntersect.Left(aIntersect.Left() - aAreaWidth/2);
        aIntersect.Width(aAreaWidth);

        if (aIntersect.HasArea()
            && rInf.OnWin()
            && rInf.GetOpt().IsFieldShadings()
            && !rInf.GetOpt().IsPagePreview())
        {
            OutputDevice* pOut = const_cast<OutputDevice*>(rInf.GetOut());
            auto popIt = pOut->ScopedPush(vcl::PushFlags::LINECOLOR | vcl::PushFlags::FILLCOLOR);
            pOut->SetFillColor(rInf.GetOpt().GetFieldShadingsColor());
            pOut->SetLineColor();
            pOut->DrawRect(aIntersect.SVRect());
        }
    }
}

bool SwTextInputFieldPortion::GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const
{
    sal_Int32 nIdx(rInf.GetIdx());
    sal_Int32 nLen(GetLen());
    if ( rInf.GetChar( rInf.GetIdx() ) == CH_TXT_ATR_INPUTFIELDSTART )
    {
        ++nIdx;
        --nLen;
    }
    if (rInf.GetChar(rInf.GetIdx() + GetLen() - TextFrameIndex(1)) == CH_TXT_ATR_INPUTFIELDEND)
    {
        --nLen;
    }
    rText = rInf.GetText().copy( nIdx, std::min( nLen, rInf.GetText().getLength() - nIdx ) );

    return true;
}

SwPositiveSize SwTextInputFieldPortion::GetTextSize( const SwTextSizeInfo &rInf ) const
{
    SwTextSlot aFormatText( &rInf, this, true, false );
    if (rInf.GetLen() == TextFrameIndex(0))
    {
        return SwPositiveSize( 0, 0 );
    }

    return rInf.GetTextSize();
}

SwHolePortion::SwHolePortion(const SwTextPortion& rPor, bool bShowUnderline)
    : m_nBlankWidth( 0 )
    , m_bShowUnderline(bShowUnderline)
{
    SetLen( TextFrameIndex(1) );
    Height( rPor.Height() );
    Width(0);
    SetAscent( rPor.GetAscent() );
    SetWhichPor( PortionType::Hole );
}

SwLinePortion *SwHolePortion::Compress() { return this; }

// The GetTextSize() assumes that the own length is correct
SwPositiveSize SwHolePortion::GetTextSize(const SwTextSizeInfo& rInf) const
{
    SwPositiveSize aSize = rInf.GetTextSize();
    if (!GetJoinBorderWithPrev())
        aSize.Width(aSize.Width() + rInf.GetFont()->GetLeftBorderSpace());
    if (!GetJoinBorderWithNext())
        aSize.Width(aSize.Width() + rInf.GetFont()->GetRightBorderSpace());

    aSize.Height(aSize.Height() +
        rInf.GetFont()->GetTopBorderSpace() +
        rInf.GetFont()->GetBottomBorderSpace());

    return aSize;
}

void SwHolePortion::Paint( const SwTextPaintInfo &rInf ) const
{
    if( !rInf.GetOut() )
        return;

    bool bPDFExport = rInf.GetVsh()->GetViewOptions()->IsPDFExport();

    // #i16816# export stuff only needed for tagged pdf support
    if (bPDFExport && !SwTaggedPDFHelper::IsExportTaggedPDF( *rInf.GetOut()) )
        return;

    // #i68503# the hole must have no decoration for a consistent visual appearance
    const SwFont* pOrigFont = rInf.GetFont();
    std::unique_ptr<SwFont> pHoleFont;
    std::optional<SwFontSave> oFontSave;
    if( !m_bShowUnderline && (pOrigFont->GetUnderline() != LINESTYLE_NONE
                           || pOrigFont->GetOverline() != LINESTYLE_NONE
                           || pOrigFont->GetStrikeout() != STRIKEOUT_NONE) )
    {
        pHoleFont.reset(new SwFont( *pOrigFont ));
        pHoleFont->SetUnderline(LINESTYLE_NONE);
        pHoleFont->SetOverline( LINESTYLE_NONE );
        pHoleFont->SetStrikeout( STRIKEOUT_NONE );
        oFontSave.emplace( rInf, pHoleFont.get() );
    }

    if (bPDFExport)
    {
        rInf.DrawText(u" "_ustr, *this, TextFrameIndex(0), TextFrameIndex(1));
    }
    else
    {
        if (m_bShowUnderline)
        {
            rInf.DrawBackBrush(*this);
            rInf.DrawBorder(*this);
        }
        // tdf#43244: Paint spaces even at end of line,
        // but only if this paint is not called for pdf export, to keep that pdf export intact
        rInf.DrawText(*this, rInf.GetLen());
    }

    oFontSave.reset();
    pHoleFont.reset();
}

bool SwHolePortion::Format( SwTextFormatInfo &rInf )
{
    return rInf.IsFull() || rInf.X() >= rInf.Width();
}

void SwHolePortion::HandlePortion( SwPortionHandler& rPH ) const
{
    rPH.Text( GetLen(), GetWhichPor() );
}

void SwHolePortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, TextFrameIndex& nOffset) const
{
    (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwHolePortion"));
    dumpAsXmlAttributes(pWriter, rText, nOffset);
    nOffset += GetLen();

    (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("blank-width"),
                                      BAD_CAST(OString::number(m_nBlankWidth).getStr()));

    (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("show-underline"),
                                      BAD_CAST(OString::boolean(m_bShowUnderline).getStr()));

    (void)xmlTextWriterEndElement(pWriter);
}

void SwFieldMarkPortion::Paint( const SwTextPaintInfo & /*rInf*/) const
{
    // These shouldn't be painted!
    //SwTextPortion::Paint(rInf);
}

bool SwFieldMarkPortion::Format( SwTextFormatInfo & )
{
    Width(0);
    return false;
}

void SwFieldFormCheckboxPortion::Paint( const SwTextPaintInfo& rInf ) const
{
    SwPosition const aPosition(rInf.GetTextFrame()->MapViewToModelPos(rInf.GetIdx()));

    Fieldmark const*const pBM = rInf.GetTextFrame()->GetDoc().getIDocumentMarkAccess()->getFieldmarkAt(aPosition);

    OSL_ENSURE(pBM && pBM->GetFieldname( ) == ODF_FORMCHECKBOX,
        "Where is my form field bookmark???");

    if (pBM && pBM->GetFieldname( ) == ODF_FORMCHECKBOX)
    {
        const CheckboxFieldmark* pCheckboxFm = dynamic_cast<CheckboxFieldmark const*>(pBM);
        bool bChecked = pCheckboxFm && pCheckboxFm->IsChecked();
        rInf.DrawCheckBox(*this, bChecked);
    }
}

bool SwFieldFormCheckboxPortion::Format( SwTextFormatInfo & rInf )
{
    SwPosition const aPosition(rInf.GetTextFrame()->MapViewToModelPos(rInf.GetIdx()));
    Fieldmark const*const pBM = rInf.GetTextFrame()->GetDoc().getIDocumentMarkAccess()->getFieldmarkAt(aPosition);
    OSL_ENSURE(pBM && pBM->GetFieldname( ) == ODF_FORMCHECKBOX, "Where is my form field bookmark???");
    if (pBM && pBM->GetFieldname( ) == ODF_FORMCHECKBOX)
    {
        // the width of the checkbox portion is the same as its height since it's a square
        // and that size depends on the font size.
        // See:
        // http://document-foundation-mail-archive.969070.n3.nabble.com/Wrong-copy-paste-in-SwFieldFormCheckboxPortion-Format-td4269112.html
        Width( rInf.GetTextHeight(  ) );
        Height( rInf.GetTextHeight(  ) );
        SetAscent( rInf.GetAscent(  ) );
    }
    return false;
}

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