/*************************************************************************
 * Copyright (C) 2014 by Hugo Pereira Da Costa <hugo.pereira@free.fr>    *
 *                                                                       *
 * 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, write to the                         *
 * Free Software Foundation, Inc.,                                       *
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA .        *
 *************************************************************************/

#include "breezeshadowhelper.h"

#include "breeze.h"
#include "breezehelper.h"
#include "breezepropertynames.h"
#include "breezestyleconfigdata.h"

#include <QDockWidget>
#include <QEvent>
#include <QApplication>
#include <QMenu>
#include <QPainter>
#include <QPixmap>
#include <QToolBar>
#include <QTextStream>

#if BREEZE_HAVE_X11
#include <QX11Info>
#endif

namespace Breeze
{

    const char ShadowHelper::netWMShadowAtomName[] ="_KDE_NET_WM_SHADOW";

    //_____________________________________________________
    ShadowHelper::ShadowHelper( QObject* parent, Helper& helper ):
        QObject( parent ),
        _helper( helper )
        #if BREEZE_HAVE_X11
        ,_gc( 0 ),
        _atom( 0 )
        #endif
    {}

    //_______________________________________________________
    ShadowHelper::~ShadowHelper( void )
    {

        #if BREEZE_HAVE_X11
        if( Helper::isX11() )
        { foreach( const quint32& value, _pixmaps  ) xcb_free_pixmap( Helper::connection(), value ); }
        #endif

    }

    //______________________________________________
    void ShadowHelper::reset( void )
    {
        #if BREEZE_HAVE_X11
        if( Helper::isX11() )
        { foreach( const quint32& value, _pixmaps  ) xcb_free_pixmap( Helper::connection(), value ); }
        #endif

        _pixmaps.clear();
        _shadowTiles = TileSet();

    }

    //_______________________________________________________
    bool ShadowHelper::registerWidget( QWidget* widget, bool force )
    {
        // make sure widget is not already registered
        if( _widgets.contains( widget ) ) return false;

        // check if widget qualifies
        if( !( force || acceptWidget( widget ) ) )
        { return false; }

        // store in map and add destroy signal connection
        widget->removeEventFilter( this );
        widget->installEventFilter( this );
        _widgets.insert( widget, 0 );

        /*
        need to install shadow directly when widget "created" state is already set
        since WinID changed is never called when this is the case
        */
        if( widget->testAttribute(Qt::WA_WState_Created) && installX11Shadows( widget ) )
        {  _widgets.insert( widget, widget->winId() ); }

        connect( widget, SIGNAL(destroyed(QObject*)), SLOT(objectDeleted(QObject*)) );

        return true;

    }

    //_______________________________________________________
    void ShadowHelper::unregisterWidget( QWidget* widget )
    {
        if( _widgets.remove( widget ) )
        { uninstallX11Shadows( widget ); }
    }

    //_______________________________________________________
    void ShadowHelper::loadConfig( void )
    {

        // reset
        reset();

        // update property for registered widgets
        for( QMap<QWidget*,WId>::const_iterator iter = _widgets.constBegin(); iter != _widgets.constEnd(); ++iter )
        { installX11Shadows( iter.key() ); }

    }

    //_______________________________________________________
    bool ShadowHelper::eventFilter( QObject* object, QEvent* event )
    {

        // check event type
        if( event->type() != QEvent::WinIdChange ) return false;

        // cast widget
        QWidget* widget( static_cast<QWidget*>( object ) );

        // install shadows and update winId
        if( installX11Shadows( widget ) )
        { _widgets.insert( widget, widget->winId() ); }

        return false;

    }

