/* -*- 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 <memory>

#include <com/sun/star/util/SearchFlags.hpp>
#include <com/sun/star/util/SearchResult.hpp>
#include <comphelper/lok.hxx>
#include <o3tl/safeint.hxx>
#include <rtl/ustrbuf.hxx>
#include <svx/svdview.hxx>
#include <svl/srchitem.hxx>
#include <sfx2/sfxsids.hrc>
#include <editeng/outliner.hxx>
#include <osl/diagnose.h>

#include <wrtsh.hxx>
#include <txatritr.hxx>
#include <fldbas.hxx>
#include <fmtfld.hxx>
#include <txtfld.hxx>
#include <txtfrm.hxx>
#include <rootfrm.hxx>
#include <swcrsr.hxx>
#include <redline.hxx>
#include <doc.hxx>
#include <IDocumentUndoRedo.hxx>
#include <IDocumentState.hxx>
#include <IDocumentDrawModelAccess.hxx>
#include <IDocumentRedlineAccess.hxx>
#include <dcontact.hxx>
#include <pamtyp.hxx>
#include <ndtxt.hxx>
#include <swundo.hxx>
#include <UndoInsert.hxx>
#include <breakit.hxx>
#include <docsh.hxx>
#include <PostItMgr.hxx>
#include <view.hxx>

using namespace ::com::sun::star;
using namespace util;

namespace {

/// because the Find may be called on the View or the Model, we need an index
/// afflicted by multiple personality disorder
struct AmbiguousIndex
{
private:
    sal_Int32 m_value;

#ifndef NDEBUG
    enum class tags : char { Any, Frame, Model };
    tags m_tag;
#endif

public:
    AmbiguousIndex() : m_value(-1)
#ifndef NDEBUG
           , m_tag(tags::Any)
#endif
    {}
    explicit AmbiguousIndex(sal_Int32 const value
#ifndef NDEBUG
            , tags const tag
#endif
        )   : m_value(value)
#ifndef NDEBUG
            , m_tag(tag)
#endif
    {}

    sal_Int32 & GetAnyIndex() { return m_value; } ///< for arithmetic
    sal_Int32 const& GetAnyIndex() const { return m_value; } ///< for arithmetic
    TextFrameIndex GetFrameIndex() const
    {
        assert(m_tag != tags::Model);
        return TextFrameIndex(m_value);
    }
    sal_Int32 GetModelIndex() const
    {
        assert(m_tag != tags::Frame);
        return m_value;
    }
    void SetFrameIndex(TextFrameIndex const value)
    {
#ifndef NDEBUG
        m_tag = tags::Frame;
#endif
        m_value = sal_Int32(value);
    }
    void SetModelIndex(sal_Int32 const value)
    {
#ifndef NDEBUG
        m_tag = tags::Model;
#endif
        m_value = value;
    }

    bool operator ==(AmbiguousIndex const& rOther) const
    {
        assert(m_tag == tags::Any || rOther.m_tag == tags::Any || m_tag == rOther.m_tag);
        return m_value == rOther.m_value;
    }
    bool operator <=(AmbiguousIndex const& rOther) const
    {
        assert(m_tag == tags::Any || rOther.m_tag == tags::Any || m_tag == rOther.m_tag);
        return m_value <= rOther.m_value;
    }
    bool operator < (AmbiguousIndex const& rOther) const
    {
        assert(m_tag == tags::Any || rOther.m_tag == tags::Any || m_tag == rOther.m_tag);
        return m_value <  rOther.m_value;
    }
    AmbiguousIndex operator - (AmbiguousIndex const& rOther) const
    {
        assert(m_tag == tags::Any || rOther.m_tag == tags::Any || m_tag == rOther.m_tag);
        return AmbiguousIndex(m_value - rOther.m_value
#ifndef NDEBUG
                , std::max(m_tag, rOther.m_tag)
#endif
            );
    }
};

class MaybeMergedIter
{
    std::optional<sw::MergedAttrIter> m_oMergedIter;
    SwTextNode const*const m_pNode;
    size_t m_HintIndex;

public:
    MaybeMergedIter(SwTextFrame const*const pFrame, SwTextNode const*const pNode)
        : m_pNode(pNode)
        , m_HintIndex(0)
    {
        if (pFrame)
        {
            m_oMergedIter.emplace(*pFrame);
        }
    }

    SwTextAttr const* NextAttr(SwTextNode const*& rpNode)
    {
        if (m_oMergedIter)
        {
            return m_oMergedIter->NextAttr(&rpNode);
        }
        if (SwpHints const*const pHints = m_pNode->GetpSwpHints())
        {
            if (m_HintIndex < pHints->Count())
            {
                rpNode = m_pNode;
                return pHints->Get(m_HintIndex++);
            }
        }
        return nullptr;
    }
};

}

static OUString
lcl_CleanStr(const SwTextNode& rNd,
             SwTextFrame const*const pFrame,
             SwRootFrame const*const pLayout,
             AmbiguousIndex const nStart, AmbiguousIndex & rEnd,
             std::vector<AmbiguousIndex> &rArr,
             bool const bRemoveSoftHyphen, bool const bRemoveCommentAnchors)
{
    OUStringBuffer buf(pLayout ? pFrame->GetText() : rNd.GetText());
    rArr.clear();

    MaybeMergedIter iter(pLayout ? pFrame : nullptr, pLayout ? nullptr : &rNd);

    AmbiguousIndex nSoftHyphen = nStart;
    AmbiguousIndex nHintStart;
    bool bNewHint       = true;
    bool bNewSoftHyphen = true;
    const AmbiguousIndex nEnd = rEnd;
    std::vector<AmbiguousIndex> aReplaced;
    SwTextNode const* pNextHintNode(nullptr);
    SwTextAttr const* pNextHint(iter.NextAttr(pNextHintNode));

    do
    {
        if ( bNewHint )
        {
            if (pLayout)
            {
                nHintStart.SetFrameIndex(pNextHint
                        ? pFrame->MapModelToView(pNextHintNode, pNextHint->GetStart())
                        : TextFrameIndex(-1));
            }
            else
            {
                nHintStart.SetModelIndex(pNextHint ? pNextHint->GetStart() : -1);
            }
        }

        if ( bNewSoftHyphen )
        {
            if (pLayout)
            {
                nSoftHyphen.SetFrameIndex(TextFrameIndex(bRemoveSoftHyphen
                    ? pFrame->GetText().indexOf(CHAR_SOFTHYPHEN, nSoftHyphen.GetAnyIndex())
                    : -1));
            }
            else
            {
                nSoftHyphen.SetModelIndex(bRemoveSoftHyphen
                    ? rNd.GetText().indexOf(CHAR_SOFTHYPHEN, nSoftHyphen.GetAnyIndex())
                    : -1);
            }
        }

        bNewHint       = false;
        bNewSoftHyphen = false;
        AmbiguousIndex nStt;

        // Check if next stop is a hint.
        if (0 <= nHintStart.GetAnyIndex()
            && (-1 == nSoftHyphen.GetAnyIndex() || nHintStart < nSoftHyphen)
            && nHintStart < nEnd )
        {
            nStt = nHintStart;
            bNewHint = true;
        }
        // Check if next stop is a soft hyphen.
        else if (   -1 != nSoftHyphen.GetAnyIndex()
                 && (-1 == nHintStart.GetAnyIndex() || nSoftHyphen < nHintStart)
                 && nSoftHyphen < nEnd)
        {
            nStt = nSoftHyphen;
            bNewSoftHyphen = true;
        }
        // If nSoftHyphen == nHintStart, the current hint *must* be a hint with an end.
        else if (-1 != nSoftHyphen.GetAnyIndex() && nSoftHyphen == nHintStart)
        {
            nStt = nSoftHyphen;
            bNewHint = true;
            bNewSoftHyphen = true;
        }
        else
            break;

        AmbiguousIndex nCurrent(nStt);
        nCurrent.GetAnyIndex() -= rArr.size();

        if ( bNewHint )
        {
            if (pNextHint && pNextHint->HasDummyChar() && (nStart <= nStt))
            {
                switch (pNextHint->Which())
                {
                case RES_TXTATR_FLYCNT:
                case RES_TXTATR_FIELD:
                case RES_TXTATR_REFMARK:
                case RES_TXTATR_TOXMARK:
                case RES_TXTATR_META:
                case RES_TXTATR_METAFIELD:
                    {
                        // (1998) they are desired as separators and
                        // belong not any longer to a word.
                        // they should also be ignored at a
                        // beginning/end of a sentence if blank. Those are
                        // simply removed if first. If at the end, we keep the
                        // replacement and remove afterwards all at a string's
                        // end (might be normal 0x7f).
                        const bool bEmpty = pNextHint->Which() != RES_TXTATR_FIELD
                            || (static_txtattr_cast<SwTextField const*>(pNextHint)->GetFormatField().GetField()->ExpandField(true, pLayout).isEmpty());
                        if ( bEmpty && nStart == nCurrent )
                        {
                            rArr.push_back( nCurrent );
                            if (rEnd.GetAnyIndex() > nCurrent.GetAnyIndex())
                            {
                                --rEnd.GetAnyIndex();
                            }
                            buf.remove(nCurrent.GetAnyIndex(), 1);
                        }
                        else
                        {
                            if ( bEmpty )
                                aReplaced.push_back( nCurrent );
                            buf[nCurrent.GetAnyIndex()] = '\x7f';
                        }
                    }
                    break;
                case RES_TXTATR_ANNOTATION:
                    {
                        if( bRemoveCommentAnchors )
                        {
                            rArr.push_back( nCurrent );
                            if (rEnd.GetAnyIndex() > nCurrent.GetAnyIndex())
                            {
                                --rEnd.GetAnyIndex();
                            }
                            buf.remove( nCurrent.GetAnyIndex(), 1 );
                        }
                    }
                    break;
                default:
                    OSL_FAIL( "unknown case in lcl_CleanStr" );
                    break;
                }
            }
            pNextHint = iter.NextAttr(pNextHintNode);
        }

        if ( bNewSoftHyphen )
        {
            rArr.push_back( nCurrent );

            // If the soft hyphen to be removed is past the end of the range we're searching in,
            // don't adjust the end.
            if (rEnd.GetAnyIndex() > nCurrent.GetAnyIndex())
            {
                --rEnd.GetAnyIndex();
            }

            buf.remove(nCurrent.GetAnyIndex(), 1);
            ++nSoftHyphen.GetAnyIndex();
        }
    }
    while ( true );

    for (auto i = aReplaced.size(); i; )
    {
        const AmbiguousIndex nTmp = aReplaced[ --i ];
        if (nTmp.GetAnyIndex() == buf.getLength() - 1)
        {
            buf.truncate(nTmp.GetAnyIndex());
            rArr.push_back( nTmp );
            --rEnd.GetAnyIndex();
        }
    }

    return buf.makeStringAndClear();
}

static bool DoSearch(SwPaM & rSearchPam,
    const i18nutil::SearchOptions2& rSearchOpt, utl::TextSearch& rSText,
    SwMoveFnCollection const & fnMove,
    bool bSrchForward, bool bRegSearch, bool bChkEmptyPara, bool bChkParaEnd,
    AmbiguousIndex & nStart, AmbiguousIndex & nEnd, AmbiguousIndex nTextLen,
    SwTextNode const* pNode, SwTextFrame const* pTextFrame,
    SwRootFrame const* pLayout, SwPaM& rPam);

namespace sw {

// @param xSearchItem allocate in parent so we can do so outside the calling loop
bool FindTextImpl(SwPaM & rSearchPam,
        const i18nutil::SearchOptions2& rSearchOpt, bool bSearchInNotes,
        utl::TextSearch& rSText,
        SwMoveFnCollection const & fnMove, const SwPaM & rRegion,
        bool bInReadOnly, SwRootFrame const*const pLayout,
        std::unique_ptr<SvxSearchItem>& xSearchItem)
{
    if( rSearchOpt.searchString.isEmpty() )
        return false;

    std::optional<SwPaM> oPam;
    sw::MakeRegion(fnMove, rRegion, oPam);
    const bool bSrchForward = &fnMove == &fnMoveForward;
    SwPosition& rPtPos = *oPam->GetPoint();

    // If bFound is true then the string was found and is between nStart and nEnd
    bool bFound = false;
    // start position in text or initial position
    bool bFirst = true;
    SwContentNode * pNode;

    const bool bRegSearch = SearchAlgorithms2::REGEXP == rSearchOpt.AlgorithmType2;
    const bool bChkEmptyPara = bRegSearch && 2 == rSearchOpt.searchString.getLength() &&
                        ( rSearchOpt.searchString == "^$" ||
                          rSearchOpt.searchString == "$^" );
    const bool bChkParaEnd = bRegSearch && rSearchOpt.searchString == "$";

    if (!xSearchItem)
    {
        xSearchItem.reset(new SvxSearchItem(SID_SEARCH_ITEM)); // this is a very expensive operation (calling configmgr etc.)
        xSearchItem->SetSearchOptions(rSearchOpt);
        xSearchItem->SetBackward(!bSrchForward);
    }

    // LanguageType eLastLang = 0;
    while (nullptr != (pNode = ::GetNode(*oPam, bFirst, fnMove, bInReadOnly, pLayout)))
    {
        if (!pNode->IsTextNode())
            continue;

        const SwTextNode& rTextNode = *pNode->GetTextNode();
        SwTextFrame const* const pFrame(
            pLayout ? static_cast<SwTextFrame const*>(rTextNode.getLayoutFrame(pLayout)) : nullptr);
        assert(!pLayout || pFrame);
        AmbiguousIndex nTextLen;
        if (pLayout)
        {
            nTextLen.SetFrameIndex(TextFrameIndex(pFrame->GetText().getLength()));
        }
        else
        {
            nTextLen.SetModelIndex(rTextNode.GetText().getLength());
        }
        AmbiguousIndex nEnd;
        if (pLayout ? FrameContainsNode(*pFrame, oPam->GetMark()->GetNodeIndex())
                    : rPtPos.GetNode() == oPam->GetMark()->GetNode())
        {
            if (pLayout)
            {
                nEnd.SetFrameIndex(pFrame->MapModelToViewPos(*oPam->GetMark()));
            }
            else
            {
                nEnd.SetModelIndex(oPam->GetMark()->GetContentIndex());
            }
        }
        else
        {
            if (bSrchForward)
            {
                nEnd = nTextLen;
            }
            else
            {
                if (pLayout)
                {
                    nEnd.SetFrameIndex(TextFrameIndex(0));
                }
                else
                {
                    nEnd.SetModelIndex(0);
                }
            }
        }
        AmbiguousIndex nStart;
        if (pLayout)
        {
            nStart.SetFrameIndex(pFrame->MapModelToViewPos(*oPam->GetPoint()));
        }
        else
        {
            nStart.SetModelIndex(rPtPos.GetContentIndex());
        }

        /* #i80135# */
        // if there are SwPostItFields inside our current node text, we
        // split the text into separate pieces and search for text inside
        // the pieces as well as inside the fields
        MaybeMergedIter iter(pLayout ? pFrame : nullptr, pLayout ? nullptr : &rTextNode);

        // count PostItFields by looping over all fields
        std::vector<std::pair<SwTextAttr const*, AmbiguousIndex>> postits;
        if (bSearchInNotes)
        {
            if (!bSrchForward)
            {
                std::swap(nStart, nEnd);
            }

            SwTextNode const* pTemp(nullptr);
            while (SwTextAttr const* const pTextAttr = iter.NextAttr(pTemp))
            {
                if (pTextAttr->Which() == RES_TXTATR_ANNOTATION)
                {
                    AmbiguousIndex aPos;
                    aPos.SetModelIndex(pTextAttr->GetStart());
                    if (pLayout)
                    {
                        aPos.SetFrameIndex(pFrame->MapModelToView(pTemp, aPos.GetModelIndex()));
                    }
                    if ((nStart <= aPos) && (aPos <= nEnd))
                    {
                        postits.emplace_back(pTextAttr, aPos);
                    }
                }
            }

            if (!bSrchForward)
            {
                std::swap(nStart, nEnd);
            }
        }

        SwDocShell* const pDocShell = pNode->GetDoc().GetDocShell();
        SwWrtShell* const pWrtShell = pDocShell ? pDocShell->GetWrtShell() : nullptr;
        SwPostItMgr* const pPostItMgr = pWrtShell ? pWrtShell->GetPostItMgr() : nullptr;

        // If there is an active text edit, then search there.
        bool bEndedTextEdit = false;
        SdrView* pSdrView = pWrtShell ? pWrtShell->GetDrawView() : nullptr;
        if (pSdrView)
        {
            // If the edited object is not anchored to this node, then ignore it.
            SdrObject* pObject = pSdrView->GetTextEditObject();
            if (pObject)
            {
                if (SwFrameFormat* pFrameFormat = FindFrameFormat(pObject))
                {
                    const SwNode* pAnchorNode = pFrameFormat->GetAnchor().GetAnchorNode();
                    if (!pAnchorNode
                        || (pLayout ? !FrameContainsNode(*pFrame, pAnchorNode->GetIndex())
                                    : pAnchorNode->GetIndex() != pNode->GetIndex()))
                        pObject = nullptr;
                }
            }

            if (pObject)
            {
                sal_uInt16 nResult
                    = pSdrView->GetTextEditOutlinerView()->StartSearchAndReplace(*xSearchItem);
                if (!nResult)
                {
                    // If not found, end the text edit.
                    pSdrView->SdrEndTextEdit();
                    const Point aPoint(pSdrView->GetAllMarkedRect().TopLeft());
                    pSdrView->UnmarkAll();
                    pWrtShell->CallSetCursor(&aPoint, true);
                    pWrtShell->Edit();
                    bEndedTextEdit = true;
                }
                else
                {
                    bFound = true;
                    break;
                }
            }
        }

        if (comphelper::LibreOfficeKit::isActive())
        {
            // Writer and editeng selections are not supported in parallel.
            SvxSearchItem* pSearchItem = SwView::GetSearchItem();
            // If we just finished search in shape text, don't attempt to do that again.
            if (!bEndedTextEdit
                && !(pSearchItem && pSearchItem->GetCommand() == SvxSearchCmd::FIND_ALL))
            {
                // If there are any shapes anchored to this node, search there.
                SwPaM aPaM(pNode->GetDoc().GetNodes().GetEndOfContent());
                if (pLayout)
                {
                    *aPaM.GetPoint() = pFrame->MapViewToModelPos(nStart.GetFrameIndex());
                }
                else
                {
                    aPaM.GetPoint()->Assign(rTextNode, nStart.GetModelIndex());
                }
                aPaM.SetMark();
                if (pLayout)
                {
                    aPaM.GetMark()->Assign(
                        (pFrame->GetMergedPara() ? *pFrame->GetMergedPara()->pLastNode : rTextNode)
                            .GetIndex()
                        + 1);
                }
                else
                {
                    aPaM.GetMark()->Assign(rTextNode.GetIndex() + 1);
                }
                if (pNode->GetDoc().getIDocumentDrawModelAccess().Search(aPaM, *xSearchItem)
                    && pSdrView)
                {
                    if (SdrObject* pObject = pSdrView->GetTextEditObject())
                    {
                        if (SwFrameFormat* pFrameFormat = FindFrameFormat(pObject))
                        {
                            const SwNode* pAnchorNode = pFrameFormat->GetAnchor().GetAnchorNode();
                            if (pAnchorNode)
                            {
                                // Set search position to the shape's anchor point.
                                rSearchPam.GetPoint()->Assign(*pAnchorNode);
                                rSearchPam.SetMark();
                                bFound = true;
                                break;
                            }
                        }
                    }
                }
            }
        }

        // do we need to finish a note?
        if (pPostItMgr && pPostItMgr->HasActiveSidebarWin())
        {
            if (bSearchInNotes)
            {
                if (!postits.empty())
                {
                    if (bSrchForward)
                    {
                        postits.erase(postits.begin());
                    }
                    else
                    {
                        postits.pop_back(); // hope that's the right one?
                    }
                }
                //search inside, finish and put focus back into the doc
                if (pPostItMgr->FinishSearchReplace(rSearchOpt, bSrchForward))
                {
                    bFound = true;
                    break;
                }
            }
            else
            {
                pPostItMgr->SetActiveSidebarWin(nullptr);
            }
        }

        if (!postits.empty())
        {
            // now we have to split
            AmbiguousIndex nStartInside;
            AmbiguousIndex nEndInside;
            sal_Int32 aLoop = bSrchForward ? 0 : postits.size();

            while ((0 <= aLoop) && (o3tl::make_unsigned(aLoop) <= postits.size()))
            {
                if (bSrchForward)
                {
                    if (aLoop == 0)
                    {
                        nStartInside = nStart;
                    }
                    else if (pLayout)
                    {
                        nStartInside.SetFrameIndex(postits[aLoop - 1].second.GetFrameIndex()
                                                   + TextFrameIndex(1));
                    }
                    else
                    {
                        nStartInside.SetModelIndex(postits[aLoop - 1].second.GetModelIndex() + 1);
                    }
                    nEndInside = static_cast<size_t>(aLoop) == postits.size()
                                     ? nEnd
                                     : postits[aLoop].second;
                    nTextLen = nEndInside - nStartInside;
                }
                else
                {
                    nStartInside = static_cast<size_t>(aLoop) == postits.size()
                                       ? nStart
                                       : postits[aLoop].second;
                    if (aLoop == 0)
                    {
                        nEndInside = nEnd;
                    }
                    else if (pLayout)
                    {
                        nEndInside.SetFrameIndex(postits[aLoop - 1].second.GetFrameIndex()
                                                 + TextFrameIndex(1));
                    }
                    else
                    {
                        nEndInside.SetModelIndex(postits[aLoop - 1].second.GetModelIndex() + 1);
                    }
                    nTextLen = nStartInside - nEndInside;
                }
                // search inside the text between a note
                bFound = DoSearch(rSearchPam, rSearchOpt, rSText, fnMove, bSrchForward, bRegSearch,
                                  bChkEmptyPara, bChkParaEnd, nStartInside, nEndInside, nTextLen,
                                  pNode->GetTextNode(), pFrame, pLayout, *oPam);
                if (bFound)
                    break;
                else
                {
                    // we should now be right in front of a note, search inside
                    if (bSrchForward ? (static_cast<size_t>(aLoop) != postits.size())
                                     : (aLoop != 0))
                    {
                        const SwTextAttr* const pTextAttr
                            = bSrchForward ? postits[aLoop].first : postits[aLoop - 1].first;
                        if (pPostItMgr
                            && pPostItMgr->SearchReplace(
                                   static_txtattr_cast<SwTextField const*>(pTextAttr)
                                       ->GetFormatField(),
                                   rSearchOpt, bSrchForward))
                        {
                            bFound = true;
                            break;
                        }
                    }
                }
                aLoop = bSrchForward ? aLoop + 1 : aLoop - 1;
            }
        }
        else
        {
            // if there is no SwPostItField inside or searching inside notes
            // is disabled, we search the whole length just like before
            bFound = DoSearch(rSearchPam, rSearchOpt, rSText, fnMove, bSrchForward, bRegSearch,
                              bChkEmptyPara, bChkParaEnd, nStart, nEnd, nTextLen,
                              pNode->GetTextNode(), pFrame, pLayout, *oPam);
        }
        if (bFound)
            break;
    }
    return bFound;
}

} // namespace sw

