/* -*- 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 <fmtanchr.hxx>
#include <fmtcntnt.hxx>
#include <acorrect.hxx>
#include <IDocumentRedlineAccess.hxx>
#include <IDocumentLayoutAccess.hxx>
#include <IDocumentUndoRedo.hxx>
#include <docsh.hxx>
#include <docary.hxx>
#include <mdiexp.hxx>
#include <mvsave.hxx>
#include <redline.hxx>
#include <rolbck.hxx>
#include <rootfrm.hxx>
#include <splargs.hxx>
#include <swcrsr.hxx>
#include <txtfrm.hxx>
#include <unoflatpara.hxx>
#include <SwGrammarMarkUp.hxx>
#include <docedt.hxx>
#include <frmfmt.hxx>
#include <ndtxt.hxx>
#include <undobj.hxx>
#include <frameformats.hxx>
#include <unotxdoc.hxx>

#include <vector>
#include <com/sun/star/linguistic2/XProofreadingIterator.hpp>
#include <osl/diagnose.h>

using namespace ::com::sun::star;
using namespace ::com::sun::star::linguistic2;


void RestFlyInRange( SaveFlyArr & rArr, const SwPosition& rStartPos,
                      const SwNode* pInsertPos, bool const isForceToStartPos)
{
    SwPosition aPos(rStartPos);
    for(const SaveFly & rSave : rArr)
    {
        // create new anchor
        SwFrameFormat* pFormat = rSave.pFrameFormat;
        SwFormatAnchor aAnchor( pFormat->GetAnchor() );

        if (rSave.isAtInsertNode || isForceToStartPos)
        {
            if( pInsertPos != nullptr )
            {
                if (aAnchor.GetAnchorId() == RndStdIds::FLY_AT_PARA)
                {
                    assert(pInsertPos->GetContentNode());
                    aPos.Assign( *pInsertPos->GetContentNode(),
                        rSave.nContentIndex);
                }
                else
                {
                    assert(aAnchor.GetAnchorId() == RndStdIds::FLY_AT_CHAR);
                    aPos = rStartPos;
                }
            }
            else
            {
                aPos.Assign(rStartPos.GetNode());
                assert(aPos.GetNode().GetContentNode());
            }
        }
        else
        {
            aPos.Assign(rStartPos.GetNodeIndex() + rSave.nNdDiff);
            assert(aPos.GetNode().GetContentNode());
            aPos.SetContent(
                rSave.nNdDiff == SwNodeOffset(0)
                    ? rStartPos.GetContentIndex() + rSave.nContentIndex
                    : rSave.nContentIndex);
        }

        aAnchor.SetAnchor( &aPos );
        pFormat->GetDoc().GetSpzFrameFormats()->push_back(static_cast<sw::SpzFrameFormat*>(pFormat));
        // SetFormatAttr should call Modify() and add it to the node
        pFormat->SetFormatAttr( aAnchor );
        SwContentNode* pCNd = aPos.GetNode().GetContentNode();
        if (pCNd && pCNd->getLayoutFrame(pFormat->GetDoc().getIDocumentLayoutAccess().GetCurrentLayout(), nullptr, nullptr))
            pFormat->MakeFrames();
    }
    sw::CheckAnchoredFlyConsistency(rStartPos.GetNode().GetDoc());
}

void SaveFlyInRange( const SwNodeRange& rRg, SaveFlyArr& rArr )
{
    sw::SpzFrameFormats& rSpzs = *rRg.aStart.GetNode().GetDoc().GetSpzFrameFormats();
    sw::FrameFormats<sw::SpzFrameFormat*>::size_type n = 0;
    while (n < rSpzs.size())
    {
        auto pSpz = rSpzs[n];
        SwFormatAnchor const*const pAnchor = &pSpz->GetAnchor();
        SwNode const*const pAnchorNode = pAnchor->GetAnchorNode();
        if (pAnchorNode &&
            ((RndStdIds::FLY_AT_PARA == pAnchor->GetAnchorId()) ||
             (RndStdIds::FLY_AT_CHAR == pAnchor->GetAnchorId())) &&
            rRg.aStart <= *pAnchorNode && *pAnchorNode < rRg.aEnd.GetNode() )
        {
            SaveFly aSave( pAnchorNode->GetIndex() - rRg.aStart.GetIndex(),
                            (RndStdIds::FLY_AT_CHAR == pAnchor->GetAnchorId())
                                ? pAnchor->GetAnchorContentOffset()
                                : 0,
                            pSpz, false );
            rArr.push_back( aSave );
            pSpz->DelFrames();
            // set a dummy anchor position to maintain anchoring invariants
            SwFormatAnchor aAnchor( pSpz->GetAnchor() );
            aAnchor.SetAnchor(nullptr);
            pSpz->SetFormatAttr(aAnchor);
            rSpzs.erase( rSpzs.begin() + n );
            continue;
        }
        ++n;
    }
    sw::CheckAnchoredFlyConsistency(rRg.aStart.GetNode().GetDoc());
}

void SaveFlyInRange( const SwPaM& rPam, const SwPosition& rInsPos,
        SaveFlyArr& rArr, bool bMoveAllFlys, SwHistory *const pHistory)
{
    sw::SpzFrameFormats& rFormats = *rPam.GetPoint()->GetNode().GetDoc().GetSpzFrameFormats();
    sw::SpzFrameFormat* pFormat;
    const SwFormatAnchor* pAnchor;

    const SwPosition* pPos = rPam.Start();
    const SwNode& rSttNd = pPos->GetNode();

    SwPosition atParaEnd(*rPam.End());
    if (bMoveAllFlys)
    {
        assert(!rPam.End()->GetNode().IsTextNode() // can be table end-node
            || rPam.End()->GetContentIndex() == rPam.End()->GetNode().GetTextNode()->Len());
        atParaEnd.Adjust(SwNodeOffset(1));
    }

    for(sw::FrameFormats<sw::SpzFrameFormat*>::size_type n = 0; n < rFormats.size(); ++n )
    {
        pFormat = rFormats[n];
        pAnchor = &pFormat->GetAnchor();
        const SwPosition* pAPos = pAnchor->GetContentAnchor();
        const SwNodeIndex* pContentIdx;
        if (pAPos &&
            ((RndStdIds::FLY_AT_PARA == pAnchor->GetAnchorId()) ||
             (RndStdIds::FLY_AT_CHAR == pAnchor->GetAnchorId())) &&
            // do not move if the InsPos is in the ContentArea of the Fly
            ( nullptr == ( pContentIdx = pFormat->GetContent().GetContentIdx() ) ||
              (*pContentIdx >= rInsPos.GetNode() ||
                rInsPos.GetNode() >= *pContentIdx->GetNode().EndOfSectionNode())))
        {
            bool bInsPos = false;

            if (       (RndStdIds::FLY_AT_CHAR == pAnchor->GetAnchorId()
                        && IsDestroyFrameAnchoredAtChar(*pAPos, *rPam.Start(), *rPam.End()))
                    || (RndStdIds::FLY_AT_PARA == pAnchor->GetAnchorId()
                        && IsSelectFrameAnchoredAtPara(*pAPos, *rPam.Start(), atParaEnd,
                            bMoveAllFlys
                                ? DelContentType::CheckNoCntnt|DelContentType::AllMask
                                : DelContentType::AllMask))
                    || (RndStdIds::FLY_AT_PARA == pAnchor->GetAnchorId()
                            && (bInsPos = (rInsPos.GetNode() == pAPos->GetNode())))
                    || (RndStdIds::FLY_AT_CHAR == pAnchor->GetAnchorId()
                            && (bInsPos = (rInsPos == *pAPos))))
            {
                if (pHistory)
                {
                    pHistory->AddChangeFlyAnchor(*pFormat);
                }
                SaveFly aSave( pAPos->GetNodeIndex() - rSttNd.GetIndex(),
                    (RndStdIds::FLY_AT_CHAR == pAnchor->GetAnchorId())
                        ? (pAPos->GetNode() == rSttNd)
                            ? pAPos->GetContentIndex() - rPam.Start()->GetContentIndex()
                            : pAPos->GetContentIndex()
                        : 0,
                                pFormat, bInsPos );
                rArr.push_back( aSave );
                pFormat->DelFrames();
                // set a dummy anchor position to maintain anchoring invariants
                SwFormatAnchor aAnchor( pFormat->GetAnchor() );
                aAnchor.SetAnchor(nullptr);
                pFormat->SetFormatAttr(aAnchor);
                rFormats.erase( rFormats.begin() + n-- );
            }
        }
    }
    sw::CheckAnchoredFlyConsistency(rPam.GetPoint()->GetNode().GetDoc());
}

/// Delete and move all Flys at the paragraph, that are within the selection.
/// If there is a Fly at the SPoint, it is moved onto the Mark.
void DelFlyInRange( SwNode& rMkNd,
                    SwNode& rPtNd,
                    std::optional<sal_Int32> oMkContentIdx, std::optional<sal_Int32> oPtContentIdx)
{
    assert(oMkContentIdx.has_value() == oPtContentIdx.has_value());
    SwPosition const point(oPtContentIdx
                            ? SwPosition(rPtNd, rPtNd.GetContentNode(), *oPtContentIdx)
                            : SwPosition(rPtNd));
    SwPosition const mark(oPtContentIdx
                            ? SwPosition(rMkNd, rMkNd.GetContentNode(), *oMkContentIdx)
                            : SwPosition(rMkNd));
    SwPosition const& rStart = mark <= point ? mark : point;
    SwPosition const& rEnd   = mark <= point ? point : mark;

    SwDoc& rDoc = rMkNd.GetDoc();
    sw::SpzFrameFormats& rTable = *rDoc.GetSpzFrameFormats();
    for ( auto i = rTable.size(); i; )
    {
        sw::SpzFrameFormat* pFormat = rTable[--i];
        const SwFormatAnchor &rAnch = pFormat->GetAnchor();
        SwPosition const*const pAPos = rAnch.GetContentAnchor();
        if (pAPos &&
            (((rAnch.GetAnchorId() == RndStdIds::FLY_AT_PARA)
                && IsSelectFrameAnchoredAtPara(*pAPos, rStart, rEnd, oPtContentIdx
                    ? DelContentType::AllMask|DelContentType::WriterfilterHack
                    : DelContentType::AllMask|DelContentType::WriterfilterHack|DelContentType::CheckNoCntnt))
            || ((rAnch.GetAnchorId() == RndStdIds::FLY_AT_CHAR)
                && IsDestroyFrameAnchoredAtChar(*pAPos, rStart, rEnd, oPtContentIdx
                    ? DelContentType::AllMask|DelContentType::WriterfilterHack
                    : DelContentType::AllMask|DelContentType::WriterfilterHack|DelContentType::CheckNoCntnt))))
        {
            // If the Fly is deleted, all Flys in its content have to be deleted too.
            const SwFormatContent &rContent = pFormat->GetContent();
            // But only fly formats own their content, not draw formats.
            if (rContent.GetContentIdx() && pFormat->Which() == RES_FLYFRMFMT)
            {
                DelFlyInRange( rContent.GetContentIdx()->GetNode(),
                               *rContent.GetContentIdx()->
                                        GetNode().EndOfSectionNode() );
                // Position could have been moved!
                if (i > rTable.size())
                    i = rTable.size();
                else if (i == rTable.size() || pFormat != rTable[i])
                    i = std::distance(rTable.begin(), rTable.find(pFormat));
            }

            rDoc.getIDocumentLayoutAccess().DelLayoutFormat( pFormat );

            // DelLayoutFormat can also trigger the deletion of objects.
            if (i > rTable.size())
                i = rTable.size();
        }
    }
}

// #i59534: Redo of insertion of multiple text nodes runs into trouble
// because of unnecessary expanded redlines
// From now on this class saves the redline positions of all redlines which ends exact at the
// insert position (node _and_ content index)
SaveRedlEndPosForRestore::SaveRedlEndPosForRestore( const SwNode& rInsIdx, sal_Int32 nCnt )
    : mnSaveContent( nCnt )
{
    const SwDoc& rDest = rInsIdx.GetDoc();
    if( rDest.getIDocumentRedlineAccess().GetRedlineTable().empty() )
        return;

    SwRedlineTable::size_type nFndPos;
    const SwPosition* pEnd;
    SwPosition aSrcPos( rInsIdx, rInsIdx.GetContentNode(), nCnt );
    rDest.getIDocumentRedlineAccess().GetRedline( aSrcPos, &nFndPos );
    const SwRangeRedline* pRedl;
    while( nFndPos--
          && *( pEnd = ( pRedl = rDest.getIDocumentRedlineAccess().GetRedlineTable()[ nFndPos ] )->End() ) == aSrcPos
          && *pRedl->Start() < aSrcPos )
    {
        if( !moSaveIndex )
        {
            moSaveIndex.emplace( rInsIdx, -1 );
        }
        mvSavArr.push_back( const_cast<SwPosition*>(pEnd) );
    }
}

SaveRedlEndPosForRestore::~SaveRedlEndPosForRestore()
{
    moSaveIndex.reset();
}

void SaveRedlEndPosForRestore::Restore()
{
    if (mvSavArr.empty())
        return;
    ++(*moSaveIndex);
    SwContentNode* pNode = moSaveIndex->GetNode().GetContentNode();
    // If there's no content node at the remembered position, we will not restore the old position
    // This may happen if a table (or section?) will be inserted.
    if( pNode )
    {
        SwPosition aPos( *moSaveIndex, pNode, mnSaveContent );
        for( auto n = mvSavArr.size(); n; )
            *mvSavArr[ --n ] = aPos;
    }
}

/// Convert list of ranges of whichIds to a corresponding list of whichIds
static std::vector<sal_uInt16> lcl_RangesToVector(const WhichRangesContainer& pRanges)
{
    std::vector<sal_uInt16> aResult;

    for(const WhichPair& rPair : pRanges)
    {
        for (sal_uInt16 j = rPair.first; j <= rPair.second; j++)
            aResult.push_back(j);
    }

    return aResult;
}

void sw_GetJoinFlags( SwPaM& rPam, bool& rJoinText, bool& rJoinPrev )
{
    rJoinText = false;
    rJoinPrev = false;
    if( rPam.GetPoint()->GetNode() == rPam.GetMark()->GetNode() )
        return;

    auto [pStart, pEnd] = rPam.StartEnd(); // SwPosition*
    SwTextNode *pSttNd = pStart->GetNode().GetTextNode();
    if( !pSttNd )
        return;

    SwTextNode *pEndNd = pEnd->GetNode().GetTextNode();
    rJoinText = nullptr != pEndNd;
    if( !rJoinText )
        return;

    bool bExchange = pStart == rPam.GetPoint();
    if( !pStart->GetContentIndex() &&
        pEndNd->GetText().getLength() != pEnd->GetContentIndex())
        bExchange = !bExchange;
    if( bExchange )
        rPam.Exchange();
    rJoinPrev = rPam.GetPoint() == pStart;
    OSL_ENSURE( !pStart->GetContentIndex() &&
        pEndNd->GetText().getLength() != pEnd->GetContentIndex()
        ? (rPam.GetPoint()->GetNode() < rPam.GetMark()->GetNode())
        : (rPam.GetPoint()->GetNode() > rPam.GetMark()->GetNode()),
        "sw_GetJoinFlags");
}

bool sw_JoinText( SwPaM& rPam, bool bJoinPrev )
{
    SwNodeIndex aIdx( rPam.GetPoint()->GetNode() );
    SwTextNode *pTextNd = aIdx.GetNode().GetTextNode();
    SwNodeIndex aOldIdx( aIdx );
    SwTextNode *pOldTextNd = pTextNd;

    if( pTextNd && pTextNd->CanJoinNext( &aIdx ) )
    {
        SwDoc& rDoc = rPam.GetDoc();
        if( bJoinPrev )
        {
            // We do not need to handle xmlids in this case, because
            // it is only invoked if one paragraph is/becomes completely empty
            // (see sw_GetJoinFlags)
            {
                // If PageBreaks are deleted/set, it must not be added to the Undo history!
                // Also, deleting the Node is not added to the Undo history!
                ::sw::UndoGuard const undoGuard(rDoc.GetIDocumentUndoRedo());

                /* PageBreaks, PageDesc, ColumnBreaks */
                // If we need to change something about the logic to copy the PageBreaks,
                // PageDesc, etc. we also have to change SwUndoDelete.
                // There, we copy the AUTO PageBreak from the GetMarkNode!

                /* The MarkNode */
                pTextNd = aIdx.GetNode().GetTextNode();
                const SfxItemSet* pSet = pTextNd->GetpSwAttrSet();
                if (pSet)
                {
                    if (SfxItemState::SET == pSet->GetItemState(RES_BREAK, false))
                        pTextNd->ResetAttr( RES_BREAK );
                    if (SfxItemState::SET == pSet->GetItemState(RES_PAGEDESC, false))
                        pTextNd->ResetAttr( RES_PAGEDESC );
                }

                /* The PointNode */
                pSet = pOldTextNd->GetpSwAttrSet();
                if (pSet)
                {
                    const SfxPoolItem* pItem;
                    SfxItemSet aSet( rDoc.GetAttrPool(), aBreakSetRange );
                    if( SfxItemState::SET == pSet->GetItemState( RES_BREAK,
                        false, &pItem ) )
                        aSet.Put( *pItem );
                    if( SfxItemState::SET == pSet->GetItemState( RES_PAGEDESC,
                        false, &pItem ) )
                        aSet.Put( *pItem );
                    if( aSet.Count() )
                        pTextNd->SetAttr( aSet );
                }
                pOldTextNd->FormatToTextAttr( pTextNd );

                const std::shared_ptr< sw::mark::ContentIdxStore> pContentStore(sw::mark::ContentIdxStore::Create());
                pContentStore->Save(rDoc, aOldIdx.GetIndex(), SAL_MAX_INT32);

                SwContentIndex aAlphaIdx(pTextNd);
                pOldTextNd->CutText( pTextNd, aAlphaIdx, SwContentIndex(pOldTextNd),
                                    pOldTextNd->Len() );
                SwPosition aAlphaPos( aIdx, aAlphaIdx );
                rDoc.CorrRel( rPam.GetPoint()->GetNode(), aAlphaPos, 0, true );

                // move all Bookmarks/TOXMarks
                if( !pContentStore->Empty() )
                    pContentStore->Restore( rDoc, aIdx.GetIndex() );

                // If the passed PaM is not in the Cursor ring,
                // treat it separately (e.g. when it's being called from AutoFormat)
                if( pOldTextNd == rPam.GetBound().GetContentNode() )
                    rPam.GetBound() = aAlphaPos;
                if( pOldTextNd == rPam.GetBound( false ).GetContentNode() )
                    rPam.GetBound( false ) = std::move(aAlphaPos);
            }
            // delete the Node, at last!
            SwNode::Merge const eOldMergeFlag(pOldTextNd->GetRedlineMergeFlag());
            if (eOldMergeFlag == SwNode::Merge::First
                && !pTextNd->IsCreateFrameWhenHidingRedlines())
            {
                sw::MoveDeletedPrevFrames(*pOldTextNd, *pTextNd);
            }
            rDoc.GetNodes().Delete( aOldIdx );
            sw::CheckResetRedlineMergeFlag(*pTextNd,
                    eOldMergeFlag == SwNode::Merge::NonFirst
                        ? sw::Recreate::Predecessor
                        : sw::Recreate::No);
        }
        else
        {
            SwTextNode* pDelNd = aIdx.GetNode().GetTextNode();
            if( pTextNd->Len() )
                pDelNd->FormatToTextAttr( pTextNd );
            else
            {
                /* This case was missed:

                   <something></something>   <-- pTextNd
                   <other>ccc</other>        <-- pDelNd

                   <something> and <other> are paragraph
                   attributes. The attribute <something> stayed if not
                   overwritten by an attribute in "ccc". Fixed by
                   first resetting all character attributes in first
                   paragraph (pTextNd).
                */
                std::vector<sal_uInt16> aShorts =
                    lcl_RangesToVector(aCharFormatSetRange);
                pTextNd->ResetAttr(aShorts);

                if( pDelNd->HasSwAttrSet() )
                {
                    // only copy the character attributes
                    SfxItemSet aTmpSet( rDoc.GetAttrPool(), aCharFormatSetRange );
                    aTmpSet.Put( *pDelNd->GetpSwAttrSet() );
                    pTextNd->SetAttr( aTmpSet );
                }
            }

            rDoc.CorrRel( aIdx.GetNode(), *rPam.GetPoint(), 0, true );
            // #i100466# adjust given <rPam>, if it does not belong to the cursors
            if ( pDelNd == rPam.GetBound().GetContentNode() )
            {
                rPam.GetBound().Assign( *pTextNd );
            }
            if( pDelNd == rPam.GetBound( false ).GetContentNode() )
            {
                rPam.GetBound( false ).Assign( *pTextNd );
            }
            pTextNd->JoinNext();
        }
        return true;
    }
    else return false;
}

