/*
 * SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa <hugo.pereira@free.fr>
 * SPDX-FileCopyrightText: 2018, 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "breezeshadowhelper.h"

#include "breezemetrics.h"
#include "breezeboxshadowrenderer.h"
#include "breezehelper.h"
#include "breezepropertynames.h"
#include "breezestyleconfigdata.h"

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

namespace
{
    using Breeze::CompositeShadowParams;
    using Breeze::ShadowParams;

    const CompositeShadowParams s_shadowParams[] = {
        // None
        CompositeShadowParams(),
        // Small
        CompositeShadowParams(
            QPoint(0, 3),
            ShadowParams(QPoint(0, 0), 12, 0.26),
            ShadowParams(QPoint(0, -2), 6, 0.16)),
        // Medium
        CompositeShadowParams(
            QPoint(0, 4),
            ShadowParams(QPoint(0, 0), 16, 0.24),
            ShadowParams(QPoint(0, -2), 8, 0.14)),
        // Large
        CompositeShadowParams(
            QPoint(0, 5),
            ShadowParams(QPoint(0, 0), 20, 0.22),
            ShadowParams(QPoint(0, -3), 10, 0.12)),
        // Very Large
        CompositeShadowParams(
            QPoint(0, 6),
            ShadowParams(QPoint(0, 0), 24, 0.2),
            ShadowParams(QPoint(0, -3), 12, 0.1))
    };
}

namespace Breeze
{

    //_____________________________________________________
    CompositeShadowParams ShadowHelper::lookupShadowParams(int shadowSizeEnum)
    {

        switch (shadowSizeEnum) {
        case StyleConfigData::ShadowNone:
            return s_shadowParams[0];
        case StyleConfigData::ShadowSmall:
            return s_shadowParams[1];
        case StyleConfigData::ShadowMedium:
            return s_shadowParams[2];
        case StyleConfigData::ShadowLarge:
            return s_shadowParams[3];
        case StyleConfigData::ShadowVeryLarge:
            return s_shadowParams[4];
        default:
            // Fallback to the Large size.
            return s_shadowParams[3];
        }

    }

    //_____________________________________________________
    ShadowHelper::ShadowHelper( QObject* parent, Helper& helper ):
        QObject( parent ),
        _helper( helper )
    {
    }

    //_______________________________________________________
    ShadowHelper::~ShadowHelper()
    {
        qDeleteAll( _shadows );
    }

    //______________________________________________
    void ShadowHelper::reset()
    {
        _tiles.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; }

        // try create shadow directly
        installShadows( widget );
        _widgets.insert( widget );

        // install event filter
        widget->removeEventFilter( this );
        widget->installEventFilter( this );

        // connect destroy signal
        connect( widget, &QObject::destroyed, this, &ShadowHelper::widgetDeleted );

        return true;

    }

    //_______________________________________________________
    void ShadowHelper::unregisterWidget( QWidget* widget )
    {
        if( _widgets.remove( widget ) )
        {
            // uninstall the event filter
            widget->removeEventFilter( this );

            // disconnect all signals
            disconnect( widget, nullptr, this, nullptr );

            // uninstall the shadow
            uninstallShadows( widget );
        }
    }

    //_______________________________________________________
    void ShadowHelper::loadConfig()
    {

        // reset
        reset();

        // update property for registered widgets
        for( QWidget* widget : _widgets)
        { installShadows( widget ); }

    }

    //_______________________________________________________
    bool ShadowHelper::eventFilter( QObject* object, QEvent* event )
    {
        if( Helper::isX11() )
        {
            // check event type
            if( event->type() != QEvent::WinIdChange ) return false;

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

            // install shadows and update winId
            installShadows( widget );

        } else {
            if( event->type() != QEvent::PlatformSurface ) return false;

            QWidget* widget( static_cast<QWidget*>( object ) );
            QPlatformSurfaceEvent* surfaceEvent( static_cast<QPlatformSurfaceEvent*>( event ) );

            switch( surfaceEvent->surfaceEventType() )
            {
                case QPlatformSurfaceEvent::SurfaceCreated:
                    installShadows( widget );
                    break;
                case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed:
                    // Don't care.
                    break;
            }
        }

        return false;

    }

    //_______________________________________________________
    TileSet ShadowHelper::shadowTiles()
    {
        const CompositeShadowParams params = lookupShadowParams(StyleConfigData::shadowSize());

        if (params.isNone()) {
            return TileSet();
        } else if (_shadowTiles.isValid()) {
            return _shadowTiles;
        }

        auto withOpacity = [](const QColor &color, qreal opacity) -> QColor {
            QColor c(color);
            c.setAlphaF(opacity);
            return c;
        };

        const QColor color = StyleConfigData::shadowColor();
        const qreal strength = static_cast<qreal>(StyleConfigData::shadowStrength()) / 255.0;

        const QSize boxSize = BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius)
            .expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius));

        const qreal dpr = qApp->devicePixelRatio();
        const qreal frameRadius = _helper.frameRadius();

        BoxShadowRenderer shadowRenderer;
        shadowRenderer.setBorderRadius(frameRadius);
        shadowRenderer.setBoxSize(boxSize);
        shadowRenderer.setDevicePixelRatio(dpr);

        shadowRenderer.addShadow(params.shadow1.offset, params.shadow1.radius,
            withOpacity(color, params.shadow1.opacity * strength));
        shadowRenderer.addShadow(params.shadow2.offset, params.shadow2.radius,
            withOpacity(color, params.shadow2.opacity * strength));

        QImage shadowTexture = shadowRenderer.render();

        const QRect outerRect(QPoint(0, 0), shadowTexture.size() / dpr);

        QRect boxRect(QPoint(0, 0), boxSize);
        boxRect.moveCenter(outerRect.center());

        // Mask out inner rect.
        QPainter painter(&shadowTexture);
        painter.setRenderHint(QPainter::Antialiasing);

        const QMargins margins = QMargins(
            boxRect.left() - outerRect.left() - Metrics::Shadow_Overlap - params.offset.x(),
            boxRect.top() - outerRect.top() - Metrics::Shadow_Overlap - params.offset.y(),
            outerRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(),
            outerRect.bottom() - boxRect.bottom() - Metrics::Shadow_Overlap + params.offset.y());

        painter.setPen(Qt::NoPen);
        painter.setBrush(Qt::black);
        painter.setCompositionMode(QPainter::CompositionMode_DestinationOut);
        painter.drawRoundedRect(
            outerRect - margins,
            frameRadius,
            frameRadius);

        // We're done.
        painter.end();

        const QPoint innerRectTopLeft = outerRect.center();
        _shadowTiles = TileSet(
            QPixmap::fromImage(shadowTexture),
            innerRectTopLeft.x(),
            innerRectTopLeft.y(),
            1, 1);

        return _shadowTiles;
    }


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

    //_______________________________________________________
    void ShadowHelper::windowDeleted( QObject* object )
    {
        QWindow* window( static_cast<QWindow*>( object ) );
        _shadows.remove( window );
    }

    //_______________________________________________________
    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<KWindowShadowTile::Ptr>& ShadowHelper::createShadowTiles()
    {

        // make sure size is valid
        if( _tiles.isEmpty() )
        {
            _tiles = {
                createTile( _shadowTiles.pixmap( 1 ) ),
                createTile( _shadowTiles.pixmap( 2 ) ),
                createTile( _shadowTiles.pixmap( 5 ) ),
                createTile( _shadowTiles.pixmap( 8 ) ),
                createTile( _shadowTiles.pixmap( 7 ) ),
                createTile( _shadowTiles.pixmap( 6 ) ),
                createTile( _shadowTiles.pixmap( 3 ) ),
                createTile( _shadowTiles.pixmap( 0 ) )
            };
        }

        // return relevant list of shadow tiles
        return _tiles;

    }

    //______________________________________________
    KWindowShadowTile::Ptr ShadowHelper::createTile( const QPixmap& source )
    {

        KWindowShadowTile::Ptr tile = KWindowShadowTile::Ptr::create();
        tile->setImage( source.toImage() );
        return tile;

    }

    //_______________________________________________________
    void ShadowHelper::installShadows( QWidget* widget )
    {
        if( !widget ) return;

        // only toplevel widgets can cast drop-shadows
        if( !widget->isWindow() ) return;

        // widget must have valid native window
        if( !widget->testAttribute( Qt::WA_WState_Created ) ) return;

        // create shadow tiles if needed
        shadowTiles();
        if( !_shadowTiles.isValid() ) return;

        // create platform shadow tiles if needed
        const QVector<KWindowShadowTile::Ptr>& tiles = createShadowTiles();
        if( tiles.count() != numTiles ) return;

        // get the underlying window for the widget
        QWindow* window = widget->windowHandle();

        // find a shadow associated with the widget
        KWindowShadow*& shadow = _shadows[ window ];

        if( !shadow )
        {
            // if there is no shadow yet, create one
            shadow = new KWindowShadow( window );

            // connect destroy signal
            connect( window, &QWindow::destroyed, this, &ShadowHelper::windowDeleted );
        }

        if( shadow->isCreated() )
        { shadow->destroy(); }

        shadow->setTopTile( tiles[ 0 ] );
        shadow->setTopRightTile( tiles[ 1 ] );
        shadow->setRightTile( tiles[ 2 ] );
        shadow->setBottomRightTile( tiles[ 3 ] );
        shadow->setBottomTile( tiles[ 4 ] );
        shadow->setBottomLeftTile( tiles[ 5 ] );
        shadow->setLeftTile( tiles[ 6 ] );
        shadow->setTopLeftTile( tiles[ 7 ] );
        shadow->setPadding( shadowMargins( widget ) );
        shadow->setWindow( window );
        shadow->create();
    }

    //_______________________________________________________
    QMargins ShadowHelper::shadowMargins( QWidget* widget ) const
    {
        const CompositeShadowParams params = lookupShadowParams(StyleConfigData::shadowSize());
        if (params.isNone()) {
            return QMargins();
        }

        const QSize boxSize = BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius)
            .expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius));

        const QSize shadowSize = BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow1.radius, params.shadow1.offset)
            .expandedTo(BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow2.radius, params.shadow2.offset));

        const QRect shadowRect(QPoint(0, 0), shadowSize);

        QRect boxRect(QPoint(0, 0), boxSize);
        boxRect.moveCenter(shadowRect.center());

        QMargins margins(
            boxRect.left() - shadowRect.left() - Metrics::Shadow_Overlap - params.offset.x(),
            boxRect.top() - shadowRect.top() - Metrics::Shadow_Overlap - params.offset.y(),
            shadowRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(),
            shadowRect.bottom() - boxRect.bottom() - Metrics::Shadow_Overlap + params.offset.y());

        if (widget->inherits("QBalloonTip")) {
            // Balloon tip needs special margins to deal with the arrow.
            int top = widget->contentsMargins().top();
            int bottom = widget->contentsMargins().bottom();

            // Need to decrement default size further due to extra hard coded round corner.
            margins -= 1;

            // Arrow can be either to the top or the bottom. Adjust margins accordingly.
            const int diff = qAbs(top - bottom);
            if (top > bottom) {
                margins.setTop(margins.top() - diff);
            } else {
                margins.setBottom(margins.bottom() - diff);
            }
        }

        margins *= _helper.devicePixelRatio(_shadowTiles.pixmap(0));

        return margins;
    }

    //_______________________________________________________
    void ShadowHelper::uninstallShadows( QWidget* widget )
    {
        delete _shadows.take( widget->windowHandle() );
    }

}
