// Copyright (C) 2016 The Qt Company Ltd.
// Copyright (C) 2016 Gunnar Sletta <gunnar@sletta.org>
// 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 reason:default

#include "qquickanimatorcontroller_p.h"
#include "qquickanimatorjob_p.h"
#include "qquickanimator_p.h"
#include "qquickanimator_p_p.h"
#include <private/qquickitem_p.h>
#if QT_CONFIG(quick_shadereffect)
#include <private/qquickshadereffect_p.h>
#endif
#include <private/qanimationgroupjob_p.h>

#include <qcoreapplication.h>
#include <qdebug.h>

QT_BEGIN_NAMESPACE

struct QQuickTransformAnimatorHelperStore
{
    QHash<QQuickItem *, QQuickTransformAnimatorJob::Helper *> store;
    QMutex mutex;

    QQuickTransformAnimatorJob::Helper *acquire(QQuickItem *item) {
        mutex.lock();
        QQuickTransformAnimatorJob::Helper *helper = store.value(item);
        if (!helper) {
            helper = new QQuickTransformAnimatorJob::Helper();
            helper->item = item;
            store[item] = helper;
        } else {
            ++helper->ref;
        }
        mutex.unlock();
        return helper;
    }

    void release(QQuickTransformAnimatorJob::Helper *helper) {
        mutex.lock();
        if (--helper->ref == 0) {
            store.remove(helper->item);
            delete helper;
        }
        mutex.unlock();
    }
};
Q_GLOBAL_STATIC(QQuickTransformAnimatorHelperStore, qquick_transform_animatorjob_helper_store);

QQuickAnimatorProxyJob::QQuickAnimatorProxyJob(QAbstractAnimationJob *job,
                                               QQuickAbstractAnimation *animation)
    : m_controller(nullptr)
    , m_internalState(State_Stopped)
{
    m_job.reset(job);

    m_isRenderThreadProxy = true;

    setLoopCount(job->loopCount());

    // Instead of setting duration to job->duration() we need to set it to -1 so that
    // it runs as long as the job is running on the render thread. If we gave it
    // an explicit duration, it would be stopped, potentially stopping the RT animation
    // prematurely.
    // This means that the animation driver will tick on the GUI thread as long
    // as the animation is running on the render thread, but this overhead will
    // be negligiblie compared to animating and re-rendering the scene on the render thread.
    m_duration = -1;

    QObject *ctx = findAnimationContext(animation);
    if (!ctx) {
        qWarning("QtQuick: unable to find animation context for RT animation...");
        return;
    }

    QQuickWindow *window = qobject_cast<QQuickWindow *>(ctx);
    if (window) {
        setWindow(window);
    } else {
        QQuickItem *item = qobject_cast<QQuickItem *>(ctx);
        if (item->window())
            setWindow(item->window());
        connect(item, &QQuickItem::windowChanged, this, &QQuickAnimatorProxyJob::windowChanged);
    }
}

void QQuickAnimatorProxyJob::updateLoopCount(int loopCount)
{
    m_job->setLoopCount(loopCount);
}

QQuickAnimatorProxyJob::~QQuickAnimatorProxyJob()
{
    if (m_job && m_controller)
        m_controller->cancel(m_job);
    m_job.reset();
}

QObject *QQuickAnimatorProxyJob::findAnimationContext(QQuickAbstractAnimation *a)
{
    QObject *p = a->parent();
    while (p != nullptr && qobject_cast<QQuickWindow *>(p) == nullptr && qobject_cast<QQuickItem *>(p) == nullptr)
        p = p->parent();
    return p;
}

void QQuickAnimatorProxyJob::updateCurrentTime(int)
{
    if (m_internalState != State_Running)
        return;

    // Copy current loop number from the job
    // we could make currentLoop() virtual but it would be less efficient
    m_currentLoop = m_job->currentLoop();

    // A proxy which is being ticked should be associated with a window, (see
    // setWindow() below). If we get here when there is no more controller we
    // have a problem.
    Q_ASSERT(m_controller);

    // We do a simple check here to see if the animator has run and stopped on
    // the render thread. isPendingStart() will perform a check against jobs
    // that have been scheduled for start, but that will not yet have entered
    // the actual running state.
    // Secondly, we make an unprotected read of the job's state to figure out
    // if it is running, but this is ok, since we're only reading the state
    // and if the render thread should happen to be writing it concurrently,
    // we might get the wrong value for this update,  but then we'll simply
    // pick it up on the next iterationm when the job is stopped and render
    // thread is no longer using it.
    if (!m_controller->isPendingStart(m_job)
        && !m_job->isRunning()) {
        stop();
    }
}

