/****************************************************************************
**
** Copyright (C) 2020 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Quick 3D.
**
** $QT_BEGIN_LICENSE:GPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 or (at your option) any later version
** approved by the KDE Free Qt Foundation. The licenses are as published by
** the Free Software Foundation and appearing in the file LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qquick3deffect_p.h"

#include <QtQuick3DRuntimeRender/private/qssgrendercontextcore_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrendereffect_p.h>
#include <QtQuick/qquickwindow.h>
#include <QtQuick3D/private/qquick3dobject_p.h>
#include <QtCore/qfile.h>
#include <QtCore/qurl.h>


QT_BEGIN_NAMESPACE

/*!
    \qmltype Effect
    \inherits Object3D
    \inqmlmodule QtQuick3D.Effects
    \instantiates QQuick3DEffect
    \brief Base component for creating a post-processing effect.

    The Effect type allows the user to implement their own post-processing effects for QtQuick3D.
    This is how to create your own effect, using \l GaussianBlur as an example:

    \qml
    Effect {
        // The property name is generated as a uniform to the shader code, so it must match
        // the name and type used in shader code.
        property real amount: 2 // 0 - 10

        // The vertex shaders are defined with the Shader type.
        Shader {
            id: vertical
            stage: Shader.Vertex
            shader: "shaders/blurvertical.vert"
        }
        Shader {
            id: horizontal
            stage: Shader.Vertex
            shader: "shaders/blurhorizontal.vert"
        }

        // The fragment shader is defined with the Shader type.
        Shader {
            id: gaussianblur
            stage: Shader.Fragment
            shader: "shaders/gaussianblur.frag"
        }

        // In this shader we need a temporary buffer to store the output of the first blur pass.
        Buffer {
            id: tempBuffer
            name: "tempBuffer"
            format: Buffer.RGBA8
            textureFilterOperation: Buffer.Linear
            textureCoordOperation: Buffer.ClampToEdge
            bufferFlags: Buffer.None // Lifetime of the buffer is one frame
        }

        // GaussianBlur needs two passes; a horizontal blur and a vertical blur.
        // Only the vertex shader is different in this case, so we can use the same fragment
        // shader for both passes.
        passes: [
            Pass {
                shaders: [ horizontal, gaussianblur ]
                output: tempBuffer
            },
            Pass {
                shaders: [ vertical, gaussianblur ]
                commands: [
                    // We feed the output of the first pass as an input for the second pass.
                    BufferInput {
                        buffer: tempBuffer
                    }
                ]
            }
        ]
    }
    \endqml

    \sa Shader, Buffer, Pass
*/

/*!
    \qmlproperty list Effect::passes
    Contains a list of render \l {Pass}{passes} implemented by the effect.
*/

template <QVariant::Type>
struct ShaderType
{
};

template<>
struct ShaderType<QVariant::Double>
{
    static constexpr QSSGRenderShaderDataType type() { return QSSGRenderShaderDataType::Float; }
    static QByteArray name() { return QByteArrayLiteral("float"); }
};

template<>
struct ShaderType<QVariant::Bool>
{
    static constexpr QSSGRenderShaderDataType type() { return QSSGRenderShaderDataType::Boolean; }
    static QByteArray name() { return QByteArrayLiteral("bool"); }
};

template<>
struct ShaderType<QVariant::Int>
{
    static constexpr QSSGRenderShaderDataType type() { return QSSGRenderShaderDataType::Integer; }
    static QByteArray name() { return QByteArrayLiteral("int"); }
};

template<>
struct ShaderType<QVariant::Vector2D>
{
    static constexpr QSSGRenderShaderDataType type() { return QSSGRenderShaderDataType::Vec2; }
    static QByteArray name() { return QByteArrayLiteral("vec2"); }
};

