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

Copyright (C) 2010 by Fredrik Höglund <fredrik@kde.org>

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 "lanczosfilter.h"
#include "effects.h"

#ifdef KWIN_HAVE_OPENGL_COMPOSITING
    #include <kwinglutils.h>
#endif

#include <kwineffects.h>
#include <KDE/KGlobalSettings>

#include <qmath.h>
#include <cmath>

namespace KWin
{

LanczosFilter::LanczosFilter( QObject* parent )
    : QObject( parent )
#ifdef KWIN_HAVE_OPENGL_COMPOSITING
    , m_offscreenTex( 0 )
    , m_offscreenTarget( 0 )
    , m_shader( 0 )
#endif
    , m_inited( false)
    {
    }

LanczosFilter::~LanczosFilter()
    {
#ifdef KWIN_HAVE_OPENGL_COMPOSITING
    delete m_offscreenTarget;
    delete m_offscreenTex;
    delete m_shader;
#endif
    }

void LanczosFilter::init()
    {
    if (m_inited)
        return;
    m_inited = true;
#ifdef KWIN_HAVE_OPENGL_COMPOSITING
    // check the blacklist
    KSharedConfigPtr config = KSharedConfig::openConfig( "kwinrc" );
    KConfigGroup blacklist = config->group( "Blacklist" ).group( "Lanczos" );
    if( effects->checkDriverBlacklist( blacklist ) )
        {
        kDebug() << "Lanczos Filter disabled by driver blacklist";
        return;
        }

    if ( GLShader::fragmentShaderSupported() &&
         GLShader::vertexShaderSupported() &&
         GLRenderTarget::supported() )
        {
        m_shader = new GLShader(":/resources/lanczos-vertex.glsl", ":/resources/lanczos-fragment.glsl");
        if (m_shader->isValid())
            {
            m_shader->bind();
            m_uTexUnit    = m_shader->uniformLocation("texUnit");
            m_uKernel     = m_shader->uniformLocation("kernel");
            m_uOffsets    = m_shader->uniformLocation("offsets");
            m_shader->unbind();
            }
        else
            {
            kDebug(1212) << "Shader is not valid";
            m_shader = 0;
            }
        }
#endif
    }


void LanczosFilter::updateOffscreenSurfaces()
    {
#ifdef KWIN_HAVE_OPENGL_COMPOSITING
    int w = displayWidth();
    int h = displayHeight();
    if ( !GLTexture::NPOTTextureSupported() )
        {
        w = nearestPowerOfTwo( w );
        h = nearestPowerOfTwo( h );
        }
    if ( !m_offscreenTex || m_offscreenTex->width() != w || m_offscreenTex->height() != h )
        {
        if ( m_offscreenTex )
            {
            delete m_offscreenTex;
            delete m_offscreenTarget;
            }
        m_offscreenTex = new GLTexture( w, h );
        m_offscreenTex->setFilter( GL_LINEAR );
        m_offscreenTex->setWrapMode( GL_CLAMP_TO_EDGE );
        m_offscreenTarget = new GLRenderTarget( m_offscreenTex );
        }
#endif
    }

static float sinc( float x )
    {
    return std::sin( x * M_PI ) / ( x * M_PI );
    }

static float lanczos( float x, float a )
    {
    if ( qFuzzyCompare( x + 1.0, 1.0 ) )
        return 1.0;

    if ( qAbs( x ) >= a )
        return 0.0;

    return sinc( x ) * sinc( x / a );
    }

void LanczosFilter::createKernel( float delta, int *size )
    {
    const float a = 2.0;

    // The two outermost samples always fall at points where the lanczos
    // function returns 0, so we'll skip them.
    const int sampleCount = qBound( 3, qCeil(delta * a) * 2 + 1 - 2, 49 );
    const int center = sampleCount / 2;
    const int kernelSize = center + 1;
    const float factor = 1.0 / delta;

    QVector<float> values( kernelSize );
    float sum = 0;

    for ( int i = 0; i < kernelSize; i++ ) {
        const float val = lanczos( i * factor, a );
        sum += i > 0 ? val * 2 : val;
        values[i] = val;
    }

    memset(m_kernel, 0, 25 * sizeof(QVector4D));

    // Normalize the kernel
    for ( int i = 0; i < kernelSize; i++ ) {
        const float val = values[i] / sum;
        m_kernel[i] = QVector4D( val, val, val, val );
    }

    *size = kernelSize;
    }

void LanczosFilter::createOffsets( int count, float width, Qt::Orientation direction )
    {
    memset(m_offsets, 0, 25 * sizeof(QVector2D));
    for ( int i = 0; i < count; i++ ) {
        m_offsets[i] = ( direction == Qt::Horizontal ) ?
                QVector2D( i / width, 0 ) : QVector2D( 0, i / width );
    }
    }

void LanczosFilter::performPaint( EffectWindowImpl* w, int mask, QRegion region, WindowPaintData& data )
    {
#ifdef KWIN_HAVE_OPENGL_COMPOSITING
    if( effects->compositingType() == KWin::OpenGLCompositing &&
        KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects )
        {
        if (!m_inited)
            init();
        if ( m_shader )
            {
            int tx = data.xTranslate + w->x();
            int ty = data.yTranslate + w->y();
            int tw = w->width()*data.xScale;
            int th = w->height()*data.yScale;

            int sw = w->width();
            int sh = w->height();

            WindowPaintData thumbData = data;
            thumbData.xScale = 1.0;
            thumbData.yScale = 1.0;
            thumbData.xTranslate = -w->x();
            thumbData.yTranslate = -w->y();

            // Bind the offscreen FBO and draw the window on it unscaled
            updateOffscreenSurfaces();
            effects->pushRenderTarget( m_offscreenTarget );

            glClear( GL_COLOR_BUFFER_BIT );
            w->sceneWindow()->performPaint( mask, QRegion(0, 0, sw, sh), thumbData );

            // Create a scratch texture and copy the rendered window into it
            GLTexture tex( sw, sh );
            tex.setFilter( GL_LINEAR );
            tex.setWrapMode( GL_CLAMP_TO_EDGE );
            tex.bind();

            glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, 0, m_offscreenTex->height() - sh, sw, sh );

            // Set up the shader for horizontal scaling
            float dx = sw / float(tw);
            int kernelSize;
            createKernel( dx, &kernelSize );
            createOffsets( kernelSize, sw, Qt::Horizontal );

            m_shader->bind();
            glUniform1i( m_uTexUnit, 0 );
            glUniform2fv( m_uOffsets, 25, (const GLfloat*)m_offsets );
            glUniform4fv( m_uKernel, 25, (const GLfloat*)m_kernel );

            // Draw the window back into the FBO, this time scaled horizontally
            glClear( GL_COLOR_BUFFER_BIT );

            glBegin( GL_QUADS );
            glTexCoord2f( 0, 0 ); glVertex2i( 0,   0 ); // Top left
            glTexCoord2f( 1, 0 ); glVertex2i( tw,  0 ); // Top right
            glTexCoord2f( 1, 1 ); glVertex2i( tw, sh ); // Bottom right
            glTexCoord2f( 0, 1 ); glVertex2i( 0,  sh ); // Bottom left
            glEnd();

            // At this point we don't need the scratch texture anymore
            tex.unbind();
            tex.discard();

            // Unbind the FBO
            effects->popRenderTarget();

            // Set up the shader for vertical scaling
            float dy = sh / float(th);
            createKernel( dy, &kernelSize );
            createOffsets( kernelSize, m_offscreenTex->height(), Qt::Vertical );

            glUniform2fv( m_uOffsets, 25, (const GLfloat*)m_offsets );
            glUniform4fv( m_uKernel, 25, (const GLfloat*)m_kernel );

            float sx2 = tw / float(m_offscreenTex->width());
            float sy2 = 1 - (sh / float(m_offscreenTex->height()));

            // Now draw the horizontally scaled window in the FBO at the right
            // coordinates on the screen, while scaling it vertically and blending it.
            m_offscreenTex->bind();

            glPushAttrib( GL_COLOR_BUFFER_BIT );
            glEnable( GL_BLEND );
            glBlendFunc( GL_ONE, GL_ONE_MINUS_SRC_ALPHA );

            glBegin( GL_QUADS );
            glTexCoord2f( 0,    sy2 ); glVertex2i( tx,       ty );      // Top left
            glTexCoord2f( sx2,  sy2 ); glVertex2i( tx + tw,  ty );      // Top right
            glTexCoord2f( sx2,  1 );   glVertex2i( tx + tw,  ty + th ); // Bottom right
            glTexCoord2f( 0,    1 );   glVertex2i( tx,       ty + th ); // Bottom left
            glEnd();

            glPopAttrib();
            m_offscreenTex->unbind();
            m_shader->unbind();

            // Delete the offscreen surface after 5 seconds
            m_timer.start( 5000, this );
            return;
            }
        } // if ( effects->compositingType() == KWin::OpenGLCompositing )
#endif
    w->sceneWindow()->performPaint( mask, region, data );
    } // End of function

void LanczosFilter::timerEvent( QTimerEvent *event )
    {
#ifdef KWIN_HAVE_OPENGL_COMPOSITING
    if (event->timerId() == m_timer.timerId())
        {
        m_timer.stop();

        delete m_offscreenTarget;
        delete m_offscreenTex;
        m_offscreenTarget = 0;
        m_offscreenTex = 0;
        }
#endif
    }

} // namespace