static void lcl_syncGrammarError( SwTextNode &rTextNode, linguistic2::ProofreadingResult& rResult,
    const ModelToViewHelper &rConversionMap )
{
    if( rTextNode.IsGrammarCheckDirty() )
        return;
    SwGrammarMarkUp* pWrong = rTextNode.GetGrammarCheck();
    linguistic2::SingleProofreadingError* pArray = rResult.aErrors.getArray();
    sal_uInt16 j = 0;
    if( pWrong )
    {
        for( sal_Int32 i = 0; i < rResult.aErrors.getLength(); ++i )
        {
            const linguistic2::SingleProofreadingError &rError = rResult.aErrors[i];
            const sal_Int32 nStart = rConversionMap.ConvertToModelPosition( rError.nErrorStart ).mnPos;
            const sal_Int32 nEnd = rConversionMap.ConvertToModelPosition( rError.nErrorStart + rError.nErrorLength ).mnPos;
            if( i != j )
                pArray[j] = pArray[i];
            if( pWrong->LookForEntry( nStart, nEnd ) )
                ++j;
        }
    }
    if( rResult.aErrors.getLength() > j )
        rResult.aErrors.realloc( j );
}

uno::Any SwDoc::Spell( SwPaM& rPaM,
                    uno::Reference< XSpellChecker1 > const &xSpeller,
                    sal_uInt16* pPageCnt, sal_uInt16* pPageSt,
                    bool bGrammarCheck,
                    SwRootFrame const*const pLayout,
                    SwConversionArgs *pConvArgs  ) const
{
    SwPosition* const pSttPos = rPaM.Start();
    SwPosition* const pEndPos = rPaM.End();

    std::unique_ptr<SwSpellArgs> pSpellArgs;
    if (pConvArgs)
    {
        pConvArgs->SetStart(*pSttPos);
        pConvArgs->SetEnd(*pEndPos);
    }
    else
        pSpellArgs.reset(new SwSpellArgs( xSpeller, *pSttPos, *pEndPos, bGrammarCheck ));

    SwNodeOffset nCurrNd = pSttPos->GetNodeIndex();
    SwNodeOffset nEndNd = pEndPos->GetNodeIndex();

    bool bIsReadOnly = false;
    const SfxObjectShell* pObjShell = GetDocShell();
    if (pObjShell && pObjShell->IsReadOnly())
        bIsReadOnly = true;

    uno::Any aRet;
    if( nCurrNd <= nEndNd )
    {
        SwContentFrame* pContentFrame;
        bool bGoOn = true;
        while( bGoOn )
        {
            SwNode* pNd = GetNodes()[ nCurrNd ];
            switch( pNd->GetNodeType() )
            {
            case SwNodeType::Text:
                if( nullptr != ( pContentFrame = pNd->GetTextNode()->getLayoutFrame( getIDocumentLayoutAccess().GetCurrentLayout() )) )
                {
                    // skip protected and hidden Cells and Flys
                    if( pContentFrame->IsProtected() )
                    {
                        nCurrNd = pNd->EndOfSectionIndex();
                    }
                    else if( !pContentFrame->IsHiddenNow() )
                    {
                        if( pPageCnt && *pPageCnt && pPageSt )
                        {
                            sal_uInt16 nPageNr = pContentFrame->GetPhyPageNum();
                            if( !*pPageSt )
                            {
                                *pPageSt = nPageNr;
                                if( *pPageCnt < *pPageSt )
                                    *pPageCnt = *pPageSt;
                            }
                            tools::Long nStat;
                            if( nPageNr >= *pPageSt )
                                nStat = nPageNr - *pPageSt + 1;
                            else
                                nStat = nPageNr + *pPageCnt - *pPageSt + 1;
                            ::SetProgressState( nStat, GetDocShell() );
                        }
                        //Spell() changes the pSpellArgs in case an error is found
                        sal_Int32 nBeginGrammarCheck = 0;
                        sal_Int32 nEndGrammarCheck = 0;
                        if( pSpellArgs && pSpellArgs->bIsGrammarCheck)
                        {
                            nBeginGrammarCheck = &pSpellArgs->pStartPos->GetNode() == pNd ?  pSpellArgs->pStartPos->GetContentIndex() : 0;
                            // if grammar checking starts inside of a sentence the start position has to be adjusted
                            if( nBeginGrammarCheck )
                            {
                                SwContentIndex aStartIndex( pNd->GetTextNode(), nBeginGrammarCheck );
                                SwPosition aStart( *pNd, aStartIndex );
                                SwCursor aCursor(aStart, nullptr);
                                SwPosition aOrigPos = *aCursor.GetPoint();
                                aCursor.GoSentence( SwCursor::START_SENT );
                                if( aOrigPos != *aCursor.GetPoint() )
                                {
                                    nBeginGrammarCheck = aCursor.GetPoint()->GetContentIndex();
                                }
                            }
                            nEndGrammarCheck = (&pSpellArgs->pEndPos->GetNode() == pNd)
                                ? pSpellArgs->pEndPos->GetContentIndex()
                                : pNd->GetTextNode()
                                    ->GetText().getLength();
                        }

                        sal_Int32 nSpellErrorPosition = pNd->GetTextNode()->GetText().getLength();
                        if ((!pConvArgs && pNd->GetTextNode()->Spell(pSpellArgs.get(), bIsReadOnly))
                            || (pConvArgs && pNd->GetTextNode()->Convert(*pConvArgs)))
                        {
                            // Cancel and remember position
                            if( pSpellArgs )
                                nSpellErrorPosition = pSpellArgs->pStartPos->GetContentIndex() > pSpellArgs->pEndPos->GetContentIndex() ?
                                            pSpellArgs->pEndPos->GetContentIndex() :
                                            pSpellArgs->pStartPos->GetContentIndex();
                            if( nCurrNd != nEndNd )
                            {
                                pSttPos->Assign(nCurrNd, pSttPos->GetContentIndex());
                                pEndPos->Assign(nCurrNd, pEndPos->GetContentIndex());
                                nCurrNd = nEndNd;
                            }
                        }

                        if( pSpellArgs && pSpellArgs->bIsGrammarCheck )
                        {
                            uno::Reference< linguistic2::XProofreadingIterator >  xGCIterator( GetGCIterator() );
                            if (xGCIterator.is())
                            {
                                if (const SwDocShell* pShell = GetDocShell())
                                {
                                    uno::Reference<uno::XInterface> xDoc = pShell->GetModel();
                                    // Expand the string:
                                    const ModelToViewHelper aConversionMap(*pNd->GetTextNode(), pLayout);
                                    const OUString& aExpandText = aConversionMap.getViewText();

                                    // get XFlatParagraph to use...
                                    uno::Reference< text::XFlatParagraph > xFlatPara = new SwXFlatParagraph( *pNd->GetTextNode(), aExpandText, aConversionMap );

                                    // get error position of cursor in XFlatParagraph
                                    linguistic2::ProofreadingResult aResult;
                                    bool bGrammarErrors;
                                    do
                                    {
                                        aConversionMap.ConvertToViewPosition( nBeginGrammarCheck );
                                        aResult = xGCIterator->checkSentenceAtPosition(
                                                xDoc, xFlatPara, aExpandText, lang::Locale(), nBeginGrammarCheck, -1, -1 );

                                        lcl_syncGrammarError( *pNd->GetTextNode(), aResult, aConversionMap );

                                        // get suggestions to use for the specific error position
                                        bGrammarErrors = aResult.aErrors.hasElements();
                                        // if grammar checking doesn't have any progress then quit
                                        if( aResult.nStartOfNextSentencePosition <= nBeginGrammarCheck )
                                            break;
                                        // prepare next iteration
                                        nBeginGrammarCheck = aResult.nStartOfNextSentencePosition;
                                    }
                                    while( nSpellErrorPosition > aResult.nBehindEndOfSentencePosition && !bGrammarErrors && aResult.nBehindEndOfSentencePosition < nEndGrammarCheck );

                                    if( bGrammarErrors && nSpellErrorPosition >= aResult.nBehindEndOfSentencePosition )
                                    {
                                        aRet <<= aResult;
                                        //put the cursor to the current error
                                        const linguistic2::SingleProofreadingError &rError = aResult.aErrors[0];
                                        pSttPos->Assign(nCurrNd, pSttPos->GetContentIndex());
                                        pEndPos->Assign(nCurrNd, pEndPos->GetContentIndex());
                                        pSpellArgs->pStartPos->Assign(*pNd->GetTextNode(), aConversionMap.ConvertToModelPosition( rError.nErrorStart ).mnPos );
                                        pSpellArgs->pEndPos->Assign(*pNd->GetTextNode(), aConversionMap.ConvertToModelPosition( rError.nErrorStart + rError.nErrorLength ).mnPos );
                                        nCurrNd = nEndNd;
                                    }
                                }
                            }
                        }
                    }
                }
                break;
            case SwNodeType::Section:
                if( static_cast<SwSectionNode*>(pNd)->GetSection().IsProtect() ||
                    static_cast<SwSectionNode*>(pNd)->GetSection().IsHidden() )
                    nCurrNd = pNd->EndOfSectionIndex();
                break;
            case SwNodeType::End:
                {
                    break;
                }
            default: break;
            }

            bGoOn = nCurrNd < nEndNd;
            ++nCurrNd;
        }
    }

    if( !aRet.hasValue() )
    {
        if (pConvArgs)
            aRet <<= pConvArgs->aConvText;
        else
            aRet <<= pSpellArgs->xSpellAlt;
    }

    return aRet;
}