bool DoSearch(SwPaM & rSearchPam,
        const i18nutil::SearchOptions2& rSearchOpt, utl::TextSearch& rSText,
                      SwMoveFnCollection const & fnMove, bool bSrchForward, bool bRegSearch,
                      bool bChkEmptyPara, bool bChkParaEnd,
        AmbiguousIndex & nStart, AmbiguousIndex & nEnd, AmbiguousIndex const nTextLen,
        SwTextNode const*const pNode, SwTextFrame const*const pFrame,
        SwRootFrame const*const pLayout, SwPaM& rPam)
{
    if (bRegSearch && rSearchOpt.searchString.endsWith("$"))
    {
        bool bAlwaysSearchingForEndOfPara = true;
        sal_Int32 nIndex = 0;
        while ((nIndex = rSearchOpt.searchString.indexOf("|", nIndex)) != -1)
        {
            if (!nIndex || rSearchOpt.searchString[nIndex - 1] != '$')
            {
                bAlwaysSearchingForEndOfPara = false;
                break;
            }
            ++nIndex;
        }
        // when searching for something at the end of the paragraph, the para end must be in range
        const AmbiguousIndex& rParaEnd = bSrchForward ? nEnd : nStart;
        if (bAlwaysSearchingForEndOfPara && nTextLen.GetAnyIndex() != rParaEnd.GetAnyIndex())
            return false;
    }

    bool bFound = false;
    OUString sCleanStr;
    std::vector<AmbiguousIndex> aFltArr;
    LanguageType eLastLang = LANGUAGE_SYSTEM;
    // if the search string contains a soft hyphen,
    // we don't strip them from the text:
    bool bRemoveSoftHyphens = true;
    // if the search string contains a comment, we don't strip them from the text
    const bool bRemoveCommentAnchors = rSearchOpt.searchString.indexOf( CH_TXTATR_INWORD ) == -1;

    if ( bRegSearch )
    {
        if (   -1 != rSearchOpt.searchString.indexOf("\\xAD")
            || -1 != rSearchOpt.searchString.indexOf("\\x{00AD}")
            || -1 != rSearchOpt.searchString.indexOf("\\u00AD")
            || -1 != rSearchOpt.searchString.indexOf("\\u00ad")
            || -1 != rSearchOpt.searchString.indexOf("\\U000000AD")
            || -1 != rSearchOpt.searchString.indexOf("\\N{SOFT HYPHEN}"))
        {
             bRemoveSoftHyphens = false;
        }
    }
    else
    {
        if ( 1 == rSearchOpt.searchString.getLength() &&
             CHAR_SOFTHYPHEN == rSearchOpt.searchString.toChar() )
             bRemoveSoftHyphens = false;
    }

    if( bSrchForward )
        sCleanStr = lcl_CleanStr(*pNode, pFrame, pLayout, nStart, nEnd,
                        aFltArr, bRemoveSoftHyphens, bRemoveCommentAnchors);
    else
        sCleanStr = lcl_CleanStr(*pNode, pFrame, pLayout, nEnd, nStart,
                        aFltArr, bRemoveSoftHyphens, bRemoveCommentAnchors);

    std::unique_ptr<SwScriptIterator> pScriptIter;
    sal_uInt16 nSearchScript = 0;
    sal_uInt16 nCurrScript = 0;

    if (SearchAlgorithms2::APPROXIMATE == rSearchOpt.AlgorithmType2)
    {
        pScriptIter.reset(new SwScriptIterator(sCleanStr, nStart.GetAnyIndex(), bSrchForward));
        nSearchScript = g_pBreakIt->GetRealScriptOfText( rSearchOpt.searchString, 0 );
    }

    const AmbiguousIndex nStringEnd = nEnd;
    bool bZeroMatch = false;    // zero-length match, i.e. only $ anchor as regex
    while ( ((bSrchForward && nStart < nStringEnd) ||
            (!bSrchForward && nStringEnd < nStart)) && !bZeroMatch )
    {
        // SearchAlgorithms_APPROXIMATE works on a per word base so we have to
        // provide the text searcher with the correct locale, because it uses
        // the break-iterator
        if ( pScriptIter )
        {
            nEnd.GetAnyIndex() = pScriptIter->GetScriptChgPos();
            nCurrScript = pScriptIter->GetCurrScript();
            if ( nSearchScript == nCurrScript )
            {
                const LanguageType eCurrLang = pLayout
                        ? pFrame->GetLangOfChar(bSrchForward
                                ? nStart.GetFrameIndex()
                                : nEnd.GetFrameIndex(),
                            0, true)
                        : pNode->GetLang(bSrchForward
                                ? nStart.GetModelIndex()
                                : nEnd.GetModelIndex());

                if ( eCurrLang != eLastLang )
                {
                    const lang::Locale aLocale(
                            g_pBreakIt->GetLocale( eCurrLang ) );
                    rSText.SetLocale(rSearchOpt, aLocale);
                    eLastLang = eCurrLang;
                }
            }
            pScriptIter->Next();
        }
        AmbiguousIndex nProxyStart = nStart;
        AmbiguousIndex nProxyEnd = nEnd;
        if( nSearchScript == nCurrScript &&
                (rSText.*fnMove.fnSearch)( sCleanStr, &nProxyStart.GetAnyIndex(), &nProxyEnd.GetAnyIndex(), nullptr) &&
                !(bZeroMatch = (nProxyStart == nProxyEnd)))
        {
            nStart = nProxyStart;
            nEnd = nProxyEnd;
            // set section correctly
            *rSearchPam.GetPoint() = *rPam.GetPoint();
            rSearchPam.SetMark();

            // adjust start and end
            if( !aFltArr.empty() )
            {
                // if backward search, switch positions temporarily
                if (!bSrchForward) { std::swap(nStart, nEnd); }

                AmbiguousIndex nNew = nStart;
                for (size_t n = 0; n < aFltArr.size() && aFltArr[ n ] <= nStart; ++n )
                {
                    ++nNew.GetAnyIndex();
                }

                nStart = nNew;
                nNew = nEnd;
                for( size_t n = 0; n < aFltArr.size() && aFltArr[ n ] < nEnd; ++n )
                {
                    ++nNew.GetAnyIndex();
                }

                nEnd = nNew;
                // if backward search, switch positions temporarily
                if( !bSrchForward ) { std::swap(nStart, nEnd); }
            }
            if (pLayout)
            {
                *rSearchPam.GetMark() = pFrame->MapViewToModelPos(nStart.GetFrameIndex());
                *rSearchPam.GetPoint() = pFrame->MapViewToModelPos(nEnd.GetFrameIndex());
            }
            else
            {
                rSearchPam.GetMark()->SetContent( nStart.GetModelIndex() );
                rSearchPam.GetPoint()->SetContent( nEnd.GetModelIndex() );
            }

            // if backward search, switch point and mark
            if( !bSrchForward )
                rSearchPam.Exchange();
            bFound = true;
            break;
        }
        else
        {
            nEnd = nProxyEnd;
        }
        nStart = nEnd;
    }

    pScriptIter.reset();

    if ( bFound )
        return true;

    if (!bChkEmptyPara && !bChkParaEnd)
        return false;

    if (bChkEmptyPara && bSrchForward && nTextLen.GetAnyIndex())
        return false; // the length is not zero - there is content here

    // move to the end (or start) of the paragraph
    *rSearchPam.GetPoint() = *rPam.GetPoint();
    if (pLayout)
    {
        *rSearchPam.GetPoint() = pFrame->MapViewToModelPos(
            bSrchForward ? nTextLen.GetFrameIndex() : TextFrameIndex(0));
    }
    else
    {
        rSearchPam.GetPoint()->SetContent(bSrchForward ? nTextLen.GetModelIndex() : 0);
    }
    rSearchPam.SetMark();

    if (!rSearchPam.Move(fnMove, GoInContent))
        return false; // at start or end of the document

    // selection must not be outside of the search area
    if (!rPam.ContainsPosition(*rSearchPam.GetPoint()))
        return false;

    if (SwNodeOffset(1) == abs(rSearchPam.GetPoint()->GetNodeIndex() -
                               rSearchPam.GetMark()->GetNodeIndex()))
    {
        if (bChkEmptyPara && !bSrchForward && rSearchPam.GetPoint()->GetContentIndex())
            return false; // the length is not zero - there is content here

        return true;
    }

    return false;
}