void QQuickAnimatorProxyJob::updateState(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State)
{
    if (m_state == Running) {
        m_internalState = State_Starting;
        if (m_controller) {
            m_internalState = State_Running;
            m_controller->start(m_job);
        }

    } else if (newState == Stopped) {
        m_internalState = State_Stopped;
        if (m_controller) {
            syncBackCurrentValues();
            m_controller->cancel(m_job);
        }
    }
}

void QQuickAnimatorProxyJob::debugAnimation(QDebug d) const
{
    d << "QuickAnimatorProxyJob("<< Qt::hex << (const void *) this << Qt::dec
      << "state:" << state() << "duration:" << duration()
      << "proxying: (" << job() << ')';
}

void QQuickAnimatorProxyJob::windowChanged(QQuickWindow *window)
{
    setWindow(window);
}

void QQuickAnimatorProxyJob::setWindow(QQuickWindow *window)
{
    if (!window) {
        if (m_job && m_controller) {
            disconnect(m_controller->window(), &QQuickWindow::sceneGraphInitialized,
                       this, &QQuickAnimatorProxyJob::sceneGraphInitialized);
            m_controller->cancel(m_job);
        }

        m_controller = nullptr;
        stop();

    } else if (!m_controller && m_job) {
        m_controller = QQuickWindowPrivate::get(window)->animationController.get();
        if (window->isSceneGraphInitialized())
            readyToAnimate();
        else
            connect(window, &QQuickWindow::sceneGraphInitialized, this, &QQuickAnimatorProxyJob::sceneGraphInitialized);
    }
}

void QQuickAnimatorProxyJob::sceneGraphInitialized()
{
    if (m_controller) {
        disconnect(m_controller->window(), &QQuickWindow::sceneGraphInitialized, this, &QQuickAnimatorProxyJob::sceneGraphInitialized);
        readyToAnimate();
    }
}

void QQuickAnimatorProxyJob::readyToAnimate()
{
    Q_ASSERT(m_controller);
    if (m_internalState == State_Starting) {
        m_internalState = State_Running;
        m_controller->start(m_job);
    }
}

static void qquick_syncback_helper(QAbstractAnimationJob *job)
{
    if (job->isRenderThreadJob()) {
        static_cast<QQuickAnimatorJob *>(job)->writeBack();

    } else if (job->isGroup()) {
        QAnimationGroupJob *g = static_cast<QAnimationGroupJob *>(job);
        for (QAbstractAnimationJob *a : *g->children())
            qquick_syncback_helper(a);
    }

}

void QQuickAnimatorProxyJob::syncBackCurrentValues()
{
    if (m_job)
        qquick_syncback_helper(m_job.data());
}

QQuickAnimatorJob::QQuickAnimatorJob()
    : m_target(nullptr)
    , m_controller(nullptr)
    , m_from(0)
    , m_to(0)
    , m_value(0)
    , m_duration(0)
    , m_isTransform(false)
    , m_isUniform(false)
{
    m_isRenderThreadJob = true;
}

void QQuickAnimatorJob::debugAnimation(QDebug d) const
{
    d << "QuickAnimatorJob(" << Qt::hex << (const void *) this << Qt::dec
      << ") state:" << state() << "duration:" << duration()
      << "target:" << m_target << "value:" << m_value;
}

qreal QQuickAnimatorJob::progress(int time) const
{
    return m_easing.valueForProgress((m_duration == 0) ? qreal(1) : qreal(time) / qreal(m_duration));
}

void QQuickAnimatorJob::boundValue()
{
    qreal rangeMin = m_from;
    qreal rangeMax = m_to;
    if (m_from > m_to) {
        rangeMax = m_from;
        rangeMin = m_to;
    }
    m_value = qBound(rangeMin, m_value, rangeMax);
}

qreal QQuickAnimatorJob::value() const
{
    qreal value = m_to;
    if (m_controller) {
        m_controller->lock();
        value = m_value;
        m_controller->unlock();
    }
    return value;
}

void QQuickAnimatorJob::setTarget(QQuickItem *target)
{
    m_target = target;
}

void QQuickAnimatorJob::initialize(QQuickAnimatorController *controller)
{
    m_controller = controller;
}

QQuickTransformAnimatorJob::QQuickTransformAnimatorJob()
    : m_helper(nullptr)
{
    m_isTransform = true;
}

QQuickTransformAnimatorJob::~QQuickTransformAnimatorJob()
{
    if (m_helper)
        qquick_transform_animatorjob_helper_store()->release(m_helper);
}

