// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant

#include <QtCore/qthreadstorage.h>

#include "private/qabstractanimationjob_p.h"
#include "private/qanimationgroupjob_p.h"
#include "private/qanimationjobutil_p.h"
#include "private/qqmlengine_p.h"
#include "private/qqmlglobal_p.h"
#include "private/qdoubleendedlist_p.h"

QT_BEGIN_NAMESPACE

#ifndef QT_NO_THREAD
Q_GLOBAL_STATIC(QThreadStorage<QQmlAnimationTimer *>, animationTimer)
#endif

DEFINE_BOOL_CONFIG_OPTION(animationTickDump, QML_ANIMATION_TICK_DUMP);

QAnimationJobChangeListener::~QAnimationJobChangeListener()
{
}

QQmlAnimationTimer::QQmlAnimationTimer() :
    QAbstractAnimationTimer(), lastTick(0),
    currentAnimationIdx(0), insideTick(false),
    startAnimationPending(false), stopTimerPending(false),
    runningLeafAnimations(0)
{
}

void QQmlAnimationTimer::unsetJobTimer(QAbstractAnimationJob *animation)
{
    if (!animation)
        return;
    if (animation->m_timer == this)
        animation->m_timer = nullptr;

    if (animation->m_isPause)
        runningPauseAnimations.removeOne(animation);

    if (animation->isGroup()) {
        QAnimationGroupJob *group = static_cast<QAnimationGroupJob *>(animation);
        if (const auto children = group->children()) {
            for (auto *child : *children)
                unsetJobTimer(child);
        }
    }
}

QQmlAnimationTimer::~QQmlAnimationTimer()
{
    for (const auto &animation : std::as_const(animations))
        unsetJobTimer(animation);
    for (const auto &animation : std::as_const(animationsToStart))
        unsetJobTimer(animation);
    for (const auto &animation : std::as_const(runningPauseAnimations))
        unsetJobTimer(animation);

    QUnifiedTimer::stopAnimationTimer(this);
}

QQmlAnimationTimer *QQmlAnimationTimer::instance(bool create)
{
    QQmlAnimationTimer *inst;
    if (create && !animationTimer()->hasLocalData()) {
        inst = new QQmlAnimationTimer;
        animationTimer()->setLocalData(inst);
    } else {
        inst = animationTimer() ? animationTimer()->localData() : 0;
    }
    return inst;
}

QQmlAnimationTimer *QQmlAnimationTimer::instance()
{
    return instance(true);
}

void QQmlAnimationTimer::ensureTimerUpdate()
{
    QUnifiedTimer *instU = QUnifiedTimer::instance(false);
    if (instU && isPaused)
        instU->updateAnimationTimers();
}

void QQmlAnimationTimer::updateAnimationsTime(qint64 delta)
{
    //setCurrentTime can get this called again while we're the for loop. At least with pauseAnimations
    if (insideTick)
        return;

    lastTick += delta;

    //we make sure we only call update time if the time has actually changed
    //it might happen in some cases that the time doesn't change because events are delayed
    //when the CPU load is high
    if (delta) {
        insideTick = true;
        for (currentAnimationIdx = 0; currentAnimationIdx < animations.size(); ++currentAnimationIdx) {
            QAbstractAnimationJob *animation = animations.at(currentAnimationIdx);
            int elapsed = animation->m_totalCurrentTime
                          + (animation->direction() == QAbstractAnimationJob::Forward ? delta : -delta);
            animation->setCurrentTime(elapsed);
        }
        if (animationTickDump()) {
            qDebug() << "***** Dumping Animation Tree ***** ( tick:" << lastTick << "delta:" << delta << ")";
            for (int i = 0; i < animations.size(); ++i)
                qDebug() << animations.at(i);
        }
        insideTick = false;
        currentAnimationIdx = 0;
    }
}

void QQmlAnimationTimer::updateAnimationTimer()
{
    restartAnimationTimer();
}

void QQmlAnimationTimer::restartAnimationTimer()
{
    if (runningLeafAnimations == 0 && !runningPauseAnimations.isEmpty())
        QUnifiedTimer::pauseAnimationTimer(this, closestPauseAnimationTimeToFinish());
    else if (isPaused)
        QUnifiedTimer::resumeAnimationTimer(this);
    else if (!isRegistered)
        QUnifiedTimer::startAnimationTimer(this);
}

