/*
 * SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa <hugo.pereira@free.fr>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "breezeframeshadow.h"

#include "breezemetrics.h"

#include <QAbstractScrollArea>
#include <QApplication>
#include <QDebug>
#include <QFrame>
#include <QMouseEvent>
#include <QPainter>
#include <QSplitter>

#include <KColorUtils>

namespace Breeze
{
//____________________________________________________________________________________
bool FrameShadowFactory::registerWidget(QWidget *widget, Helper &helper)
{
    if (!widget)
        return false;
    if (isRegistered(widget))
        return false;

    // check whether widget is a frame, and has the proper shape
    bool accepted = false;

    // cast to frame and check
    QFrame *frame(qobject_cast<QFrame *>(widget));
    if (frame) {
        // also do not install on QSplitter
        /*
        due to Qt, splitters are set with a frame style that matches the condition below,
        though no shadow should be installed, obviously
        */
        if (qobject_cast<QSplitter *>(widget))
            return false;

        // further checks on frame shape, and parent
        if (frame->frameStyle() == (QFrame::StyledPanel | QFrame::Sunken))
            accepted = true;

    } else if (widget->inherits("KTextEditor::View"))
        accepted = true;

    if (!accepted)
        return false;

    // make sure that the widget is not embedded into a KHTMLView
    QWidget *parent(widget->parentWidget());
    while (parent && !parent->isTopLevel()) {
        if (parent->inherits("KHTMLView"))
            return false;
        parent = parent->parentWidget();
    }

    // store in set
    _registeredWidgets.insert(widget);

    // catch object destruction
    connect(widget, &QObject::destroyed, this, &FrameShadowFactory::widgetDestroyed);

    // install shadow
    installShadows(widget, helper);

    return true;
}

//____________________________________________________________________________________
void FrameShadowFactory::unregisterWidget(QWidget *widget)
{
    if (!isRegistered(widget))
        return;
    _registeredWidgets.remove(widget);
    removeShadows(widget);
}

//____________________________________________________________________________________
bool FrameShadowFactory::eventFilter(QObject *object, QEvent *event)
{
    switch (event->type()) {
    // TODO: possibly implement ZOrderChange event, to make sure that
    // the shadow is always painted on top
    case QEvent::ZOrderChange: {
        raiseShadows(object);
        break;
    }

    default:
        break;
    }

    return QObject::eventFilter(object, event);
}

//____________________________________________________________________________________
void FrameShadowFactory::installShadows(QWidget *widget, Helper &helper)
{
    removeShadows(widget);

    widget->installEventFilter(this);

    widget->installEventFilter(&_addEventFilter);
    installShadow(widget, helper, SideTop);
    installShadow(widget, helper, SideBottom);
    widget->removeEventFilter(&_addEventFilter);
}

//____________________________________________________________________________________
void FrameShadowFactory::removeShadows(QWidget *widget)
{
    widget->removeEventFilter(this);

    const QList<QObject *> children = widget->children();
    foreach (QObject *child, children) {
        if (FrameShadow *shadow = qobject_cast<FrameShadow *>(child)) {
            shadow->hide();
            shadow->setParent(nullptr);
            shadow->deleteLater();
        }
    }
}

//____________________________________________________________________________________
void FrameShadowFactory::updateShadowsGeometry(const QObject *object, QRect rect) const
{
    const QList<QObject *> &children = object->children();
    foreach (QObject *child, children) {
        if (FrameShadow *shadow = qobject_cast<FrameShadow *>(child)) {
            shadow->updateGeometry(rect);
        }
    }
}

//____________________________________________________________________________________
void FrameShadowFactory::raiseShadows(QObject *object) const
{
    const QList<QObject *> &children = object->children();
    foreach (QObject *child, children) {
        if (FrameShadow *shadow = qobject_cast<FrameShadow *>(child)) {
            shadow->raise();
        }
    }
}

//____________________________________________________________________________________
void FrameShadowFactory::update(QObject *object) const
{
    const QList<QObject *> &children = object->children();
    foreach (QObject *child, children) {
        if (FrameShadow *shadow = qobject_cast<FrameShadow *>(child)) {
            shadow->update();
        }
    }
}

//____________________________________________________________________________________
void FrameShadowFactory::updateState(const QWidget *widget, bool focus, bool hover, qreal opacity, AnimationMode mode) const
{
    const QList<QObject *> &children = widget->children();
    foreach (QObject *child, children) {
        if (FrameShadow *shadow = qobject_cast<FrameShadow *>(child)) {
            shadow->updateState(focus, hover, opacity, mode);
        }
    }
}

