/* -*- 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 <tools/debug.hxx>
#include <comphelper/diagnose_ex.hxx>
#include <sal/log.hxx>
#include <com/sun/star/animations/AnimationNodeType.hpp>
#include <com/sun/star/animations/AnimateColor.hpp>
#include <com/sun/star/animations/AnimateMotion.hpp>
#include <com/sun/star/animations/AnimateSet.hpp>
#include <com/sun/star/animations/AnimationFill.hpp>
#include <com/sun/star/animations/Audio.hpp>
#include <com/sun/star/animations/Command.hpp>
#include <com/sun/star/animations/Event.hpp>
#include <com/sun/star/animations/EventTrigger.hpp>
#include <com/sun/star/animations/IterateContainer.hpp>
#include <com/sun/star/animations/ParallelTimeContainer.hpp>
#include <com/sun/star/animations/SequenceTimeContainer.hpp>
#include <com/sun/star/animations/XCommand.hpp>
#include <com/sun/star/animations/XIterateContainer.hpp>
#include <com/sun/star/animations/XAnimateTransform.hpp>
#include <com/sun/star/animations/XAnimateMotion.hpp>
#include <com/sun/star/animations/XAnimate.hpp>
#include <com/sun/star/animations/AnimationRestart.hpp>
#include <com/sun/star/beans/NamedValue.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/container/XEnumerationAccess.hpp>
#include <com/sun/star/lang/XInitialization.hpp>
#include <com/sun/star/presentation/EffectNodeType.hpp>
#include <com/sun/star/presentation/EffectCommands.hpp>
#include <com/sun/star/presentation/EffectPresetClass.hpp>
#include <com/sun/star/presentation/ParagraphTarget.hpp>
#include <com/sun/star/presentation/ShapeAnimationSubType.hpp>
#include <com/sun/star/text/XText.hpp>
#include <com/sun/star/util/XCloneable.hpp>
#include <com/sun/star/util/XChangesNotifier.hpp>
#include <comphelper/processfactory.hxx>
#include <comphelper/sequence.hxx>
#include <com/sun/star/lang/Locale.hpp>
#include <com/sun/star/i18n/BreakIterator.hpp>
#include <com/sun/star/i18n/CharacterIteratorMode.hpp>
#include <com/sun/star/i18n/WordType.hpp>
#include <com/sun/star/presentation/TextAnimationType.hpp>

#include <basegfx/polygon/b2dpolypolygon.hxx>
#include <basegfx/polygon/b2dpolypolygontools.hxx>
#include <basegfx/range/b2drange.hxx>
#include <basegfx/matrix/b2dhommatrixtools.hxx>

#include <algorithm>
#include <deque>
#include <numeric>

#include <cppuhelper/implbase.hxx>

#include <drawinglayer/geometry/viewinformation2d.hxx>
#include <o3tl/safeint.hxx>
#include <svx/sdr/contact/viewcontact.hxx>
#include <svx/svdopath.hxx>
#include <svx/svdpage.hxx>
#include <CustomAnimationEffect.hxx>
#include <CustomAnimationPreset.hxx>
#include <animations.hxx>
#include <utility>

using namespace ::com::sun::star;
using namespace ::com::sun::star::uno;
using namespace ::com::sun::star::presentation;
using namespace ::com::sun::star::animations;

using ::com::sun::star::container::XEnumerationAccess;
using ::com::sun::star::container::XEnumeration;
using ::com::sun::star::beans::NamedValue;
using ::com::sun::star::container::XChild;
using ::com::sun::star::drawing::XShape;
using ::com::sun::star::lang::XInitialization;
using ::com::sun::star::text::XText;
using ::com::sun::star::text::XTextRange;
using ::com::sun::star::beans::XPropertySet;
using ::com::sun::star::util::XCloneable;
using ::com::sun::star::lang::Locale;
using ::com::sun::star::util::XChangesNotifier;
using ::com::sun::star::util::XChangesListener;

namespace sd
{
class MainSequenceChangeGuard
{
public:
    explicit MainSequenceChangeGuard( EffectSequenceHelper* pSequence )
    {
        mpMainSequence = dynamic_cast< MainSequence* >( pSequence );
        if( mpMainSequence == nullptr )
        {
            InteractiveSequence* pI = dynamic_cast< InteractiveSequence* >( pSequence );
            if( pI )
                mpMainSequence = pI->mpMainSequence;
        }
        DBG_ASSERT( mpMainSequence, "sd::MainSequenceChangeGuard::MainSequenceChangeGuard(), no main sequence to guard!" );

        if( mpMainSequence )
            mpMainSequence->mbIgnoreChanges++;
    }

    ~MainSequenceChangeGuard()
    {
        if( mpMainSequence )
            mpMainSequence->mbIgnoreChanges++;
    }

private:
    MainSequence* mpMainSequence;
};

CustomAnimationEffect::CustomAnimationEffect( const css::uno::Reference< css::animations::XAnimationNode >& xNode )
:   mnNodeType(-1),
    mnPresetClass(-1),
    mnFill(AnimationFill::HOLD),
    mfBegin(-1.0),
    mfDuration(-1.0),
    mfAbsoluteDuration(-1.0),
    mnGroupId(-1),
    mnIterateType(0),
    mfIterateInterval(0.0),
    mnParaDepth( -1 ),
    mbHasText(false),
    mfAcceleration( 1.0 ),
    mfDecelerate( 1.0 ),
    mbAutoReverse(false),
    mnTargetSubItem(0),
    mnCommand(0),
    mpEffectSequence( nullptr ),
    mbHasAfterEffect(false),
    mbAfterEffectOnNextEffect(false)
{
    setNode( xNode );
}

void CustomAnimationEffect::setNode( const css::uno::Reference< css::animations::XAnimationNode >& xNode )
{
    mxNode = xNode;
    mxAudio.clear();
    mnCommand = 0;

    const Sequence< NamedValue > aUserData( mxNode->getUserData() );

    for( const NamedValue& rProp : aUserData )
    {
        if ( rProp.Name == "node-type" )
        {
            rProp.Value >>= mnNodeType;
        }
        else if ( rProp.Name == "preset-id" )
        {
            rProp.Value >>= maPresetId;
        }
        else if ( rProp.Name == "preset-sub-type" )
        {
            rProp.Value >>= maPresetSubType;
        }
        else if ( rProp.Name == "preset-class" )
        {
            rProp.Value >>= mnPresetClass;
        }
        else if ( rProp.Name == "preset-property" )
        {
            rProp.Value >>= maProperty;
        }
        else if ( rProp.Name == "group-id" )
        {
            rProp.Value >>= mnGroupId;
        }
    }

    // get effect start time
    mxNode->getBegin() >>= mfBegin;

    mfAcceleration = mxNode->getAcceleration();
    mfDecelerate = mxNode->getDecelerate();
    mbAutoReverse = mxNode->getAutoReverse();

    mnFill = mxNode->getFill();

    // get iteration data
    Reference< XIterateContainer > xIter( mxNode, UNO_QUERY );
    if( xIter.is() )
    {
        mfIterateInterval = xIter->getIterateInterval();
        mnIterateType = xIter->getIterateType();
        maTarget = xIter->getTarget();
        mnTargetSubItem = xIter->getSubItem();
    }
    else
    {
        mfIterateInterval = 0.0f;
        mnIterateType = 0;
    }

    // calculate effect duration and get target shape
    Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY );
    if( xEnumerationAccess.is() )
    {
        Reference< XEnumeration > xEnumeration = xEnumerationAccess->createEnumeration();
        if( xEnumeration.is() )
        {
            while( xEnumeration->hasMoreElements() )
            {
                Reference< XAnimationNode > xChildNode( xEnumeration->nextElement(), UNO_QUERY );
                if( !xChildNode.is() )
                    continue;

                if( xChildNode->getType() == AnimationNodeType::AUDIO )
                {
                    mxAudio.set( xChildNode, UNO_QUERY );
                }
                else if( xChildNode->getType() == AnimationNodeType::COMMAND )
                {
                    Reference< XCommand > xCommand( xChildNode, UNO_QUERY );
                    if( xCommand.is() )
                    {
                        mnCommand = xCommand->getCommand();
                        if( !maTarget.hasValue() )
                            maTarget = xCommand->getTarget();
                    }
                }
                else
                {
                    double fBegin = 0.0;
                    double fDuration = 0.0;
                    xChildNode->getBegin() >>= fBegin;
                    xChildNode->getDuration() >>= fDuration;

                    fDuration += fBegin;
                    if( fDuration > mfDuration )
                        mfDuration = fDuration;

                    // no target shape yet?
                    if( !maTarget.hasValue() )
                    {
                        // go get it boys!
                        Reference< XAnimate > xAnimate( xChildNode, UNO_QUERY );
                        if( xAnimate.is() )
                        {
                            maTarget = xAnimate->getTarget();
                            mnTargetSubItem = xAnimate->getSubItem();
                        }
                    }
                }
            }
        }
    }

    mfAbsoluteDuration = mfDuration;
    double fRepeatCount = 1.0;
    if( (mxNode->getRepeatCount()) >>= fRepeatCount )
        mfAbsoluteDuration *= fRepeatCount;

    checkForText();
}

sal_Int32 CustomAnimationEffect::getNumberOfSubitems( const Any& aTarget, sal_Int16 nIterateType )
{
    sal_Int32 nSubItems = 0;

    try
    {
        // first get target text
        sal_Int32 nOnlyPara = -1;

        Reference< XText > xShape;
        aTarget >>= xShape;
        if( !xShape.is() )
        {
            ParagraphTarget aParaTarget;
            if( aTarget >>= aParaTarget )
            {
                xShape.set( aParaTarget.Shape, UNO_QUERY );
                nOnlyPara = aParaTarget.Paragraph;
            }
        }

        // now use the break iterator to iterate over the given text
        // and count the sub items

        if( xShape.is() )
        {
            // TODO/LATER: Optimize this, don't create a break iterator each time
            const Reference< uno::XComponentContext >& xContext( ::comphelper::getProcessComponentContext() );
            Reference < i18n::XBreakIterator > xBI = i18n::BreakIterator::create(xContext);

            Reference< XEnumerationAccess > xEA( xShape, UNO_QUERY_THROW );
            Reference< XEnumeration > xEnumeration( xEA->createEnumeration(), UNO_SET_THROW );
            css::lang::Locale aLocale;
            static constexpr OUStringLiteral aStrLocaleName( u"CharLocale" );
            Reference< XTextRange > xParagraph;

            sal_Int32 nPara = 0;
            while( xEnumeration->hasMoreElements() )
            {
                xEnumeration->nextElement() >>= xParagraph;

                // skip this if it's not the only paragraph we want to count
                if( (nOnlyPara != -1) && (nOnlyPara != nPara ) )
                    continue;

                if( nIterateType == TextAnimationType::BY_PARAGRAPH )
                {
                    nSubItems++;
                }
                else
                {
                    const OUString aText( xParagraph->getString() );
                    Reference< XPropertySet > xSet( xParagraph, UNO_QUERY_THROW );
                    xSet->getPropertyValue( aStrLocaleName ) >>= aLocale;

                    sal_Int32 nPos;
                    const sal_Int32 nEndPos = aText.getLength();

                    if( nIterateType == TextAnimationType::BY_WORD )
                    {
                        for( nPos = 0; nPos < nEndPos; nPos++ )
                        {
                            nPos = xBI->getWordBoundary(aText, nPos, aLocale, i18n::WordType::ANY_WORD, true).endPos;
                            nSubItems++;
                        }
                        break;
                    }
                    else
                    {
                        sal_Int32 nDone;
                        for( nPos = 0; nPos < nEndPos; nPos++ )
                        {
                            nPos = xBI->nextCharacters(aText, nPos, aLocale, i18n::CharacterIteratorMode::SKIPCELL, 0, nDone);
                            nSubItems++;
                        }
                    }
                }

                if( nPara == nOnlyPara )
                    break;

                nPara++;
            }
        }
    }
    catch( Exception& )
    {
        nSubItems = 0;
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::getNumberOfSubitems(), exception caught!" );
    }

    return nSubItems;
}

CustomAnimationEffect::~CustomAnimationEffect()
{
}

CustomAnimationEffectPtr CustomAnimationEffect::clone() const
{
    Reference< XCloneable > xCloneable( mxNode, UNO_QUERY_THROW );
    Reference< XAnimationNode > xNode( xCloneable->createClone(), UNO_QUERY_THROW );
    CustomAnimationEffectPtr pEffect = std::make_shared<CustomAnimationEffect>( xNode );
    pEffect->setEffectSequence( getEffectSequence() );
    return pEffect;
}

sal_Int32 CustomAnimationEffect::get_node_type( const Reference< XAnimationNode >& xNode )
{
    sal_Int16 nNodeType = -1;

    if( xNode.is() )
    {
        const Sequence< NamedValue > aUserData( xNode->getUserData() );
        if( aUserData.hasElements() )
        {
            const NamedValue* pProp = std::find_if(aUserData.begin(), aUserData.end(),
                [](const NamedValue& rProp) { return rProp.Name == "node-type"; });
            if (pProp != aUserData.end())
                pProp->Value >>= nNodeType;
        }
    }

    return nNodeType;
}

void CustomAnimationEffect::setPresetClassAndId( sal_Int16 nPresetClass, const OUString& rPresetId )
{
    if( mnPresetClass == nPresetClass && maPresetId == rPresetId )
        return;

    mnPresetClass = nPresetClass;
    maPresetId = rPresetId;
    if( !mxNode.is() )
        return;

    // first try to find a "preset-class" entry in the user data
    // and change it
    Sequence< NamedValue > aUserData( mxNode->getUserData() );
    sal_Int32 nLength = aUserData.getLength();
    bool bFoundPresetClass = false;
    bool bFoundPresetId = false;
    if( nLength )
    {
        auto [begin, end] = asNonConstRange(aUserData);
        NamedValue* pProp = std::find_if(begin, end,
            [](const NamedValue& rProp) { return rProp.Name == "preset-class"; });
        if (pProp != end)
        {
            pProp->Value <<= mnPresetClass;
            bFoundPresetClass = true;
        }

        pProp = std::find_if(begin, end,
            [](const NamedValue& rProp) { return rProp.Name == "preset-id"; });
        if (pProp != end)
        {
            pProp->Value <<= mnPresetClass;
            bFoundPresetId = true;
        }
    }

    // no "preset-class" entry inside user data, so add it
    if( !bFoundPresetClass )
    {
        aUserData.realloc( nLength + 1);
        auto& el = aUserData.getArray()[nLength];
        el.Name = "preset-class";
        el.Value <<= mnPresetClass;
        ++nLength;
    }

    if( !bFoundPresetId && maPresetId.getLength() > 0 )
    {
        aUserData.realloc( nLength + 1);
        auto& el = aUserData.getArray()[nLength];
        el.Name = "preset-id";
        el.Value <<= maPresetId;
    }

    mxNode->setUserData( aUserData );
}

void CustomAnimationEffect::setNodeType( sal_Int16 nNodeType )
{
    if( mnNodeType == nNodeType )
        return;

    mnNodeType = nNodeType;
    if( !mxNode.is() )
        return;

    // first try to find a "node-type" entry in the user data
    // and change it
    Sequence< NamedValue > aUserData( mxNode->getUserData() );
    sal_Int32 nLength = aUserData.getLength();
    bool bFound = false;
    if( nLength )
    {
        auto [begin, end] = asNonConstRange(aUserData);
        NamedValue* pProp = std::find_if(begin, end,
            [](const NamedValue& rProp) { return rProp.Name == "node-type"; });
        if (pProp != end)
        {
            pProp->Value <<= mnNodeType;
            bFound = true;
        }
    }

    // no "node-type" entry inside user data, so add it
    if( !bFound )
    {
        aUserData.realloc( nLength + 1);
        auto& el = aUserData.getArray()[nLength];
        el.Name = "node-type";
        el.Value <<= mnNodeType;
    }

    mxNode->setUserData( aUserData );
}

void CustomAnimationEffect::setGroupId( sal_Int32 nGroupId )
{
    mnGroupId = nGroupId;
    if( !mxNode.is() )
        return;

    // first try to find a "group-id" entry in the user data
    // and change it
    Sequence< NamedValue > aUserData( mxNode->getUserData() );
    sal_Int32 nLength = aUserData.getLength();
    bool bFound = false;
    if( nLength )
    {
        auto [begin, end] = asNonConstRange(aUserData);
        NamedValue* pProp = std::find_if(begin, end,
            [](const NamedValue& rProp) { return rProp.Name == "group-id"; });
        if (pProp != end)
        {
            pProp->Value <<= mnGroupId;
            bFound = true;
        }
    }

    // no "group-id" entry inside user data, so add it
    if( !bFound )
    {
        aUserData.realloc( nLength + 1);
        auto& el = aUserData.getArray()[nLength];
        el.Name = "group-id";
        el.Value <<= mnGroupId;
    }

    mxNode->setUserData( aUserData );
}

/** checks if the text for this effect has changed and updates internal flags.
    returns true if something changed.
*/
bool CustomAnimationEffect::checkForText( const std::vector<sal_Int32>* paragraphNumberingLevel )
{
    bool bChange = false;

    Reference< XText > xText;

    if( maTarget.getValueType() == ::cppu::UnoType<ParagraphTarget>::get() )
    {
        // calc para depth
        ParagraphTarget aParaTarget;
        maTarget >>= aParaTarget;

        xText.set( aParaTarget.Shape, UNO_QUERY );

        // get paragraph
        if( xText.is() )
        {
            sal_Int32 nPara = aParaTarget.Paragraph;

            bool bHasText = false;
            sal_Int32 nParaDepth = 0;

            if ( paragraphNumberingLevel )
            {
                bHasText = !paragraphNumberingLevel->empty();
                if (nPara >= 0 && o3tl::make_unsigned(nPara) < paragraphNumberingLevel->size())
                    nParaDepth = paragraphNumberingLevel->at(nPara);
            }
            else
            {
                Reference< XEnumerationAccess > xEA( xText, UNO_QUERY );
                if( xEA.is() )
                {
                    Reference< XEnumeration > xEnumeration = xEA->createEnumeration();
                    if( xEnumeration.is() )
                    {
                        bHasText = xEnumeration->hasMoreElements();

                        while( xEnumeration->hasMoreElements() && nPara-- )
                            xEnumeration->nextElement();

                        if( xEnumeration->hasMoreElements() )
                        {
                            Reference< XPropertySet > xParaSet;
                            xEnumeration->nextElement() >>= xParaSet;
                            if( xParaSet.is() )
                            {
                                xParaSet->getPropertyValue( u"NumberingLevel"_ustr ) >>= nParaDepth;
                            }
                        }
                    }
                }
            }

            if( bHasText )
            {
                bChange |= bHasText != mbHasText;
                mbHasText = bHasText;

                bChange |= nParaDepth != mnParaDepth;
                mnParaDepth = nParaDepth;
            }
        }
    }
    else
    {
        maTarget >>= xText;
        bool bHasText = xText.is() && !xText->getString().isEmpty();
        bChange |= bHasText != mbHasText;
        mbHasText = bHasText;
    }

    bChange |= calculateIterateDuration();
    return bChange;
}

