/*
 *   Copyright © 2010 Fredrik Höglund <fredrik@kde.org>
 *   Copyright © 2018 Alex Nemeth <alex.nemeth329@gmail.com>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *   General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; see the file COPYING.  if not, write to
 *   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 *   Boston, MA 02110-1301, USA.
 */

#include "blurshader.h"

#include <kwineffects.h>
#include "kwinglutils.h"
#include <kwinglplatform.h>

#include <QByteArray>
#include <QMatrix4x4>
#include <QTextStream>
#include <QVector2D>

#include <cmath>

using namespace KWin;


BlurShader::BlurShader()
    : mValid(false)
{
}

BlurShader::~BlurShader()
{
}

BlurShader *BlurShader::create()
{
    return new GLSLBlurShader();
}

// ----------------------------------------------------------------------------



GLSLBlurShader::GLSLBlurShader()
{
    init();
}

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

void GLSLBlurShader::reset()
{
    delete m_shaderDownsample;
    m_shaderDownsample = nullptr;

    delete m_shaderUpsample;
    m_shaderUpsample = nullptr;

    delete m_shaderCopysample;
    m_shaderCopysample = nullptr;

    delete m_shaderNoisesample;
    m_shaderNoisesample = nullptr;

    setIsValid(false);
}

void GLSLBlurShader::setModelViewProjectionMatrix(const QMatrix4x4 &matrix)
{
    if (!isValid())
        return;

    switch (m_activeSampleType) {
        case CopySampleType:
            if (matrix == m_matrixCopysample)
                return;

            m_matrixCopysample = matrix;
            m_shaderCopysample->setUniform(m_mvpMatrixLocationCopysample, matrix);
            break;

        case UpSampleType:
            if (matrix == m_matrixUpsample)
                return;

            m_matrixUpsample = matrix;
            m_shaderUpsample->setUniform(m_mvpMatrixLocationUpsample, matrix);
            break;

        case DownSampleType:
            if (matrix == m_matrixDownsample)
                return;

            m_matrixDownsample = matrix;
            m_shaderDownsample->setUniform(m_mvpMatrixLocationDownsample, matrix);
            break;

        case NoiseSampleType:
            if (matrix == m_matrixNoisesample)
                return;

            m_matrixNoisesample = matrix;
            m_shaderNoisesample->setUniform(m_mvpMatrixLocationNoisesample, matrix);
            break;
    }
}

void GLSLBlurShader::setOffset(float offset)
{
    if (!isValid())
        return;

    switch (m_activeSampleType) {
        case UpSampleType:
            if (offset == m_offsetUpsample)
                return;

            m_offsetUpsample = offset;
            m_shaderUpsample->setUniform(m_offsetLocationUpsample, offset);
            break;

        case DownSampleType:
            if (offset == m_offsetDownsample)
                return;

            m_offsetDownsample = offset;
            m_shaderDownsample->setUniform(m_offsetLocationDownsample, offset);
            break;

        case NoiseSampleType:
            if (offset == m_offsetNoisesample)
                return;

            m_offsetNoisesample = offset;
            m_shaderNoisesample->setUniform(m_offsetLocationNoisesample, offset);
            break;
    }
}

void GLSLBlurShader::setTargetTextureSize(QSize renderTextureSize)
{
    if (!isValid())
        return;

    QVector2D texSize = QVector2D(renderTextureSize.width(), renderTextureSize.height());

    switch (m_activeSampleType) {
        case CopySampleType:
            m_shaderCopysample->setUniform(m_renderTextureSizeLocationCopysample, texSize);
            break;

        case UpSampleType:
            m_shaderUpsample->setUniform(m_renderTextureSizeLocationUpsample, texSize);
            m_shaderUpsample->setUniform(m_halfpixelLocationUpsample, QVector2D(0.5 / texSize.x(), 0.5 / texSize.y()));
            break;

        case DownSampleType:
            m_shaderDownsample->setUniform(m_renderTextureSizeLocationDownsample, texSize);
            m_shaderDownsample->setUniform(m_halfpixelLocationDownsample, QVector2D(0.5 / texSize.x(), 0.5 / texSize.y()));
            break;

        case NoiseSampleType:
            m_shaderNoisesample->setUniform(m_renderTextureSizeLocationNoisesample, texSize);
            m_shaderNoisesample->setUniform(m_halfpixelLocationNoisesample, QVector2D(0.5 / texSize.x(), 0.5 / texSize.y()));
            break;
    }
}