    //_______________________________________________________
    TileSet ShadowHelper::shadowTiles( void )
    {
        if( !_shadowTiles.isValid() )
        {

            const QPalette palette( QApplication::palette() );
            const QColor shadowColor( StyleConfigData::shadowColor() );

            // metrics
            const int shadowSize = StyleConfigData::shadowSize()*12/16;
            const int shadowStrength = StyleConfigData::shadowStrength();

            // pixmap
            QPixmap pixmap = _helper.highDpiPixmap( shadowSize*2 );
            pixmap.fill( Qt::transparent );

            // create gradient
            // gaussian delta function
            auto alpha = [](qreal x) { return std::exp( -x*x/0.15 ); };

            // color calculation delta function
            auto gradientStopColor = [](QColor color, int alpha)
            {
                color.setAlpha(alpha);
                return color;
            };

            QRadialGradient radialGradient( shadowSize, shadowSize, shadowSize);
            for( int i = 0; i < 10; ++i )
            {
                const qreal x( qreal( i )/9 );
                radialGradient.setColorAt(x,  gradientStopColor(shadowColor, alpha(x)*shadowStrength));
            }

            radialGradient.setColorAt(1, gradientStopColor( shadowColor, 0 ) );

            // fill
            QPainter p(&pixmap);
            p.setCompositionMode(QPainter::CompositionMode_Source);
            p.fillRect( pixmap.rect(), radialGradient);
            p.end();

            // create tiles from pixmap
            _shadowTiles = TileSet( pixmap,
                shadowSize,
                shadowSize,
                1, 1 );

        }

        return _shadowTiles;

    }


    //_______________________________________________________
    void ShadowHelper::objectDeleted( QObject* object )
    { _widgets.remove( static_cast<QWidget*>( object ) ); }

    //_______________________________________________________
    bool ShadowHelper::isMenu( QWidget* widget ) const
    { return qobject_cast<QMenu*>( widget ); }

    //_______________________________________________________
    bool ShadowHelper::isToolTip( QWidget* widget ) const
    { return widget->inherits( "QTipLabel" ) || (widget->windowFlags() & Qt::WindowType_Mask) == Qt::ToolTip; }

    //_______________________________________________________
    bool ShadowHelper::isDockWidget( QWidget* widget ) const
    { return qobject_cast<QDockWidget*>( widget ); }

    //_______________________________________________________
    bool ShadowHelper::isToolBar( QWidget* widget ) const
    { return qobject_cast<QToolBar*>( widget ); }

    //_______________________________________________________
    bool ShadowHelper::acceptWidget( QWidget* widget ) const
    {

        // flags
        if( widget->property( PropertyNames::netWMSkipShadow ).toBool() ) return false;
        if( widget->property( PropertyNames::netWMForceShadow ).toBool() ) return true;

        // menus
        if( isMenu( widget ) ) return true;

        // combobox dropdown lists
        if( widget->inherits( "QComboBoxPrivateContainer" ) ) return true;

        // tooltips
        if( isToolTip( widget ) && !widget->inherits( "Plasma::ToolTip" ) )
        { return true; }

        // detached widgets
        if( isDockWidget( widget ) || isToolBar( widget ) )
        { return true; }

        // reject
        return false;
    }

    //______________________________________________
    const QVector<quint32>& ShadowHelper::createPixmapHandles( void )
    {

        /**
        shadow atom and property specification available at
        http://community.kde.org/KWin/Shadow
        */

        // create atom
        #if BREEZE_HAVE_X11
        if( !_atom && Helper::isX11() ) _atom = _helper.createAtom( QLatin1String( netWMShadowAtomName ) );
        #endif

        shadowTiles();

        // make sure size is valid
        if( _pixmaps.empty() && _shadowTiles.isValid() )
        {

            _pixmaps.append( createPixmap( _shadowTiles.pixmap( 1 ) ) );
            _pixmaps.append( createPixmap( _shadowTiles.pixmap( 2 ) ) );
            _pixmaps.append( createPixmap( _shadowTiles.pixmap( 5 ) ) );
            _pixmaps.append( createPixmap( _shadowTiles.pixmap( 8 ) ) );
            _pixmaps.append( createPixmap( _shadowTiles.pixmap( 7 ) ) );
            _pixmaps.append( createPixmap( _shadowTiles.pixmap( 6 ) ) );
            _pixmaps.append( createPixmap( _shadowTiles.pixmap( 3 ) ) );
            _pixmaps.append( createPixmap( _shadowTiles.pixmap( 0 ) ) );

        }

        // return relevant list of pixmap handles
        return _pixmaps;

    }

