// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include "qblendanimationnode_p.h"

#include <QVariant>
#include <QVector2D>
#include <QVector3D>
#include <QVector4D>
#include <QQuaternion>
#include <QColor>
#include <QRect>
#include <QRectF>

QT_BEGIN_NAMESPACE

/*!
    \qmltype BlendAnimationNode
    \inherits BlendTreeNode
    \nativetype QBlendAnimationNode
    \inqmlmodule QtQuick.Timeline.BlendTrees
    \ingroup qtqmltypes

    \brief A blend tree node that blends between two animation sources.

    BlendAnimationNode is a blend tree node that blends between two animation
    sources based on a weight value. The weight value can be animated to
    dynamically blend between the two animation sources.
*/

/*!
    \qmlproperty BlendTreeNode BlendAnimationNode::source1

    This property holds the first animation source.
*/

/*!
    \qmlproperty BlendTreeNode BlendAnimationNode::source2

    This property holds the second animation source.
*/

/*!
    \qmlproperty real BlendAnimationNode::weight

    This property holds the weight value used to blend between the two animation
    sources.
    The weight value determines how much of the first animation source is blended
    with the second animation source. A weight value of \c 0.0 means the first
    animation source is used exclusively, a weight value of \c 1.0 means the
    second animation source is used exclusively, and a weight value of \c 0.5 means
    both animation sources are blended equally.  The default value is \c 0.5.
*/

QBlendAnimationNode::QBlendAnimationNode(QObject *parent)
    : QBlendTreeNode(parent)
{
    connect(this,
            &QBlendAnimationNode::weightChanged,
            this,
            &QBlendAnimationNode::handleInputFrameDataChanged);
}

QBlendTreeNode *QBlendAnimationNode::source1() const
{
    return m_source1;
}

void QBlendAnimationNode::setSource1(QBlendTreeNode *newSource1)
{
    if (m_source1 == newSource1)
        return;

    if (m_source1) {
        disconnect(m_source1OutputConnection);
        disconnect(m_source1DestroyedConnection);
    }

    m_source1 = newSource1;

    if (m_source1) {
        m_source1OutputConnection = connect(m_source1,
                                            &QBlendTreeNode::frameDataChanged,
                                            this,
                                            &QBlendAnimationNode::handleInputFrameDataChanged);
        m_source1DestroyedConnection = connect(m_source1,
                                               &QObject::destroyed,
                                               this,
                                               [this] { setSource1(nullptr);});
    }
    Q_EMIT source1Changed();
}

QBlendTreeNode *QBlendAnimationNode::source2() const
{
    return m_source2;
}

void QBlendAnimationNode::setSource2(QBlendTreeNode *newSource2)
{
    if (m_source2 == newSource2)
        return;

    if (m_source2) {
        disconnect(m_source2OutputConnection);
        disconnect(m_source2DestroyedConnection);
    }

    m_source2 = newSource2;

    if (m_source2) {
        m_source2OutputConnection = connect(m_source2,
                                            &QBlendTreeNode::frameDataChanged,
                                            this,
                                            &QBlendAnimationNode::handleInputFrameDataChanged);
        m_source2DestroyedConnection = connect(m_source2,
                                               &QObject::destroyed,
                                               this,
                                               [this] { setSource2(nullptr);});
    }

    Q_EMIT source2Changed();
}

qreal QBlendAnimationNode::weight() const
{
    return m_weight;
}

void QBlendAnimationNode::setWeight(qreal newWeight)
{
    if (qFuzzyCompare(m_weight, newWeight))
        return;
    m_weight = newWeight;
    Q_EMIT weightChanged();
}