namespace {

/// parameters for search and replace in text
struct SwFindParaText : public SwFindParas
{
    const i18nutil::SearchOptions2& m_rSearchOpt;
    SwCursor& m_rCursor;
    SwRootFrame const* m_pLayout;
    utl::TextSearch m_aSText;
    bool m_bReplace;
    bool m_bSearchInNotes;

    SwFindParaText(const i18nutil::SearchOptions2& rOpt, bool bSearchInNotes,
            bool bRepl, SwCursor& rCursor, SwRootFrame const*const pLayout)
        : m_rSearchOpt( rOpt )
        , m_rCursor( rCursor )
        , m_pLayout(pLayout)
        , m_aSText(rOpt)
        , m_bReplace( bRepl )
        , m_bSearchInNotes( bSearchInNotes )
    {}
    virtual int DoFind(SwPaM &, SwMoveFnCollection const &, const SwPaM &, bool bInReadOnly, std::unique_ptr<SvxSearchItem>& xSearchItem) override;
    virtual bool IsReplaceMode() const override;
    virtual ~SwFindParaText();
};

}

SwFindParaText::~SwFindParaText()
{
}

int SwFindParaText::DoFind(SwPaM & rCursor, SwMoveFnCollection const & fnMove,
                          const SwPaM & rRegion, bool bInReadOnly,
                          std::unique_ptr<SvxSearchItem>& xSearchItem)
{
    if( bInReadOnly && m_bReplace )
        bInReadOnly = false;

    const bool bFnd = sw::FindTextImpl(rCursor, m_rSearchOpt, m_bSearchInNotes,
            m_aSText, fnMove, rRegion, bInReadOnly, m_pLayout, xSearchItem);

    if( bFnd && m_bReplace ) // replace string
    {
        // use replace method in SwDoc
        const bool bRegExp(SearchAlgorithms2::REGEXP == m_rSearchOpt.AlgorithmType2);
        const sal_Int32 nSttCnt = rCursor.Start()->GetContentIndex();
        // add to shell-cursor-ring so that the regions will be moved eventually
        SwPaM* pPrev(nullptr);
        if( bRegExp )
        {
            pPrev = const_cast<SwPaM&>(rRegion).GetPrev();
            const_cast<SwPaM&>(rRegion).GetRingContainer().merge( m_rCursor.GetRingContainer() );
        }

        std::optional<OUString> xRepl;
        if (bRegExp)
            xRepl = sw::ReplaceBackReferences(m_rSearchOpt, &rCursor, m_pLayout);
        bool const bReplaced = sw::ReplaceImpl(rCursor,
                xRepl ? *xRepl : m_rSearchOpt.replaceString,
                bRegExp, m_rCursor.GetDoc(), m_pLayout);

        m_rCursor.SaveTableBoxContent( rCursor.GetPoint() );

        if( bRegExp )
        {
            // and remove region again
            SwPaM* p;
            SwPaM* pNext(const_cast<SwPaM*>(&rRegion));
            do {
                p = pNext;
                pNext = p->GetNext();
                p->MoveTo(const_cast<SwPaM*>(&rRegion));
            } while( p != pPrev );
        }
        if (bRegExp && !bReplaced)
        {   // fdo#80715 avoid infinite loop if join failed
            bool bRet = ((&fnMoveForward == &fnMove) ? &GoNextPara : &GoPrevPara)
                (rCursor, fnMove);
            (void) bRet;
            assert(bRet); // if join failed, next node must be SwTextNode
        }
        else
            rCursor.Start()->SetContent(nSttCnt);
        return FIND_NO_RING;
    }
    return bFnd ? FIND_FOUND : FIND_NOT_FOUND;
}

