/* -*- 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 <ToxTextGenerator.hxx>
 
#include <chpfld.hxx>
#include <cntfrm.hxx>
#include <txtfrm.hxx>
#include <rootfrm.hxx>
#include <ndindex.hxx>
#include <fchrfmt.hxx>
#include <doc.hxx>
#include <IDocumentLayoutAccess.hxx>
#include <IDocumentStylePoolAccess.hxx>
#include <ndtxt.hxx>
#include <tox.hxx>
#include <txmsrt.hxx>
#include <fmtautofmt.hxx>
#include <swatrset.hxx>
#include <ToxWhitespaceStripper.hxx>
#include <ToxLinkProcessor.hxx>
#include <ToxTabStopTokenHandler.hxx>
#include <txatbase.hxx>
#include <modeltoviewhelper.hxx>
#include <strings.hrc>
 
#include <osl/diagnose.h>
#include <rtl/ustrbuf.hxx>
#include <svl/itemiter.hxx>
 
#include <cassert>
#include <memory>
#include <utility>
 
namespace {
 
bool sortTabHasNoToxSourcesOrFirstToxSourceHasNoNode(const SwTOXSortTabBase& sortTab)
{
    if (sortTab.aTOXSources.empty()) {
        return true;
    }
    if (sortTab.aTOXSources.at(0).pNd == nullptr) {
        return true;
    }
    return false;
}
 
// Similar to rtl::isAsciiWhiteSpace, but applicable to ToC entry number
bool isWhiteSpace(sal_Unicode ch) { return ch == ' ' || ch == '\t'; }
 
} // end anonymous namespace
 
namespace sw {
 
OUString
ToxTextGenerator::GetNumStringOfFirstNode(const SwTOXSortTabBase& rBase,
        bool bUsePrefix, sal_uInt8 nLevel,
        SwRootFrame const*const pLayout, bool bAddSpace)
{
    if (sortTabHasNoToxSourcesOrFirstToxSourceHasNoNode(rBase)) {
        return OUString();
    }
 
    OUString sRet;
    if (rBase.pTextMark) { // only if it's not a Mark
        return sRet;
    }
 
    const SwTextNode* pNd = rBase.aTOXSources[0].pNd->GetTextNode();
    if (!pNd) {
        return sRet;
    }
    if (pLayout && pLayout->HasMergedParas())
    {   // note: pNd could be any node, since it could be Sequence etc.
        pNd = sw::GetParaPropsNode(*pLayout, *pNd);
    }
 
    const SwNumRule* pRule = pNd->GetNumRule();
    if (!pRule) {
        return sRet;
    }
 
    if (pNd->GetActualListLevel() < MAXLEVEL) {
        sRet = pNd->GetNumString(bUsePrefix, nLevel, pLayout);
    }
 
    if (bAddSpace && !sRet.isEmpty() && !isWhiteSpace(sRet[sRet.getLength() - 1])) {
        sRet += " ";// Makes sure spacing is done only when there is outline numbering
    }
 
    return sRet;
}
 
 
ToxTextGenerator::ToxTextGenerator(const SwForm& toxForm,
        std::shared_ptr<ToxTabStopTokenHandler> tabStopHandler)
: mToxForm(toxForm),
  mLinkProcessor(std::make_shared<ToxLinkProcessor>()),
  mTabStopTokenHandler(std::move(tabStopHandler))
{}
 
ToxTextGenerator::~ToxTextGenerator()
{}
 
OUString
ToxTextGenerator::HandleChapterToken(const SwTOXSortTabBase& rBase,
        const SwFormToken& aToken, SwRootFrame const*const pLayout) const
{
    if (sortTabHasNoToxSourcesOrFirstToxSourceHasNoNode(rBase)) {
        return OUString();
    }
 
    // A bit tricky: Find a random Frame
    const SwContentNode* contentNode = rBase.aTOXSources.at(0).pNd->GetContentNode();
    if (!contentNode) {
        return OUString();
    }
 
    // #i53420#
    const SwContentFrame* contentFrame = contentNode->getLayoutFrame(pLayout);
    if (!contentFrame) {
        return OUString();
    }
 
    return GenerateTextForChapterToken(aToken, contentFrame, contentNode, pLayout);
}
 
OUString
ToxTextGenerator::GenerateTextForChapterToken(const SwFormToken& chapterToken, const SwContentFrame* contentFrame,
        const SwContentNode *contentNode,
        SwRootFrame const*const pLayout) const
{
    OUString retval;
 
    SwChapterFieldType chapterFieldType;
    SwChapterField aField = ObtainChapterField(&chapterFieldType, &chapterToken, contentFrame, contentNode);
 
    //---> #i89791#
    // continue to support CF_NUMBER and CF_NUM_TITLE in order to handle ODF 1.0/1.1 written by OOo 3.x
    // in the same way as OOo 2.x would handle them.
    if (CF_NUM_NOPREPST_TITLE == chapterToken.nChapterFormat || CF_NUMBER == chapterToken.nChapterFormat) {
        retval += aField.GetNumber(pLayout); // get the string number without pre/postfix
    }
    else if (CF_NUMBER_NOPREPST == chapterToken.nChapterFormat || CF_NUM_TITLE == chapterToken.nChapterFormat) {
        retval += aField.GetNumber(pLayout) + " " + aField.GetTitle(pLayout);
    } else if (CF_TITLE == chapterToken.nChapterFormat) {
        retval += aField.GetTitle(pLayout);
    }
    return retval;
}
 
std::optional<std::pair<SwTextNode *, SvxTabStopItem>>
ToxTextGenerator::GenerateText(SwDoc* pDoc,
        std::unordered_map<OUString, int> & rMarkURLs,
        const std::vector<std::unique_ptr<SwTOXSortTabBase>> &entries,
        sal_uInt16 indexOfEntryToProcess, sal_uInt16 numberOfEntriesToProcess,
        SwRootFrame const*const pLayout)
{
    std::optional<std::pair<SwTextNode *, SvxTabStopItem>> oRet;
    // pTOXNd is only set at the first mark
    SwTextNode* pTOXNd = const_cast<SwTextNode*>(entries.at(indexOfEntryToProcess)->pTOXNd);
    // FIXME this operates directly on the node text
    OUString & rText = const_cast<OUString&>(pTOXNd->GetText());
    rText.clear();
    for(sal_uInt16 nIndex = indexOfEntryToProcess; nIndex < indexOfEntryToProcess + numberOfEntriesToProcess; nIndex++)
    {
        if(nIndex > indexOfEntryToProcess)
            rText += ", "; // comma separation
        // Initialize String with the Pattern from the form
        const SwTOXSortTabBase& rBase = *entries.at(nIndex);
        sal_uInt16 nLvl = rBase.GetLevel();
        OSL_ENSURE( nLvl < mToxForm.GetFormMax(), "invalid FORM_LEVEL");
 
        oRet.emplace(pTOXNd, SvxTabStopItem(0, 0, SvxTabAdjust::Default, RES_PARATR_TABSTOP));
        // create an enumerator
        // #i21237#
        SwFormTokens aPattern = mToxForm.GetPattern(nLvl);
        // remove text from node
        for (size_t i = 0; i < aPattern.size(); ++i) // #i21237#
        {
            const auto& aToken = aPattern[i];
            sal_Int32 nStartCharStyle = rText.getLength();
            OUString aCharStyleName = aToken.sCharStyleName;
            switch( aToken.eTokenType )
            {
            case TOKEN_ENTRY_NO:
                // for TOC numbering
                // Only add space when there is outline numbering, and also when the next token
                // is the entry text: it can also be e.g. a tab, or the entry number can be used
                // in page number area like "2-15" for chapter 2, page 15.
                rText += GetNumStringOfFirstNode(rBase,
                    aToken.nChapterFormat == CF_NUMBER,
                    static_cast<sal_uInt8>(aToken.nOutlineLevel - 1), pLayout,
                    i < aPattern.size() - 1 && aPattern[i + 1].eTokenType == TOKEN_ENTRY_TEXT);
                break;
 
            case TOKEN_ENTRY_TEXT: {
                HandledTextToken htt = HandleTextToken(rBase, pDoc->GetAttrPool(), pLayout);
                ApplyHandledTextToken(htt, *pTOXNd);
            }
                break;
 
            case TOKEN_ENTRY:
                {
                    // for TOC numbering
                    rText += GetNumStringOfFirstNode(rBase, true, MAXLEVEL, pLayout);
                    HandledTextToken htt = HandleTextToken(rBase, pDoc->GetAttrPool(), pLayout);
                    ApplyHandledTextToken(htt, *pTOXNd);
                }
                break;
 
            case TOKEN_TAB_STOP: {
                ToxTabStopTokenHandler::HandledTabStopToken htst =
                    mTabStopTokenHandler->HandleTabStopToken(aToken, *pTOXNd);
                rText += htst.text;
                oRet->second.Insert(htst.tabStop);
                break;
            }
 
            case TOKEN_TEXT:
                rText += aToken.sText;
                break;
 
            case TOKEN_PAGE_NUMS:
                rText += ConstructPageNumberPlaceholder(rBase.aTOXSources.size());
                break;
 
            case TOKEN_CHAPTER_INFO:
                rText += HandleChapterToken(rBase, aToken, pLayout);
                break;
 
            case TOKEN_LINK_START:
                mLinkProcessor->StartNewLink(rText.getLength(), aToken.sCharStyleName);
                break;
 
            case TOKEN_LINK_END:
                {
                    auto [url, isMark] = rBase.GetURL(pLayout);
                    if (isMark)
                    {
                        auto [iter, _] = rMarkURLs.emplace(url, 0);
                        (void) _; // sigh... ignore it more explicitly
                        ++iter->second;
                        url = "#" + OUString::number(iter->second) + url;
                    }
                    mLinkProcessor->CloseLink(rText.getLength(), url, /*bRelative=*/true);
                }
                break;
 
            case TOKEN_AUTHORITY:
                {
                    ToxAuthorityField eField = static_cast<ToxAuthorityField>(aToken.nAuthorityField);
                    SwContentIndex aIdx( pTOXNd, rText.getLength() );
                    if (eField == ToxAuthorityField::AUTH_FIELD_URL)
                    {
                        aCharStyleName = SwResId(STR_POOLCHR_INET_NORMAL);
                        mLinkProcessor->StartNewLink(rText.getLength(), aCharStyleName);
                    }
                    rBase.FillText( *pTOXNd, aIdx, o3tl::narrowing<sal_uInt16>(eField), pLayout );
                    if (eField == ToxAuthorityField::AUTH_FIELD_URL)
                    {
                        // Get the absolute URL, the text may be a relative one.
                        const auto& rAuthority = static_cast<const SwTOXAuthority&>(rBase);
                        OUString aURL = SwTOXAuthority::GetSourceURL(
                            rAuthority.GetText(AUTH_FIELD_URL, pLayout));
 
                        mLinkProcessor->CloseLink(rText.getLength(), aURL, /*bRelative=*/false);
                    }
                }
                break;
            case TOKEN_END: break;
            }
 
            if (!aCharStyleName.isEmpty())
            {
                SwCharFormat* pCharFormat;
                if( USHRT_MAX != aToken.nPoolId )
                    pCharFormat = pDoc->getIDocumentStylePoolAccess().GetCharFormatFromPool( aToken.nPoolId );
                else
                    pCharFormat = pDoc->FindCharFormatByName(aCharStyleName);
 
                if (pCharFormat)
                {
                    SwFormatCharFormat aFormat( pCharFormat );
                    pTOXNd->InsertItem( aFormat, nStartCharStyle,
                        rText.getLength(), SetAttrMode::DONTEXPAND );
                }
            }
        }
    }
    mLinkProcessor->InsertLinkAttributes(*pTOXNd);
    return oRet;
}
 
