/*
 * KWin Style UKUI
 *
 * Copyright (C) 2020, KylinSoft Co., Ltd.
 *
 * 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 3 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.  If not, see <https://www.gnu.org/licenses/>.
 *
 * Authors: Yue Lan <lanyue@kylinos.cn>
 *
 */

#include "ubreffect.h"
#include "xatom-helper.h"
#include "ubrtexturehelper.h"
#include "shaderhelper.h"

#include <kwineffects.h>
#include <kwinglutils.h>
#include <kwinglplatform.h>
#include <KWayland/Server/surface_interface.h>
#include <KWayland/Server/xdgshell_interface.h>
#include <KWayland/Server/xdgdecoration_interface.h>
#include <KWaylandServer/surface_interface.h>

#include <QPainterPath>
#include <QX11Info>
#include <QDebug>
#include <QMetaMethod>
#include <QVector4D>
#include <QtDBus/QtDBus>       //必须放xatom-helper.h前面

#include <KConfigGroup>


#define UNITY_BORDER_RADIUS 12

#define MIN_BORDER_RADIUS 2 //圆角小于2时，按0处理

static QList<KWin::EffectWindow *> maximizedWindows;

typedef void (*ToplevelSetDepth)(void *, int);

static ToplevelSetDepth setDepthfunc = nullptr;

UBREffect::UBREffect(QObject *parent, const QVariantList &args)
    :m_radius(UNITY_BORDER_RADIUS)
{
    // try get Toplevel::setDepth(), for resolving ubr effect has black corners of opaque window.
    setDepthfunc = (ToplevelSetDepth) QLibrary::resolve("kwin.so." + qApp->applicationVersion(), "_ZN4KWin8Toplevel8setDepthEi");

    m_ubrShader = ShaderHelper::getShader();
    if (!m_ubrShader->isValid()) {
        m_ubrShader = KWin::ShaderManager::instance()->generateCustomShader(KWin::ShaderTrait::MapTexture|KWin::ShaderTrait::Modulate|KWin::ShaderTrait::AdjustSaturation);
    }

    QDBusConnection::sessionBus().connect(QString(),
                                          QStringLiteral("/KGlobalSettings"),
                                          QStringLiteral("org.kde.KGlobalSettings"),
                                          QStringLiteral("slotThemeChange"),
                                          this, SLOT(themeUpdate(int)));

    auto config = KSharedConfig::openConfig("ukui-decorationrc", KConfig::SimpleConfig);
    auto group = KConfigGroup(config, "Theme");
    m_radius = group.readEntry("radius",m_radius);

    auto effectsManager = KWin::effects;
    for (auto window : effectsManager->stackingOrder()) {
        initEffectWindow(window);
    }

    connect(effectsManager, &KWin::EffectsHandler::windowAdded, this, [=](KWin::EffectWindow *window){
        initEffectWindow(window);
    });

    connect(effectsManager, &KWin::EffectsHandler::windowDeleted, this, [=](KWin::EffectWindow *window){
        maximizedWindows.removeOne(window);

        window->setData(IgnoreUBR, QVariant());
        window->setData(UnityBorderRadius, QVariant());
        window->setData(IsCSD, QVariant());
        window->setData(IsUKUIDecoration, QVariant());
    });

    connect(effectsManager, &KWin::EffectsHandler::windowMaximizedStateChanged, this, [=](KWin::EffectWindow *w, bool maxHorz, bool maxVert){
        if (maxHorz || maxVert) {
            maximizedWindows.append(w);
        } else {
            maximizedWindows.removeOne(w);
        }
    });
}

UBREffect::~UBREffect()
{
    //ShaderHelper::releaseShaders();
    maximizedWindows.clear();
    // clear the dirty texture.
    // NOTE: do not comment this code.
    UBRTextureHelper::getInstance()->release();

    for (auto window : KWin::effects->stackingOrder()) {
        window->setData(IgnoreUBR, QVariant());
        //window->setData(UnityBorderRadius, QVariant());
    }
}