void QQmlAnimationTimer::startAnimations()
{
    if (!startAnimationPending)
        return;
    startAnimationPending = false;
    //force timer to update, which prevents large deltas for our newly added animations
    QUnifiedTimer::instance()->maybeUpdateAnimationsToCurrentTime();

    //we transfer the waiting animations into the "really running" state
    animations += animationsToStart;
    animationsToStart.clear();
    if (!animations.isEmpty())
        restartAnimationTimer();
}

void QQmlAnimationTimer::stopTimer()
{
    stopTimerPending = false;
    bool pendingStart = startAnimationPending && animationsToStart.size() > 0;
    if (animations.isEmpty() && !pendingStart) {
        QUnifiedTimer::resumeAnimationTimer(this);
        QUnifiedTimer::stopAnimationTimer(this);
        // invalidate the start reference time
        lastTick = 0;
    }
}

void QQmlAnimationTimer::registerAnimation(QAbstractAnimationJob *animation, bool isTopLevel)
{
    if (animation->userControlDisabled())
        return;

    registerRunningAnimation(animation);
    if (isTopLevel) {
        Q_ASSERT(!animation->m_hasRegisteredTimer);
        animation->m_hasRegisteredTimer = true;
        animationsToStart << animation;
        if (!startAnimationPending) {
            startAnimationPending = true;
            QMetaObject::invokeMethod(this, "startAnimations", Qt::QueuedConnection);
        }
    }
}

void QQmlAnimationTimer::unregisterAnimation(QAbstractAnimationJob *animation)
{
    unregisterRunningAnimation(animation);

    if (!animation->m_hasRegisteredTimer)
        return;

    int idx = animations.indexOf(animation);
    if (idx != -1) {
        animations.removeAt(idx);
        // this is needed if we unregister an animation while its running
        if (idx <= currentAnimationIdx)
            --currentAnimationIdx;

        if (animations.isEmpty() && !stopTimerPending) {
            stopTimerPending = true;
            QMetaObject::invokeMethod(this, "stopTimer", Qt::QueuedConnection);
        }
    } else {
        animationsToStart.removeOne(animation);
    }
    animation->m_hasRegisteredTimer = false;
}

void QQmlAnimationTimer::registerRunningAnimation(QAbstractAnimationJob *animation)
{
    Q_ASSERT(!animation->userControlDisabled());

    if (animation->m_isGroup)
        return;

    if (animation->m_isPause) {
        runningPauseAnimations << animation;
    } else
        runningLeafAnimations++;
}

void QQmlAnimationTimer::unregisterRunningAnimation(QAbstractAnimationJob *animation)
{
    unsetJobTimer(animation);
    if (animation->userControlDisabled())
        return;

    if (animation->m_isGroup)
        return;

    if (!animation->m_isPause)
        runningLeafAnimations--;

    Q_ASSERT(runningLeafAnimations >= 0);
}