/*static*/ std::shared_ptr<SfxItemSet>
ToxTextGenerator::CollectAttributesForTox(const SwTextAttr& hint, SwAttrPool& pool)
{
    auto retval = std::make_shared<SfxItemSet>(pool);
    if (hint.Which() != RES_TXTATR_AUTOFMT) {
        return retval;
    }
    const SwFormatAutoFormat& afmt = hint.GetAutoFormat();
    SfxItemIter aIter( *afmt.GetStyleHandle());
    const SfxPoolItem* pItem = aIter.GetCurItem();
    do
    {
        if (pItem->Which() == RES_CHRATR_ESCAPEMENT ||
            pItem->Which() == RES_CHRATR_POSTURE ||
            pItem->Which() == RES_CHRATR_CJK_POSTURE ||
            pItem->Which() == RES_CHRATR_CTL_POSTURE)
        {
            retval->Put(std::unique_ptr<SfxPoolItem>(pItem->Clone()));
        }
        pItem = aIter.NextItem();
    } while (pItem);
    return retval;
}
 
void ToxTextGenerator::GetAttributesForNode(
    ToxTextGenerator::HandledTextToken & rResult,
    sal_Int32 & rOffset,
    SwTextNode const& rNode,
    ToxWhitespaceStripper const& rStripper,
    SwAttrPool & rPool,
    SwRootFrame const*const pLayout)
{
    // note: this *must* use the same flags as SwTextNode::GetExpandText()
    // or indexes will be off!
    ExpandMode eMode = ExpandMode::ExpandFields | ExpandMode::HideFieldmarkCommands;
    if (pLayout && pLayout->IsHideRedlines())
    {
        eMode |= ExpandMode::HideDeletions;
    }
    ModelToViewHelper aConversionMap(rNode, pLayout, eMode);
    if (SwpHints const*const pHints = rNode.GetpSwpHints())
    {
        for (size_t i = 0; i < pHints->Count(); ++i)
        {
            const SwTextAttr* pHint = pHints->Get(i);
            std::shared_ptr<SfxItemSet> attributesToClone =
                CollectAttributesForTox(*pHint, rPool);
            if (attributesToClone->Count() <= 0) {
                continue;
            }
 
            // sw_redlinehide: due to the ... interesting ... multi-level index
            // mapping going on here, can't use the usual merged attr iterators :(
 
            sal_Int32 const nStart(aConversionMap.ConvertToViewPosition(pHint->GetStart()));
            sal_Int32 const nEnd(aConversionMap.ConvertToViewPosition(pHint->GetAnyEnd()));
            if (nStart != nEnd) // might be in delete redline, and useless anyway
            {
                std::unique_ptr<SwFormatAutoFormat> pClone(pHint->GetAutoFormat().Clone());
                pClone->SetStyleHandle(attributesToClone);
                rResult.autoFormats.push_back(std::move(pClone));
                // note the rStripper is on the whole merged text, so need rOffset
                rResult.startPositions.push_back(
                    rStripper.GetPositionInStrippedString(rOffset + nStart));
                rResult.endPositions.push_back(
                    rStripper.GetPositionInStrippedString(rOffset + nEnd));
            }
        }
    }
    rOffset += aConversionMap.getViewText().getLength();
}
 