    //______________________________________________
    quint32 ShadowHelper::createPixmap( const QPixmap& source )
    {

        // do nothing for invalid pixmaps
        if( source.isNull() ) return 0;
        if( !Helper::isX11() ) return 0;

        /*
        in some cases, pixmap handle is invalid. This is the case notably
        when Qt uses to RasterEngine. In this case, we create an X11 Pixmap
        explicitly and draw the source pixmap on it.
        */

        #if BREEZE_HAVE_X11

        const int width( source.width() );
        const int height( source.height() );

        // create X11 pixmap
        xcb_pixmap_t pixmap = xcb_generate_id( Helper::connection() );
        xcb_create_pixmap( Helper::connection(), 32, pixmap, QX11Info::appRootWindow(), width, height );

        // create gc
        if( !_gc )
        {
            _gc = xcb_generate_id( Helper::connection() );
            xcb_create_gc( Helper::connection(), _gc, pixmap, 0, 0x0 );
        }

        // create image from QPixmap and assign to pixmap
        QImage image( source.toImage() );
        xcb_put_image( Helper::connection(), XCB_IMAGE_FORMAT_Z_PIXMAP, pixmap, _gc, image.width(), image.height(), 0, 0, 0, 32, image.byteCount(), image.constBits());

        return pixmap;

        #else
        return 0;
        #endif

    }

    //_______________________________________________________
    bool ShadowHelper::installX11Shadows( QWidget* widget )
    {
        // check widget and shadow
        if( !widget ) return false;
        if( !Helper::isX11() ) return false;

        #if BREEZE_HAVE_X11
        #ifndef QT_NO_XRENDER

        /*
        From bespin code. Supposibly prevent playing with some 'pseudo-widgets'
        that have winId matching some other -random- window
        */
        if( !(widget->testAttribute(Qt::WA_WState_Created) || widget->internalWinId() ))
        { return false; }

        // create pixmap handles if needed
        const QVector<quint32>& pixmaps( createPixmapHandles() );
        if( pixmaps.size() != numPixmaps ) return false;

        // create data
        // add pixmap handles
        QVector<quint32> data;
        foreach( const quint32& value, pixmaps )
        { data.append( value ); }

        // get devicePixelRatio
        // for testing purposes only
        const qreal devicePixelRatio( _helper.devicePixelRatio( _shadowTiles.pixmap( 0 ) ) );

        // metrics
        const int shadowSize = StyleConfigData::shadowSize()*12/16;
        const int shadowOffset = qMax( shadowSize/2, Metrics::Shadow_Overlap*2 );

        // define shadows padding
        int size( shadowSize - Metrics::Shadow_Overlap );
        int topSize = ( size - shadowOffset ) * devicePixelRatio;
        int bottomSize = size * devicePixelRatio;
        const int leftSize( (size - shadowOffset) * devicePixelRatio );
        const int rightSize( size * devicePixelRatio );

        if( widget->inherits( "QBalloonTip" ) )
        {

            // balloon tip needs special margins to deal with the arrow
            int top = 0;
            int bottom = 0;
            widget->getContentsMargins( nullptr, &top, nullptr, &bottom );

            // also need to decrement default size further due to extra hard coded round corner
            size -= 2 * devicePixelRatio;

            // it seems arrow can be either to the top or the bottom. Adjust margins accordingly
            if( top > bottom ) topSize -= (top - bottom);
            else bottomSize -= (bottom - top );

        }

        // assign to data and xcb property
        data << topSize << rightSize << bottomSize << leftSize;
        xcb_change_property( Helper::connection(), XCB_PROP_MODE_REPLACE, widget->winId(), _atom, XCB_ATOM_CARDINAL, 32, data.size(), data.constData() );
        xcb_flush( Helper::connection() );

        return true;

        #endif
        #endif

        return false;

    }

    //_______________________________________________________
    void ShadowHelper::uninstallX11Shadows( QWidget* widget ) const
    {

        #if BREEZE_HAVE_X11
        if( !Helper::isX11() ) return;
        if( !( widget && widget->testAttribute(Qt::WA_WState_Created) ) ) return;
        xcb_delete_property( Helper::connection(), widget->winId(), _atom);
        #else
        Q_UNUSED( widget )
        #endif

    }

}