template<>
struct ShaderType<QVariant::Vector3D>
{
    static constexpr QSSGRenderShaderDataType type() { return QSSGRenderShaderDataType::Vec3; }
    static QByteArray name() { return QByteArrayLiteral("vec3"); }
};

template<>
struct ShaderType<QVariant::Vector4D>
{
    static constexpr QSSGRenderShaderDataType type() { return QSSGRenderShaderDataType::Vec4; }
    static QByteArray name() { return QByteArrayLiteral("vec4"); }
};

template<>
struct ShaderType<QVariant::Color>
{
    static constexpr QSSGRenderShaderDataType type() { return QSSGRenderShaderDataType::Rgba; }
    static QByteArray name() { return QByteArrayLiteral("vec4"); }
};

static QByteArray uniformTypeName(QVariant::Type type)
{
    if (type == QVariant::Double) {
        return ShaderType<QVariant::Double>::name();
    } else if (type == QVariant::Bool) {
        return ShaderType<QVariant::Bool>::name();
    } else if (type == QVariant::Vector2D) {
        return ShaderType<QVariant::Vector2D>::name();
    } else if (type == QVariant::Vector3D) {
        return ShaderType<QVariant::Vector3D>::name();
    } else if (type == QVariant::Vector4D) {
        return ShaderType<QVariant::Vector4D>::name();
    } else if (type == QVariant::Int) {
        return ShaderType<QVariant::Int>::name();
    } else if (type == QVariant::Color) {
        return ShaderType<QVariant::Color>::name();
    }

    return QByteArray();
}

static QSSGRenderShaderDataType uniformType(QVariant::Type type)
{
    if (type == QVariant::Double) {
        return ShaderType<QVariant::Double>::type();
    } else if (type == QVariant::Bool) {
        return ShaderType<QVariant::Bool>::type();
    } else if (type == QVariant::Vector2D) {
        return ShaderType<QVariant::Vector2D>::type();
    } else if (type == QVariant::Vector3D) {
        return ShaderType<QVariant::Vector3D>::type();
    } else if (type == QVariant::Vector4D) {
        return ShaderType<QVariant::Vector4D>::type();
    } else if (type == QVariant::Int) {
        return ShaderType<QVariant::Int>::type();
    } else if (type == QVariant::Color) {
        return ShaderType<QVariant::Color>::type();
    }

    return QSSGRenderShaderDataType::Unknown;
}

QQuick3DEffect::QQuick3DEffect(QQuick3DObject *parent)
    : QQuick3DObject(*(new QQuick3DObjectPrivate(QQuick3DObjectPrivate::Type::Effect)), parent)
{
}

QQmlListProperty<QQuick3DShaderUtilsRenderPass> QQuick3DEffect::passes()
{
    return QQmlListProperty<QQuick3DShaderUtilsRenderPass>(this,
                                                      nullptr,
                                                      QQuick3DEffect::qmlAppendPass,
                                                      QQuick3DEffect::qmlPassCount,
                                                      QQuick3DEffect::qmlPassAt,
                                                      QQuick3DEffect::qmlPassClear);
}