bool CustomAnimationEffect::calculateIterateDuration()
{
    bool bChange = false;

    // if we have an iteration, we must also calculate the
    // 'true' container duration, that is
    // ( ( is form animated ) ? [contained effects duration] : 0 ) +
    // ( [number of animated children] - 1 ) * [interval-delay] + [contained effects duration]
    Reference< XIterateContainer > xIter( mxNode, UNO_QUERY );
    if( xIter.is() )
    {
        double fDuration = mfDuration;
        const double fSubEffectDuration = mfDuration;

        if( mnTargetSubItem != ShapeAnimationSubType::ONLY_BACKGROUND ) // does not make sense for iterate container but better check
        {
            const sal_Int32 nSubItems = getNumberOfSubitems( maTarget, mnIterateType );
            if( nSubItems )
            {
                const double f = (nSubItems-1) * mfIterateInterval;
                fDuration += f;
            }
        }

        // if we also animate the form first, we have to add the
        // sub effect duration to the whole effect duration
        if( mnTargetSubItem == ShapeAnimationSubType::AS_WHOLE )
            fDuration += fSubEffectDuration;

        bChange |= fDuration != mfAbsoluteDuration;
        mfAbsoluteDuration = fDuration;
    }

    return bChange;
}

void CustomAnimationEffect::setTarget( const css::uno::Any& rTarget )
{
    try
    {
        maTarget = rTarget;

        // first, check special case for random node
        Reference< XInitialization > xInit( mxNode, UNO_QUERY );
        if( xInit.is() )
        {
            const Sequence< Any > aArgs( &maTarget, 1 );
            xInit->initialize( aArgs );
        }
        else
        {
            Reference< XIterateContainer > xIter( mxNode, UNO_QUERY );
            if( xIter.is() )
            {
                xIter->setTarget(maTarget);
            }
            else
            {
                Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY );
                if( xEnumerationAccess.is() )
                {
                    Reference< XEnumeration > xEnumeration = xEnumerationAccess->createEnumeration();
                    if( xEnumeration.is() )
                    {
                        while( xEnumeration->hasMoreElements() )
                        {
                            const Any aElem( xEnumeration->nextElement() );
                            Reference< XAnimate > xAnimate( aElem, UNO_QUERY );
                            if( xAnimate.is() )
                                xAnimate->setTarget( rTarget );
                            else
                            {
                                Reference< XCommand > xCommand( aElem, UNO_QUERY );
                                if( xCommand.is() )
                                    xCommand->setTarget( rTarget );
                            }
                        }
                    }
                }
            }
        }
        checkForText();
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setTarget()" );
    }
}

void CustomAnimationEffect::setTargetSubItem( sal_Int16 nSubItem )
{
    try
    {
        mnTargetSubItem = nSubItem;

        Reference< XIterateContainer > xIter( mxNode, UNO_QUERY );
        if( xIter.is() )
        {
            xIter->setSubItem(mnTargetSubItem);
        }
        else
        {
            Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY );
            if( xEnumerationAccess.is() )
            {
                Reference< XEnumeration > xEnumeration = xEnumerationAccess->createEnumeration();
                if( xEnumeration.is() )
                {
                    while( xEnumeration->hasMoreElements() )
                    {
                        Reference< XAnimate > xAnimate( xEnumeration->nextElement(), UNO_QUERY );
                        if( xAnimate.is() )
                            xAnimate->setSubItem( mnTargetSubItem );
                    }
                }
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setTargetSubItem()" );
    }
}

void CustomAnimationEffect::setDuration( double fDuration )
{
    if( (mfDuration == -1.0) || (mfDuration == fDuration) )
        return;

    try
    {
        double fScale = fDuration / mfDuration;
        mfDuration = fDuration;
        double fRepeatCount = 1.0;
        getRepeatCount() >>= fRepeatCount;
        mfAbsoluteDuration = mfDuration * fRepeatCount;

        // calculate effect duration and get target shape
        Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY );
        if( xEnumerationAccess.is() )
        {
            Reference< XEnumeration > xEnumeration = xEnumerationAccess->createEnumeration();
            if( xEnumeration.is() )
            {
                while( xEnumeration->hasMoreElements() )
                {
                    Reference< XAnimationNode > xChildNode( xEnumeration->nextElement(), UNO_QUERY );
                    if( !xChildNode.is() )
                        continue;

                    double fChildBegin = 0.0;
                    xChildNode->getBegin() >>= fChildBegin;
                    if(  fChildBegin != 0.0 )
                    {
                        fChildBegin *= fScale;
                        xChildNode->setBegin( Any( fChildBegin ) );
                    }

                    double fChildDuration = 0.0;
                    xChildNode->getDuration() >>= fChildDuration;
                    if( fChildDuration != 0.0 )
                    {
                        fChildDuration *= fScale;
                        xChildNode->setDuration( Any( fChildDuration ) );
                    }
                }
            }
        }
        calculateIterateDuration();
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setDuration()" );
    }
}

void CustomAnimationEffect::setBegin( double fBegin )
{
    if( mxNode.is() ) try
    {
        mfBegin = fBegin;
        mxNode->setBegin( Any( fBegin ) );
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setBegin()" );
    }
}

void CustomAnimationEffect::setAcceleration( double fAcceleration )
{
    if( mxNode.is() ) try
    {
        mfAcceleration = fAcceleration;
        mxNode->setAcceleration( fAcceleration );
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setAcceleration()" );
    }
}

void CustomAnimationEffect::setDecelerate( double fDecelerate )
{
    if( mxNode.is() ) try
    {
        mfDecelerate = fDecelerate;
        mxNode->setDecelerate( fDecelerate );
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setDecelerate()" );
    }
}

void CustomAnimationEffect::setAutoReverse( bool bAutoReverse )
{
    if( mxNode.is() ) try
    {
        mbAutoReverse = bAutoReverse;
        mxNode->setAutoReverse( bAutoReverse );
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setAutoReverse()" );
    }
}

void CustomAnimationEffect::replaceNode( const css::uno::Reference< css::animations::XAnimationNode >& xNode )
{
    sal_Int16 nNodeType = mnNodeType;
    Any aTarget = maTarget;

    sal_Int16 nFill = mnFill;
    double fBegin = mfBegin;
    double fDuration = mfDuration;
    double fAcceleration = mfAcceleration;
    double fDecelerate = mfDecelerate ;
    bool bAutoReverse = mbAutoReverse;
    Reference< XAudio > xAudio( mxAudio );
    sal_Int16 nIterateType = mnIterateType;
    double fIterateInterval = mfIterateInterval;
    sal_Int16 nSubItem = mnTargetSubItem;

    setNode( xNode );

    setAudio( xAudio );
    setNodeType( nNodeType );
    setTarget( aTarget );
    setTargetSubItem( nSubItem );
    setDuration( fDuration );
    setBegin( fBegin );
    setFill( nFill );

    setAcceleration( fAcceleration );
    setDecelerate( fDecelerate );
    setAutoReverse( bAutoReverse );

    if( nIterateType != mnIterateType )
        setIterateType( nIterateType );

    if( mnIterateType && ( fIterateInterval != mfIterateInterval ) )
        setIterateInterval( fIterateInterval );
}

Reference< XShape > CustomAnimationEffect::getTargetShape() const
{
    Reference< XShape > xShape;
    maTarget >>= xShape;
    if( !xShape.is() )
    {
        ParagraphTarget aParaTarget;
        if( maTarget >>= aParaTarget )
            xShape = aParaTarget.Shape;
    }

    return xShape;
}

Any CustomAnimationEffect::getRepeatCount() const
{
    if( mxNode.is() )
    {
        return mxNode->getRepeatCount();
    }
    else
    {
        Any aAny;
        return aAny;
    }
}

Any CustomAnimationEffect::getEnd() const
{
    if( mxNode.is() )
    {
        return mxNode->getEnd();
    }
    else
    {
        Any aAny;
        return aAny;
    }
}

void CustomAnimationEffect::setRepeatCount( const Any& rRepeatCount )
{
    if( mxNode.is() )
    {
        mxNode->setRepeatCount( rRepeatCount );
        double fRepeatCount = 1.0;
        rRepeatCount >>= fRepeatCount;
        mfAbsoluteDuration = mfDuration * fRepeatCount;
    }
}

void CustomAnimationEffect::setEnd( const Any& rEnd )
{
    if( mxNode.is() )
        mxNode->setEnd( rEnd );
}