namespace {

class SwHyphArgs : public SwInterHyphInfo
{
    SwNodeIndex m_aNodeIdx;
    const SwNode *m_pStart;
    const SwNode *m_pEnd;
    sal_uInt16 *m_pPageCnt;
    sal_uInt16 *m_pPageSt;

    sal_Int32 m_nPamStart;
    sal_Int32 m_nPamLen;

public:
    SwHyphArgs( const SwPaM *pPam, const Point &rPoint,
                sal_uInt16* pPageCount, sal_uInt16* pPageStart );
    void SetPam( SwPaM *pPam ) const;
    void SetNode( const SwNode& rNew ) { m_aNodeIdx.Assign(rNew); }
    inline void SetRange( const SwNode *pNew );
    void NextNode() { ++m_aNodeIdx; }
    sal_uInt16 *GetPageCnt() { return m_pPageCnt; }
    sal_uInt16 *GetPageSt() { return m_pPageSt; }
};

}

SwHyphArgs::SwHyphArgs( const SwPaM *pPam, const Point &rCursorPos,
                         sal_uInt16* pPageCount, sal_uInt16* pPageStart )
     : SwInterHyphInfo( rCursorPos ), m_aNodeIdx(pPam->GetPoint()->GetNode()),
     m_pPageCnt( pPageCount ), m_pPageSt( pPageStart )
{
    // The following constraints have to be met:
    // 1) there is at least one Selection
    // 2) SPoint() == Start()
    OSL_ENSURE( pPam->HasMark(), "SwDoc::Hyphenate: blowing in the wind");
    OSL_ENSURE( *pPam->GetPoint() <= *pPam->GetMark(),
            "SwDoc::Hyphenate: New York, New York");

    const SwPosition *pPoint = pPam->GetPoint();

    // Set start
    m_pStart = pPoint->GetNode().GetTextNode();
    m_nPamStart = pPoint->GetContentIndex();

    // Set End and Length
    const SwPosition *pMark = pPam->GetMark();
    m_pEnd = pMark->GetNode().GetTextNode();
    m_nPamLen = pMark->GetContentIndex();
    if( pPoint->GetNode() == pMark->GetNode() )
        m_nPamLen = m_nPamLen - pPoint->GetContentIndex();
}