ToxTextGenerator::HandledTextToken
ToxTextGenerator::HandleTextToken(const SwTOXSortTabBase& source,
        SwAttrPool& pool, SwRootFrame const*const pLayout)
{
    HandledTextToken result;
    ToxWhitespaceStripper stripper(source.GetText().sText);
    result.text = stripper.GetStrippedString();
 
    // FIXME: there is a pre-existing problem that the index mapping of the
    // attributes only works if the paragraph is fully selected
    if (!source.IsFullPara() || source.aTOXSources.empty())
        return result;
 
    const SwTextNode* pSrc = source.aTOXSources.front().pNd->GetTextNode();
    if (!pSrc)
    {
        return result;
    }
 
    sal_Int32 nOffset(0);
    GetAttributesForNode(result, nOffset, *pSrc, stripper, pool, pLayout);
    if (pLayout && pLayout->HasMergedParas())
    {
        if (SwTextFrame const*const pFrame = static_cast<SwTextFrame*>(pSrc->getLayoutFrame(pLayout)))
        {
            if (sw::MergedPara const*const pMerged = pFrame->GetMergedPara())
            {
                // pSrc already copied above
                assert(pSrc == pMerged->pParaPropsNode);
                for (SwNodeOffset i = pSrc->GetIndex() + 1;
                     i <= pMerged->pLastNode->GetIndex(); ++i)
                {
                    SwNode *const pTmp(pSrc->GetNodes()[i]);
                    if (pTmp->GetRedlineMergeFlag() == SwNode::Merge::NonFirst)
                    {
                        GetAttributesForNode(result, nOffset,
                                *pTmp->GetTextNode(), stripper, pool, pLayout);
                    }
                }
            }
        }
    }
 
    return result;
}
 