void CustomAnimationEffect::setFill( sal_Int16 nFill )
{
    if (mxNode.is())
    {
        mnFill = nFill;
        mxNode->setFill( nFill );
    }
}

Reference< XAnimationNode > CustomAnimationEffect::createAfterEffectNode() const
{
    DBG_ASSERT( mbHasAfterEffect, "sd::CustomAnimationEffect::createAfterEffectNode(), this node has no after effect!" );

    const Reference< XComponentContext >& xContext( ::comphelper::getProcessComponentContext() );

    Reference< XAnimate > xAnimate;
    if( maDimColor.hasValue() )
        xAnimate = AnimateColor::create( xContext );
    else
        xAnimate = AnimateSet::create( xContext );

    Any aTo;
    OUString aAttributeName;

    if( maDimColor.hasValue() )
    {
        aTo = maDimColor;
        aAttributeName = "DimColor";
    }
    else
    {
        aTo <<= false;
        aAttributeName = "Visibility";
    }

    Any aBegin;
    if( !mbAfterEffectOnNextEffect ) // sameClick
    {
        Event aEvent;

        aEvent.Source <<= getNode();
        aEvent.Trigger = EventTrigger::END_EVENT;
        aEvent.Repeat = 0;

        aBegin <<= aEvent;
    }
    else
    {
        aBegin <<= 0.0;
    }

    xAnimate->setBegin( aBegin );
    xAnimate->setTo( aTo );
    xAnimate->setAttributeName( aAttributeName );

    xAnimate->setDuration( Any( 0.001 ) );
    xAnimate->setFill( AnimationFill::HOLD );
    xAnimate->setTarget( maTarget );

    return xAnimate;
}

void CustomAnimationEffect::setIterateType( sal_Int16 nIterateType )
{
    if( mnIterateType == nIterateType )
        return;

    try
    {
        // do we need to exchange the container node?
        if( (mnIterateType == 0) || (nIterateType == 0) )
        {
            sal_Int16 nTargetSubItem = mnTargetSubItem;

            const Reference< XComponentContext >& xContext( ::comphelper::getProcessComponentContext() );
            Reference< XTimeContainer > xNewContainer;
            if(nIterateType)
            {
                xNewContainer.set( IterateContainer::create( xContext ) );
            }
            else
                xNewContainer.set( ParallelTimeContainer::create( xContext ), UNO_QUERY_THROW );

            Reference< XTimeContainer > xOldContainer( mxNode, UNO_QUERY_THROW );
            Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY_THROW );
            Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
            while( xEnumeration->hasMoreElements() )
            {
                Reference< XAnimationNode > xChildNode( xEnumeration->nextElement(), UNO_QUERY_THROW );
                xOldContainer->removeChild( xChildNode );
                xNewContainer->appendChild( xChildNode );
            }

            xNewContainer->setBegin( mxNode->getBegin() );
            xNewContainer->setDuration( mxNode->getDuration() );
            xNewContainer->setEnd( mxNode->getEnd() );
            xNewContainer->setEndSync( mxNode->getEndSync() );
            xNewContainer->setRepeatCount( mxNode->getRepeatCount() );
            xNewContainer->setFill( mxNode->getFill() );
            xNewContainer->setFillDefault( mxNode->getFillDefault() );
            xNewContainer->setRestart( mxNode->getRestart() );
            xNewContainer->setRestartDefault( mxNode->getRestartDefault() );
            xNewContainer->setAcceleration( mxNode->getAcceleration() );
            xNewContainer->setDecelerate( mxNode->getDecelerate() );
            xNewContainer->setAutoReverse( mxNode->getAutoReverse() );
            xNewContainer->setRepeatDuration( mxNode->getRepeatDuration() );
            xNewContainer->setEndSync( mxNode->getEndSync() );
            xNewContainer->setRepeatCount( mxNode->getRepeatCount() );
            xNewContainer->setUserData( mxNode->getUserData() );

            mxNode = xNewContainer;

            Any aTarget;
            if( nIterateType )
            {
                Reference< XIterateContainer > xIter( mxNode, UNO_QUERY_THROW );
                xIter->setTarget(maTarget);
                xIter->setSubItem( nTargetSubItem );
            }
            else
            {
                aTarget = maTarget;
            }

            Reference< XEnumerationAccess > xEA( mxNode, UNO_QUERY_THROW );
            Reference< XEnumeration > xE( xEA->createEnumeration(), UNO_SET_THROW );
            while( xE->hasMoreElements() )
            {
                Reference< XAnimate > xAnimate( xE->nextElement(), UNO_QUERY );
                if( xAnimate.is() )
                {
                    xAnimate->setTarget( aTarget );
                    xAnimate->setSubItem( nTargetSubItem );
                }
            }
        }

        mnIterateType = nIterateType;

        // if we have an iteration container, we must set its type
        if( mnIterateType )
        {
            Reference< XIterateContainer > xIter( mxNode, UNO_QUERY_THROW );
            xIter->setIterateType( nIterateType );
        }

        checkForText();
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setIterateType()" );
    }
}

void CustomAnimationEffect::setIterateInterval( double fIterateInterval )
{
    if( mfIterateInterval == fIterateInterval )
        return;

    Reference< XIterateContainer > xIter( mxNode, UNO_QUERY );

    DBG_ASSERT( xIter.is(), "sd::CustomAnimationEffect::setIterateInterval(), not an iteration node" );
    if( xIter.is() )
    {
        mfIterateInterval = fIterateInterval;
        xIter->setIterateInterval( fIterateInterval );
    }

    calculateIterateDuration();
}

OUString CustomAnimationEffect::getPath() const
{
    OUString aPath;

    if( mxNode.is() ) try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY_THROW );
        Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
        while( xEnumeration->hasMoreElements() )
        {
            Reference< XAnimateMotion > xMotion( xEnumeration->nextElement(), UNO_QUERY );
            if( xMotion.is() )
            {
                xMotion->getPath() >>= aPath;
                break;
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::getPath()" );
    }

    return aPath;
}

void CustomAnimationEffect::setPath( const OUString& rPath )
{
    if( !mxNode.is() )
        return;

    try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY_THROW );
        Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
        while( xEnumeration->hasMoreElements() )
        {
            Reference< XAnimateMotion > xMotion( xEnumeration->nextElement(), UNO_QUERY );
            if( xMotion.is() )
            {

                MainSequenceChangeGuard aGuard( mpEffectSequence );
                xMotion->setPath( Any( rPath ) );
                break;
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setPath()" );
    }
}

Any CustomAnimationEffect::getProperty( sal_Int32 nNodeType, std::u16string_view rAttributeName, EValue eValue )
{
    Any aProperty;
    if( mxNode.is() ) try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY );
        if( xEnumerationAccess.is() )
        {
            Reference< XEnumeration > xEnumeration = xEnumerationAccess->createEnumeration();
            if( xEnumeration.is() )
            {
                while( xEnumeration->hasMoreElements() && !aProperty.hasValue() )
                {
                    Reference< XAnimate > xAnimate( xEnumeration->nextElement(), UNO_QUERY );
                    if( !xAnimate.is() )
                        continue;

                    if( xAnimate->getType() == nNodeType )
                    {
                        if( xAnimate->getAttributeName() == rAttributeName )
                        {
                            switch( eValue )
                            {
                            case EValue::To:   aProperty = xAnimate->getTo(); break;
                            case EValue::By:   aProperty = xAnimate->getBy(); break;
                            }
                        }
                    }
                }
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::getProperty()" );
    }

    return aProperty;
}