inline void SwHyphArgs::SetRange( const SwNode *pNew )
{
    m_nStart = m_pStart == pNew ? m_nPamStart : 0;
    m_nEnd   = m_pEnd   == pNew ? m_nPamStart + m_nPamLen : SAL_MAX_INT32;
}

void SwHyphArgs::SetPam( SwPaM *pPam ) const
{
    pPam->GetPoint()->Assign( m_aNodeIdx, m_nWordStart );
    pPam->GetMark()->Assign( m_aNodeIdx, m_nWordStart + m_nWordLen );
}

// Returns true if we can proceed.
static bool lcl_HyphenateNode( SwNode* pNd, void* pArgs )
{
    // Hyphenate returns true if there is a hyphenation point and sets pPam
    SwTextNode *pNode = pNd->GetTextNode();
    SwHyphArgs *pHyphArgs = static_cast<SwHyphArgs*>(pArgs);
    if( pNode )
    {
        // sw_redlinehide: this will be called once per node for merged nodes;
        // the fully deleted ones won't have frames so are skipped.
        SwContentFrame* pContentFrame = pNode->getLayoutFrame( pNode->GetDoc().getIDocumentLayoutAccess().GetCurrentLayout() );
        if( pContentFrame && !pContentFrame->IsHiddenNow() )
        {
            sal_uInt16 *pPageSt = pHyphArgs->GetPageSt();
            sal_uInt16 *pPageCnt = pHyphArgs->GetPageCnt();
            if( pPageCnt && *pPageCnt && pPageSt )
            {
                sal_uInt16 nPageNr = pContentFrame->GetPhyPageNum();
                if( !*pPageSt )
                {
                    *pPageSt = nPageNr;
                    if( *pPageCnt < *pPageSt )
                        *pPageCnt = *pPageSt;
                }
                tools::Long nStat = nPageNr >= *pPageSt ? nPageNr - *pPageSt + 1
                                         : nPageNr + *pPageCnt - *pPageSt + 1;
                ::SetProgressState( nStat, pNode->GetDoc().GetDocShell() );
            }
            pHyphArgs->SetRange( pNd );
            if( pNode->Hyphenate( *pHyphArgs ) )
            {
                pHyphArgs->SetNode( *pNd );
                return false;
            }
        }
    }
    pHyphArgs->NextNode();
    return true;
}