void GLSLBlurShader::setNoiseTextureSize(QSize noiseTextureSize)
{
    QVector2D noiseTexSize = QVector2D(noiseTextureSize.width(), noiseTextureSize.height());

    if (noiseTexSize != m_noiseTextureSizeNoisesample) {
        m_noiseTextureSizeNoisesample = noiseTexSize;
        m_shaderNoisesample->setUniform(m_noiseTextureSizeLocationNoisesample, noiseTexSize);
    }
}

void GLSLBlurShader::setTexturePosition(QPoint texPos)
{
    m_shaderNoisesample->setUniform(m_texStartPosLocationNoisesample, QVector2D(-texPos.x(), texPos.y()));
}

void GLSLBlurShader::setBlurRect(QRect blurRect, QSize screenSize)
{
    if (!isValid() || blurRect == m_blurRectCopysample)
        return;

    m_blurRectCopysample = blurRect;

    QVector4D rect = QVector4D(
        blurRect.bottomLeft().x()       / float(screenSize.width()),
                               1.0 - blurRect.bottomLeft().y() / float(screenSize.height()),
                               blurRect.topRight().x()         / float(screenSize.width()),
                               1.0 - blurRect.topRight().y()   / float(screenSize.height())
    );

    m_shaderCopysample->setUniform(m_blurRectLocationCopysample, rect);
}

void GLSLBlurShader::bind(SampleType sampleType)
{
    if (!isValid())
        return;

    switch (sampleType) {
        case CopySampleType:
            ShaderManager::instance()->pushShader(m_shaderCopysample);
            break;

        case UpSampleType:
            ShaderManager::instance()->pushShader(m_shaderUpsample);
            break;

        case DownSampleType:
            ShaderManager::instance()->pushShader(m_shaderDownsample);
            break;

        case NoiseSampleType:
            ShaderManager::instance()->pushShader(m_shaderNoisesample);
            break;
    }

    m_activeSampleType = sampleType;
}

void GLSLBlurShader::unbind()
{
    ShaderManager::instance()->popShader();
}