bool CustomAnimationEffect::setProperty( sal_Int32 nNodeType, std::u16string_view rAttributeName, EValue eValue, const Any& rValue )
{
    bool bChanged = false;
    if( mxNode.is() ) try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY );
        if( xEnumerationAccess.is() )
        {
            Reference< XEnumeration > xEnumeration = xEnumerationAccess->createEnumeration();
            if( xEnumeration.is() )
            {
                while( xEnumeration->hasMoreElements() )
                {
                    Reference< XAnimate > xAnimate( xEnumeration->nextElement(), UNO_QUERY );
                    if( !xAnimate.is() )
                        continue;

                    if( xAnimate->getType() == nNodeType )
                    {
                        if( xAnimate->getAttributeName() == rAttributeName )
                        {
                            switch( eValue )
                            {
                            case EValue::To:
                                if( xAnimate->getTo() != rValue )
                                {
                                    xAnimate->setTo( rValue );
                                    bChanged = true;
                                }
                                break;
                            case EValue::By:
                                if( xAnimate->getTo() != rValue )
                                {
                                    xAnimate->setBy( rValue );
                                    bChanged = true;
                                }
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setProperty()" );
    }

    return bChanged;
}

static bool implIsColorAttribute( std::u16string_view rAttributeName )
{
    return rAttributeName == u"FillColor" || rAttributeName == u"LineColor" || rAttributeName == u"CharColor";
}

Any CustomAnimationEffect::getColor( sal_Int32 nIndex )
{
    Any aColor;
    if( mxNode.is() ) try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY );
        if( xEnumerationAccess.is() )
        {
            Reference< XEnumeration > xEnumeration = xEnumerationAccess->createEnumeration();
            if( xEnumeration.is() )
            {
                while( xEnumeration->hasMoreElements() && !aColor.hasValue() )
                {
                    Reference< XAnimate > xAnimate( xEnumeration->nextElement(), UNO_QUERY );
                    if( !xAnimate.is() )
                        continue;

                    switch( xAnimate->getType() )
                    {
                    case AnimationNodeType::SET:
                    case AnimationNodeType::ANIMATE:
                        if( !implIsColorAttribute( xAnimate->getAttributeName() ) )
                            break;
                        [[fallthrough]];
                    case AnimationNodeType::ANIMATECOLOR:
                        Sequence<Any> aValues( xAnimate->getValues() );
                        if( aValues.hasElements() )
                        {
                            if( aValues.getLength() > nIndex )
                                aColor = aValues[nIndex];
                        }
                        else if( nIndex == 0 )
                            aColor = xAnimate->getFrom();
                        else
                            aColor = xAnimate->getTo();
                    }
                }
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::getColor()" );
    }

    return aColor;
}

void CustomAnimationEffect::setColor( sal_Int32 nIndex, const Any& rColor )
{
    if( !mxNode.is() )
        return;

    try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY );
        if( xEnumerationAccess.is() )
        {
            Reference< XEnumeration > xEnumeration = xEnumerationAccess->createEnumeration();
            if( xEnumeration.is() )
            {
                while( xEnumeration->hasMoreElements() )
                {
                    Reference< XAnimate > xAnimate( xEnumeration->nextElement(), UNO_QUERY );
                    if( !xAnimate.is() )
                        continue;

                    switch( xAnimate->getType() )
                    {
                    case AnimationNodeType::SET:
                    case AnimationNodeType::ANIMATE:
                        if( !implIsColorAttribute( xAnimate->getAttributeName() ) )
                            break;
                        [[fallthrough]];
                    case AnimationNodeType::ANIMATECOLOR:
                    {
                        Sequence<Any> aValues( xAnimate->getValues() );
                        if( aValues.hasElements() )
                        {
                            if( aValues.getLength() > nIndex )
                            {
                                aValues.getArray()[nIndex] = rColor;
                                xAnimate->setValues( aValues );
                            }
                        }
                        else if( (nIndex == 0) && xAnimate->getFrom().hasValue() )
                            xAnimate->setFrom(rColor);
                        else if( (nIndex == 1) && xAnimate->getTo().hasValue() )
                            xAnimate->setTo(rColor);
                    }
                    break;

                    }
                }
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setColor()" );
    }
}

Any CustomAnimationEffect::getTransformationProperty( sal_Int32 nTransformType, EValue eValue )
{
    Any aProperty;
    if( mxNode.is() ) try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY );
        if( xEnumerationAccess.is() )
        {
            Reference< XEnumeration > xEnumeration = xEnumerationAccess->createEnumeration();
            if( xEnumeration.is() )
            {
                while( xEnumeration->hasMoreElements() && !aProperty.hasValue() )
                {
                    Reference< XAnimateTransform > xTransform( xEnumeration->nextElement(), UNO_QUERY );
                    if( !xTransform.is() )
                        continue;

                    if( xTransform->getTransformType() == nTransformType )
                    {
                        switch( eValue )
                        {
                        case EValue::To:   aProperty = xTransform->getTo(); break;
                        case EValue::By:   aProperty = xTransform->getBy(); break;
                        }
                    }
                }
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::getTransformationProperty()" );
    }

    return aProperty;
}

bool CustomAnimationEffect::setTransformationProperty( sal_Int32 nTransformType, EValue eValue, const Any& rValue )
{
    bool bChanged = false;
    if( mxNode.is() ) try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( mxNode, UNO_QUERY );
        if( xEnumerationAccess.is() )
        {
            Reference< XEnumeration > xEnumeration = xEnumerationAccess->createEnumeration();
            if( xEnumeration.is() )
            {
                while( xEnumeration->hasMoreElements() )
                {
                    Reference< XAnimateTransform > xTransform( xEnumeration->nextElement(), UNO_QUERY );
                    if( !xTransform.is() )
                        continue;

                    if( xTransform->getTransformType() == nTransformType )
                    {
                        switch( eValue )
                        {
                        case EValue::To:
                            if( xTransform->getTo() != rValue )
                            {
                                xTransform->setTo( rValue );
                                bChanged = true;
                            }
                            break;
                        case EValue::By:
                            if( xTransform->getBy() != rValue )
                            {
                                xTransform->setBy( rValue );
                                bChanged = true;
                            }
                            break;
                        }
                    }
                }
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setTransformationProperty()" );
    }

    return bChanged;
}

void CustomAnimationEffect::createAudio( const css::uno::Any& rSource )
{
    DBG_ASSERT( !mxAudio.is(), "sd::CustomAnimationEffect::createAudio(), node already has an audio!" );

    if( mxAudio.is() )
        return;

    try
    {
        const Reference< XComponentContext >& xContext( ::comphelper::getProcessComponentContext() );
        Reference< XAudio > xAudio( Audio::create( xContext ) );
        xAudio->setSource( rSource );
        xAudio->setVolume( 1.0 );
        setAudio( xAudio );
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::createAudio()" );
    }
}

static Reference< XCommand > findCommandNode( const Reference< XAnimationNode >& xRootNode )
{
    Reference< XCommand > xCommand;

    if( xRootNode.is() ) try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( xRootNode, UNO_QUERY_THROW );
        Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
        while( !xCommand.is() && xEnumeration->hasMoreElements() )
        {
            Reference< XAnimationNode > xNode( xEnumeration->nextElement(), UNO_QUERY );
            if( xNode.is() && (xNode->getType() == AnimationNodeType::COMMAND) )
                xCommand.set( xNode, UNO_QUERY_THROW );
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::findCommandNode()" );
    }

    return xCommand;
}

void CustomAnimationEffect::removeAudio()
{
    try
    {
        Reference< XAnimationNode > xChild;

        if( mxAudio.is() )
        {
            xChild = mxAudio;
            mxAudio.clear();
        }
        else if( mnCommand == EffectCommands::STOPAUDIO )
        {
            xChild = findCommandNode( mxNode );
            mnCommand = 0;
        }

        if( xChild.is() )
        {
            Reference< XTimeContainer > xContainer( mxNode, UNO_QUERY );
            if( xContainer.is() )
                xContainer->removeChild( xChild );
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::removeAudio()" );
    }

}

void CustomAnimationEffect::setAudio( const Reference< css::animations::XAudio >& xAudio )
{
    if( mxAudio == xAudio )
        return;

    try
    {
        removeAudio();
        mxAudio = xAudio;
        Reference< XTimeContainer > xContainer( mxNode, UNO_QUERY );
        if( xContainer.is() && mxAudio.is() )
            xContainer->appendChild( mxAudio );
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setAudio()" );
    }
}

void CustomAnimationEffect::setStopAudio()
{
    if( mnCommand == EffectCommands::STOPAUDIO )
        return;

    try
    {
        if( mxAudio.is() )
            removeAudio();

        const Reference< XComponentContext >& xContext( ::comphelper::getProcessComponentContext() );
        Reference< XCommand > xCommand( Command::create( xContext ) );

        xCommand->setCommand( EffectCommands::STOPAUDIO );

        Reference< XTimeContainer > xContainer( mxNode, UNO_QUERY_THROW );
        xContainer->appendChild( xCommand );

        mnCommand = EffectCommands::STOPAUDIO;
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationEffect::setStopAudio()" );
    }
}

bool CustomAnimationEffect::getStopAudio() const
{
    return mnCommand == EffectCommands::STOPAUDIO;
}

rtl::Reference<SdrPathObj> CustomAnimationEffect::createSdrPathObjFromPath(SdrModel& rTargetModel)
{
    rtl::Reference<SdrPathObj> pPathObj = new SdrPathObj(rTargetModel, SdrObjKind::PathLine);
    updateSdrPathObjFromPath( *pPathObj );
    return pPathObj;
}

void CustomAnimationEffect::updateSdrPathObjFromPath( SdrPathObj& rPathObj )
{
    ::basegfx::B2DPolyPolygon aPolyPoly;
    if( ::basegfx::utils::importFromSvgD( aPolyPoly, getPath(), true, nullptr ) )
    {
        SdrObject* pObj = SdrObject::getSdrObjectFromXShape(getTargetShape());
        if( pObj )
        {
            SdrPage* pPage = pObj->getSdrPageFromSdrObject();
            if( pPage )
            {
                const Size aPageSize( pPage->GetSize() );
                aPolyPoly.transform(basegfx::utils::createScaleB2DHomMatrix(static_cast<double>(aPageSize.Width()), static_cast<double>(aPageSize.Height())));
            }

            const ::tools::Rectangle aBoundRect( pObj->GetCurrentBoundRect() );
            const Point aCenter( aBoundRect.Center() );
            aPolyPoly.translate(aCenter.X(), aCenter.Y());
        }
    }

    rPathObj.SetPathPoly( aPolyPoly );
}

void CustomAnimationEffect::updatePathFromSdrPathObj( const SdrPathObj& rPathObj )
{
    ::basegfx::B2DPolyPolygon aPolyPoly( rPathObj.GetPathPoly() );

    SdrObject* pObj = SdrObject::getSdrObjectFromXShape(getTargetShape());
    if( pObj )
    {
        ::tools::Rectangle aBoundRect(0,0,0,0);

        drawinglayer::primitive2d::Primitive2DContainer xPrimitives;
        pObj->GetViewContact().getViewIndependentPrimitive2DContainer(xPrimitives);
        const drawinglayer::geometry::ViewInformation2D aViewInformation2D;
        const basegfx::B2DRange aRange(xPrimitives.getB2DRange(aViewInformation2D));

        if(!aRange.isEmpty())
        {
            aBoundRect = ::tools::Rectangle(
                    static_cast<sal_Int32>(floor(aRange.getMinX())), static_cast<sal_Int32>(floor(aRange.getMinY())),
                    static_cast<sal_Int32>(ceil(aRange.getMaxX())), static_cast<sal_Int32>(ceil(aRange.getMaxY())));
        }

        const Point aCenter( aBoundRect.Center() );

        aPolyPoly.translate(-aCenter.X(), -aCenter.Y());

        SdrPage* pPage = pObj->getSdrPageFromSdrObject();
        if( pPage )
        {
            const Size aPageSize( pPage->GetSize() );
            aPolyPoly.transform(basegfx::utils::createScaleB2DHomMatrix(
                1.0 / static_cast<double>(aPageSize.Width()), 1.0 / static_cast<double>(aPageSize.Height())));
        }
    }

    setPath( ::basegfx::utils::exportToSvgD( aPolyPoly, true, true, true) );
}

EffectSequenceHelper::EffectSequenceHelper()
: mnSequenceType( EffectNodeType::DEFAULT )
{
}

EffectSequenceHelper::EffectSequenceHelper( css::uno::Reference< css::animations::XTimeContainer > xSequenceRoot )
: mxSequenceRoot(std::move( xSequenceRoot )), mnSequenceType( EffectNodeType::DEFAULT )
{
    Reference< XAnimationNode > xNode( mxSequenceRoot, UNO_QUERY_THROW );
    create( xNode );
}

EffectSequenceHelper::~EffectSequenceHelper()
{
    reset();
}

void EffectSequenceHelper::reset()
{
    for( CustomAnimationEffectPtr& pEffect : maEffects )
    {
        pEffect->setEffectSequence(nullptr);
    }
    maEffects.clear();
}

Reference< XAnimationNode > EffectSequenceHelper::getRootNode()
{
    return mxSequenceRoot;
}

void EffectSequenceHelper::append( const CustomAnimationEffectPtr& pEffect )
{
    pEffect->setEffectSequence( this );
    maEffects.push_back(pEffect);
    rebuild();
}

CustomAnimationEffectPtr EffectSequenceHelper::append( const CustomAnimationPresetPtr& pPreset, const Any& rTarget, double fDuration /* = -1.0 */ )
{
    CustomAnimationEffectPtr pEffect;

    if( pPreset )
    {
        Reference< XAnimationNode > xNode( pPreset->create( u""_ustr ) );
        if( xNode.is() )
        {
            // first, filter all only ui relevant user data
            std::vector< NamedValue > aNewUserData;
            Sequence< NamedValue > aUserData( xNode->getUserData() );

            std::copy_if(std::cbegin(aUserData), std::cend(aUserData), std::back_inserter(aNewUserData),
                [](const NamedValue& rProp) { return rProp.Name != "text-only" && rProp.Name != "preset-property"; });

            if( !aNewUserData.empty() )
            {
                aUserData = ::comphelper::containerToSequence( aNewUserData );
                xNode->setUserData( aUserData );
            }

            // check target, maybe we need to force it to text
            sal_Int16 nSubItem = ShapeAnimationSubType::AS_WHOLE;

            if( rTarget.getValueType() == ::cppu::UnoType<ParagraphTarget>::get() )
            {
                nSubItem = ShapeAnimationSubType::ONLY_TEXT;
            }
            else if( pPreset->isTextOnly() )
            {
                Reference< XShape > xShape;
                rTarget >>= xShape;
                if( xShape.is() )
                {
                    // that's bad, we target a shape here but the effect is only for text
                    // so change subitem
                    nSubItem = ShapeAnimationSubType::ONLY_TEXT;
                }
            }

            // now create effect from preset
            pEffect = std::make_shared<CustomAnimationEffect>( xNode );
            pEffect->setEffectSequence( this );
            pEffect->setTarget( rTarget );
            pEffect->setTargetSubItem( nSubItem );
            if( fDuration != -1.0 )
                pEffect->setDuration( fDuration );

            maEffects.push_back(pEffect);

            rebuild();
        }
    }

    DBG_ASSERT( pEffect, "sd::EffectSequenceHelper::append(), failed!" );
    return pEffect;
}

CustomAnimationEffectPtr EffectSequenceHelper::append( const SdrPathObj& rPathObj, const Any& rTarget, double fDuration /* = -1.0 */, const OUString& rPresetId )
{
    CustomAnimationEffectPtr pEffect;

    if( fDuration <= 0.0 )
        fDuration = 2.0;

    try
    {
        Reference< XTimeContainer > xEffectContainer( ParallelTimeContainer::create( ::comphelper::getProcessComponentContext() ), UNO_QUERY_THROW );
        Reference< XAnimationNode > xAnimateMotion( AnimateMotion::create( ::comphelper::getProcessComponentContext() ) );

        xAnimateMotion->setDuration( Any( fDuration ) );
        xAnimateMotion->setFill( AnimationFill::HOLD );
        xEffectContainer->appendChild( xAnimateMotion );

        sal_Int16 nSubItem = ShapeAnimationSubType::AS_WHOLE;

        if( rTarget.getValueType() == ::cppu::UnoType<ParagraphTarget>::get() )
            nSubItem = ShapeAnimationSubType::ONLY_TEXT;

        pEffect = std::make_shared<CustomAnimationEffect>( xEffectContainer );
        pEffect->setEffectSequence( this );
        pEffect->setTarget( rTarget );
        pEffect->setTargetSubItem( nSubItem );
        pEffect->setNodeType( css::presentation::EffectNodeType::ON_CLICK );
        pEffect->setPresetClassAndId( css::presentation::EffectPresetClass::MOTIONPATH, rPresetId );
        pEffect->setAcceleration( 0.5 );
        pEffect->setDecelerate( 0.5 );
        pEffect->setFill( AnimationFill::HOLD );
        pEffect->setBegin( 0.0 );
        pEffect->updatePathFromSdrPathObj( rPathObj );
        if( fDuration != -1.0 )
            pEffect->setDuration( fDuration );

        maEffects.push_back(pEffect);

        rebuild();
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::EffectSequenceHelper::append()" );
    }

    return pEffect;
}

void EffectSequenceHelper::replace( const CustomAnimationEffectPtr& pEffect, const CustomAnimationPresetPtr& pPreset, const OUString& rPresetSubType, double fDuration /* = -1.0 */ )
{
    if( !(pEffect && pPreset) )
        return;

    try
    {
        Reference< XAnimationNode > xNewNode( pPreset->create( rPresetSubType ) );
        if( xNewNode.is() )
        {
            pEffect->replaceNode( xNewNode );
            if( fDuration != -1.0 )
                pEffect->setDuration( fDuration );
        }

        rebuild();
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::EffectSequenceHelper::replace()" );
    }
}

void EffectSequenceHelper::replace( const CustomAnimationEffectPtr& pEffect, const CustomAnimationPresetPtr& pPreset, double fDuration /* = -1.0 */ )
{
    replace( pEffect, pPreset, u""_ustr, fDuration );
}

void EffectSequenceHelper::remove( const CustomAnimationEffectPtr& pEffect )
{
    if( pEffect )
    {
        pEffect->setEffectSequence( nullptr );
        maEffects.remove( pEffect );
    }

    rebuild();
}

void EffectSequenceHelper::moveToBeforeEffect( const CustomAnimationEffectPtr& pEffect, const CustomAnimationEffectPtr& pInsertBefore)
{
    if ( pEffect )
    {
        maEffects.remove( pEffect );
        EffectSequence::iterator aInsertIter( find( pInsertBefore ) );

        // aInsertIter being end() is OK: pInsertBefore could be null, so put at end.
        maEffects.insert( aInsertIter, pEffect );

        rebuild();
    }
}

void EffectSequenceHelper::rebuild()
{
    implRebuild();
}

void EffectSequenceHelper::implRebuild()
{
    try
    {
        // first we delete all time containers on the first two levels
        Reference< XEnumerationAccess > xEnumerationAccess( mxSequenceRoot, UNO_QUERY_THROW );
        Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
        while( xEnumeration->hasMoreElements() )
        {
            Reference< XAnimationNode > xChildNode( xEnumeration->nextElement(), UNO_QUERY_THROW );
            Reference< XTimeContainer > xChildContainer( xChildNode, UNO_QUERY_THROW );

            Reference< XEnumerationAccess > xChildEnumerationAccess( xChildNode, UNO_QUERY_THROW );
            Reference< XEnumeration > xChildEnumeration( xChildEnumerationAccess->createEnumeration(), UNO_SET_THROW );
            while( xChildEnumeration->hasMoreElements() )
            {
                Reference< XAnimationNode > xNode( xChildEnumeration->nextElement(), UNO_QUERY_THROW );
                xChildContainer->removeChild( xNode );
            }

            mxSequenceRoot->removeChild( xChildNode );
        }

        // second, rebuild main sequence
        EffectSequence::iterator aIter( maEffects.begin() );
        EffectSequence::iterator aEnd( maEffects.end() );
        if( aIter != aEnd )
        {
            std::vector< sd::AfterEffectNode > aAfterEffects;

            CustomAnimationEffectPtr pEffect = *aIter++;

            bool bFirst = true;
            do
            {
                // create a par container for the next click node and all following with and after effects
                Reference< XTimeContainer > xOnClickContainer( ParallelTimeContainer::create( ::comphelper::getProcessComponentContext() ), UNO_QUERY_THROW );

                Event aEvent;
                if( mxEventSource.is() )
                {
                    aEvent.Source <<= mxEventSource;
                    aEvent.Trigger = EventTrigger::ON_CLICK;
                }
                else
                {
                    aEvent.Trigger = EventTrigger::ON_NEXT;
                }
                aEvent.Repeat = 0;

                Any aBegin( aEvent );
                if( bFirst )
                {
                    // if the first node is not a click action, this click container
                    // must not have INDEFINITE begin but start at 0s
                    bFirst = false;
                    if( pEffect->getNodeType() != EffectNodeType::ON_CLICK )
                        aBegin <<= 0.0;
                }

                xOnClickContainer->setBegin( aBegin );

                mxSequenceRoot->appendChild( xOnClickContainer );

                double fBegin = 0.0;

                do
                {
                    // create a par container for the current click or after effect node and all following with effects
                    Reference< XTimeContainer > xWithContainer( ParallelTimeContainer::create( ::comphelper::getProcessComponentContext() ), UNO_QUERY_THROW );
                    xWithContainer->setBegin( Any( fBegin ) );
                    xOnClickContainer->appendChild( xWithContainer );

                    double fDuration = 0.0;
                    do
                    {
                        Reference< XAnimationNode > xEffectNode( pEffect->getNode() );
                        xWithContainer->appendChild( xEffectNode );

                        if( pEffect->hasAfterEffect() )
                        {
                            Reference< XAnimationNode > xAfterEffect( pEffect->createAfterEffectNode() );
                            AfterEffectNode a( xAfterEffect, xEffectNode, pEffect->IsAfterEffectOnNext() );
                            aAfterEffects.push_back( a );
                        }

                        double fTemp = pEffect->getBegin() + pEffect->getAbsoluteDuration();
                        if( fTemp > fDuration )
                            fDuration = fTemp;

                        if( aIter != aEnd )
                            pEffect = *aIter++;
                        else
                            pEffect.reset();
                    }
                    while( pEffect && (pEffect->getNodeType() == EffectNodeType::WITH_PREVIOUS) );

                    fBegin += fDuration;
                }
                while( pEffect && (pEffect->getNodeType() != EffectNodeType::ON_CLICK) );
            }
            while( pEffect );

            // process after effect nodes
            std::for_each( aAfterEffects.begin(), aAfterEffects.end(), stl_process_after_effect_node_func );

            updateTextGroups();

            // reset duration, might have been altered (see below)
            mxSequenceRoot->setDuration( Any() );
        }
        else
        {
            // empty sequence, set duration to 0.0 explicitly
            // (otherwise, this sequence will never end)
            mxSequenceRoot->setDuration( Any(0.0) );
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::EffectSequenceHelper::rebuild()" );
    }
}

stl_CustomAnimationEffect_search_node_predict::stl_CustomAnimationEffect_search_node_predict( const css::uno::Reference< css::animations::XAnimationNode >& xSearchNode )
: mxSearchNode( xSearchNode )
{
}

bool stl_CustomAnimationEffect_search_node_predict::operator()( const CustomAnimationEffectPtr& pEffect ) const
{
    return pEffect->getNode() == mxSearchNode;
}

/// @throws Exception
static bool implFindNextContainer( Reference< XTimeContainer > const & xParent, Reference< XTimeContainer > const & xCurrent, Reference< XTimeContainer >& xNext )
{
    Reference< XEnumerationAccess > xEnumerationAccess( xParent, UNO_QUERY_THROW );
    Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration() );
    if( xEnumeration.is() )
    {
        Reference< XInterface > x;
        while( xEnumeration->hasMoreElements() && !xNext.is() )
        {
            if( (xEnumeration->nextElement() >>= x) && (x == xCurrent) )
            {
                if( xEnumeration->hasMoreElements() )
                    xEnumeration->nextElement() >>= xNext;
            }
        }
    }
    return xNext.is();
}

void stl_process_after_effect_node_func(AfterEffectNode const & rNode)
{
    try
    {
        if( rNode.mxNode.is() && rNode.mxMaster.is() )
        {
            // set master node
            Reference< XAnimationNode > xMasterNode( rNode.mxMaster, UNO_SET_THROW );
            Sequence< NamedValue > aUserData( rNode.mxNode->getUserData() );
            sal_Int32 nSize = aUserData.getLength();
            aUserData.realloc(nSize+1);
            auto pUserData = aUserData.getArray();
            pUserData[nSize].Name = "master-element";
            pUserData[nSize].Value <<= xMasterNode;
            rNode.mxNode->setUserData( aUserData );

            // insert after effect node into timeline
            Reference< XTimeContainer > xContainer( rNode.mxMaster->getParent(), UNO_QUERY_THROW );

            if( !rNode.mbOnNextEffect ) // sameClick
            {
                // insert the aftereffect after its effect is animated
                xContainer->insertAfter( rNode.mxNode, rNode.mxMaster );
            }
            else // nextClick
            {
                const Reference< XComponentContext >& xContext( ::comphelper::getProcessComponentContext() );
                // insert the aftereffect in the next group

                Reference< XTimeContainer > xClickContainer( xContainer->getParent(), UNO_QUERY_THROW );
                Reference< XTimeContainer > xSequenceContainer( xClickContainer->getParent(), UNO_QUERY_THROW );

                Reference< XTimeContainer > xNextContainer;

                // first try if we have an after effect container
                if( !implFindNextContainer( xClickContainer, xContainer, xNextContainer ) )
                {
                    Reference< XTimeContainer > xNextClickContainer;
                    // if not, try to find the next click effect container
                    if( implFindNextContainer( xSequenceContainer, xClickContainer, xNextClickContainer ) )
                    {
                        Reference< XEnumerationAccess > xEnumerationAccess( xNextClickContainer, UNO_QUERY_THROW );
                        Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
                        if( xEnumeration->hasMoreElements() )
                        {
                            // the next container is the first child container
                            xEnumeration->nextElement() >>= xNextContainer;
                        }
                        else
                        {
                            // this does not yet have a child container, create one
                            xNextContainer.set( ParallelTimeContainer::create(xContext), UNO_QUERY_THROW );

                            xNextContainer->setBegin( Any( 0.0 ) );
                            xNextClickContainer->appendChild( xNextContainer );
                        }
                        DBG_ASSERT( xNextContainer.is(), "ppt::stl_process_after_effect_node_func::operator(), could not find/create container!" );
                    }
                }

                // if we don't have a next container, we add one to the sequence container
                if( !xNextContainer.is() )
                {
                    Reference< XTimeContainer > xNewClickContainer( ParallelTimeContainer::create( xContext ), UNO_QUERY_THROW );

                    Event aEvent;
                    aEvent.Trigger = EventTrigger::ON_NEXT;
                    aEvent.Repeat = 0;
                    xNewClickContainer->setBegin( Any( aEvent ) );

                    xSequenceContainer->insertAfter( xNewClickContainer, xClickContainer );

                    xNextContainer.set( ParallelTimeContainer::create( xContext ), UNO_QUERY_THROW );

                    xNextContainer->setBegin( Any( 0.0 ) );
                    xNewClickContainer->appendChild( xNextContainer );
                }

                if( xNextContainer.is() )
                {
                    // find begin time of first element
                    Reference< XEnumerationAccess > xEnumerationAccess( xNextContainer, UNO_QUERY_THROW );
                    Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
                    if( xEnumeration->hasMoreElements() )
                    {
                        Reference< XAnimationNode > xChild;
                        // the next container is the first child container
                        xEnumeration->nextElement() >>= xChild;
                        if( xChild.is() )
                        {
                            Any aBegin( xChild->getBegin() );
                            double fBegin = 0.0;
                            if( (aBegin >>= fBegin) && (fBegin >= 0.0))
                                rNode.mxNode->setBegin( aBegin );
                        }
                    }

                    xNextContainer->appendChild( rNode.mxNode );
                }
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "ppt::stl_process_after_effect_node_func::operator()" );
    }
}

EffectSequence::iterator EffectSequenceHelper::find( const CustomAnimationEffectPtr& pEffect )
{
    return std::find( maEffects.begin(), maEffects.end(), pEffect );
}

CustomAnimationEffectPtr EffectSequenceHelper::findEffect( const css::uno::Reference< css::animations::XAnimationNode >& xNode ) const
{
    CustomAnimationEffectPtr pEffect;

    EffectSequence::const_iterator aIter = std::find_if(maEffects.begin(), maEffects.end(),
        [&xNode](const CustomAnimationEffectPtr& rxEffect) { return rxEffect->getNode() == xNode; });
    if (aIter != maEffects.end())
        pEffect = *aIter;

    return pEffect;
}

sal_Int32 EffectSequenceHelper::getOffsetFromEffect( const CustomAnimationEffectPtr& xEffect ) const
{
    auto aIter = std::find(maEffects.begin(), maEffects.end(), xEffect);
    if (aIter != maEffects.end())
        return static_cast<sal_Int32>(std::distance(maEffects.begin(), aIter));

    return -1;
}

CustomAnimationEffectPtr EffectSequenceHelper::getEffectFromOffset( sal_Int32 nOffset ) const
{
    EffectSequence::const_iterator aIter( maEffects.begin() );
    nOffset = std::min(nOffset, static_cast<sal_Int32>(maEffects.size()));
    std::advance(aIter, nOffset);

    CustomAnimationEffectPtr pEffect;
    if( aIter != maEffects.end() )
        pEffect = *aIter;

    return pEffect;
}

bool EffectSequenceHelper::disposeShape( const Reference< XShape >& xShape )
{
    bool bChanges = false;

    EffectSequence::iterator aIter( maEffects.begin() );
    while( aIter != maEffects.end() )
    {
        if( (*aIter)->getTargetShape() == xShape )
        {
            (*aIter)->setEffectSequence( nullptr );
            bChanges = true;
            aIter = maEffects.erase( aIter );
        }
        else
        {
            ++aIter;
        }
    }

    return bChanges;
}

bool EffectSequenceHelper::hasEffect( const css::uno::Reference< css::drawing::XShape >& xShape )
{
    return std::any_of(maEffects.begin(), maEffects.end(),
        [&xShape](const CustomAnimationEffectPtr& rxEffect) { return rxEffect->getTargetShape() == xShape; });
}

bool EffectSequenceHelper::getParagraphNumberingLevels( const Reference< XShape >& xShape, std::vector< sal_Int32 >& rParagraphNumberingLevel )
{
    rParagraphNumberingLevel.clear();

    if( !hasEffect( xShape ) )
        return false;

    Reference< XText > xText( xShape, UNO_QUERY );
    if( xText.is() )
    {
        Reference< XEnumerationAccess > xEA( xText, UNO_QUERY );
        if( xEA.is() )
        {
            Reference< XEnumeration > xEnumeration = xEA->createEnumeration();

            if( xEnumeration.is() )
            {
                while( xEnumeration->hasMoreElements() )
                {
                    Reference< XPropertySet > xParaSet;
                    xEnumeration->nextElement() >>= xParaSet;

                    sal_Int32 nParaDepth = 0;
                    if( xParaSet.is() )
                    {
                        xParaSet->getPropertyValue( u"NumberingLevel"_ustr ) >>= nParaDepth;
                    }

                    rParagraphNumberingLevel.push_back( nParaDepth );
                }
            }
        }
    }

    return true;
}

void EffectSequenceHelper::insertTextRange( const css::uno::Any& aTarget )
{
    ParagraphTarget aParaTarget;
    if( !(aTarget >>= aParaTarget ) )
        return;

    // get map [paragraph index] -> [NumberingLevel]
    // for following reusage inside all animation effects
    std::vector< sal_Int32 > paragraphNumberingLevel;
    std::vector< sal_Int32 >* paragraphNumberingLevelParam = nullptr;
    if ( getParagraphNumberingLevels( aParaTarget.Shape, paragraphNumberingLevel ) )
        paragraphNumberingLevelParam = &paragraphNumberingLevel;

    // update internal flags for each animation effect
    const bool bChanges = std::accumulate(maEffects.begin(), maEffects.end(), false,
        [&aParaTarget, &paragraphNumberingLevelParam](const bool bCheck, const CustomAnimationEffectPtr& rxEffect) {
            bool bRes = bCheck;
            if (rxEffect->getTargetShape() == aParaTarget.Shape)
                bRes |= rxEffect->checkForText( paragraphNumberingLevelParam );
            return bRes;
        });

    if( bChanges )
        rebuild();
}

static bool isParagraphTargetTextEmpty( ParagraphTarget aParaTarget )
{
    // get paragraph
    Reference< XText > xText ( aParaTarget.Shape, UNO_QUERY );
    if( xText.is() )
    {
        Reference< XEnumerationAccess > xEA( xText, UNO_QUERY );
        if( xEA.is() )
        {
            Reference< XEnumeration > xEnumeration = xEA->createEnumeration();
            if( xEnumeration.is() )
            {
                // advance to the Nth paragraph
                sal_Int32 nPara = aParaTarget.Paragraph;
                while( xEnumeration->hasMoreElements() && nPara-- )
                    xEnumeration->nextElement();

                // get Nth paragraph's text and check if it's empty
                if( xEnumeration->hasMoreElements() )
                {
                    Reference< XTextRange > xRange( xEnumeration->nextElement(), UNO_QUERY );
                    if( xRange.is() )
                    {
                        OUString text = xRange->getString();
                        return text.isEmpty();
                    }
                }
            }
        }
    }
    return false;
}

void EffectSequenceHelper::disposeTextRange( const css::uno::Any& aTarget )
{
    ParagraphTarget aParaTarget;
    if( !(aTarget >>= aParaTarget ) )
        return;

    bool bChanges = false;

    // building list of effects for target shape; process effects not on target shape
    EffectSequence aTargetParagraphEffects;
    for( const auto &pEffect : maEffects )
    {
        Any aIterTarget( pEffect->getTarget() );
        if( aIterTarget.getValueType() == ::cppu::UnoType<ParagraphTarget>::get() )
        {
            ParagraphTarget aIterParaTarget;
            if( (aIterTarget >>= aIterParaTarget) && (aIterParaTarget.Shape == aParaTarget.Shape) )
            {
                aTargetParagraphEffects.push_back(pEffect);
            }
        }
        else if( pEffect->getTargetShape() == aParaTarget.Shape )
        {
            bChanges |= pEffect->checkForText();
        }
    }

    // select effect to delete:
    // if paragraph before target is blank, then delete its animation effect (if any) instead
    ParagraphTarget aPreviousParagraph = aParaTarget;
    --aPreviousParagraph.Paragraph;
    bool bIsPreviousParagraphEmpty = isParagraphTargetTextEmpty( aPreviousParagraph );
    sal_Int16 anParaNumToDelete = bIsPreviousParagraphEmpty ? aPreviousParagraph.Paragraph : aParaTarget.Paragraph;

    // update effects
    for( const auto &pEffect : aTargetParagraphEffects )
    {
        Any aIterTarget( pEffect->getTarget() );

        ParagraphTarget aIterParaTarget;
        aIterTarget >>= aIterParaTarget;

        // delete effect for target paragraph (may have effects in more than one text group)
        if( aIterParaTarget.Paragraph == anParaNumToDelete )
        {
            auto aItr = find( pEffect );
            DBG_ASSERT( aItr != maEffects.end(), "sd::EffectSequenceHelper::disposeTextRange(), Expected effect missing.");
            if( aItr != maEffects.end() )
            {
                (*aItr)->setEffectSequence( nullptr );
                maEffects.erase(aItr);
                bChanges = true;
            }
        }

        // shift all paragraphs after disposed paragraph
        if( aIterParaTarget.Paragraph > anParaNumToDelete )
        {
            --aIterParaTarget.Paragraph;
            pEffect->setTarget( Any( aIterParaTarget ) );
            bChanges = true;
        }
    }

    if( bChanges )
    {
        rebuild();
    }
}

CustomAnimationTextGroup::CustomAnimationTextGroup( const Reference< XShape >& rTarget, sal_Int32 nGroupId )
:   maTarget( rTarget ),
    mnGroupId( nGroupId )
{
    reset();
}

void CustomAnimationTextGroup::reset()
{
    mnTextGrouping = -1;
    mbAnimateForm = false;
    mbTextReverse = false;
    mfGroupingAuto = -1.0;
    mnLastPara = -1; // used to check for TextReverse

    for (sal_Int8 & rn : mnDepthFlags)
    {
        rn = 0;
    }

    maEffects.clear();
}

void CustomAnimationTextGroup::addEffect( CustomAnimationEffectPtr const & pEffect )
{
    maEffects.push_back( pEffect );

    Any aTarget( pEffect->getTarget() );
    if( aTarget.getValueType() == ::cppu::UnoType<ParagraphTarget>::get() )
    {
        // now look at the paragraph
        ParagraphTarget aParaTarget;
        aTarget >>= aParaTarget;

        if( mnLastPara != -1 )
            mbTextReverse = mnLastPara > aParaTarget.Paragraph;

        mnLastPara = aParaTarget.Paragraph;

        const sal_Int32 nParaDepth = pEffect->getParaDepth();

        // only look at the first PARA_LEVELS levels
        if( nParaDepth < PARA_LEVELS )
        {
            // our first paragraph with this level?
            if( mnDepthFlags[nParaDepth] == 0 )
            {
                // so set it to the first found
                mnDepthFlags[nParaDepth] = static_cast<sal_Int8>(pEffect->getNodeType());
            }
            else if( mnDepthFlags[nParaDepth] != pEffect->getNodeType() )
            {
                mnDepthFlags[nParaDepth] = -1;
            }

            if( pEffect->getNodeType() == EffectNodeType::AFTER_PREVIOUS )
                mfGroupingAuto = pEffect->getBegin();

            mnTextGrouping = PARA_LEVELS;
            while( (mnTextGrouping > 0)
                   && (mnDepthFlags[mnTextGrouping - 1] <= 0) )
                --mnTextGrouping;
        }
    }
    else
    {
        // if we have an effect with the shape as a target, we animate the background
        mbAnimateForm = pEffect->getTargetSubItem() != ShapeAnimationSubType::ONLY_TEXT;
    }
}

CustomAnimationTextGroupPtr EffectSequenceHelper::findGroup( sal_Int32 nGroupId )
{
    CustomAnimationTextGroupPtr aPtr;

    CustomAnimationTextGroupMap::iterator aIter( maGroupMap.find( nGroupId ) );
    if( aIter != maGroupMap.end() )
        aPtr = (*aIter).second;

    return aPtr;
}

void EffectSequenceHelper::updateTextGroups()
{
    maGroupMap.clear();

    // first create all the groups
    for( const CustomAnimationEffectPtr& pEffect : maEffects )
    {
        const sal_Int32 nGroupId = pEffect->getGroupId();

        if( nGroupId == -1 )
            continue; // trivial case, no group

        CustomAnimationTextGroupPtr pGroup = findGroup( nGroupId );
        if( !pGroup )
        {
            pGroup = std::make_shared<CustomAnimationTextGroup>( pEffect->getTargetShape(), nGroupId );
            maGroupMap[nGroupId] = pGroup;
        }

        pGroup->addEffect( pEffect );
    }

    // Now that all the text groups have been cleared up and rebuilt, we need to update its
    // text grouping. addEffect() already make mnTextGrouping the last possible level,
    // so just continue to find the last level that is not EffectNodeType::WITH_PREVIOUS.
    for(const auto &rGroupMapItem: maGroupMap)
    {
        const CustomAnimationTextGroupPtr &pGroup = rGroupMapItem.second;
        while(pGroup->mnTextGrouping > 0 && pGroup->mnDepthFlags[pGroup->mnTextGrouping - 1] == EffectNodeType::WITH_PREVIOUS)
            --pGroup->mnTextGrouping;
    }
}

CustomAnimationTextGroupPtr
EffectSequenceHelper::createTextGroup(const CustomAnimationEffectPtr& pEffect,
                                      sal_Int32 nTextGrouping, double fTextGroupingAuto,
                                      bool bAnimateForm, bool bTextReverse)
{
    // first find a free group-id
    sal_Int32 nGroupId = 0;

    CustomAnimationTextGroupMap::iterator aIter( maGroupMap.begin() );
    const CustomAnimationTextGroupMap::iterator aEnd( maGroupMap.end() );
    while( aIter != aEnd )
    {
        if( (*aIter).first == nGroupId )
        {
            nGroupId++;
            aIter = maGroupMap.begin();
        }
        else
        {
            ++aIter;
        }
    }

    Reference< XShape > xTarget( pEffect->getTargetShape() );

    CustomAnimationTextGroupPtr pTextGroup = std::make_shared<CustomAnimationTextGroup>( xTarget, nGroupId );
    maGroupMap[nGroupId] = pTextGroup;

    bool bUsed = false;

    // do we need to target the shape?
    if( (nTextGrouping == 0) || bAnimateForm )
    {
        sal_Int16 nSubItem;
        if( nTextGrouping == 0)
            nSubItem = bAnimateForm ? ShapeAnimationSubType::AS_WHOLE : ShapeAnimationSubType::ONLY_TEXT;
        else
            nSubItem = ShapeAnimationSubType::ONLY_BACKGROUND;

        pEffect->setTarget( Any( xTarget ) );
        pEffect->setTargetSubItem( nSubItem );
        pEffect->setEffectSequence( this );
        pEffect->setGroupId( nGroupId );

        pTextGroup->addEffect( pEffect );
        bUsed = true;
    }

    pTextGroup->mnTextGrouping = nTextGrouping;
    pTextGroup->mfGroupingAuto = fTextGroupingAuto;
    pTextGroup->mbTextReverse = bTextReverse;

    // now add an effect for each paragraph
    createTextGroupParagraphEffects( pTextGroup, pEffect, bUsed );

    notify_listeners();

    return pTextGroup;
}

void EffectSequenceHelper::createTextGroupParagraphEffects( const CustomAnimationTextGroupPtr& pTextGroup, const CustomAnimationEffectPtr& pEffect, bool bUsed )
{
    Reference< XShape > xTarget( pTextGroup->maTarget );

    sal_Int32 nTextGrouping = pTextGroup->mnTextGrouping;
    double fTextGroupingAuto = pTextGroup->mfGroupingAuto;
    bool bTextReverse = pTextGroup->mbTextReverse;

    // now add an effect for each paragraph
    if( nTextGrouping < 0 )
        return;

    try
    {
        EffectSequence::iterator aInsertIter( find( pEffect ) );

        Reference< XEnumerationAccess > xText( xTarget, UNO_QUERY_THROW );
        Reference< XEnumeration > xEnumeration( xText->createEnumeration(), UNO_SET_THROW );

        std::deque< sal_Int16 > aParaList;
        sal_Int16 nPara;

        // fill the list with all valid paragraphs
        for( nPara = 0; xEnumeration->hasMoreElements(); nPara++ )
        {
            Reference< XTextRange > xRange( xEnumeration->nextElement(), UNO_QUERY );
            if( xRange.is() && !xRange->getString().isEmpty() )
            {
                if( bTextReverse ) // sort them
                    aParaList.push_front( nPara );
                else
                    aParaList.push_back( nPara );
            }
        }

        ParagraphTarget aTarget;
        aTarget.Shape = std::move(xTarget);

        for( const auto i : aParaList )
        {
            aTarget.Paragraph = i;

            CustomAnimationEffectPtr pNewEffect;
            if( bUsed )
            {
                // clone a new effect from first effect
                pNewEffect = pEffect->clone();
                ++aInsertIter;
                aInsertIter = maEffects.insert( aInsertIter, pNewEffect );
            }
            else
            {
                // reuse first effect if it's not yet used
                pNewEffect = pEffect;
                bUsed = true;
                aInsertIter = find( pNewEffect );
            }

            // set target and group-id
            pNewEffect->setTarget( Any( aTarget ) );
            pNewEffect->setTargetSubItem( ShapeAnimationSubType::ONLY_TEXT );
            pNewEffect->setGroupId( pTextGroup->mnGroupId );
            pNewEffect->setEffectSequence( this );

            // set correct node type
            if( pNewEffect->getParaDepth() < nTextGrouping )
            {
                if( fTextGroupingAuto == -1.0 )
                {
                    pNewEffect->setNodeType( EffectNodeType::ON_CLICK );
                    pNewEffect->setBegin( 0.0 );
                }
                else
                {
                    pNewEffect->setNodeType( EffectNodeType::AFTER_PREVIOUS );
                    pNewEffect->setBegin( fTextGroupingAuto );
                }
            }
            else
            {
                pNewEffect->setNodeType( EffectNodeType::WITH_PREVIOUS );
                pNewEffect->setBegin( 0.0 );
            }

            pTextGroup->addEffect( pNewEffect );
        }
        notify_listeners();
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::EffectSequenceHelper::createTextGroup()" );
    }
}

void EffectSequenceHelper::setTextGrouping( const CustomAnimationTextGroupPtr& pTextGroup, sal_Int32 nTextGrouping )
{
    if( pTextGroup->mnTextGrouping == nTextGrouping )
    {
        // first case, trivial case, do nothing
    }
    else if( (pTextGroup->mnTextGrouping == -1) && (nTextGrouping >= 0) )
    {
        // second case, we need to add new effects for each paragraph

        CustomAnimationEffectPtr pEffect( pTextGroup->maEffects.front() );

        pTextGroup->mnTextGrouping = nTextGrouping;
        createTextGroupParagraphEffects( pTextGroup, pEffect, true );
        notify_listeners();
    }
    else if( (pTextGroup->mnTextGrouping >= 0) && (nTextGrouping == -1 ) )
    {
        // third case, we need to remove effects for each paragraph

        EffectSequence aEffects( pTextGroup->maEffects );
        pTextGroup->reset();

        for( const CustomAnimationEffectPtr& pEffect : aEffects )
        {
            if( pEffect->getTarget().getValueType() == ::cppu::UnoType<ParagraphTarget>::get() )
                remove( pEffect );
            else
                pTextGroup->addEffect( pEffect );
        }
        notify_listeners();
    }
    else
    {
        // fourth case, we need to change the node types for the text nodes
        double fTextGroupingAuto = pTextGroup->mfGroupingAuto;

        EffectSequence aEffects( pTextGroup->maEffects );
        pTextGroup->reset();

        for( CustomAnimationEffectPtr& pEffect : aEffects )
        {
            if( pEffect->getTarget().getValueType() == ::cppu::UnoType<ParagraphTarget>::get() )
            {
                // set correct node type
                if( pEffect->getParaDepth() < nTextGrouping )
                {
                    if( fTextGroupingAuto == -1.0 )
                    {
                        pEffect->setNodeType( EffectNodeType::ON_CLICK );
                        pEffect->setBegin( 0.0 );
                    }
                    else
                    {
                        pEffect->setNodeType( EffectNodeType::AFTER_PREVIOUS );
                        pEffect->setBegin( fTextGroupingAuto );
                    }
                }
                else
                {
                    pEffect->setNodeType( EffectNodeType::WITH_PREVIOUS );
                    pEffect->setBegin( 0.0 );
                }
            }

            pTextGroup->addEffect( pEffect );

        }
        notify_listeners();
    }
}

void EffectSequenceHelper::setAnimateForm( const CustomAnimationTextGroupPtr& pTextGroup, bool bAnimateForm )
{
    if( pTextGroup->mbAnimateForm == bAnimateForm )
    {
        // trivial case, do nothing
    }
    else
    {
        EffectSequence aEffects( pTextGroup->maEffects );
        pTextGroup->reset();

        SAL_WARN_IF(aEffects.empty(), "sd", "EffectSequenceHelper::setAnimateForm effects empty" );

        if (aEffects.empty())
            return;

        EffectSequence::iterator aIter( aEffects.begin() );
        const EffectSequence::iterator aEnd( aEffects.end() );

        // first insert if we have to
        if( bAnimateForm )
        {
            EffectSequence::iterator aInsertIter( find( *aIter ) );

            CustomAnimationEffectPtr pEffect;
            if( (aEffects.size() == 1) && ((*aIter)->getTarget().getValueType() != ::cppu::UnoType<ParagraphTarget>::get() ) )
            {
                // special case, only one effect and that targets whole text,
                // convert this to target whole shape
                pEffect = *aIter++;
                pEffect->setTargetSubItem( ShapeAnimationSubType::AS_WHOLE );
            }
            else
            {
                pEffect = (*aIter)->clone();
                pEffect->setTarget( Any( (*aIter)->getTargetShape() ) );
                pEffect->setTargetSubItem( ShapeAnimationSubType::ONLY_BACKGROUND );
                maEffects.insert( aInsertIter, pEffect );
            }

            pTextGroup->addEffect( pEffect );
        }

        if( !bAnimateForm && (aEffects.size() == 1) )
        {
            const CustomAnimationEffectPtr& pEffect( *aIter );
            pEffect->setTarget( Any( (*aIter)->getTargetShape() ) );
            pEffect->setTargetSubItem( ShapeAnimationSubType::ONLY_TEXT );
            pTextGroup->addEffect( pEffect );
        }
        else
        {
            // read the rest to the group again
            while( aIter != aEnd )
            {
                CustomAnimationEffectPtr pEffect( *aIter++ );

                if( pEffect->getTarget().getValueType() == ::cppu::UnoType<ParagraphTarget>::get() )
                {
                    pTextGroup->addEffect( pEffect );
                }
                else
                {
                    DBG_ASSERT( !bAnimateForm, "sd::EffectSequenceHelper::setAnimateForm(), something is wrong here!" );
                    remove( pEffect );
                }
            }
        }
        notify_listeners();
    }
}

void EffectSequenceHelper::setTextGroupingAuto( const CustomAnimationTextGroupPtr& pTextGroup, double fTextGroupingAuto )
{
    sal_Int32 nTextGrouping = pTextGroup->mnTextGrouping;

    EffectSequence aEffects( pTextGroup->maEffects );
    pTextGroup->reset();

    for( CustomAnimationEffectPtr& pEffect : aEffects )
    {
        if( pEffect->getTarget().getValueType() == ::cppu::UnoType<ParagraphTarget>::get() )
        {
            // set correct node type
            if( pEffect->getParaDepth() < nTextGrouping )
            {
                if( fTextGroupingAuto == -1.0 )
                {
                    pEffect->setNodeType( EffectNodeType::ON_CLICK );
                    pEffect->setBegin( 0.0 );
                }
                else
                {
                    pEffect->setNodeType( EffectNodeType::AFTER_PREVIOUS );
                    pEffect->setBegin( fTextGroupingAuto );
                }
            }
            else
            {
                pEffect->setNodeType( EffectNodeType::WITH_PREVIOUS );
                pEffect->setBegin( 0.0 );
            }
        }

        pTextGroup->addEffect( pEffect );

    }
    notify_listeners();
}

namespace {

struct ImplStlTextGroupSortHelper
{
    explicit ImplStlTextGroupSortHelper( bool bReverse ) : mbReverse( bReverse ) {};
    bool operator()( const CustomAnimationEffectPtr& p1, const CustomAnimationEffectPtr& p2 );
    bool mbReverse;
    sal_Int32 getTargetParagraph( const CustomAnimationEffectPtr& p1 );
};

}

sal_Int32 ImplStlTextGroupSortHelper::getTargetParagraph( const CustomAnimationEffectPtr& p1 )
{
    const Any aTarget(p1->getTarget());
    if( aTarget.hasValue() && aTarget.getValueType() == ::cppu::UnoType<ParagraphTarget>::get() )
    {
        ParagraphTarget aParaTarget;
        aTarget >>= aParaTarget;
        return aParaTarget.Paragraph;
    }
    else
    {
        return mbReverse ? 0x7fffffff : -1;
    }
}

bool ImplStlTextGroupSortHelper::operator()( const CustomAnimationEffectPtr& p1, const CustomAnimationEffectPtr& p2 )
{
    if( mbReverse )
    {
        return getTargetParagraph( p2 ) < getTargetParagraph( p1 );
    }
    else
    {
        return getTargetParagraph( p1 ) < getTargetParagraph( p2 );
    }
}

void EffectSequenceHelper::setTextReverse( const CustomAnimationTextGroupPtr& pTextGroup, bool bTextReverse )
{
    if( pTextGroup->mbTextReverse == bTextReverse )
    {
        // do nothing
    }
    else
    {
        std::vector< CustomAnimationEffectPtr > aSortedVector( pTextGroup->maEffects.begin(), pTextGroup->maEffects.end() );
        ImplStlTextGroupSortHelper aSortHelper( bTextReverse );
        std::sort( aSortedVector.begin(), aSortedVector.end(), aSortHelper );

        pTextGroup->reset();

        std::vector< CustomAnimationEffectPtr >::iterator aIter( aSortedVector.begin() );
        const std::vector< CustomAnimationEffectPtr >::iterator aEnd( aSortedVector.end() );

        if( aIter != aEnd )
        {
            pTextGroup->addEffect( *aIter );
            EffectSequence::iterator aInsertIter( find( *aIter++ ) );
            while( aIter != aEnd )
            {
                CustomAnimationEffectPtr pEffect( *aIter++ );
                maEffects.erase( find( pEffect ) );
                aInsertIter = maEffects.insert( ++aInsertIter, pEffect );
                pTextGroup->addEffect( pEffect );
            }
        }
        notify_listeners();
    }
}

void EffectSequenceHelper::addListener( ISequenceListener* pListener )
{
    if( std::find( maListeners.begin(), maListeners.end(), pListener ) == maListeners.end() )
        maListeners.push_back( pListener );
}

void EffectSequenceHelper::removeListener( ISequenceListener* pListener )
{
    maListeners.remove( pListener );
}

namespace {

struct stl_notify_listeners_func
{
    stl_notify_listeners_func() {}
    void operator()(ISequenceListener* pListener) { pListener->notify_change(); }
};

}

void EffectSequenceHelper::notify_listeners()
{
    stl_notify_listeners_func aFunc;
    std::for_each( maListeners.begin(), maListeners.end(), aFunc );
}

void EffectSequenceHelper::create( const css::uno::Reference< css::animations::XAnimationNode >& xNode )
{
    DBG_ASSERT( xNode.is(), "sd::EffectSequenceHelper::create(), illegal argument" );

    if( !xNode.is() )
        return;

    try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( xNode, UNO_QUERY_THROW );
        Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
        while( xEnumeration->hasMoreElements() )
        {
            Reference< XAnimationNode > xChildNode( xEnumeration->nextElement(), UNO_QUERY_THROW );
            createEffectsequence( xChildNode );
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::EffectSequenceHelper::create()" );
    }
}

void EffectSequenceHelper::createEffectsequence( const Reference< XAnimationNode >& xNode )
{
    DBG_ASSERT( xNode.is(), "sd::EffectSequenceHelper::createEffectsequence(), illegal argument" );

    if( !xNode.is() )
        return;

    try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( xNode, UNO_QUERY_THROW );
        Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
        while( xEnumeration->hasMoreElements() )
        {
            Reference< XAnimationNode > xChildNode( xEnumeration->nextElement(), UNO_QUERY_THROW );

            createEffects( xChildNode );
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::EffectSequenceHelper::createEffectsequence()" );
    }
}

void EffectSequenceHelper::createEffects( const Reference< XAnimationNode >& xNode )
{
    DBG_ASSERT( xNode.is(), "sd::EffectSequenceHelper::createEffects(), illegal argument" );

    if( !xNode.is() )
        return;

    try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( xNode, UNO_QUERY_THROW );
        Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
        while( xEnumeration->hasMoreElements() )
        {
            Reference< XAnimationNode > xChildNode( xEnumeration->nextElement(), UNO_QUERY_THROW );

            switch( xChildNode->getType() )
            {
            // found an effect
            case AnimationNodeType::PAR:
            case AnimationNodeType::ITERATE:
                {
                    CustomAnimationEffectPtr pEffect = std::make_shared<CustomAnimationEffect>( xChildNode );

                    if( pEffect->mnNodeType != -1 )
                    {
                        pEffect->setEffectSequence( this );
                        maEffects.push_back(std::move(pEffect));
                    }
                }
                break;

            // found an after effect
            case AnimationNodeType::SET:
            case AnimationNodeType::ANIMATECOLOR:
                {
                    processAfterEffect( xChildNode );
                }
                break;
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::EffectSequenceHelper::createEffects()" );
    }
}

void EffectSequenceHelper::processAfterEffect( const Reference< XAnimationNode >& xNode )
{
    try
    {
        Reference< XAnimationNode > xMaster;

        const Sequence< NamedValue > aUserData( xNode->getUserData() );
        const NamedValue* pProp = std::find_if(aUserData.begin(), aUserData.end(),
            [](const NamedValue& rProp) { return rProp.Name == "master-element"; });

        if (pProp != aUserData.end())
            pProp->Value >>= xMaster;

        // only process if this is a valid after effect
        if( xMaster.is() )
        {
            CustomAnimationEffectPtr pMasterEffect;

            // find the master effect
            stl_CustomAnimationEffect_search_node_predict aSearchPredict( xMaster );
            EffectSequence::iterator aIter( std::find_if( maEffects.begin(), maEffects.end(), aSearchPredict ) );
            if( aIter != maEffects.end() )
                pMasterEffect = *aIter;

            if( pMasterEffect )
            {
                pMasterEffect->setHasAfterEffect( true );

                // find out what kind of after effect this is
                if( xNode->getType() == AnimationNodeType::ANIMATECOLOR )
                {
                    // it's a dim
                    Reference< XAnimate > xAnimate( xNode, UNO_QUERY_THROW );
                    pMasterEffect->setDimColor( xAnimate->getTo() );
                    pMasterEffect->setAfterEffectOnNext( true );
                }
                else
                {
                    // it's a hide
                    pMasterEffect->setAfterEffectOnNext( xNode->getParent() != xMaster->getParent() );
                }
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::EffectSequenceHelper::processAfterEffect()" );
    }
}

namespace {

class AnimationChangeListener : public cppu::WeakImplHelper< XChangesListener >
{
public:
    explicit AnimationChangeListener( MainSequence* pMainSequence ) : mpMainSequence( pMainSequence ) {}

    virtual void SAL_CALL changesOccurred( const css::util::ChangesEvent& Event ) override;
    virtual void SAL_CALL disposing( const css::lang::EventObject& Source ) override;
private:
    MainSequence* mpMainSequence;
};

}

void SAL_CALL AnimationChangeListener::changesOccurred( const css::util::ChangesEvent& )
{
    if( mpMainSequence )
        mpMainSequence->startRecreateTimer();
}

void SAL_CALL AnimationChangeListener::disposing( const css::lang::EventObject& )
{
}

MainSequence::MainSequence()
    : mxTimingRootNode(SequenceTimeContainer::create(::comphelper::getProcessComponentContext()))
    , maTimer("sd MainSequence maTimer")
    , mbTimerMode(false)
    , mbRebuilding( false )
    , mnRebuildLockGuard( 0 )
    , mbPendingRebuildRequest( false )
    , mbIgnoreChanges( 0 )
{
    if( mxTimingRootNode.is() )
    {
        Sequence< css::beans::NamedValue > aUserData
            { { u"node-type"_ustr, css::uno::Any(css::presentation::EffectNodeType::MAIN_SEQUENCE) } };
        mxTimingRootNode->setUserData( aUserData );
    }
    init();
}

MainSequence::MainSequence( const css::uno::Reference< css::animations::XAnimationNode >& xNode )
    : mxTimingRootNode( xNode, UNO_QUERY )
    , maTimer("sd MainSequence maTimer")
    , mbTimerMode( false )
    , mbRebuilding( false )
    , mnRebuildLockGuard( 0 )
    , mbPendingRebuildRequest( false )
    , mbIgnoreChanges( 0 )
{
    init();
}

MainSequence::~MainSequence()
{
    reset();
}

void MainSequence::init()
{
    mnSequenceType = EffectNodeType::MAIN_SEQUENCE;

    maTimer.SetInvokeHandler( LINK(this, MainSequence, onTimerHdl) );
    maTimer.SetTimeout(50);

    mxChangesListener.set( new AnimationChangeListener( this ) );

    createMainSequence();
}

void MainSequence::reset( const css::uno::Reference< css::animations::XAnimationNode >& xTimingRootNode )
{
    reset();

    mxTimingRootNode.set( xTimingRootNode, UNO_QUERY );

    createMainSequence();
}

Reference< css::animations::XAnimationNode > MainSequence::getRootNode()
{
    DBG_ASSERT( mnRebuildLockGuard == 0, "MainSequence::getRootNode(), rebuild is locked, is this really what you want?" );

    if( maTimer.IsActive() && mbTimerMode )
    {
        // force a rebuild NOW if one is pending
        maTimer.Stop();
        implRebuild();
    }

    return EffectSequenceHelper::getRootNode();
}

void MainSequence::createMainSequence()
{
    if( mxTimingRootNode.is() ) try
    {
        Reference< XEnumerationAccess > xEnumerationAccess( mxTimingRootNode, UNO_QUERY_THROW );
        Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
        while( xEnumeration->hasMoreElements() )
        {
            Reference< XAnimationNode > xChildNode( xEnumeration->nextElement(), UNO_QUERY_THROW );
            sal_Int32 nNodeType = CustomAnimationEffect::get_node_type( xChildNode );
            if( nNodeType == EffectNodeType::MAIN_SEQUENCE )
            {
                mxSequenceRoot.set( xChildNode, UNO_QUERY );
                EffectSequenceHelper::create( xChildNode );
            }
            else if( nNodeType == EffectNodeType::INTERACTIVE_SEQUENCE )
            {
                Reference< XTimeContainer > xInteractiveRoot( xChildNode, UNO_QUERY_THROW );
                InteractiveSequencePtr pIS = std::make_shared<InteractiveSequence>( xInteractiveRoot, this );
                pIS->addListener( this );
                maInteractiveSequenceVector.push_back(std::move(pIS));
            }
        }

        // see if we have a mainsequence at all. if not, create one...
        if( !mxSequenceRoot.is() )
        {
            mxSequenceRoot = SequenceTimeContainer::create( ::comphelper::getProcessComponentContext() );

            uno::Sequence< css::beans::NamedValue > aUserData
                { { u"node-type"_ustr, css::uno::Any(css::presentation::EffectNodeType::MAIN_SEQUENCE) } };
            mxSequenceRoot->setUserData( aUserData );

            // empty sequence until now, set duration to 0.0
            // explicitly (otherwise, this sequence will never
            // end)
            mxSequenceRoot->setDuration( Any(0.0) );

            Reference< XAnimationNode > xMainSequenceNode( mxSequenceRoot, UNO_QUERY_THROW );
            mxTimingRootNode->appendChild( xMainSequenceNode );
        }

        updateTextGroups();

        notify_listeners();

        Reference< XChangesNotifier > xNotifier( mxTimingRootNode, UNO_QUERY );
        if( xNotifier.is() )
            xNotifier->addChangesListener( mxChangesListener );
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::MainSequence::create()" );
        return;
    }

    DBG_ASSERT( mxSequenceRoot.is(), "sd::MainSequence::create(), found no main sequence!" );
}

void MainSequence::reset()
{
    EffectSequenceHelper::reset();

    for (auto const& interactiveSequence : maInteractiveSequenceVector)
        interactiveSequence->reset();
    maInteractiveSequenceVector.clear();

    try
    {
        Reference< XChangesNotifier > xNotifier( mxTimingRootNode, UNO_QUERY );
        if( xNotifier.is() )
            xNotifier->removeChangesListener( mxChangesListener );
    }
    catch( Exception& )
    {

    }
}

InteractiveSequencePtr MainSequence::createInteractiveSequence( const css::uno::Reference< css::drawing::XShape >& xShape )
{
    InteractiveSequencePtr pIS;

    // create a new interactive sequence container
    Reference< XTimeContainer > xISRoot = SequenceTimeContainer::create( ::comphelper::getProcessComponentContext() );

    uno::Sequence< css::beans::NamedValue > aUserData
        { { u"node-type"_ustr, css::uno::Any(css::presentation::EffectNodeType::INTERACTIVE_SEQUENCE) } };
    xISRoot->setUserData( aUserData );
    xISRoot->setRestart( css::animations::AnimationRestart::WHEN_NOT_ACTIVE );

    Reference< XChild > xChild( mxSequenceRoot, UNO_QUERY_THROW );
    Reference< XTimeContainer > xParent( xChild->getParent(), UNO_QUERY_THROW );
    xParent->appendChild( xISRoot );

    pIS = std::make_shared<InteractiveSequence>( xISRoot, this);
    pIS->setTriggerShape( xShape );
    pIS->addListener( this );
    maInteractiveSequenceVector.push_back( pIS );
    return pIS;
}

CustomAnimationEffectPtr MainSequence::findEffect( const css::uno::Reference< css::animations::XAnimationNode >& xNode ) const
{
    CustomAnimationEffectPtr pEffect = EffectSequenceHelper::findEffect( xNode );

    if( !pEffect )
    {
        for (auto const& interactiveSequence : maInteractiveSequenceVector)
        {
            pEffect = interactiveSequence->findEffect( xNode );
            if (pEffect)
                break;
        }
    }
    return pEffect;
}

sal_Int32 MainSequence::getOffsetFromEffect( const CustomAnimationEffectPtr& pEffect ) const
{
    sal_Int32 nOffset = EffectSequenceHelper::getOffsetFromEffect( pEffect );

    if( nOffset != -1 )
        return nOffset;

    nOffset = EffectSequenceHelper::getCount();

    for (auto const& interactiveSequence : maInteractiveSequenceVector)
    {
        sal_Int32 nTemp = interactiveSequence->getOffsetFromEffect( pEffect );
        if( nTemp != -1 )
            return nOffset + nTemp;

        nOffset += interactiveSequence->getCount();
    }

    return -1;
}

CustomAnimationEffectPtr MainSequence::getEffectFromOffset( sal_Int32 nOffset ) const
{
    if( nOffset >= 0 )
    {
        if( nOffset < getCount() )
            return EffectSequenceHelper::getEffectFromOffset( nOffset );

        nOffset -= getCount();

        auto aIter( maInteractiveSequenceVector.begin() );

        while( (aIter != maInteractiveSequenceVector.end()) && (nOffset > (*aIter)->getCount()) )
            nOffset -= (*aIter++)->getCount();

        if( (aIter != maInteractiveSequenceVector.end()) && (nOffset >= 0) )
            return (*aIter)->getEffectFromOffset( nOffset );
    }

    return CustomAnimationEffectPtr();
}

bool MainSequence::disposeShape( const Reference< XShape >& xShape )
{
    bool bChanges = EffectSequenceHelper::disposeShape( xShape );

    for (auto const& iterativeSequence : maInteractiveSequenceVector)
    {
            bChanges |= iterativeSequence->disposeShape( xShape );
    }

    if( bChanges )
        startRebuildTimer();

    return bChanges;
}

bool MainSequence::hasEffect( const css::uno::Reference< css::drawing::XShape >& xShape )
{
    if( EffectSequenceHelper::hasEffect( xShape ) )
        return true;

    for (auto const& iterativeSequence : maInteractiveSequenceVector)
    {
        if( iterativeSequence->getTriggerShape() == xShape )
            return true;

        if( iterativeSequence->hasEffect( xShape ) )
            return true;
    }

    return false;
}

void MainSequence::insertTextRange( const css::uno::Any& aTarget )
{
    EffectSequenceHelper::insertTextRange( aTarget );

    for (auto const& iterativeSequence : maInteractiveSequenceVector)
    {
        iterativeSequence->insertTextRange( aTarget );
    }
}

void MainSequence::disposeTextRange( const css::uno::Any& aTarget )
{
    EffectSequenceHelper::disposeTextRange( aTarget );

    for (auto const& iterativeSequence : maInteractiveSequenceVector)
    {
        iterativeSequence->disposeTextRange( aTarget );
    }
}

/** callback from the sd::View when an object just left text edit mode */
void MainSequence::onTextChanged( const Reference< XShape >& xShape )
{
    EffectSequenceHelper::onTextChanged( xShape );

    for (auto const& iterativeSequence : maInteractiveSequenceVector)
    {
        iterativeSequence->onTextChanged( xShape );
    }
}

void EffectSequenceHelper::onTextChanged( const Reference< XShape >& xShape )
{
    // get map [paragraph index] -> [NumberingLevel]
    // for following reusage inside all animation effects
    std::vector< sal_Int32 > paragraphNumberingLevel;
    std::vector< sal_Int32 >* paragraphNumberingLevelParam = nullptr;
    if ( getParagraphNumberingLevels( xShape, paragraphNumberingLevel ) )
        paragraphNumberingLevelParam = &paragraphNumberingLevel;

    // update internal flags for each animation effect
    const bool bChanges = std::accumulate(maEffects.begin(), maEffects.end(), false,
        [&xShape, &paragraphNumberingLevelParam](const bool bCheck, const CustomAnimationEffectPtr& rxEffect) {
            bool bRes = bCheck;
            if (rxEffect->getTargetShape() == xShape)
                bRes |= rxEffect->checkForText( paragraphNumberingLevelParam );
            return bRes;
        });

    if( bChanges )
        rebuild();
}

void MainSequence::rebuild()
{
    startRebuildTimer();
}

void MainSequence::lockRebuilds()
{
    mnRebuildLockGuard++;
}

void MainSequence::unlockRebuilds()
{
    DBG_ASSERT( mnRebuildLockGuard, "sd::MainSequence::unlockRebuilds(), no corresponding lockRebuilds() call!" );
    if( mnRebuildLockGuard )
        mnRebuildLockGuard--;

    if( (mnRebuildLockGuard == 0) && mbPendingRebuildRequest )
    {
        mbPendingRebuildRequest = false;
        startRebuildTimer();
    }
}

void MainSequence::implRebuild()
{
    if( mnRebuildLockGuard )
    {
        mbPendingRebuildRequest = true;
        return;
    }

    mbRebuilding = true;

    EffectSequenceHelper::implRebuild();

    auto aIter( maInteractiveSequenceVector.begin() );
    while( aIter != maInteractiveSequenceVector.end() )
    {
        InteractiveSequencePtr pIS( *aIter );
        if( pIS->maEffects.empty() )
        {
            // remove empty interactive sequences
            aIter = maInteractiveSequenceVector.erase( aIter );

            Reference< XChild > xChild( mxSequenceRoot, UNO_QUERY_THROW );
            Reference< XTimeContainer > xParent( xChild->getParent(), UNO_QUERY_THROW );
            Reference< XAnimationNode > xISNode( pIS->mxSequenceRoot, UNO_QUERY_THROW );
            xParent->removeChild( xISNode );
        }
        else
        {
            pIS->implRebuild();
            ++aIter;
        }
    }

    notify_listeners();
    mbRebuilding = false;
}

void MainSequence::notify_change()
{
    notify_listeners();
}

bool MainSequence::setTrigger( const CustomAnimationEffectPtr& pEffect, const css::uno::Reference< css::drawing::XShape >& xTriggerShape )
{
    EffectSequenceHelper* pOldSequence = pEffect->getEffectSequence();

    EffectSequenceHelper* pNewSequence = nullptr;
    if( xTriggerShape.is() )
    {
        for (InteractiveSequencePtr const& pIS : maInteractiveSequenceVector)
        {
            if( pIS->getTriggerShape() == xTriggerShape )
            {
                pNewSequence = pIS.get();
                break;
            }
        }

        if( !pNewSequence )
            pNewSequence = createInteractiveSequence( xTriggerShape ).get();
    }
    else
    {
        pNewSequence = this;
    }

    if( pOldSequence != pNewSequence )
    {
        if( pOldSequence )
            pOldSequence->maEffects.remove( pEffect );
        if( pNewSequence )
            pNewSequence->maEffects.push_back( pEffect );
        pEffect->setEffectSequence( pNewSequence );
        return true;
    }
    else
    {
        return false;
    }

}

IMPL_LINK_NOARG(MainSequence, onTimerHdl, Timer *, void)
{
    if( mbTimerMode )
    {
        implRebuild();
    }
    else
    {
        reset();
        createMainSequence();
    }
}

/** starts a timer that recreates the internal structure from the API core */
void MainSequence::startRecreateTimer()
{
    if( !mbRebuilding && (mbIgnoreChanges == 0) )
    {
        mbTimerMode = false;
        maTimer.Start();
    }
}

/**
 * starts a timer that rebuilds the API core from the internal structure
 * This is used to reduce the number of screen redraws due to animation changes.
*/
void MainSequence::startRebuildTimer()
{
    mbTimerMode = true;
    maTimer.Start();
}

InteractiveSequence::InteractiveSequence( const Reference< XTimeContainer >& xSequenceRoot, MainSequence* pMainSequence )
: EffectSequenceHelper( xSequenceRoot ), mpMainSequence( pMainSequence )
{
    mnSequenceType = EffectNodeType::INTERACTIVE_SEQUENCE;

    try
    {
        if( mxSequenceRoot.is() )
        {
            Reference< XEnumerationAccess > xEnumerationAccess( mxSequenceRoot, UNO_QUERY_THROW );
            Reference< XEnumeration > xEnumeration( xEnumerationAccess->createEnumeration(), UNO_SET_THROW );
            while( !mxEventSource.is() && xEnumeration->hasMoreElements() )
            {
                Reference< XAnimationNode > xChildNode( xEnumeration->nextElement(), UNO_QUERY_THROW );

                Event aEvent;
                if( (xChildNode->getBegin() >>= aEvent) && (aEvent.Trigger == EventTrigger::ON_CLICK) )
                    aEvent.Source >>= mxEventSource;
            }
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::InteractiveSequence::InteractiveSequence()" );
        return;
    }
}

void InteractiveSequence::rebuild()
{
    mpMainSequence->rebuild();
}

void InteractiveSequence::implRebuild()
{
    EffectSequenceHelper::implRebuild();
}

MainSequenceRebuildGuard::MainSequenceRebuildGuard( MainSequencePtr pMainSequence )
: mpMainSequence(std::move( pMainSequence ))
{
    if( mpMainSequence )
        mpMainSequence->lockRebuilds();
}

MainSequenceRebuildGuard::~MainSequenceRebuildGuard()
{
    if( mpMainSequence )
        mpMainSequence->unlockRebuilds();
}

}

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