bool SwFindParaText::IsReplaceMode() const
{
    return m_bReplace;
}

sal_Int32 SwCursor::Find_Text( const i18nutil::SearchOptions2& rSearchOpt, bool bSearchInNotes,
                          SwDocPositions nStart, SwDocPositions nEnd,
                          bool& bCancel, FindRanges eFndRngs, bool bReplace,
                          SwRootFrame const*const pLayout)
{
    // switch off OLE-notifications
    SwDoc& rDoc = GetDoc();
    Link<bool,void> aLnk( rDoc.GetOle2Link() );
    rDoc.SetOle2Link( Link<bool,void>() );

    bool const bStartUndo = rDoc.GetIDocumentUndoRedo().DoesUndo() && bReplace;
    if (bStartUndo)
    {
        rDoc.GetIDocumentUndoRedo().StartUndo( SwUndoId::REPLACE, nullptr );
    }

    bool bSearchSel = 0 != (rSearchOpt.searchFlag & SearchFlags::REG_NOT_BEGINOFLINE);
    if( bSearchSel )
        eFndRngs = static_cast<FindRanges>(eFndRngs | FindRanges::InSel);
    SwFindParaText aSwFindParaText(rSearchOpt, bSearchInNotes, bReplace, *this, pLayout);
    sal_Int32 nRet = FindAll( aSwFindParaText, nStart, nEnd, eFndRngs, bCancel );
    rDoc.SetOle2Link( aLnk );
    if( nRet && bReplace )
        rDoc.getIDocumentState().SetModified();

    if (bStartUndo)
    {
        SwRewriter rewriter(MakeUndoReplaceRewriter(
                nRet, rSearchOpt.searchString, rSearchOpt.replaceString));
        rDoc.GetIDocumentUndoRedo().EndUndo( SwUndoId::REPLACE, & rewriter );
    }
    return nRet;
}

