/********************************************************************
 KWin - the KDE window manager
 This file is part of the KDE project.

Copyright (C) 2009 Lucas Murray <lmurray@undefinedfire.com>
Copyright (C) 2018 Vlad Zagorodniy <vladzzag@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.  If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/

#include "snaphelper.h"

#include <kwinglutils.h>

#ifdef KWIN_HAVE_XRENDER_COMPOSITING
#include <kwinxrenderutils.h>
#include <xcb/render.h>
#endif

#include <QPainter>

namespace KWin
{

static const int s_lineWidth = 4;
static const QColor s_lineColor = QColor(128, 128, 128, 128);

static QRegion computeDirtyRegion(const QRect &windowRect)
{
    const QMargins outlineMargins(
        s_lineWidth / 2,
        s_lineWidth / 2,
        s_lineWidth / 2,
        s_lineWidth / 2
    );

    QRegion dirtyRegion;
    for (int i = 0; i < effects->numScreens(); ++i) {
        const QRect screenRect = effects->clientArea(ScreenArea, i, 0);

        QRect screenWindowRect = windowRect;
        screenWindowRect.moveCenter(screenRect.center());

        QRect verticalBarRect(0, 0, s_lineWidth, screenRect.height());
        verticalBarRect.moveCenter(screenRect.center());
        verticalBarRect.adjust(-1, -1, 1, 1);
        dirtyRegion += verticalBarRect;

        QRect horizontalBarRect(0, 0, screenRect.width(), s_lineWidth);
        horizontalBarRect.moveCenter(screenRect.center());
        horizontalBarRect.adjust(-1, -1, 1, 1);
        dirtyRegion += horizontalBarRect;

        const QRect outlineOuterRect = screenWindowRect
            .marginsAdded(outlineMargins)
            .adjusted(-1, -1, 1, 1);
        const QRect outlineInnerRect = screenWindowRect
            .marginsRemoved(outlineMargins)
            .adjusted(1, 1, -1, -1);
        dirtyRegion += QRegion(outlineOuterRect) - QRegion(outlineInnerRect);
    }

    return dirtyRegion;
}

SnapHelperEffect::SnapHelperEffect()
{
    reconfigure(ReconfigureAll);

    connect(effects, &EffectsHandler::windowClosed, this, &SnapHelperEffect::slotWindowClosed);
    connect(effects, &EffectsHandler::windowStartUserMovedResized, this, &SnapHelperEffect::slotWindowStartUserMovedResized);
    connect(effects, &EffectsHandler::windowFinishUserMovedResized, this, &SnapHelperEffect::slotWindowFinishUserMovedResized);
    connect(effects, &EffectsHandler::windowGeometryShapeChanged, this, &SnapHelperEffect::slotWindowGeometryShapeChanged);
}

SnapHelperEffect::~SnapHelperEffect()
{
}

void SnapHelperEffect::reconfigure(ReconfigureFlags flags)
{
    Q_UNUSED(flags)

    m_animation.timeLine.setDuration(
        std::chrono::milliseconds(static_cast<int>(animationTime(250))));
}

void SnapHelperEffect::prePaintScreen(ScreenPrePaintData &data, int time)
{
    if (m_animation.active) {
        m_animation.timeLine.update(std::chrono::milliseconds(time));
    }

    effects->prePaintScreen(data, time);
}

void SnapHelperEffect::paintScreen(int mask, QRegion region, ScreenPaintData &data)
{
    effects->paintScreen(mask, region, data);

    const qreal opacityFactor = m_animation.active
        ? m_animation.timeLine.value()
        : 1.0;

    // Display the guide
    if (effects->isOpenGLCompositing()) {
        GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer();
        vbo->reset();
        vbo->setUseColor(true);
        ShaderBinder binder(ShaderTrait::UniformColor);
        binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, data.projectionMatrix());
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

        QColor color = s_lineColor;
        color.setAlphaF(color.alphaF() * opacityFactor);
        vbo->setColor(color);

        glLineWidth(s_lineWidth);
        QVector<float> verts;
        verts.reserve(effects->numScreens() * 24);
        for (int i = 0; i < effects->numScreens(); ++i) {
            const QRect rect = effects->clientArea(ScreenArea, i, 0);
            const int midX = rect.x() + rect.width() / 2;
            const int midY = rect.y() + rect.height() / 2 ;
            const int halfWidth = m_geometry.width() / 2;
            const int halfHeight = m_geometry.height() / 2;

            // Center vertical line.
            verts << rect.x() + rect.width() / 2 << rect.y();
            verts << rect.x() + rect.width() / 2 << rect.y() + rect.height();

            // Center horizontal line.
            verts << rect.x() << rect.y() + rect.height() / 2;
            verts << rect.x() + rect.width() << rect.y() + rect.height() / 2;

            // Top edge of the window outline.
            verts << midX - halfWidth - s_lineWidth / 2 << midY - halfHeight;
            verts << midX + halfWidth + s_lineWidth / 2 << midY - halfHeight;

            // Right edge of the window outline.
            verts << midX + halfWidth << midY - halfHeight + s_lineWidth / 2;
            verts << midX + halfWidth << midY + halfHeight - s_lineWidth / 2;

            // Bottom edge of the window outline.
            verts << midX + halfWidth + s_lineWidth / 2 << midY + halfHeight;
            verts << midX - halfWidth - s_lineWidth / 2 << midY + halfHeight;

            // Left edge of the window outline.
            verts << midX - halfWidth << midY + halfHeight - s_lineWidth / 2;
            verts << midX - halfWidth << midY - halfHeight + s_lineWidth / 2;
        }
        vbo->setData(verts.count() / 2, 2, verts.data(), nullptr);
        vbo->render(GL_LINES);

        glDisable(GL_BLEND);
        glLineWidth(1.0);
    }
    if (effects->compositingType() == XRenderCompositing) {
#ifdef KWIN_HAVE_XRENDER_COMPOSITING
        for (int i = 0; i < effects->numScreens(); ++i) {
            const QRect rect = effects->clientArea(ScreenArea, i, 0);
            const int midX = rect.x() + rect.width() / 2;
            const int midY = rect.y() + rect.height() / 2 ;
            const int halfWidth = m_geometry.width() / 2;
            const int halfHeight = m_geometry.height() / 2;

            xcb_rectangle_t rects[6];

            // Center vertical line.
            rects[0].x = rect.x() + rect.width() / 2 - s_lineWidth / 2;
            rects[0].y = rect.y();
            rects[0].width = s_lineWidth;
            rects[0].height = rect.height();

            // Center horizontal line.
            rects[1].x = rect.x();
            rects[1].y = rect.y() + rect.height() / 2 - s_lineWidth / 2;
            rects[1].width = rect.width();
            rects[1].height = s_lineWidth;

            // Top edge of the window outline.
            rects[2].x = midX - halfWidth - s_lineWidth / 2;
            rects[2].y = midY - halfHeight - s_lineWidth / 2;
            rects[2].width = 2 * halfWidth + s_lineWidth;
            rects[2].height = s_lineWidth;

            // Right edge of the window outline.
            rects[3].x = midX + halfWidth - s_lineWidth / 2;
            rects[3].y = midY - halfHeight + s_lineWidth / 2;
            rects[3].width = s_lineWidth;
            rects[3].height = 2 * halfHeight - s_lineWidth;

            // Bottom edge of the window outline.
            rects[4].x = midX - halfWidth - s_lineWidth / 2;
            rects[4].y = midY + halfHeight - s_lineWidth / 2;
            rects[4].width = 2 * halfWidth + s_lineWidth;
            rects[4].height = s_lineWidth;

            // Left edge of the window outline.
            rects[5].x = midX - halfWidth - s_lineWidth / 2;
            rects[5].y = midY - halfHeight + s_lineWidth / 2;
            rects[5].width = s_lineWidth;
            rects[5].height = 2 * halfHeight - s_lineWidth;

            QColor color = s_lineColor;
            color.setAlphaF(color.alphaF() * opacityFactor);

            xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_OVER, effects->xrenderBufferPicture(),
                                       preMultiply(color), 6, rects);
        }
#endif
    }
    if (effects->compositingType() == QPainterCompositing) {
        QPainter *painter = effects->scenePainter();
        painter->save();
        QColor color = s_lineColor;
        color.setAlphaF(color.alphaF() * opacityFactor);
        QPen pen(color);
        pen.setWidth(s_lineWidth);
        painter->setPen(pen);
        painter->setBrush(Qt::NoBrush);

        for (int i = 0; i < effects->numScreens(); ++i) {
            const QRect rect = effects->clientArea(ScreenArea, i, 0);
            // Center lines.
            painter->drawLine(rect.center().x(), rect.y(), rect.center().x(), rect.y() + rect.height());
            painter->drawLine(rect.x(), rect.center().y(), rect.x() + rect.width(), rect.center().y());

            // Window outline.
            QRect outlineRect(0, 0, m_geometry.width(), m_geometry.height());
            outlineRect.moveCenter(rect.center());
            painter->drawRect(outlineRect);
        }
        painter->restore();
    }
}

void SnapHelperEffect::postPaintScreen()
{
    if (m_animation.active) {
        effects->addRepaint(computeDirtyRegion(m_geometry));
    }

    if (m_animation.timeLine.done()) {
        m_animation.active = false;
    }

    effects->postPaintScreen();
}

void SnapHelperEffect::slotWindowClosed(EffectWindow *w)
{
    if (w != m_window) {
        return;
    }

    m_window = nullptr;

    m_animation.active = true;
    m_animation.timeLine.setDirection(TimeLine::Backward);

    if (m_animation.timeLine.done()) {
        m_animation.timeLine.reset();
    }

    effects->addRepaint(computeDirtyRegion(m_geometry));
}

void SnapHelperEffect::slotWindowStartUserMovedResized(EffectWindow *w)
{
    if (!w->isMovable()) {
        return;
    }

    m_window = w;
    m_geometry = w->geometry();

    m_animation.active = true;
    m_animation.timeLine.setDirection(TimeLine::Forward);

    if (m_animation.timeLine.done()) {
        m_animation.timeLine.reset();
    }

    effects->addRepaint(computeDirtyRegion(m_geometry));
}

void SnapHelperEffect::slotWindowFinishUserMovedResized(EffectWindow *w)
{
    if (w != m_window) {
        return;
    }

    m_window = nullptr;
    m_geometry = w->geometry();

    m_animation.active = true;
    m_animation.timeLine.setDirection(TimeLine::Backward);

    if (m_animation.timeLine.done()) {
        m_animation.timeLine.reset();
    }

    effects->addRepaint(computeDirtyRegion(m_geometry));
}

void SnapHelperEffect::slotWindowGeometryShapeChanged(EffectWindow *w, const QRect &old)
{
    if (w != m_window) {
        return;
    }

    m_geometry = w->geometry();

    effects->addRepaint(computeDirtyRegion(old));
}

bool SnapHelperEffect::isActive() const
{
    return m_window != nullptr || m_animation.active;
}

} // namespace KWin