/*static*/ void
ToxTextGenerator::ApplyHandledTextToken(const HandledTextToken& htt, SwTextNode& targetNode)
{
    sal_Int32 offset = targetNode.GetText().getLength();
    SwContentIndex aIdx(&targetNode, offset);
    targetNode.InsertText(htt.text, aIdx);
    for (size_t i=0; i < htt.autoFormats.size(); ++i) {
        targetNode.InsertItem(*htt.autoFormats.at(i),
                htt.startPositions.at(i) + offset,
                htt.endPositions.at(i) + offset);
    }
}
 
/*static*/ OUString
ToxTextGenerator::ConstructPageNumberPlaceholder(size_t numberOfToxSources)
{
    if (numberOfToxSources == 0) {
        return OUString();
    }
    OUStringBuffer retval;
    // Place holder for the PageNumber; we only respect the first one
    retval.append(C_NUM_REPL);
    for (size_t i = 1; i < numberOfToxSources; ++i) {
        retval.append(SwTOXMark::S_PAGE_DELI + OUStringChar(C_NUM_REPL));
    }
    retval.append(C_END_PAGE_NUM);
    return retval.makeStringAndClear();
}
 
/*virtual*/ SwChapterField
ToxTextGenerator::ObtainChapterField(SwChapterFieldType* chapterFieldType,
        const SwFormToken* chapterToken, const SwContentFrame* contentFrame,
        const SwContentNode* contentNode) const
{
    assert(chapterToken);
    assert(chapterToken->nOutlineLevel >= 1);
 
    SwChapterField retval(chapterFieldType, chapterToken->nChapterFormat);
    retval.SetLevel(static_cast<sal_uInt8>(chapterToken->nOutlineLevel - 1));
    // #i53420#
    retval.ChangeExpansion(*contentFrame, contentNode, true);
    return retval;
}
} // end namespace sw
 
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */

V530 The return value of function 'InsertText' is required to be utilized.

V530 The return value of function 'append' is required to be utilized.

V530 The return value of function 'append' is required to be utilized.