void QQuickTransformAnimatorJob::setTarget(QQuickItem *item)
{
    // In the extremely unlikely event that the target of an animator has been
    // changed into a new item that sits in the exact same pointer address, we
    // want to force syncing it again.
    if (m_helper && m_target)
        m_helper->wasSynced = false;
    QQuickAnimatorJob::setTarget(item);
}

void QQuickTransformAnimatorJob::preSync()
{
    // If the target has changed or become null, release and reset the helper
    if (m_helper && (m_helper->item != m_target || !m_target)) {
        qquick_transform_animatorjob_helper_store()->release(m_helper);
        m_helper = nullptr;
    }

    if (!m_target) {
        invalidate();
        return;
    }

    if (!m_helper) {
        m_helper = qquick_transform_animatorjob_helper_store()->acquire(m_target);

        // This is a bit superfluous, but it ends up being simpler than the
        // alternative.  When an item happens to land on the same address as a
        // previous item, that helper might not have been fully cleaned up by
        // the time it gets taken back into use. As an alternative to storing
        // connections to each and every item's QObject::destroyed() and
        // having to clean those up afterwards, we simply sync all helpers on
        // the first run. The sync is only done once for the run of an
        // animation and it is a fairly light function (compared to storing
        // potentially thousands of connections and managing their lifetime.
        m_helper->wasSynced = false;
    }

    m_helper->sync();
}

void QQuickTransformAnimatorJob::invalidate()
{
    if (m_helper)
        m_helper->node = nullptr;
}

void QQuickTransformAnimatorJob::Helper::sync()
{
    const quint32 mask = QQuickItemPrivate::Position
            | QQuickItemPrivate::BasicTransform
            | QQuickItemPrivate::TransformOrigin
            | QQuickItemPrivate::Size;

    QQuickItemPrivate *d = QQuickItemPrivate::get(item);
#if QT_CONFIG(quick_shadereffect)
    if (d->extra.isAllocated()
            && d->extra->layer
            && d->extra->layer->enabled()) {
        d = QQuickItemPrivate::get(d->extra->layer->m_effectSource);
    }
#endif

    quint32 dirty = mask & d->dirtyAttributes;

    if (!wasSynced) {
        dirty = 0xffffffffu;
        wasSynced = true;
    }

    // We update the node before checking on dirty, as the node might have changed without the animator running
    node = d->itemNode();

    if (dirty == 0)
        return;

    if (dirty & QQuickItemPrivate::Position) {
        dx = item->x();
        dy = item->y();
    }

    if (dirty & QQuickItemPrivate::BasicTransform) {
        scale = item->scale();
        rotation = item->rotation();
    }

    if (dirty & (QQuickItemPrivate::TransformOrigin | QQuickItemPrivate::Size)) {
        QPointF o = item->transformOriginPoint();
        ox = o.x();
        oy = o.y();
    }
}

void QQuickTransformAnimatorJob::Helper::commit()
{
    if (!wasChanged || !node)
        return;

    QMatrix4x4 m;
    m.translate(dx, dy);
    m.translate(ox, oy);
    m.scale(scale);
    m.rotate(rotation, 0, 0, 1);
    m.translate(-ox, -oy);
    node->setMatrix(m);

    wasChanged = false;
}

void QQuickTransformAnimatorJob::commit()
{
    if (m_helper)
        m_helper->commit();
}

void QQuickXAnimatorJob::writeBack()
{
    if (m_target)
        m_target->setX(value());
}

void QQuickXAnimatorJob::updateCurrentTime(int time)
{
    if (!m_helper)
        return;

    m_value = m_from + (m_to - m_from) * progress(time);
    m_helper->dx = m_value;
    m_helper->wasChanged = true;
}

void QQuickYAnimatorJob::writeBack()
{
    if (m_target)
        m_target->setY(value());
}

void QQuickYAnimatorJob::updateCurrentTime(int time)
{
    if (!m_helper)
        return;

    m_value = m_from + (m_to - m_from) * progress(time);
    m_helper->dy = m_value;
    m_helper->wasChanged = true;
}

void QQuickScaleAnimatorJob::writeBack()
{
    if (m_target)
        m_target->setScale(value());
}

void QQuickScaleAnimatorJob::updateCurrentTime(int time)
{
    if (!m_helper)
        return;

    m_value = m_from + (m_to - m_from) * progress(time);
    m_helper->scale = m_value;
    m_helper->wasChanged = true;
}


QQuickRotationAnimatorJob::QQuickRotationAnimatorJob()
    : m_direction(QQuickRotationAnimator::Numerical)
{
}

extern QVariant _q_interpolateShortestRotation(qreal &f, qreal &t, qreal progress);
extern QVariant _q_interpolateClockwiseRotation(qreal &f, qreal &t, qreal progress);
extern QVariant _q_interpolateCounterclockwiseRotation(qreal &f, qreal &t, qreal progress);