QSSGRenderGraphObject *QQuick3DEffect::updateSpatialNode(QSSGRenderGraphObject *node)
{
    // Find the parent window
    QObject *p = this;
    QQuickWindow *window = nullptr;
    while (p != nullptr && window == nullptr) {
        p = p->parent();
        if ((window = qobject_cast<QQuickWindow *>(p)))
            break;
    }

    static const auto addUniform = [](QVariant::Type propType, const char *propName, QByteArray &uniforms) {
        uniforms += QByteArray("uniform ") + uniformTypeName(propType) + " " + propName + ";\n";
    };

    const auto &renderContext
            = QSSGRenderContextInterface::getRenderContextInterface(quintptr(window));

    QSSGRenderEffect *effectNode = static_cast<QSSGRenderEffect *>(node);
    if (!effectNode) {
        QByteArray shared = QByteArrayLiteral("#include \"effect.glsllib\"\n");

        markAllDirty();
        effectNode = new QSSGRenderEffect;
        effectNode->setActive(true, *renderContext->effectSystem());

        QMetaMethod propertyDirtyMethod;
        const int idx = metaObject()->indexOfSlot("onPropertyDirty()");
        if (idx != -1)
            propertyDirtyMethod = metaObject()->method(idx);

        // Properties -> uniforms
        QByteArray uniforms;
        const int propCount = metaObject()->propertyCount();
        int propOffset = metaObject()->propertyOffset();

        // Effect can have multilayered inheritance structure, so find the actual propOffset
        const QMetaObject *superClass = metaObject()->superClass();
        while (superClass && qstrcmp(superClass->className(), "QQuick3DEffect") != 0)  {
            propOffset = superClass->propertyOffset();
            superClass = superClass->superClass();
        }

        QVector<QMetaProperty> textureProperties; // We'll deal with these later
        for (int i = propOffset; i != propCount; ++i) {
            const auto property = metaObject()->property(i);
            if (Q_UNLIKELY(!property.isValid()))
                continue;

            QVariant::Type propType = property.type();
            QVariant propValue = property.read(this);
            if (static_cast<QMetaType::Type>(propType) == QMetaType::QVariant)
                propType = propValue.type();

            if (propType == QVariant::UserType) {
                if (property.userType() == qMetaTypeId<QQuick3DShaderUtilsTextureInput *>())
                    textureProperties.push_back(property);
            } else if (static_cast<QMetaType::Type>(propType) == QMetaType::QObjectStar) {
                QObject *obj = qobject_cast<QQuick3DShaderUtilsTextureInput *>(propValue.value<QObject *>());
                if (obj)
                    textureProperties.push_back(property);
            } else {
                const auto type = uniformType(propType);
                if (type != QSSGRenderShaderDataType::Unknown) {
                    addUniform(propType, property.name(), uniforms);
                    effectNode->properties.push_back({ property.name(), property.read(this), type, i});
                    // Track the property changes
                    if (property.hasNotifySignal() && propertyDirtyMethod.isValid())
                        connect(this, property.notifySignal(), this, propertyDirtyMethod);
                } else {
                    qWarning("No know uniform convertion found for property %s. Skipping", property.name());
                }
            }
        }

        // Textures
        QByteArray textureData;
        for (const auto &property : qAsConst(textureProperties)) {
            QSSGRenderEffect::TextureProperty texProp;
            QQuick3DShaderUtilsTextureInput *texture = property.read(this).value<QQuick3DShaderUtilsTextureInput *>();
            const QByteArray &name = property.name();
            if (name.isEmpty()) // Warnings here will just drown in the shader error messages
                continue;
            QQuick3DTexture *tex = texture->texture(); //
            connect(texture, &QQuick3DShaderUtilsTextureInput::textureDirty, this, &QQuick3DEffect::onTextureDirty);
            texProp.name = name;
            if (texture->enabled)
                texProp.texImage = tex->getRenderImage();
            texProp.shaderDataType = QSSGRenderShaderDataType::Texture2D;
            texProp.clampType = tex->horizontalTiling() == QQuick3DTexture::Repeat ? QSSGRenderTextureCoordOp::Repeat
                                                                                       : (tex->horizontalTiling() == QQuick3DTexture::ClampToEdge) ? QSSGRenderTextureCoordOp::ClampToEdge
                                                                                                                                                   : QSSGRenderTextureCoordOp::MirroredRepeat;
            QSSGShaderUtils::addSnapperSampler(texProp.name, textureData);
            effectNode->textureProperties.push_back(texProp);
        }

        if (!m_passes.isEmpty()) {
            QByteArray vertex, geometry, fragment, shaderCode;
            for (const auto &pass : qAsConst(m_passes)) {
                QQuick3DShaderUtilsShader *sharedShader = pass->shader(QQuick3DShaderUtilsShader::Stage::Shared);
                QQuick3DShaderUtilsShader *vertShader = pass->shader(QQuick3DShaderUtilsShader::Stage::Vertex);
                QQuick3DShaderUtilsShader *fragShader = pass->shader(QQuick3DShaderUtilsShader::Stage::Fragment);
                QQuick3DShaderUtilsShader *geomShader = pass->shader(QQuick3DShaderUtilsShader::Stage::Geometry);
                if (!sharedShader && !vertShader && !fragShader && !geomShader) {
                    qWarning("Pass with no shader attatched!");
                    continue;
                }

                // Build up shader code
                QByteArray shaderPath;
                if (sharedShader)
                    shared += QSSGShaderUtils::resolveShader(sharedShader->shader, shaderPath, this);
                if (vertShader)
                    vertex = QSSGShaderUtils::resolveShader(vertShader->shader, shaderPath, this);
                if (fragShader)
                    fragment = QSSGShaderUtils::resolveShader(fragShader->shader, shaderPath, this);
                if (geomShader)
                    geometry = QSSGShaderUtils::resolveShader(geomShader->shader, shaderPath, this);


                shaderCode = QSSGShaderUtils::mergeShaderCode(shared, uniforms, textureData, vertex, geometry, fragment);

                // Bind shader
                effectNode->commands.push_back(new dynamic::QSSGBindShader(shaderPath));
                effectNode->commands.push_back(new dynamic::QSSGApplyInstanceValue());

                // Buffers
                QQuick3DShaderUtilsBuffer *outputBuffer = pass->outputBuffer;
                if (outputBuffer) {
                    const QByteArray &outBufferName = outputBuffer->name;
                    if (outBufferName.isEmpty()) {
                        // default output buffer (with settings)
                        auto outputFormat = QQuick3DShaderUtilsBuffer::mapTextureFormat(outputBuffer->format());
                        effectNode->commands.push_back(new dynamic::QSSGBindTarget(outputFormat));
                        effectNode->outputFormat = outputFormat;
                    } else {
                        // Allocate buffer command
                        effectNode->commands.push_back(outputBuffer->getCommand());
                        // bind buffer
                        effectNode->commands.push_back(new dynamic::QSSGBindBuffer(outBufferName, true));
                    }
                } else {
                    // Use the default output buffer, same format as the source buffer
                    effectNode->commands.push_back(new dynamic::QSSGBindTarget(QSSGRenderTextureFormat::Unknown));
                    effectNode->outputFormat = QSSGRenderTextureFormat::Unknown;
                }

                // Other commands (BufferInput, Blending ... )
                const auto &extraCommands = pass->m_commands;
                for (const auto &command : extraCommands) {
                    const int bufferCount = command->bufferCount();
                    for (int i = 0; i != bufferCount; ++i)
                        effectNode->commands.push_back(command->bufferAt(i)->getCommand());
                    effectNode->commands.push_back(command->getCommand());
                }

                effectNode->commands.push_back(new dynamic::QSSGRender);

                renderContext->effectSystem()->setShaderData(shaderPath, shaderCode, "GLSL", "330", false, false);
            }
        }
    }

    // We need to mark the commands that we allocated since they will need to be deallocted when the customMaterial is deleted.
    // We achieve this by filtering on the command type.
    // Just redo the list without deleting it, or we end up calling already deleted commands in
    // render if a shader variable changes
    effectNode->commandsToDelete.clear();
    for (auto command : effectNode->commands) {
        if (command->m_type > dynamic::CommandType::Unknown &&
            command->m_type < dynamic::CommandType::ApplyCullMode &&
            command->m_type != dynamic::CommandType::AllocateBuffer &&
            command->m_type != dynamic::CommandType::ApplyBufferValue &&
            command->m_type != dynamic::CommandType::ApplyBlitFramebuffer &&
            command->m_type != dynamic::CommandType::ApplyBlending &&
            command->m_type != dynamic::CommandType::ApplyRenderState &&
            command->m_type != dynamic::CommandType::ApplyCullMode &&
            command->m_type != dynamic::CommandType::ApplyDepthValue &&
            command->m_type != dynamic::CommandType::ApplyValue) {
            effectNode->commandsToDelete.insert(command);
        }
    }

    if (m_dirtyAttributes & Dirty::PropertyDirty) {
        for (const auto &prop : qAsConst(effectNode->properties)) {
            auto p = metaObject()->property(prop.pid);
            if (Q_LIKELY(p.isValid()))
                prop.value = p.read(this);
        }
    }

    m_dirtyAttributes = 0;

    return effectNode;
}