namespace sw {

bool ReplaceImpl(
        SwPaM & rCursor,
        OUString const& rReplacement,
        bool const bRegExp,
        SwDoc & rDoc,
        SwRootFrame const*const pLayout)
{
    bool bReplaced(true);
    IDocumentContentOperations & rIDCO(rDoc.getIDocumentContentOperations());
#if 0
    // FIXME there's some problem with multiple redlines here on Undo
    std::vector<std::shared_ptr<SwUnoCursor>> ranges;
    if (rDoc.getIDocumentRedlineAccess().IsRedlineOn()
        || !pLayout
        || !pLayout->IsHideRedlines()
        || sw::GetRanges(ranges, rDoc, rCursor))
    {
        bReplaced = rIDCO.ReplaceRange(rCursor, rReplacement, bRegExp);
    }
    else
    {
        assert(!ranges.empty());
        assert(ranges.front()->GetPoint()->GetNode() == ranges.front()->GetMark()->GetNode());
        bReplaced = rIDCO.ReplaceRange(*ranges.front(), rReplacement, bRegExp);
        for (auto it = ranges.begin() + 1; it != ranges.end(); ++it)
        {
            bReplaced &= rIDCO.DeleteAndJoin(**it);
        }
    }
#else
    IDocumentRedlineAccess const& rIDRA(rDoc.getIDocumentRedlineAccess());
    if (pLayout && pLayout->IsHideRedlines()
        && !rIDRA.IsRedlineOn() // otherwise: ReplaceRange will handle it
        && (rIDRA.GetRedlineFlags() & RedlineFlags::ShowDelete)) // otherwise: ReplaceRange will DeleteRedline()
    {
        SwRedlineTable::size_type tmp;
        rIDRA.GetRedline(*rCursor.Start(), &tmp);
        while (tmp < rIDRA.GetRedlineTable().size())
        {
            SwRangeRedline const*const pRedline(rIDRA.GetRedlineTable()[tmp]);
            if (*rCursor.End() <= *pRedline->Start())
            {
                break;
            }
            if (*pRedline->End() <= *rCursor.Start())
            {
                ++tmp;
                continue;
            }
            if (pRedline->GetType() == RedlineType::Delete)
            {
                assert(*pRedline->Start() != *pRedline->End());
                // search in hidden layout can't overlap redlines
                assert(*rCursor.Start() <= *pRedline->Start() && *pRedline->End() <= *rCursor.End());
                SwPaM pam(*pRedline, nullptr);
                bReplaced &= rIDCO.DeleteAndJoin(pam);
            }
            else
            {
                ++tmp;
            }
        }
    }
    bReplaced &= rIDCO.ReplaceRange(rCursor, rReplacement, bRegExp);
#endif
    return bReplaced;
}

std::optional<OUString> ReplaceBackReferences(const i18nutil::SearchOptions2& rSearchOpt,
        SwPaM *const pPam, SwRootFrame const*const pLayout)
{
    std::optional<OUString> xRet;
    if( pPam && pPam->HasMark() &&
        SearchAlgorithms2::REGEXP == rSearchOpt.AlgorithmType2 )
    {
        SwContentNode const*const pTextNode = pPam->GetPointContentNode();
        SwContentNode const*const pMarkTextNode = pPam->GetMarkContentNode();
        if (!pTextNode || !pTextNode->IsTextNode()
            || !pMarkTextNode || !pMarkTextNode->IsTextNode())
        {
            return xRet;
        }
        SwTextFrame const*const pFrame(pLayout
            ? static_cast<SwTextFrame const*>(pTextNode->getLayoutFrame(pLayout))
            : nullptr);
        const bool bParaEnd = rSearchOpt.searchString == "$" || rSearchOpt.searchString == "^$" || rSearchOpt.searchString == "$^";
        if (bParaEnd || (pLayout
                ? sw::FrameContainsNode(*pFrame, pPam->GetMark()->GetNodeIndex())
                : pTextNode == pMarkTextNode))
        {
            utl::TextSearch aSText(rSearchOpt);
            SearchResult aResult;
            OUString aReplaceStr( rSearchOpt.replaceString );
            if (bParaEnd)
            {
                static constexpr OUString aStr(u"\\n"_ustr);
                aResult.subRegExpressions = 1;
                aResult.startOffset = { 0 };
                aResult.endOffset = { aStr.getLength() };
                utl::TextSearch::ReplaceBackReferences( aReplaceStr, aStr, aResult );
                xRet = aReplaceStr;
            }
            else
            {
                AmbiguousIndex nStart;
                AmbiguousIndex nEnd;
                if (pLayout)
                {
                    nStart.SetFrameIndex(pFrame->MapModelToViewPos(*pPam->Start()));
                    nEnd.SetFrameIndex(pFrame->MapModelToViewPos(*pPam->End()));
                }
                else
                {
                    nStart.SetModelIndex(pPam->Start()->GetContentIndex());
                    nEnd.SetModelIndex(pPam->End()->GetContentIndex());
                }
                std::vector<AmbiguousIndex> aFltArr;
                OUString const aStr = lcl_CleanStr(*pTextNode->GetTextNode(), pFrame, pLayout,
                                                   nStart, nEnd, aFltArr, false, false);
                if (aSText.SearchForward(aStr, &nStart.GetAnyIndex(), &nEnd.GetAnyIndex(), &aResult))
                {
                    utl::TextSearch::ReplaceBackReferences( aReplaceStr, aStr, aResult );
                    xRet = aReplaceStr;
                }
            }
        }
    }
    return xRet;
}

} // namespace sw

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