int QQmlAnimationTimer::closestPauseAnimationTimeToFinish()
{
    int closestTimeToFinish = INT_MAX;
    for (int i = 0; i < runningPauseAnimations.size(); ++i) {
        QAbstractAnimationJob *animation = runningPauseAnimations.at(i);
        int timeToFinish;

        if (animation->direction() == QAbstractAnimationJob::Forward)
            timeToFinish = animation->duration() - animation->currentLoopTime();
        else
            timeToFinish = animation->currentLoopTime();

        if (timeToFinish < closestTimeToFinish)
            closestTimeToFinish = timeToFinish;
    }
    return closestTimeToFinish;
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////

QAbstractAnimationJob::QAbstractAnimationJob()
    : m_loopCount(1)
    , m_group(nullptr)
    , m_direction(QAbstractAnimationJob::Forward)
    , m_state(QAbstractAnimationJob::Stopped)
    , m_totalCurrentTime(0)
    , m_currentTime(0)
    , m_currentLoop(0)
    , m_uncontrolledFinishTime(-1)
    , m_currentLoopStartTime(0)
    , m_hasRegisteredTimer(false)
    , m_isPause(false)
    , m_isGroup(false)
    , m_disableUserControl(false)
    , m_hasCurrentTimeChangeListeners(false)
    , m_isRenderThreadJob(false)
    , m_isRenderThreadProxy(false)

{
}

QAbstractAnimationJob::~QAbstractAnimationJob()
{
    //we can't call stop here. Otherwise we get pure virtual calls
    if (m_state != Stopped) {
        State oldState = m_state;
        m_state = Stopped;
        stateChanged(oldState, m_state);

        Q_ASSERT(m_state == Stopped);
        if (oldState == Running) {
            if (m_timer) {
                Q_ASSERT(QQmlAnimationTimer::instance(false) == m_timer);
                m_timer->unregisterAnimation(this);
            }
        }
        Q_ASSERT(!m_hasRegisteredTimer);
    }

    if (m_group)
        m_group->removeAnimation(this);
}

void QAbstractAnimationJob::fireTopLevelAnimationLoopChanged()
{
    m_uncontrolledFinishTime = -1;
    if (m_group)
        m_currentLoopStartTime = 0;
    topLevelAnimationLoopChanged();
}

void QAbstractAnimationJob::setState(QAbstractAnimationJob::State newState)
{
    if (m_state == newState)
        return;

    if (m_loopCount == 0)
        return;

    if (!m_timer) // don't create a timer just to stop the animation
        m_timer = QQmlAnimationTimer::instance(newState != Stopped);
    Q_ASSERT(m_timer || newState == Stopped);

    State oldState = m_state;
    int oldCurrentTime = m_currentTime;
    int oldCurrentLoop = m_currentLoop;
    Direction oldDirection = m_direction;

    // check if we should Rewind
    if ((newState == Paused || newState == Running) && oldState == Stopped) {
        //here we reset the time if needed
        //we don't call setCurrentTime because this might change the way the animation
        //behaves: changing the state or changing the current value
        m_totalCurrentTime = m_currentTime = (m_direction == Forward) ?
            0 : (m_loopCount == -1 ? duration() : totalDuration());

        // Reset uncontrolled finish time and currentLoopStartTime for this run.
        m_uncontrolledFinishTime = -1;
        if (!m_group)
            m_currentLoopStartTime = m_totalCurrentTime;
    }

    m_state = newState;
    //(un)registration of the animation must always happen before calls to
    //virtual function (updateState) to ensure a correct state of the timer
    bool isTopLevel = !m_group || m_group->isStopped();
    if (oldState == Running) {
        if (newState == Paused && m_hasRegisteredTimer)
            m_timer->ensureTimerUpdate();
        // the animation is not running any more
        if (m_timer)
            m_timer->unregisterAnimation(this);
    } else if (newState == Running) {
        m_timer->registerAnimation(this, isTopLevel);
    }

    //starting an animation qualifies as a top level loop change
    if (newState == Running && oldState == Stopped && !m_group)
        fireTopLevelAnimationLoopChanged();

    RETURN_IF_DELETED(updateState(newState, oldState));

    if (newState != m_state) //this is to be safe if updateState changes the state
        return;

    // Notify state change
    RETURN_IF_DELETED(stateChanged(newState, oldState));
    if (newState != m_state) //this is to be safe if updateState changes the state
        return;

    switch (m_state) {
    case Paused:
        break;
    case Running:
        {
            // this ensures that the value is updated now that the animation is running
            if (oldState == Stopped) {
                m_currentLoop = 0;
                if (isTopLevel) {
                    // currentTime needs to be updated if pauseTimer is active
                    RETURN_IF_DELETED(m_timer->ensureTimerUpdate());
                    RETURN_IF_DELETED(setCurrentTime(m_totalCurrentTime));
                }
            }
        }
        break;
    case Stopped:
        // Leave running state.
        int dura = duration();

        if (dura == -1 || m_loopCount < 0
            || (oldDirection == Forward && (oldCurrentTime * (oldCurrentLoop + 1)) == (dura * m_loopCount))
            || (oldDirection == Backward && oldCurrentTime == 0)) {
               finished();
        }
        break;
    }
}

void QAbstractAnimationJob::setDirection(Direction direction)
{
    if (m_direction == direction)
        return;

    if (m_state == Stopped) {
        if (m_direction == Backward) {
            m_currentTime = duration();
            m_currentLoop = m_loopCount - 1;
        } else {
            m_currentTime = 0;
            m_currentLoop = 0;
        }
    }

    // the commands order below is important: first we need to setCurrentTime with the old direction,
    // then update the direction on this and all children and finally restart the pauseTimer if needed
    if (m_hasRegisteredTimer)
        m_timer->ensureTimerUpdate();

    m_direction = direction;
    updateDirection(direction);

    if (m_hasRegisteredTimer)
        // needed to update the timer interval in case of a pause animation
        m_timer->updateAnimationTimer();
}

void QAbstractAnimationJob::setLoopCount(int loopCount)
{
    if (m_loopCount == loopCount)
        return;
    m_loopCount = loopCount;
    updateLoopCount(loopCount);
}

int QAbstractAnimationJob::totalDuration() const
{
    int dura = duration();
    if (dura <= 0)
        return dura;
    int loopcount = loopCount();
    if (loopcount < 0)
        return -1;
    return dura * loopcount;
}

void QAbstractAnimationJob::setCurrentTime(int msecs)
{
    msecs = qMax(msecs, 0);
    // Calculate new time and loop.
    int dura = duration();
    int totalDura;
    int oldLoop = m_currentLoop;

    if (dura < 0 && m_direction == Forward)  {
        totalDura = -1;
        if (m_uncontrolledFinishTime >= 0 && msecs >= m_uncontrolledFinishTime) {
            msecs = m_uncontrolledFinishTime;
            if (m_currentLoop == m_loopCount - 1) {
                totalDura = m_uncontrolledFinishTime;
            } else {
                ++m_currentLoop;
                m_currentLoopStartTime = msecs;
                m_uncontrolledFinishTime = -1;
            }
        }
        m_totalCurrentTime = msecs;
        m_currentTime = msecs - m_currentLoopStartTime;
    } else {
        totalDura = dura <= 0 ? dura : ((m_loopCount < 0) ? -1 : dura * m_loopCount);
        if (totalDura != -1)
            msecs = qMin(totalDura, msecs);
        m_totalCurrentTime = msecs;

        // Update new values.
        m_currentLoop = ((dura <= 0) ? 0 : (msecs / dura));
        if (m_currentLoop == m_loopCount) {
            //we're at the end
            m_currentTime = qMax(0, dura);
            m_currentLoop = qMax(0, m_loopCount - 1);
        } else {
            if (m_direction == Forward) {
                m_currentTime = (dura <= 0) ? msecs : (msecs % dura);
            } else {
                m_currentTime = (dura <= 0) ? msecs : ((msecs - 1) % dura) + 1;
                if (m_currentTime == dura)
                    --m_currentLoop;
            }
        }
    }


    if (m_currentLoop != oldLoop && !m_group)   //### verify Running as well?
        fireTopLevelAnimationLoopChanged();

    RETURN_IF_DELETED(updateCurrentTime(m_currentTime));

    if (m_currentLoop != oldLoop) {
        // CurrentLoop listeners may restart the job if e.g. from has changed. Stopping a job will
        // destroy it, so account for that here.
        RETURN_IF_DELETED(currentLoopChanged());
    }

    // All animations are responsible for stopping the animation when their
    // own end state is reached; in this case the animation is time driven,
    // and has reached the end.
    if ((m_direction == Forward && m_totalCurrentTime == totalDura)
        || (m_direction == Backward && m_totalCurrentTime == 0)) {
        RETURN_IF_DELETED(stop());
    }

    if (m_hasCurrentTimeChangeListeners)
        currentTimeChanged(m_currentTime);
}

void QAbstractAnimationJob::start()
{
    if (m_state == Running)
        return;

    if (QQmlEnginePrivate::designerMode()) {
        if (state() != Stopped) {
            m_currentTime = duration();
            m_totalCurrentTime = totalDuration();
            setState(Running);
            setState(Stopped);
        }
    } else {
        setState(Running);
    }
}

void QAbstractAnimationJob::stop()
{
    if (m_state == Stopped)
        return;
    setState(Stopped);
}

void QAbstractAnimationJob::complete()
{
    // Simulate the full animation cycle
    setState(Running);
    setCurrentTime(m_direction == Forward ? duration() : 0);
    setState(Stopped);
}

void QAbstractAnimationJob::pause()
{
    if (m_state == Stopped) {
        qWarning("QAbstractAnimationJob::pause: Cannot pause a stopped animation");
        return;
    }

    setState(Paused);
}

void QAbstractAnimationJob::resume()
{
    if (m_state != Paused) {
        qWarning("QAbstractAnimationJob::resume: "
                 "Cannot resume an animation that is not paused");
        return;
    }
    setState(Running);
}

void QAbstractAnimationJob::setEnableUserControl()
{
    m_disableUserControl = false;
}

bool QAbstractAnimationJob::userControlDisabled() const
{
    return m_disableUserControl;
}

void QAbstractAnimationJob::setDisableUserControl()
{
    m_disableUserControl = true;
    start();
    pause();
}

void QAbstractAnimationJob::updateState(QAbstractAnimationJob::State newState,
                                     QAbstractAnimationJob::State oldState)
{
    Q_UNUSED(oldState);
    Q_UNUSED(newState);
}

void QAbstractAnimationJob::updateDirection(QAbstractAnimationJob::Direction direction)
{
    Q_UNUSED(direction);
}

void QAbstractAnimationJob::finished()
{
    //TODO: update this code so it is valid to delete the animation in animationFinished
    for (const auto &change : changeListeners) {
        if (change.types & QAbstractAnimationJob::Completion) {
            RETURN_IF_DELETED(change.listener->animationFinished(this));
        }
    }

    if (m_group && (duration() == -1 || loopCount() < 0)) {
        //this is an uncontrolled animation, need to notify the group animation we are finished
        m_group->uncontrolledAnimationFinished(this);
    }
}

void QAbstractAnimationJob::stateChanged(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State oldState)
{
    for (const auto &change : changeListeners) {
        if (change.types & QAbstractAnimationJob::StateChange) {
            RETURN_IF_DELETED(change.listener->animationStateChanged(this, newState, oldState));
        }
    }
}

void QAbstractAnimationJob::currentLoopChanged()
{
    for (const auto &change : changeListeners) {
        if (change.types & QAbstractAnimationJob::CurrentLoop) {
           RETURN_IF_DELETED(change.listener->animationCurrentLoopChanged(this));
        }
    }
}

void QAbstractAnimationJob::currentTimeChanged(int currentTime)
{
    Q_ASSERT(m_hasCurrentTimeChangeListeners);

    for (const auto &change : changeListeners) {
        if (change.types & QAbstractAnimationJob::CurrentTime) {
           RETURN_IF_DELETED(change.listener->animationCurrentTimeChanged(this, currentTime));
        }
    }
}

void QAbstractAnimationJob::addAnimationChangeListener(QAnimationJobChangeListener *listener, QAbstractAnimationJob::ChangeTypes changes)
{
    if (changes & QAbstractAnimationJob::CurrentTime)
        m_hasCurrentTimeChangeListeners = true;

    changeListeners.push_back(ChangeListener(listener, changes));
}

void QAbstractAnimationJob::removeAnimationChangeListener(QAnimationJobChangeListener *listener, QAbstractAnimationJob::ChangeTypes changes)
{
    m_hasCurrentTimeChangeListeners = false;

    const auto it = std::find(changeListeners.begin(), changeListeners.end(), ChangeListener(listener, changes));
    if (it != changeListeners.end())
        changeListeners.erase(it);

    for (const auto &change: changeListeners) {
        if (change.types & QAbstractAnimationJob::CurrentTime) {
            m_hasCurrentTimeChangeListeners = true;
            break;
        }
    }
}

void QAbstractAnimationJob::debugAnimation(QDebug d) const
{
    d << "AbstractAnimationJob(" << Qt::hex << (const void *) this << Qt::dec << ") state:"
      << m_state << "duration:" << duration();
}

QDebug operator<<(QDebug d, const QAbstractAnimationJob *job)
{
    if (!job) {
        d << "AbstractAnimationJob(null)";
        return d;
    }
    job->debugAnimation(d);
    return d;
}

QT_END_NAMESPACE

//#include "moc_qabstractanimation2_p.cpp"
#include "moc_qabstractanimationjob_p.cpp"
