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

Copyright (C) 2007 Rivo Laks <rivolaks@hot.ee>

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 "blur.h"

#include <kwinglutils.h>

#include <QString>

#include <kdebug.h>


namespace KWin
{

KWIN_EFFECT( blur, BlurEffect )
KWIN_EFFECT_SUPPORTED( blur, BlurEffect::supported() )


BlurEffect::BlurEffect() : Effect()
    {
    mSceneTexture = 0;
    mTmpTexture = 0;
    mBlurTexture = 0;
    mSceneTarget = 0;
    mTmpTarget = 0;
    mBlurTarget = 0;
    mBlurShader = 0;
    mWindowShader = 0;

    mBlurRadius = 4;
    mValid = loadData();
    if( !mValid )
        {
        kWarning(1212) << "Loading failed";
        }
    effects->addRepaintFull();
    }

BlurEffect::~BlurEffect()
    {
    effects->addRepaintFull();
    delete mSceneTexture;
    delete mTmpTexture;
    delete mBlurTexture;
    delete mSceneTarget;
    delete mTmpTarget;
    delete mBlurTarget;
    delete mBlurShader;
    delete mWindowShader;
    }


bool BlurEffect::loadData()
    {
    // Create texture and render target
    int texw = displayWidth();
    int texh = displayHeight();
    if( !GLTexture::NPOTTextureSupported() )
    {
        kWarning( 1212 ) << "NPOT textures not supported, wasting some memory" ;
        texw = nearestPowerOfTwo(texw);
        texh = nearestPowerOfTwo(texh);
    }
    mSceneTexture = new GLTexture(texw, texh);
    mSceneTexture->setFilter(GL_LINEAR);
    mTmpTexture = new GLTexture(texw, texh);
    mTmpTexture->setFilter(GL_LINEAR);
    mBlurTexture = new GLTexture(texw, texh);

    mSceneTarget = new GLRenderTarget(mSceneTexture);
    if( !mSceneTarget->valid() )
        return false;
    mTmpTarget = new GLRenderTarget(mTmpTexture);
    if( !mTmpTarget->valid() )
        return false;
    mBlurTarget = new GLRenderTarget(mBlurTexture);
    if( !mBlurTarget->valid() )
        return false;

    mBlurShader = loadShader("blur");
    if( !mBlurShader )
        return false;
    mWindowShader = loadShader("blur-render");
    if( !mWindowShader )
        return false;

    mBlurShader->bind();
    mBlurShader->setUniform("inputTex", 0);
    mBlurShader->setUniform("textureWidth", (float)texw);
    mBlurShader->setUniform("textureHeight", (float)texh);
    mBlurShader->unbind();

    mWindowShader->bind();
    mWindowShader->setUniform("windowTex", 0);
    mWindowShader->setUniform("backgroundTex", 4);
    mWindowShader->setUniform("blurTextureWidth", (float)texw);
    mWindowShader->setUniform("blurTextureHeight", (float)texh);
    mWindowShader->unbind();

    return true;
    }

GLShader* BlurEffect::loadShader(const QString& name)
{
    QString fragmentshader =  KGlobal::dirs()->findResource("data", "kwin/" + name + ".frag");
    QString vertexshader =  KGlobal::dirs()->findResource("data", "kwin/" + name + ".vert");
    if(fragmentshader.isEmpty() || vertexshader.isEmpty())
    {
        kError(1212) << "Couldn't locate shader files for '" << name << "'" << endl;
        return false;
    }
    GLShader* shader = new GLShader(vertexshader, fragmentshader);
    if(!shader->isValid())
    {
        kError(1212) << "Shader '" << name << "' failed to load!" << endl;
        delete shader;
        return 0;
    }
    return shader;
}

bool BlurEffect::supported()
    {
    return GLRenderTarget::supported() &&
            GLShader::fragmentShaderSupported() &&
            (effects->compositingType() == OpenGLCompositing);
    }

QRegion BlurEffect::expandedRegion( const QRegion& region ) const
    {
    QRegion expandedregion;
    foreach( const QRect& r, region.rects() )
        expandedregion += r.adjusted( -mBlurRadius, -mBlurRadius, mBlurRadius, mBlurRadius );
    return expandedregion;
    }

void BlurEffect::prePaintScreen( ScreenPrePaintData& data, int time )
    {
    mTransparentWindows = 0;
    mScreenDirty = QRegion();
    mBlurDirty = QRegion();
    mBlurMask = QRegion();

    effects->prePaintScreen(data, time);
    }

void BlurEffect::prePaintWindow( EffectWindow* w, WindowPrePaintData& data, int time )
    {
    // Expand the painted area
    mBlurMask |= expandedRegion( data.paint );
    data.paint |= expandedRegion( mBlurMask );
    effects->prePaintWindow( w, data, time );

    if( w->isPaintingEnabled() && ( data.mask & PAINT_WINDOW_TRANSLUCENT ))
        mTransparentWindows++;
    data.setTranslucent();
    }

void BlurEffect::paintScreen( int mask, QRegion region, ScreenPaintData& data )
    {
    // TODO: prePaintWindow() gets called _after_ paintScreen(), so we have no
    //  way of knowing here whether there will be any translucent windows or
    //  not. If we'd know that there's no translucent windows then we could
    //  render straight onto screen, saving some time.
    // HACK disable blur when there is a fullscreen effects. Needed for e.g. cube to work
    if( mValid && !effects->activeFullScreenEffect() /*&& mTransparentWindows*/ )
        {
        // rendering everything onto render target
        effects->pushRenderTarget(mSceneTarget);
        effects->paintScreen( mask, region, data );
        effects->popRenderTarget();

        // Copy changed areas back onto screen
        mScreenDirty &= mBlurMask;
        if( !mScreenDirty.isEmpty() )
            {
            if( mask & PAINT_SCREEN_TRANSFORMED )
                {
                // We don't want any transformations when working with our own
                //  textures, so load an identity matrix
                glMatrixMode( GL_MODELVIEW );
                glPushMatrix();
                glLoadIdentity();
                }

            GLTexture* tex = mSceneTexture;
            int pixels = 0;
            tex->bind();
            tex->enableUnnormalizedTexCoords();
            foreach( QRect r, mScreenDirty.rects() ) // krazy:exclude=foreach
                {
                r.adjust(0, -1, 0, -1);
                int rx2 = r.x() + r.width();
                int ry2 = r.y() + r.height();
                glBegin(GL_QUADS);
                    glTexCoord2f( r.x(), ry2 );   glVertex2f( r.x(), ry2 );
                    glTexCoord2f( rx2  , ry2 );   glVertex2f( rx2  , ry2 );
                    glTexCoord2f( rx2  , r.y() ); glVertex2f( rx2  , r.y() );
                    glTexCoord2f( r.x(), r.y() ); glVertex2f( r.x(), r.y() );
                glEnd();
                pixels += r.width()*r.height();
                }
            tex->disableUnnormalizedTexCoords();
            tex->unbind();

            if( mask & PAINT_SCREEN_TRANSFORMED )
                {
                // Restore the original matrix
                glPopMatrix();
                }
//             kDebug(1212) << "Copied" << mScreenDirty.rects().count() << "rects,    pixels:" << pixels;
            }

        }
    else
        {
        effects->paintScreen( mask, region, data );
        }
    }

void BlurEffect::drawWindow( EffectWindow* w, int mask, QRegion region, WindowPaintData& data )
    {
    // HACK disable blur when there is a fullscreen effects. Needed for e.g. cube to work
    if( mValid && !effects->activeFullScreenEffect() /*&& mTransparentWindows*/ )
        {
            if( mask & PAINT_WINDOW_TRANSLUCENT &&
                ((data.opacity != 1.0 || data.contents_opacity != 1.0 || data.decoration_opacity != 1.0 ) ||
                w->hasAlpha()))
            {

            double blurAmount = data.opacity;
            if( blurAmount >= 1.0 )
            {
                blurAmount = data.contents_opacity;
                if( blurAmount >= 1.0 )
                    {
                    blurAmount = data.decoration_opacity;
                    if( blurAmount >= 1.0 )
                        blurAmount = 1.0;
                    }
            }
            // Round to nearest 0.05
            blurAmount = double( qRound( blurAmount * 20.0 )) / 20.0;

            // Make sure the blur texture is up to date
            if( mask & PAINT_SCREEN_TRANSFORMED )
                {
                // We don't want any transformations when working with our own
                //  textures, so load an identity matrix
                glPushMatrix();
                glLoadIdentity();
                }
            // If we're having transformations, we don't know the window's
            //  transformed position on the screen and thus have to update the
            //  entire screen
            if( mask & ( PAINT_WINDOW_TRANSFORMED | PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS ) )
                updateBlurTexture( QRegion(0, 0, displayWidth(), displayHeight()) , blurAmount );
            else
                updateBlurTexture( mBlurDirty , blurAmount );
            mBlurDirty = QRegion();
            if( mask & PAINT_SCREEN_TRANSFORMED )
                // Restore the original matrix
                glPopMatrix();

            // Set custom shader to render the window with
            mWindowShader->bind();
            data.shader = mWindowShader;
            // Put the blur texture to tex unit 4
            glActiveTexture(GL_TEXTURE4);
            mBlurTexture->bind();
            glActiveTexture(GL_TEXTURE0);

            // Paint
            effects->drawWindow( w, mask, region, data );

            // Disable blur texture and shader
            glActiveTexture(GL_TEXTURE4);
            mBlurTexture->unbind();
            glActiveTexture(GL_TEXTURE0);
            mWindowShader->unbind();
            }
        else
            {
            // Opaque window
            // Paint to the rendertarget (which is already being used)
            effects->drawWindow( w, mask, region, data );
            }
        // Mark the window's region as dirty
        mScreenDirty += region;
        mBlurDirty += region & mBlurMask;
        }
    else
        // If there are no translucent windows then paint as usual
        effects->drawWindow( w, mask, region, data );
    }

void BlurEffect::updateBlurTexture(const QRegion& region, double blurAmount )
    {
    QRect bounding = region.boundingRect();
    QVector<QRect> rects = region.rects();
    int totalarea = 0;
    foreach( const QRect &r, rects )
        totalarea += r.width() * r.height();
    if( (int)(totalarea * 1.33 + 100 ) < bounding.width() * bounding.height() )
        {
        // Use small rects
        updateBlurTexture(rects, blurAmount);
        }
    else
        {
        // Bounding rect is probably cheaper
        QVector<QRect> tmp( 1, bounding );
        updateBlurTexture( tmp , blurAmount );
        }
    }

void BlurEffect::updateBlurTexture(const QVector<QRect>& rects, double blurAmount )
    {
    // Blur
    // First pass (vertical)
    mBlurShader->bind();
    effects->pushRenderTarget(mTmpTarget);
    mBlurShader->setAttribute("xBlur", 0.0f);
    mBlurShader->setAttribute("yBlur", float(blurAmount) );

    mSceneTexture->bind();

    foreach( const QRect &r, rects )
        {
        // We change x coordinates here because horizontal blur pass (which
        //  comes after this one) also uses pixels that are horizontally edging
        //  the blurred area. Thus we need to make sure that those pixels are
        //  also updated.
        glBegin(GL_QUADS);
            glVertex2f( r.x()-mBlurRadius            , r.y() + r.height() );
            glVertex2f( r.x() + r.width()+mBlurRadius, r.y() + r.height() );
            glVertex2f( r.x() + r.width()+mBlurRadius, r.y() );
            glVertex2f( r.x()-mBlurRadius            , r.y() );
        glEnd();
        }


    mSceneTexture->unbind();
    effects->popRenderTarget();

    // Second pass (horizontal)
    effects->pushRenderTarget(mBlurTarget);
    mBlurShader->setAttribute("xBlur", float(blurAmount) );
    mBlurShader->setAttribute("yBlur", 0.0f);

    mTmpTexture->bind();

    foreach( const QRect &r, rects )
        {
        glBegin(GL_QUADS);
            glVertex2f( r.x()            , r.y() + r.height() );
            glVertex2f( r.x() + r.width(), r.y() + r.height() );
            glVertex2f( r.x() + r.width(), r.y() );
            glVertex2f( r.x()            , r.y() );
        glEnd();
        }


    mTmpTexture->unbind();
    effects->popRenderTarget();
    mBlurShader->unbind();
    }

} // namespace