//____________________________________________________________________________________
void FrameShadowFactory::installShadow(QWidget *widget, Helper &helper, Side area) const
{
    FrameShadow *shadow(nullptr);
    shadow = new FrameShadow(area, helper);
    shadow->setParent(widget);
    shadow->hide();
}

//____________________________________________________________________________________
void FrameShadowFactory::widgetDestroyed(QObject *object)
{
    _registeredWidgets.remove(object);
}

//____________________________________________________________________________________
FrameShadow::FrameShadow(Side area, Helper &helper)
    : _helper(helper)
    , _area(area)
{
    setAttribute(Qt::WA_OpaquePaintEvent, false);

    setFocusPolicy(Qt::NoFocus);
    setAttribute(Qt::WA_TransparentForMouseEvents, true);
    setContextMenuPolicy(Qt::NoContextMenu);

    // grab viewport widget
    QWidget *viewport(this->viewport());

    // set cursor from viewport
    if (viewport)
        setCursor(viewport->cursor());
}

//____________________________________________________________________________________
void FrameShadow::updateGeometry(QRect rect)
{
    // show on first call
    if (isHidden())
        show();

    // store offsets between passed rect and parent widget rect
    QRect parentRect(parentWidget()->contentsRect());
    _margins = QMargins(rect.left() - parentRect.left(), rect.top() - parentRect.top(), rect.right() - parentRect.right(), rect.bottom() - parentRect.bottom());

    // for efficiency, take out the part for which nothing is rendered
    rect.adjust(1, 1, -1, -1);

    // adjust geometry
    const int shadowSize(Metrics::Frame_FrameRadius);
    switch (_area) {
    case SideTop:
        rect.setHeight(shadowSize);
        break;

    case SideBottom:
        rect.setTop(rect.bottom() - shadowSize + 1);
        break;

    case SideLeft:
        rect.setWidth(shadowSize);
        rect.adjust(0, shadowSize, 0, -shadowSize);
        break;

    case SideRight:
        rect.setLeft(rect.right() - shadowSize + 1);
        rect.adjust(0, shadowSize, 0, -shadowSize);
        break;

    default:
        return;
    }

    setGeometry(rect);
}

//____________________________________________________________________________________
void FrameShadow::updateState(bool focus, bool hover, qreal opacity, AnimationMode mode)
{
    bool changed(false);
    if (_hasFocus != focus) {
        _hasFocus = focus;
        changed |= true;
    }
    if (_mouseOver != hover) {
        _mouseOver = hover;
        changed |= !_hasFocus;
    }
    if (_mode != mode) {
        _mode = mode;
        changed |= (_mode == AnimationNone) || (_mode == AnimationFocus) || (_mode == AnimationHover && !_hasFocus);
    }

    if (_opacity != opacity) {
        _opacity = opacity;
        changed |= (_mode != AnimationNone);
    }
    if (changed) {
        if (QWidget *viewport = this->viewport()) {
            // need to disable viewport updates to avoid some redundant painting
            // besides it fixes one visual glitch (from Qt) in QTableViews
            viewport->setUpdatesEnabled(false);
            update();
            viewport->setUpdatesEnabled(true);

        } else
            update();
    }
}

//____________________________________________________________________________________
void FrameShadow::paintEvent(QPaintEvent *event)
{
    // this fixes shadows in frames that change frameStyle() after polish()
    if (QFrame *frame = qobject_cast<QFrame *>(parentWidget())) {
        if (frame->frameStyle() != (QFrame::StyledPanel | QFrame::Sunken))
            return;
    }

    const QRect parentRect(parentWidget()->contentsRect().translated(mapFromParent(QPoint(0, 0))));
    const QRect rect(parentRect.adjusted(_margins.left(), _margins.top(), _margins.right(), _margins.bottom()));

    // render
    QPainter painter(this);
    painter.setClipRegion(event->region());
    painter.setRenderHint(QPainter::Antialiasing);

    const QColor outline(_helper.frameOutlineColor(palette(), _mouseOver, _hasFocus, _opacity, _mode));
    painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
    _helper.renderFrame(&painter, rect, QColor(), outline);
}

//____________________________________________________________________________________
QWidget *FrameShadow::viewport() const
{
    if (!parentWidget())
        return nullptr;
    else if (QAbstractScrollArea *widget = qobject_cast<QAbstractScrollArea *>(parentWidget())) {
        return widget->viewport();

    } else
        return nullptr;
}

}