static QVariant lerp(const QVariant &first, const QVariant &second, float weight)
{
    // Don't bother with weights if there is no data (for now)
    if (first.isNull())
        return second;
    else if (second.isNull())
        return first;

    const QMetaType type = first.metaType();
    switch (type.id()) {
    case QMetaType::Bool:
        return QVariant((1.0f - weight) * first.toBool() + weight * second.toBool() >= 0.5f);
    case QMetaType::Int:
        return QVariant((1.0f - weight) * first.toInt() + weight * second.toInt());
    case QMetaType::Float:
        return QVariant((1.0f - weight) * first.toFloat() + weight * second.toFloat());
    case QMetaType::Double:
        return QVariant((1.0 - weight) * first.toDouble() + weight * second.toDouble());
    case QMetaType::QVector2D: {
        QVector2D firstVec = first.value<QVector2D>();
        QVector2D secondVec = second.value<QVector2D>();
        return QVariant::fromValue<QVector2D>(firstVec * (1.0f - weight) + secondVec * weight);
    }
    case QMetaType::QVector3D: {
        QVector3D firstVec = first.value<QVector3D>();
        QVector3D secondVec = second.value<QVector3D>();
        return QVariant::fromValue<QVector3D>(firstVec * (1.0f - weight) + secondVec * weight);
    }
    case QMetaType::QVector4D: {
        QVector4D firstVec = first.value<QVector4D>();
        QVector4D secondVec = second.value<QVector4D>();
        return QVariant::fromValue<QVector4D>(firstVec * (1.0f - weight) + secondVec * weight);
    }
    case QMetaType::QQuaternion: {
        QQuaternion firstQuat = first.value<QQuaternion>();
        QQuaternion secondQuat = second.value<QQuaternion>();
        return QVariant::fromValue<QQuaternion>(QQuaternion::nlerp(firstQuat, secondQuat, weight));
    }
    case QMetaType::QColor: {
        QColor firstColor = first.value<QColor>();
        QColor secondColor = second.value<QColor>();
        int r = (1.0f - weight) * firstColor.red() + weight * secondColor.red();
        int g = (1.0f - weight) * firstColor.green() + weight * secondColor.green();
        int b = (1.0f - weight) * firstColor.blue() + weight * secondColor.blue();
        int a = (1.0f - weight) * firstColor.alpha() + weight * secondColor.alpha();
        return QVariant::fromValue<QColor>(QColor(r, g, b, a));
    }
    case QMetaType::QRect: {
        QRect firstRect = first.value<QRect>();
        QRect secondRect = second.value<QRect>();
        int x = (1.0f - weight) * firstRect.x() + weight * secondRect.x();
        int y = (1.0f - weight) * firstRect.y() + weight * secondRect.y();
        int width = (1.0f - weight) * firstRect.width() + weight * secondRect.width();
        int height = (1.0f - weight) * firstRect.height() + weight * secondRect.height();
        return QVariant::fromValue<QRect>(QRect(x, y, width, height));
    }
    case QMetaType::QRectF: {
        QRectF firstRectF = first.value<QRectF>();
        QRectF secondRectF = second.value<QRectF>();
        qreal x = (1.0 - weight) * firstRectF.x() + weight * secondRectF.x();
        qreal y = (1.0 - weight) * firstRectF.y() + weight * secondRectF.y();
        qreal width = (1.0 - weight) * firstRectF.width() + weight * secondRectF.width();
        qreal height = (1.0 - weight) * firstRectF.height() + weight * secondRectF.height();
        return QVariant::fromValue<QRectF>(QRectF(x, y, width, height));
    }
    default:
        // Unsupported type, return an invalid QVariant
        return QVariant();
    }

}

void QBlendAnimationNode::handleInputFrameDataChanged()
{
    const QHash<QQmlProperty, QVariant> &frameData1 = m_source1 ? m_source1->frameData() : QHash<QQmlProperty, QVariant>();
    const QHash<QQmlProperty, QVariant> &frameData2 = m_source2 ? m_source2->frameData() : QHash<QQmlProperty, QVariant>();

    // Do the LERP blending here
    if (m_weight <= 0.0) {
        // all source1
        m_frameData = frameData1;
    } else if (m_weight >= 1.0) {
        // all source2
        m_frameData = frameData2;
    } else {
        // is a mix
        QHash<QQmlProperty, QPair<QVariant, QVariant>> allData;
        const auto &keys1 = frameData1.keys();
        for (const auto &property : keys1)
            allData.insert(property, QPair<QVariant, QVariant>(frameData1[property], QVariant()));
        const auto &keys2 = frameData2.keys();
        for (const auto &property : keys2) {
            // first check if property is already in all data, and if so modify the pair to include this value
            if (allData.contains(property)) {
                allData[property].second = frameData2[property];
            } else {
                allData.insert(property, QPair<QVariant, QVariant>(QVariant(), frameData2[property]));
            }
        }

        QHash<QQmlProperty, QVariant> newFrameData;

        const auto &keys = allData.keys();
        for (const auto &property : keys) {
            const auto &dataPair = allData[property];
            newFrameData.insert(property, lerp(dataPair.first, dataPair.second, m_weight));
        }
        m_frameData = newFrameData;
    }

    Q_EMIT frameDataChanged();
}

QT_END_NAMESPACE