void QQuickRotationAnimatorJob::updateCurrentTime(int time)
{
    if (!m_helper)
        return;

    float t = progress(time);

    switch (m_direction) {
    case QQuickRotationAnimator::Clockwise:
        m_value = _q_interpolateClockwiseRotation(m_from, m_to, t).toFloat();
        // The logic in _q_interpolateClockwise comes out a bit wrong
        // for the case of X->0 where 0<X<360. It ends on 360 which it
        // shouldn't.
        if (t == 1)
            m_value = m_to;
        break;
    case QQuickRotationAnimator::Counterclockwise:
        m_value = _q_interpolateCounterclockwiseRotation(m_from, m_to, t).toFloat();
        break;
    case QQuickRotationAnimator::Shortest:
        m_value = _q_interpolateShortestRotation(m_from, m_to, t).toFloat();
        break;
    case QQuickRotationAnimator::Numerical:
        m_value = m_from + (m_to - m_from) * t;
        break;
    }
    m_helper->rotation = m_value;
    m_helper->wasChanged = true;
}

void QQuickRotationAnimatorJob::writeBack()
{
    if (m_target)
        m_target->setRotation(value());
}


QQuickOpacityAnimatorJob::QQuickOpacityAnimatorJob()
    : m_opacityNode(nullptr)
{
}

void QQuickOpacityAnimatorJob::postSync()
{
    if (!m_target) {
        invalidate();
        return;
    }

    QQuickItemPrivate *d = QQuickItemPrivate::get(m_target);
#if QT_CONFIG(quick_shadereffect)
    if (d->extra.isAllocated()
            && d->extra->layer
            && d->extra->layer->enabled()) {
        d = QQuickItemPrivate::get(d->extra->layer->m_effectSource);
    }
#endif

    m_opacityNode = d->opacityNode();

    if (!m_opacityNode) {
        m_opacityNode = new QSGOpacityNode();

        /* The item node subtree is like this
         *
         * itemNode
         * (opacityNode)            optional
         * (clipNode)               optional
         * (rootNode)               optional
         * children / paintNode
         *
         * If the opacity node doesn't exist, we need to insert it into
         * the hierarchy between itemNode and clipNode or rootNode. If
         * neither clip or root exists, we need to reparent all children
         * from itemNode to opacityNode.
         */
        QSGNode *iNode = d->itemNode();
        QSGNode *child = d->childContainerNode();
        if (child != iNode) {
            if (child->parent())
                child->parent()->removeChildNode(child);
            m_opacityNode->appendChildNode(child);
            iNode->appendChildNode(m_opacityNode);
        } else {
            iNode->reparentChildNodesTo(m_opacityNode);
            iNode->appendChildNode(m_opacityNode);
        }

        d->extra.value().opacityNode = m_opacityNode;
        updateCurrentTime(0);
    }
    Q_ASSERT(m_opacityNode);
}

void QQuickOpacityAnimatorJob::invalidate()
{
    m_opacityNode = nullptr;
}

void QQuickOpacityAnimatorJob::writeBack()
{
    if (m_target)
        m_target->setOpacity(value());
}

void QQuickOpacityAnimatorJob::updateCurrentTime(int time)
{
    if (!m_opacityNode)
        return;

    m_value = m_from + (m_to - m_from) * progress(time);
    m_opacityNode->setOpacity(m_value);
}

#if QT_CONFIG(quick_shadereffect)
QQuickUniformAnimatorJob::QQuickUniformAnimatorJob()
{
    m_isUniform = true;
}

void QQuickUniformAnimatorJob::setTarget(QQuickItem *target)
{
    // Check target is of expected type
    if (qobject_cast<QQuickShaderEffect *>(target) != nullptr)
        m_target = target;
}

void QQuickUniformAnimatorJob::updateCurrentTime(int time)
{
    if (!m_effect || m_target != m_effect)
        return;

    m_value = m_from + (m_to - m_from) * progress(time);
    m_effect->updateUniformValue(m_uniform, m_value);
}

void QQuickUniformAnimatorJob::writeBack()
{
    if (m_target)
        m_target->setProperty(m_uniform, value());
}

void QQuickUniformAnimatorJob::postSync()
{
    if (!m_target) {
        invalidate();
        return;
    }

    m_effect = qobject_cast<QQuickShaderEffect *>(m_target);
}

void QQuickUniformAnimatorJob::invalidate()
{

}
#endif

QT_END_NAMESPACE

#include "moc_qquickanimatorjob_p.cpp"