void QQuick3DEffect::onPropertyDirty()
{
    markDirty(Dirty::PropertyDirty);
}

void QQuick3DEffect::onTextureDirty(QQuick3DShaderUtilsTextureInput *texture)
{
    Q_UNUSED(texture)
    markDirty(Dirty::TextureDirty);
}

void QQuick3DEffect::markDirty(QQuick3DEffect::Dirty type)
{
    if (!(m_dirtyAttributes & quint32(type))) {
        m_dirtyAttributes |= quint32(type);
        update();
    }
}

void QQuick3DEffect::updateSceneManager(const QSharedPointer<QQuick3DSceneManager> &sceneManager)
{
    if (sceneManager) {
        for (auto it : m_dynamicTextureMaps)
            QQuick3DObjectPrivate::refSceneManager(it, sceneManager);
    } else {
        for (auto it : m_dynamicTextureMaps)
            QQuick3DObjectPrivate::derefSceneManager(it);
    }
}

void QQuick3DEffect::itemChange(QQuick3DObject::ItemChange change, const QQuick3DObject::ItemChangeData &value)
{
    if (change == QQuick3DObject::ItemSceneChange)
        updateSceneManager(value.sceneManager);
}

void QQuick3DEffect::qmlAppendPass(QQmlListProperty<QQuick3DShaderUtilsRenderPass> *list, QQuick3DShaderUtilsRenderPass *pass)
{
    if (!pass)
        return;

    QQuick3DEffect *that = qobject_cast<QQuick3DEffect *>(list->object);
    that->m_passes.push_back(pass);
}