uno::Reference< XHyphenatedWord >  SwDoc::Hyphenate(
                            SwPaM *pPam, const Point &rCursorPos,
                             sal_uInt16* pPageCnt, sal_uInt16* pPageSt )
{
    OSL_ENSURE(this == &pPam->GetDoc(), "SwDoc::Hyphenate: strangers in the night");

    if( *pPam->GetPoint() > *pPam->GetMark() )
        pPam->Exchange();

    SwHyphArgs aHyphArg( pPam, rCursorPos, pPageCnt, pPageSt );
    SwNodeIndex aTmpIdx( pPam->GetMark()->GetNode(), 1 );
    GetNodes().ForEach( pPam->GetPoint()->GetNode(), aTmpIdx.GetNode(),
                    lcl_HyphenateNode, &aHyphArg );
    aHyphArg.SetPam( pPam );
    return aHyphArg.GetHyphWord();  // will be set by lcl_HyphenateNode
}

// Save the current values to add them as automatic entries to AutoCorrect.
void SwDoc::SetAutoCorrExceptWord( std::unique_ptr<SwAutoCorrExceptWord> pNew )
{
    mpACEWord = std::move(pNew);
}

void SwDoc::DeleteAutoCorrExceptWord()
{
    mpACEWord.reset();
}

void SwDoc::CountWords( const SwPaM& rPaM, SwDocStat& rStat )
{
    // This is a modified version of SwDoc::TransliterateText
    auto [pStart, pEnd] = rPaM.StartEnd(); // SwPosition*

    const SwNodeOffset nSttNd = pStart->GetNodeIndex();
    const SwNodeOffset nEndNd = pEnd->GetNodeIndex();

    const sal_Int32 nSttCnt = pStart->GetContentIndex();
    const sal_Int32 nEndCnt = pEnd->GetContentIndex();

    const SwTextNode* pTNd = pStart->GetNode().GetTextNode();
    if( pStart == pEnd && pTNd )                  // no region ?
    {
        // do nothing
        return;
    }

    if( nSttNd != nEndNd )
    {
        SwNodeIndex aIdx( pStart->GetNode() );
        if( nSttCnt )
        {
            ++aIdx;
            if( pTNd )
                pTNd->CountWords( rStat, nSttCnt, pTNd->GetText().getLength() );
        }

        for( ; aIdx.GetIndex() < nEndNd; ++aIdx )
            if( nullptr != ( pTNd = aIdx.GetNode().GetTextNode() ))
                pTNd->CountWords( rStat, 0, pTNd->GetText().getLength() );

        if( nEndCnt && nullptr != ( pTNd = pEnd->GetNode().GetTextNode() ))
            pTNd->CountWords( rStat, 0, nEndCnt );
    }
    else if( pTNd && nSttCnt < nEndCnt )
        pTNd->CountWords( rStat, nSttCnt, nEndCnt );
}


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