bool UBREffect::supported()
{
    bool supported = KWin::effects->isOpenGLCompositing() && KWin::GLRenderTarget::supported();
    return supported;
}

bool UBREffect::enabledByDefault()
{
    return true;
}

void UBREffect::prePaintScreen(KWin::ScreenPrePaintData &data, std::chrono::milliseconds time)
{
    KWin::Effect::prePaintScreen(data, time);
}

void UBREffect::prePaintWindow(KWin::EffectWindow *w, KWin::WindowPrePaintData &data, std::chrono::milliseconds time)
{
    data.mask &= ~PAINT_WINDOW_OPAQUE;
    data.mask &= PAINT_WINDOW_TRANSLUCENT;
    data.clip = QRegion();
    return KWin::Effect::prePaintWindow(w, data, time);
}

void UBREffect::drawWindow(KWin::EffectWindow *w, int mask, const QRegion &region, KWin::WindowPaintData &data)
{
    if (m_radius < MIN_BORDER_RADIUS) {
        return KWin::Effect::drawWindow(w, mask, region, data);
    }

    if (this->ignoreWindowType[w->windowType()]) {
        return KWin::Effect::drawWindow(w, mask, region, data);
    }

    if (w->data(IgnoreUBR).isValid()) {
        return KWin::Effect::drawWindow(w, mask, region, data);
    }

    bool isUKUIDecoration = false;

    if (!w->data(IsUKUIDecoration).toBool() && !w->hasDecoration()) {
        // recheck wayland window
        if (w->surface()) {
            // FIXME: quads has been dropped.
//            for (auto quad : data.quads) {
//                if (quad.type() != KWin::WindowQuadType::WindowQuadContents) {
                    isUKUIDecoration = true;
                    w->setData(IsUKUIDecoration, true);
//                    break;
//                }
//            }
            if (!isUKUIDecoration) {
                w->setData(IgnoreUBR, true);
            }
        }
        if (!w->data(IsUKUIDecoration).toBool() && !w->hasDecoration())
            return KWin::Effect::drawWindow(w, mask, region, data);
    }

    if (!KWin::effects->isOpenGLCompositing()) {
        return KWin::Effect::drawWindow(w, mask, region, data);
    }

    if (!w->isPaintingEnabled() || ((mask & PAINT_WINDOW_LANCZOS))) {
        return KWin::Effect::drawWindow(w, mask, region, data);
    }

    if (!w->surface()) {
        // wayland window doesn't has such window type
        if (w->windowType() != NET::WindowType::Normal && !w->isDialog()  && !w->isUtility()) {
            return KWin::Effect::drawWindow(w, mask, region, data);
        }

        // wayland window is always unmanaged
        if (!w->isManaged()) {
            return KWin::Effect::drawWindow(w, mask, region, data);
        }
    }

    if ((mask & PAINT_WINDOW_TRANSFORMED) && !(mask & PAINT_SCREEN_TRANSFORMED)) {
        //return KWin::Effect::drawWindow(w, mask, region, data);
    }

    if (maximizedWindows.contains(w)) {
        return KWin::Effect::drawWindow(w, mask, region, data);
    }

    if (!w->hasAlpha()) {
        //return KWin::Effect::drawWindow(w, mask, region, data);
        if (setDepthfunc) {
            setDepthfunc(w->parent(), 32);
        }
    }

    auto ubr = qvariant_cast<QVector4D>(w->data(UnityBorderRadius));

    KWin::WindowPaintData tmpData = data;

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    float borderColor[] = { 1.0f, 1.0f, 1.0f, 1.0f };

    bool isCSD = w->data(IsCSD).toBool();

    if (w->isWaylandClient()) {
        auto textureTopLeft = UBRTextureHelper::getInstance()->getTexture(isCSD? ubr.x(): 0);
        glActiveTexture(GL_TEXTURE10);
        textureTopLeft->bind();
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glActiveTexture(GL_TEXTURE0);

        auto textureTopRight = UBRTextureHelper::getInstance()->getTexture(isCSD? ubr.y(): 0);
        glActiveTexture(GL_TEXTURE11);
        textureTopRight->bind();
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glActiveTexture(GL_TEXTURE0);

        auto textureBottomLeft = UBRTextureHelper::getInstance()->getTexture(ubr.z());
        glActiveTexture(GL_TEXTURE12);
        textureBottomLeft->bind();
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glActiveTexture(GL_TEXTURE0);

        auto textureBottomRight = UBRTextureHelper::getInstance()->getTexture(ubr.w());
        glActiveTexture(GL_TEXTURE13);
        textureBottomRight->bind();
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glActiveTexture(GL_TEXTURE0);

        tmpData.shader = m_ubrShader;
        KWin::ShaderManager::instance()->pushShader(m_ubrShader);

        m_ubrShader->setUniform("topleft", 10);
        m_ubrShader->setUniform("scale", QVector2D(w->width()*1.0/textureTopLeft->width(), w->height()*1.0/textureTopLeft->height()));

        m_ubrShader->setUniform("topright", 11);
        m_ubrShader->setUniform("scale1", QVector2D(w->width()*1.0/textureTopRight->width(), w->height()*1.0/textureTopRight->height()));

        m_ubrShader->setUniform("bottomleft", 12);
        m_ubrShader->setUniform("scale2", QVector2D(w->width()*1.0/textureBottomLeft->width(), w->height()*1.0/textureBottomLeft->height()));

        m_ubrShader->setUniform("bottomright", 13);
        m_ubrShader->setUniform("scale3", QVector2D(w->width()*1.0/textureBottomRight->width(), w->height()*1.0/textureBottomRight->height()));

        KWin::Effect::drawWindow(w, mask, region, tmpData);

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

        glActiveTexture(GL_TEXTURE10);
        textureTopLeft->unbind();
        glActiveTexture(GL_TEXTURE0);

        glActiveTexture(GL_TEXTURE11);
        textureTopRight->unbind();
        glActiveTexture(GL_TEXTURE0);

        glActiveTexture(GL_TEXTURE12);
        textureBottomLeft->unbind();
        glActiveTexture(GL_TEXTURE0);

        glActiveTexture(GL_TEXTURE13);
        textureBottomRight->unbind();
        glActiveTexture(GL_TEXTURE0);

        glDisable(GL_BLEND);
    } else {
        auto textureTopLeft = UBRTextureHelper::getInstance()->getTexture(ubr.x());
        glActiveTexture(GL_TEXTURE10);
        textureTopLeft->bind();
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glActiveTexture(GL_TEXTURE0);

        auto textureTopRight = UBRTextureHelper::getInstance()->getTexture(ubr.y());
        glActiveTexture(GL_TEXTURE11);
        textureTopRight->bind();
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glActiveTexture(GL_TEXTURE0);

        auto textureBottomLeft = UBRTextureHelper::getInstance()->getTexture(ubr.z());
        glActiveTexture(GL_TEXTURE12);
        textureBottomLeft->bind();
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glActiveTexture(GL_TEXTURE0);

        auto textureBottomRight = UBRTextureHelper::getInstance()->getTexture(ubr.w());
        glActiveTexture(GL_TEXTURE13);
        textureBottomRight->bind();
        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
        glActiveTexture(GL_TEXTURE0);

        tmpData.shader = m_ubrShader;
        KWin::ShaderManager::instance()->pushShader(m_ubrShader);

        m_ubrShader->setUniform("topleft", 10);
        m_ubrShader->setUniform("scale", QVector2D(w->width()*1.0/textureTopLeft->width(), w->height()*1.0/textureTopLeft->height()));

        m_ubrShader->setUniform("topright", 11);
        m_ubrShader->setUniform("scale1", QVector2D(w->width()*1.0/textureTopRight->width(), w->height()*1.0/textureTopRight->height()));

        m_ubrShader->setUniform("bottomleft", 12);
        m_ubrShader->setUniform("scale2", QVector2D(w->width()*1.0/textureBottomLeft->width(), w->height()*1.0/textureBottomLeft->height()));

        m_ubrShader->setUniform("bottomright", 13);
        m_ubrShader->setUniform("scale3", QVector2D(w->width()*1.0/textureBottomRight->width(), w->height()*1.0/textureBottomRight->height()));

        KWin::Effect::drawWindow(w, mask, region, tmpData);

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

        glActiveTexture(GL_TEXTURE10);
        textureTopLeft->unbind();
        glActiveTexture(GL_TEXTURE0);

        glActiveTexture(GL_TEXTURE11);
        textureTopRight->unbind();
        glActiveTexture(GL_TEXTURE0);

        glActiveTexture(GL_TEXTURE12);
        textureBottomLeft->unbind();
        glActiveTexture(GL_TEXTURE0);

        glActiveTexture(GL_TEXTURE13);
        textureBottomRight->unbind();
        glActiveTexture(GL_TEXTURE0);

        glDisable(GL_BLEND);
    }
}