QQuick3DShaderUtilsRenderPass *QQuick3DEffect::qmlPassAt(QQmlListProperty<QQuick3DShaderUtilsRenderPass> *list, int index)
{
    QQuick3DEffect *that = qobject_cast<QQuick3DEffect *>(list->object);
    return that->m_passes.at(index);
}

int QQuick3DEffect::qmlPassCount(QQmlListProperty<QQuick3DShaderUtilsRenderPass> *list)
{
    QQuick3DEffect *that = qobject_cast<QQuick3DEffect *>(list->object);
    return that->m_passes.count();
}

void QQuick3DEffect::qmlPassClear(QQmlListProperty<QQuick3DShaderUtilsRenderPass> *list)
{
    QQuick3DEffect *that = qobject_cast<QQuick3DEffect *>(list->object);
    that->m_passes.clear();
}

void QQuick3DEffect::setDynamicTextureMap(QQuick3DTexture *textureMap, const QByteArray &name)
{
    if (!textureMap)
        return;

    auto it = m_dynamicTextureMaps.begin();
    const auto end = m_dynamicTextureMaps.end();
    for (; it != end; ++it) {
        if (*it == textureMap)
            break;
    }

    if (it != end)
        return;

    updatePropertyListener(textureMap, nullptr, QQuick3DObjectPrivate::get(this)->sceneManager, name, m_connections, [this, name](QQuick3DObject *n) {
        setDynamicTextureMap(qobject_cast<QQuick3DTexture *>(n), name);
    });

    m_dynamicTextureMaps.push_back(textureMap);
    update();
}

QT_END_NAMESPACE