void GLSLBlurShader::init()
{
    const bool gles = GLPlatform::instance()->isGLES();
    const bool glsl_140 = !gles && GLPlatform::instance()->glslVersion() >= kVersionNumber(1, 40);
    const bool core = glsl_140 || (gles && GLPlatform::instance()->glslVersion() >= kVersionNumber(3, 0));

    QByteArray vertexSource;
    QByteArray fragmentDownSource;
    QByteArray fragmentUpSource;
    QByteArray fragmentCopySource;
    QByteArray fragmentNoiseSource;

    const QByteArray attribute = core ? "in"        : "attribute";
    const QByteArray texture2D = core ? "texture"   : "texture2D";
    const QByteArray fragColor = core ? "fragColor" : "gl_FragColor";

    QString glHeaderString;

    if (gles) {
        if (core) {
            glHeaderString += "#version 300 es\n\n";
        }

        glHeaderString += "precision highp float;\n";
    } else if (glsl_140) {
        glHeaderString += "#version 140\n\n";
    }

    QString glUniformString = "uniform sampler2D texUnit;\n"
        "uniform float offset;\n"
        "uniform vec2 renderTextureSize;\n"
        "uniform vec2 halfpixel;\n";

    if (core) {
        glUniformString += "out vec4 fragColor;\n\n";
    }


    // Vertex shader
    // ===================================================================
    QTextStream streamVert(&vertexSource);

    streamVert << glHeaderString;

    streamVert << "uniform mat4 modelViewProjectionMatrix;\n";
    streamVert << attribute << " vec4 vertex;\n\n";
    streamVert << "\n";
    streamVert << "void main(void)\n";
    streamVert << "{\n";
    streamVert << "    gl_Position = modelViewProjectionMatrix * vertex;\n";
    streamVert << "}\n";

    streamVert.flush();

    // Fragment shader (Dual Kawase Blur) - Downsample
    // ===================================================================
    QTextStream streamFragDown(&fragmentDownSource);

    streamFragDown << glHeaderString << glUniformString;

    streamFragDown << "void main(void)\n";
    streamFragDown << "{\n";
    streamFragDown << "    vec2 uv = vec2(gl_FragCoord.xy / renderTextureSize);\n";
    streamFragDown << "    \n";
    streamFragDown << "    vec4 sum = " << texture2D << "(texUnit, uv) * 4.0;\n";
    streamFragDown << "    sum += " << texture2D << "(texUnit, uv - halfpixel.xy * offset);\n";
    streamFragDown << "    sum += " << texture2D << "(texUnit, uv + halfpixel.xy * offset);\n";
    streamFragDown << "    sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x, -halfpixel.y) * offset);\n";
    streamFragDown << "    sum += " << texture2D << "(texUnit, uv - vec2(halfpixel.x, -halfpixel.y) * offset);\n";
    streamFragDown << "    \n";
    streamFragDown << "    " << fragColor << " = sum / 8.0;\n";
    streamFragDown << "}\n";

    streamFragDown.flush();

    // Fragment shader (Dual Kawase Blur) - Upsample
    // ===================================================================
    QTextStream streamFragUp(&fragmentUpSource);

    streamFragUp << glHeaderString << glUniformString;

    streamFragUp << "void main(void)\n";
    streamFragUp << "{\n";
    streamFragUp << "    vec2 uv = vec2(gl_FragCoord.xy / renderTextureSize);\n";
    streamFragUp << "    \n";
    streamFragUp << "    vec4 sum = " << texture2D << "(texUnit, uv + vec2(-halfpixel.x * 2.0, 0.0) * offset);\n";
    streamFragUp << "    sum += " << texture2D << "(texUnit, uv + vec2(-halfpixel.x, halfpixel.y) * offset) * 2.0;\n";
    streamFragUp << "    sum += " << texture2D << "(texUnit, uv + vec2(0.0, halfpixel.y * 2.0) * offset);\n";
    streamFragUp << "    sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x, halfpixel.y) * offset) * 2.0;\n";
    streamFragUp << "    sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x * 2.0, 0.0) * offset);\n";
    streamFragUp << "    sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x, -halfpixel.y) * offset) * 2.0;\n";
    streamFragUp << "    sum += " << texture2D << "(texUnit, uv + vec2(0.0, -halfpixel.y * 2.0) * offset);\n";
    streamFragUp << "    sum += " << texture2D << "(texUnit, uv + vec2(-halfpixel.x, -halfpixel.y) * offset) * 2.0;\n";
    streamFragUp << "    \n";
    streamFragUp << "    " << fragColor << " = sum / 12.0;\n";
    streamFragUp << "}\n";

    streamFragUp.flush();

    // Fragment shader - Copy texture
    // ===================================================================
    QTextStream streamFragCopy(&fragmentCopySource);

    streamFragCopy << glHeaderString;

    streamFragCopy << "uniform sampler2D texUnit;\n";
    streamFragCopy << "uniform vec2 renderTextureSize;\n";
    streamFragCopy << "uniform vec4 blurRect;\n";

    if (core)
        streamFragCopy << "out vec4 fragColor;\n\n";

    streamFragCopy << "void main(void)\n";
    streamFragCopy << "{\n";
    streamFragCopy << "     vec2 uv = vec2(gl_FragCoord.xy / renderTextureSize);\n";
    streamFragCopy << "    " << fragColor << " = " << texture2D << "(texUnit, clamp(uv, blurRect.xy, blurRect.zw));\n";
    streamFragCopy << "}\n";

    streamFragCopy.flush();

    // Fragment shader - Noise texture
    // ===================================================================
    QTextStream streamFragNoise(&fragmentNoiseSource);

    streamFragNoise << glHeaderString << glUniformString;

    streamFragNoise << "uniform sampler2D noiseTexUnit;\n";
    streamFragNoise << "uniform vec2 noiseTextureSize;\n";
    streamFragNoise << "uniform vec2 texStartPos;\n";

    // Upsampling + Noise
    streamFragNoise << "void main(void)\n";
    streamFragNoise << "{\n";
    streamFragNoise << "    vec2 uv = vec2(gl_FragCoord.xy / renderTextureSize);\n";
    streamFragNoise << "    vec2 uvNoise = vec2((texStartPos.xy + gl_FragCoord.xy) / noiseTextureSize);\n";
    streamFragNoise << "    \n";
    streamFragNoise << "    vec4 sum = " << texture2D << "(texUnit, uv + vec2(-halfpixel.x * 2.0, 0.0) * offset);\n";
    streamFragNoise << "    sum += " << texture2D << "(texUnit, uv + vec2(-halfpixel.x, halfpixel.y) * offset) * 2.0;\n";
    streamFragNoise << "    sum += " << texture2D << "(texUnit, uv + vec2(0.0, halfpixel.y * 2.0) * offset);\n";
    streamFragNoise << "    sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x, halfpixel.y) * offset) * 2.0;\n";
    streamFragNoise << "    sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x * 2.0, 0.0) * offset);\n";
    streamFragNoise << "    sum += " << texture2D << "(texUnit, uv + vec2(halfpixel.x, -halfpixel.y) * offset) * 2.0;\n";
    streamFragNoise << "    sum += " << texture2D << "(texUnit, uv + vec2(0.0, -halfpixel.y * 2.0) * offset);\n";
    streamFragNoise << "    sum += " << texture2D << "(texUnit, uv + vec2(-halfpixel.x, -halfpixel.y) * offset) * 2.0;\n";
    streamFragNoise << "    \n";
    streamFragNoise << "    " << fragColor << " = sum / 12.0 - (vec4(0.5, 0.5, 0.5, 0) - vec4(" << texture2D << "(noiseTexUnit, uvNoise).rrr, 0));\n";
    streamFragNoise << "}\n";

    streamFragNoise.flush();


    m_shaderDownsample  = ShaderManager::instance()->loadShaderFromCode(vertexSource, fragmentDownSource);
    m_shaderUpsample    = ShaderManager::instance()->loadShaderFromCode(vertexSource, fragmentUpSource);
    m_shaderCopysample  = ShaderManager::instance()->loadShaderFromCode(vertexSource, fragmentCopySource);
    m_shaderNoisesample = ShaderManager::instance()->loadShaderFromCode(vertexSource, fragmentNoiseSource);

    bool areShadersValid = m_shaderDownsample->isValid() &&
        m_shaderUpsample->isValid() &&
        m_shaderCopysample->isValid() &&
        m_shaderNoisesample->isValid();
    setIsValid(areShadersValid);

    if (areShadersValid) {
        m_mvpMatrixLocationDownsample = m_shaderDownsample->uniformLocation("modelViewProjectionMatrix");
        m_offsetLocationDownsample = m_shaderDownsample->uniformLocation("offset");
        m_renderTextureSizeLocationDownsample = m_shaderDownsample->uniformLocation("renderTextureSize");
        m_halfpixelLocationDownsample = m_shaderDownsample->uniformLocation("halfpixel");

        m_mvpMatrixLocationUpsample = m_shaderUpsample->uniformLocation("modelViewProjectionMatrix");
        m_offsetLocationUpsample = m_shaderUpsample->uniformLocation("offset");
        m_renderTextureSizeLocationUpsample = m_shaderUpsample->uniformLocation("renderTextureSize");
        m_halfpixelLocationUpsample = m_shaderUpsample->uniformLocation("halfpixel");

        m_mvpMatrixLocationCopysample = m_shaderCopysample->uniformLocation("modelViewProjectionMatrix");
        m_renderTextureSizeLocationCopysample = m_shaderCopysample->uniformLocation("renderTextureSize");
        m_blurRectLocationCopysample = m_shaderCopysample->uniformLocation("blurRect");

        m_mvpMatrixLocationNoisesample = m_shaderNoisesample->uniformLocation("modelViewProjectionMatrix");
        m_offsetLocationNoisesample = m_shaderNoisesample->uniformLocation("offset");
        m_renderTextureSizeLocationNoisesample = m_shaderNoisesample->uniformLocation("renderTextureSize");
        m_noiseTextureSizeLocationNoisesample = m_shaderNoisesample->uniformLocation("noiseTextureSize");
        m_texStartPosLocationNoisesample = m_shaderNoisesample->uniformLocation("texStartPos");
        m_halfpixelLocationNoisesample = m_shaderNoisesample->uniformLocation("halfpixel");

        QMatrix4x4 modelViewProjection;
        const QSize screenSize = effects->virtualScreenSize();
        modelViewProjection.ortho(0, screenSize.width(), screenSize.height(), 0, 0, 65535);

        //Add default values to the uniforms of the shaders
        ShaderManager::instance()->pushShader(m_shaderDownsample);
        m_shaderDownsample->setUniform(m_mvpMatrixLocationDownsample, modelViewProjection);
        m_shaderDownsample->setUniform(m_offsetLocationDownsample, float(1.0));
        m_shaderDownsample->setUniform(m_renderTextureSizeLocationDownsample, QVector2D(1.0, 1.0));
        m_shaderDownsample->setUniform(m_halfpixelLocationDownsample, QVector2D(1.0, 1.0));
        ShaderManager::instance()->popShader();

        ShaderManager::instance()->pushShader(m_shaderUpsample);
        m_shaderUpsample->setUniform(m_mvpMatrixLocationUpsample, modelViewProjection);
        m_shaderUpsample->setUniform(m_offsetLocationUpsample, float(1.0));
        m_shaderUpsample->setUniform(m_renderTextureSizeLocationUpsample, QVector2D(1.0, 1.0));
        m_shaderUpsample->setUniform(m_halfpixelLocationUpsample, QVector2D(1.0, 1.0));
        ShaderManager::instance()->popShader();

        ShaderManager::instance()->pushShader(m_shaderCopysample);
        m_shaderCopysample->setUniform(m_mvpMatrixLocationCopysample, modelViewProjection);
        m_shaderCopysample->setUniform(m_renderTextureSizeLocationCopysample, QVector2D(1.0, 1.0));
        m_shaderCopysample->setUniform(m_blurRectLocationCopysample, QVector4D(1.0, 1.0, 1.0, 1.0));
        ShaderManager::instance()->popShader();

        ShaderManager::instance()->pushShader(m_shaderNoisesample);
        m_shaderNoisesample->setUniform(m_mvpMatrixLocationNoisesample, modelViewProjection);
        m_shaderNoisesample->setUniform(m_offsetLocationNoisesample, float(1.0));
        m_shaderNoisesample->setUniform(m_renderTextureSizeLocationNoisesample, QVector2D(1.0, 1.0));
        m_shaderNoisesample->setUniform(m_noiseTextureSizeLocationNoisesample, QVector2D(1.0, 1.0));
        m_shaderNoisesample->setUniform(m_texStartPosLocationNoisesample, QVector2D(1.0, 1.0));
        m_shaderNoisesample->setUniform(m_halfpixelLocationNoisesample, QVector2D(1.0, 1.0));

        glUniform1i(m_shaderNoisesample->uniformLocation("texUnit"), 0);
        glUniform1i(m_shaderNoisesample->uniformLocation("noiseTexUnit"), 1);

        ShaderManager::instance()->popShader();

        m_activeSampleType = -1;

        m_offsetDownsample = 0.0;
        m_matrixDownsample = QMatrix4x4();

        m_offsetUpsample = 0.0;
        m_matrixUpsample = QMatrix4x4();

        m_matrixCopysample = QMatrix4x4();
        m_blurRectCopysample = QRect();

        m_offsetNoisesample = 0.0;
        m_noiseTextureSizeNoisesample = QVector2D();
        m_matrixNoisesample = QMatrix4x4();
    }
}