void UBREffect::themeUpdate(int themeId)
{
    Q_UNUSED(themeId)
    auto config = KSharedConfig::openConfig("ukui-decorationrc", KConfig::SimpleConfig);
    auto group = KConfigGroup(config, "Theme");
    m_radius = group.readEntry("radius",m_radius);
    auto effectsManager = KWin::effects;

    for (auto window : effectsManager->stackingOrder()) {
        initEffectWindow(window);
    }
}

void UBREffect::initEffectWindow(KWin::EffectWindow *window)
{
    if (!window->surface()) {
        // init on x11
        bool isWindowMaximized = XAtomHelper::getInstance()->isWindowMaximized(window);
        if (isWindowMaximized)
            maximizedWindows.append(window);

        bool isUKUIDecoration = XAtomHelper::getInstance()->isUKUIDecorationWindow(window);
        window->setData(IsUKUIDecoration, isUKUIDecoration);

        if (!window->data(UnityBorderRadius).isValid()) {
            auto ubr = XAtomHelper::getInstance()->getWindowBorderRadius(window);
            if (ubr.topLeft <= 0) {
                ubr.topLeft = (m_radius < MIN_BORDER_RADIUS) ? 0 : m_radius;
            }
            if (ubr.topRight <= 0) {
                ubr.topRight = (m_radius < MIN_BORDER_RADIUS) ? 0 : m_radius;
            }
            if (ubr.bottomLeft <= 0) {
                ubr.bottomLeft = (m_radius < MIN_BORDER_RADIUS) ? 0 : m_radius;
            }
            if (ubr.bottomRight <= 0) {
                ubr.bottomRight = (m_radius < MIN_BORDER_RADIUS) ? 0 : m_radius;
            }
            window->setData(UnityBorderRadius, QVector4D(ubr.topLeft, ubr.topRight, ubr.bottomLeft, ubr.bottomRight));
        }

        bool isCSD = XAtomHelper::getInstance()->isWindowDecorateBorderOnly(window);
        window->setData(IsCSD, isCSD);
    } else {
        // init on wayland

        bool isWindowMaximized = window->data(IgnoreUBR).toBool();
        if (isWindowMaximized)
            maximizedWindows.append(window);

        bool isUKUIDecoration = window->surface()->property("hasDecoration").toBool() || window->hasDecoration();
        window->setData(IsUKUIDecoration, isUKUIDecoration);

        window->setData(IsCSD, window->surface()->property("isCSD").toBool());

        if (!window->data(UnityBorderRadius).isValid()) {
            QVector4D ubr = qvariant_cast<QVector4D>(window->surface()->property("ubr"));
            if (ubr.x() <= 0) {
                ubr.setX((m_radius < MIN_BORDER_RADIUS) ? 0 : m_radius);
            }
            if (ubr.y() <= 0) {
                ubr.setY((m_radius < MIN_BORDER_RADIUS) ? 0 : m_radius);
            }
            if (ubr.z() <= 0) {
                ubr.setZ((m_radius < MIN_BORDER_RADIUS) ? 0 : m_radius);
            }
            if (ubr.w() <= 0) {
                ubr.setW((m_radius < MIN_BORDER_RADIUS) ? 0 : m_radius);
            }
            window->setData(UnityBorderRadius, ubr);
        }
    }

}
