/****************************************************************
 *
 * 作   者：王建军
 * 时   间：2020/08/24
 * 开发语言：C++
 * 开发工具：QtCreator(Qt)
 * 文   件：framelesshelper.cpp
 * 版   权：武汉创景可视技术有限公司
 *
 ***************************************************************/

#include "framelesshelper.h"

#include <QApplication>
#include <QHoverEvent>
#include <QMouseEvent>
#include <QRect>
#include <QRubberBand>
#include <QMainWindow>

#ifdef Q_OS_WINDOWS
#include <windows.h>
#include <windowsx.h>
#include <winuser.h>
#include <QScreen>
#endif

class WidgetData;

/*****
 * FramelessHelperPrivate
 * 存储界面对应的数据集合，以及是否可移动、可缩放属性
 *****/
class FramelessHelperPrivate
{
public:
    QHash<QWidget *, WidgetData *> m_widgetDataHash;
    bool m_bWidgetMovable : true;
    bool m_bWidgetResizable : true;
    bool m_bRubberBandOnResize : true;
    bool m_bRubberBandOnMove : true;
};

/*****
 * CursorPosCalculator
 * 计算鼠标是否位于左、上、右、下、左上角、左下角、右上角、右下角
 *****/
class CursorPosCalculator
{
public:
    explicit CursorPosCalculator();
    void reset();
    void recalculate(const QPoint &globalMousePos, const QRect &frameRect);
public:
    bool m_bOnEdges              : true;
    bool m_bOnLeftEdge           : true;
    bool m_bOnRightEdge          : true;
    bool m_bOnTopEdge            : true;
    bool m_bOnBottomEdge         : true;
    bool m_bOnTopLeftEdge        : true;
    bool m_bOnBottomLeftEdge     : true;
    bool m_bOnTopRightEdge       : true;
    bool m_bOnBottomRightEdge    : true;

    static int m_nBorderWidth;
    static int m_nTitleHeight;
};

int CursorPosCalculator::m_nBorderWidth = 5;
int CursorPosCalculator::m_nTitleHeight = 30;

/***** CursorPosCalculator *****/
CursorPosCalculator::CursorPosCalculator()
{
    reset();
}

void CursorPosCalculator::reset()
{
    m_bOnEdges = false;
    m_bOnLeftEdge = false;
    m_bOnRightEdge = false;
    m_bOnTopEdge = false;
    m_bOnBottomEdge = false;
    m_bOnTopLeftEdge = false;
    m_bOnBottomLeftEdge = false;
    m_bOnTopRightEdge = false;
    m_bOnBottomRightEdge = false;
}

void CursorPosCalculator::recalculate(const QPoint &gMousePos, const QRect &frameRect)
{
    int globalMouseX = gMousePos.x();
    int globalMouseY = gMousePos.y();

    int frameX = frameRect.x();
    int frameY = frameRect.y();

    int frameWidth = frameRect.width();
    int frameHeight = frameRect.height();

    m_bOnLeftEdge = (globalMouseX >= frameX &&
                  globalMouseX <= frameX + m_nBorderWidth );


    m_bOnRightEdge = (globalMouseX >= frameX + frameWidth - m_nBorderWidth &&
                   globalMouseX <= frameX + frameWidth);

    m_bOnTopEdge = (globalMouseY >= frameY &&
                 globalMouseY <= frameY + m_nBorderWidth );

    m_bOnBottomEdge = (globalMouseY >= frameY + frameHeight - m_nBorderWidth &&
                    globalMouseY <= frameY + frameHeight);

    m_bOnTopLeftEdge = m_bOnTopEdge && m_bOnLeftEdge;
    m_bOnBottomLeftEdge = m_bOnBottomEdge && m_bOnLeftEdge;
    m_bOnTopRightEdge = m_bOnTopEdge && m_bOnRightEdge;
    m_bOnBottomRightEdge = m_bOnBottomEdge && m_bOnRightEdge;

    m_bOnEdges = m_bOnLeftEdge || m_bOnRightEdge || m_bOnTopEdge || m_bOnBottomEdge;
}

/*****
 * WidgetData
 * 更新鼠标样式、移动窗体、缩放窗体
 *****/
class WidgetData
{
public:
    explicit WidgetData(FramelessHelperPrivate *d, QWidget *pTopLevelWidget);
    ~WidgetData();
    QWidget *widget();
    // 处理鼠标事件-划过、按下、释放、移动
    bool handleWidgetEvent(QEvent *event);
    // 更新橡皮筋状态
    void updateRubberBandStatus();
private:
    // 更新鼠标样式
    void updateCursorShape(const QPoint &gMousePos);
    // 重置窗体大小
    void resizeWidget(const QPoint &gMousePos);
    // 移动窗体
    void moveWidget(const QPoint &gMousePos);
    // 处理鼠标按下
    bool handleMousePressEvent(QMouseEvent *event);
    // 处理鼠标释放
    bool handleMouseReleaseEvent(QMouseEvent *event);
    // 处理鼠标移动
    bool handleMouseMoveEvent(QMouseEvent *event);
    // 处理鼠标离开
    bool handleLeaveEvent(QEvent *event);
    // 处理鼠标进入
    bool handleHoverMoveEvent(QHoverEvent *event);
    // 处理鼠标双击事件
    bool handleDoubleClickedMouseEvent(QMouseEvent *event);
private:
    FramelessHelperPrivate *d;
    QRubberBand *m_pRubberBand;
    QWidget *m_pWidget;
    QPoint m_ptDragPos;
    CursorPosCalculator m_pressedMousePos;
    CursorPosCalculator m_moveMousePos;
    bool m_bLeftButtonPressed;
    bool m_bCursorShapeChanged;
    bool m_bLeftButtonTitlePressed;
    Qt::WindowFlags m_windowFlags;
};

/***** WidgetData *****/
WidgetData::WidgetData(FramelessHelperPrivate *_d, QWidget *pTopLevelWidget)
{
    Q_ASSERT(pTopLevelWidget);
    d = _d;
    m_pWidget = pTopLevelWidget;
    m_bLeftButtonPressed = false;
    m_bCursorShapeChanged = false;
    m_bLeftButtonTitlePressed = false;
    m_pRubberBand = nullptr;

    m_windowFlags = m_pWidget->windowFlags();
    m_pWidget->setMouseTracking(true);
    m_pWidget->setAttribute(Qt::WA_Hover, true);

    updateRubberBandStatus();
}

WidgetData::~WidgetData()
{
    m_pWidget->setMouseTracking(false);
    m_pWidget->setAttribute(Qt::WA_Hover, false);

    delete m_pRubberBand;
    m_pRubberBand = nullptr;
}

QWidget *WidgetData::widget()
{
    return m_pWidget;
}

bool WidgetData::handleWidgetEvent(QEvent *event)
{
    switch (event->type()) {
    case QEvent::MouseButtonPress:
        return handleMousePressEvent(static_cast<QMouseEvent *>(event));
    case QEvent::MouseButtonRelease:
        return handleMouseReleaseEvent(static_cast<QMouseEvent *>(event));
    case QEvent::MouseMove:
        return handleMouseMoveEvent(static_cast<QMouseEvent *>(event));
    case QEvent::Leave:
        return handleLeaveEvent(event);
    case QEvent::HoverMove:
        return handleHoverMoveEvent(static_cast<QHoverEvent *>(event));
    case QEvent::MouseButtonDblClick:
        return handleDoubleClickedMouseEvent(static_cast<QMouseEvent *>(event));
    default:
        break;
    }
    return false;
}

void WidgetData::updateRubberBandStatus()
{
    if (d->m_bRubberBandOnMove || d->m_bRubberBandOnResize) {
        if (nullptr == m_pRubberBand) {
            m_pRubberBand = new QRubberBand(QRubberBand::Rectangle);
        }
    } else {
        delete m_pRubberBand;
        m_pRubberBand = nullptr;
    }
}

void WidgetData::updateCursorShape(const QPoint &gMousePos)
{
    if (m_pWidget->isFullScreen() || m_pWidget->isMaximized()) {
        if (m_bCursorShapeChanged) {
            m_pWidget->unsetCursor();
        }
        return;
    }

    m_moveMousePos.recalculate(gMousePos, m_pWidget->frameGeometry());

    if (m_moveMousePos.m_bOnTopLeftEdge || m_moveMousePos.m_bOnBottomRightEdge) {
        m_pWidget->setCursor(Qt::SizeFDiagCursor);
        m_bCursorShapeChanged = true;
    } else if (m_moveMousePos.m_bOnTopRightEdge || m_moveMousePos.m_bOnBottomLeftEdge) {
        m_pWidget->setCursor(Qt::SizeBDiagCursor);
        m_bCursorShapeChanged = true;
    } else if (m_moveMousePos.m_bOnLeftEdge || m_moveMousePos.m_bOnRightEdge) {
        m_pWidget->setCursor(Qt::SizeHorCursor);
        m_bCursorShapeChanged = true;
    } else if (m_moveMousePos.m_bOnTopEdge || m_moveMousePos.m_bOnBottomEdge) {
        m_pWidget->setCursor(Qt::SizeVerCursor);
        m_bCursorShapeChanged = true;
    } else {
        if (m_bCursorShapeChanged) {
            m_pWidget->unsetCursor();
            m_bCursorShapeChanged = false;
        }
    }
}

void WidgetData::resizeWidget(const QPoint &gMousePos)
{
    QRect origRect;

    if (d->m_bRubberBandOnResize) {
        origRect = m_pRubberBand->frameGeometry();
    } else {
        origRect = m_pWidget->frameGeometry();
    }

    int left = origRect.left();
    int top = origRect.top();
    int right = origRect.right();
    int bottom = origRect.bottom();

    origRect.getCoords(&left, &top, &right, &bottom);

    int minWidth = m_pWidget->minimumWidth();
    int minHeight = m_pWidget->minimumHeight();

    if (m_pressedMousePos.m_bOnTopLeftEdge) {
        left = gMousePos.x();
        top = gMousePos.y();
    } else if (m_pressedMousePos.m_bOnBottomLeftEdge) {
        left = gMousePos.x();
        bottom = gMousePos.y();
    } else if (m_pressedMousePos.m_bOnTopRightEdge) {
        right = gMousePos.x();
        top = gMousePos.y();
    } else if (m_pressedMousePos.m_bOnBottomRightEdge) {
        right = gMousePos.x();
        bottom = gMousePos.y();
    } else if (m_pressedMousePos.m_bOnLeftEdge) {
        left = gMousePos.x();
    } else if (m_pressedMousePos.m_bOnRightEdge) {
        right = gMousePos.x();
    } else if (m_pressedMousePos.m_bOnTopEdge) {
        top = gMousePos.y();
    } else if (m_pressedMousePos.m_bOnBottomEdge) {
        bottom = gMousePos.y();
    }

    QRect newRect(QPoint(left, top), QPoint(right, bottom));

    if (newRect.isValid()) {
        if (minWidth > newRect.width()) {
            if (left != origRect.left()) {
                newRect.setLeft(origRect.left());
            } else {
                newRect.setRight(origRect.right());
            }
        }
        if (minHeight > newRect.height()) {
            if (top != origRect.top()) {
                newRect.setTop(origRect.top());
            } else {
                newRect.setBottom(origRect.bottom());
            }
        }

        if (d->m_bRubberBandOnResize) {
            m_pRubberBand->setGeometry(newRect);
        } else {
            m_pWidget->setGeometry(newRect);
        }
    }
}

void WidgetData::moveWidget(const QPoint &gMousePos)
{
    if (d->m_bRubberBandOnMove) {
        m_pRubberBand->move(gMousePos - m_ptDragPos);
    } else {
        m_pWidget->move(gMousePos - m_ptDragPos);
    }
}

bool WidgetData::handleMousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        m_bLeftButtonPressed = true;
        m_bLeftButtonTitlePressed = event->pos().y() < m_moveMousePos.m_nTitleHeight;

        QRect frameRect = m_pWidget->frameGeometry();
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        m_pressedMousePos.recalculate(event->globalPos(), frameRect);
        m_ptDragPos = event->globalPos() - frameRect.topLeft();
#else
        m_pressedMousePos.recalculate(event->globalPosition().toPoint(), frameRect);
        m_ptDragPos = event->globalPosition().toPoint() - frameRect.topLeft();
#endif

        if (m_pressedMousePos.m_bOnEdges) {
            if (m_pWidget->isMaximized() || m_pWidget->isFullScreen()) {
                // 窗口在最大化状态时，点击边界不做任何处理
                return false;
            }
            if (d->m_bRubberBandOnResize) {
                m_pRubberBand->setGeometry(frameRect);
                m_pRubberBand->show();
                return true;
            }
        } else if (d->m_bRubberBandOnMove && m_bLeftButtonTitlePressed) {
            if (m_pWidget->isMaximized() || m_pWidget->isFullScreen()) {
                // 窗口在最大化状态时，点击标题栏不做任何处理
                return false;
            }
            m_pRubberBand->setGeometry(frameRect);
            m_pRubberBand->show();
            return true;
        }
    }
    return false;
}

bool WidgetData::handleMouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        m_bLeftButtonPressed = false;
        m_bLeftButtonTitlePressed = false;
        m_pressedMousePos.reset();
        if (m_pRubberBand && m_pRubberBand->isVisible()) {
            m_pRubberBand->hide();
            m_pWidget->setGeometry(m_pRubberBand->geometry());
            return true;
        }
    }
    return false;
}

bool WidgetData::handleMouseMoveEvent(QMouseEvent *event)
{
    if (m_bLeftButtonPressed) {
        if (d->m_bWidgetResizable && m_pressedMousePos.m_bOnEdges) {
            if (m_pWidget->isMaximized() || m_pWidget->isFullScreen()) {
                // 窗口在最大化状态时，点击边界不做任何处理
                return false;
            }
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
            resizeWidget(event->globalPos());
#else
            resizeWidget(event->globalPosition().toPoint());
#endif
            return true;
        } else if (d->m_bWidgetMovable && m_bLeftButtonTitlePressed) {
            if (m_pWidget->isFullScreen()) {
                return false;
            }
            if (m_pWidget->isMaximized()) {
                // FIXME：1）Ubuntu 16.04 LTS/Unity 7.4.0环境中，最大化后鼠标拖拽标题栏获得的normalGeometry不是原始的，
                // 而是无边框最大化后的尺寸，鼠标拖拽窗口最上面的Unity为窗口设计的标题栏才能获得真实的normalGeometry
                // 2）GNOME环境中，最大化后鼠标拖拽标题栏获得的normalGeometry是原始的，但是会一闪而过，然后尺寸变成最大化的尺寸，
                // 此时，由于尺寸已是最大化，又因为无边框后，鼠标移动窗口无法将窗口移到非显示区，导致根本无法移动
                // 建议：Linux环境下不使用无边框方案

                // 先求出窗口到鼠标的相对位置
                QRect normalGeometry = m_pWidget->normalGeometry();
                m_pWidget->showNormal();
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
                QPoint p = event->globalPos();
#else
                QPoint p = event->globalPosition().toPoint();
#endif
                p.ry() -= 10;
                p.rx() -= (normalGeometry.width() / 2);
                m_pWidget->move(p);
                // 这时要重置m_ptDragPos
                m_ptDragPos = QPoint(normalGeometry.width() / 2, 10);
                return true;
            }
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
            moveWidget(event->globalPos());
#else
            moveWidget(event->globalPosition().toPoint());
#endif
            return true;
        }
        return false;
    } else if (d->m_bWidgetResizable) {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        updateCursorShape(event->globalPos());
#else
        updateCursorShape(event->globalPosition().toPoint());
#endif
    }
    return false;
}

bool WidgetData::handleLeaveEvent(QEvent *event)
{
    Q_UNUSED(event)
    if (!m_bLeftButtonPressed) {
        m_pWidget->unsetCursor();
        return true;
    }
    return false;
}

bool WidgetData::handleHoverMoveEvent(QHoverEvent *event)
{
    if (d->m_bWidgetResizable) {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        updateCursorShape(m_pWidget->mapToGlobal(event->pos()));
#else
        updateCursorShape(m_pWidget->mapToGlobal(event->position().toPoint()));
#endif
    }
    return false;
}

bool WidgetData::handleDoubleClickedMouseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        QMainWindow *mainwindow = qobject_cast<QMainWindow *>(m_pWidget);
        if (mainwindow) {
            if (mainwindow->windowFlags() & Qt::WindowMaximizeButtonHint) {
                // 在最大化按钮显示时才进行showNormal处理
                bool titlePressed = event->pos().y() < m_moveMousePos.m_nTitleHeight;
                if (titlePressed && !m_pWidget->isFullScreen()) {
                    if (m_pWidget->isMaximized()) {
                        m_pWidget->showNormal();
                    } else {
                        m_pWidget->showMaximized();
                    }
                    return true;
                }
            }
        }
    }
    return false;
}

FramelessHelper::FramelessHelper(QObject *parent)
    : QObject(parent)
    , d(new FramelessHelperPrivate())
{
    d->m_bWidgetMovable = true;
    d->m_bWidgetResizable = true;
    d->m_bRubberBandOnResize = false;
    d->m_bRubberBandOnMove = false;

    if (parent) {
        QWidget *w = qobject_cast<QWidget *>(parent);
        if (w) {
            w->setWindowFlags(w->windowFlags() | Qt::FramelessWindowHint);
            activateOn(w);
        }
    }
#ifdef Q_OS_WINDOWS
    qApp->installNativeEventFilter(this);
#endif
}

FramelessHelper::~FramelessHelper()
{
#ifdef Q_OS_WINDOWS
    qApp->removeNativeEventFilter(this);
#endif
    QList<QWidget *> keys = d->m_widgetDataHash.keys();
    int size = keys.size();

    for (int i = 0; i < size; ++i) {
        keys[i]->removeEventFilter(this);
        delete d->m_widgetDataHash.take(keys[i]);
    }

    delete d;
}

bool FramelessHelper::eventFilter(QObject *obj, QEvent *event)
{
    switch (event->type()) {
    case QEvent::MouseMove:
    case QEvent::HoverMove:
    case QEvent::MouseButtonPress:
    case QEvent::MouseButtonRelease:
    case QEvent::MouseButtonDblClick:
    case QEvent::Leave: {
        WidgetData *data = d->m_widgetDataHash.value(static_cast<QWidget *>(obj));
        if (data) {
            return data->handleWidgetEvent(event);
        }
        break;
    }
    default:
        break;
    }
    return (QObject::eventFilter(obj, event));
}

#ifdef Q_OS_WINDOWS
bool FramelessHelper::nativeEventFilter(const QByteArray &eventType, void *message, QTRESULT *result)
{
    if (eventType == "windows_generic_MSG") {
        MSG *msg = reinterpret_cast<MSG *>(message);
        QWidget *widget = QWidget::find(reinterpret_cast<WId>(msg->hwnd));
        WidgetData *data = d->m_widgetDataHash.value(widget);
        if (data == nullptr) {
            return false;
        }
        switch (msg->message) {
            case WM_GETMINMAXINFO: {
                // prevent taskbar is covered when maximized
                if (widget->isMaximized()) {
                    const QRect rc = widget->screen()->availableGeometry();
                    MINMAXINFO *p = (MINMAXINFO *)(msg->lParam);
                    p->ptMaxPosition.x = 0;
                    p->ptMaxPosition.y = 0;
                    p->ptMaxSize.x = rc.width();
                    p->ptMaxSize.y = rc.height();
                    *result = ::DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam);
                    return true;
                }
            }
        }
    }
    return false;
}
#endif

void FramelessHelper::activateOn(QWidget *topLevelWidget)
{
    if (!d->m_widgetDataHash.contains(topLevelWidget)) {
        WidgetData *data = new WidgetData(d, topLevelWidget);
        d->m_widgetDataHash.insert(topLevelWidget, data);

        topLevelWidget->installEventFilter(this);
    }
}

void FramelessHelper::removeFrom(QWidget *topLevelWidget)
{
    WidgetData *data = d->m_widgetDataHash.take(topLevelWidget);

    if (data) {
        topLevelWidget->removeEventFilter(this);
        delete data;
    }
}

void FramelessHelper::setRubberBandOnMove(bool movable)
{
    d->m_bRubberBandOnMove = movable;
    QList<WidgetData *> list = d->m_widgetDataHash.values();

    foreach (WidgetData * data, list) {
        data->updateRubberBandStatus();
    }
}

void FramelessHelper::setWidgetMovable(bool movable)
{
    d->m_bWidgetMovable = movable;
}

void FramelessHelper::setWidgetResizable(bool resizable)
{
    d->m_bWidgetResizable = resizable;
}

void FramelessHelper::setRubberBandOnResize(bool resizable)
{
    d->m_bRubberBandOnResize = resizable;
    QList<WidgetData *> list = d->m_widgetDataHash.values();

    foreach (WidgetData * data, list) {
        data->updateRubberBandStatus();
    }
}

void FramelessHelper::setBorderWidth(int width)
{
    if (width > 0) {
        CursorPosCalculator::m_nBorderWidth = width;
    }
}

void FramelessHelper::setTitleHeight(int height)
{
    if (height > 0) {
        CursorPosCalculator::m_nTitleHeight = height;
    }
}

bool FramelessHelper::widgetMovable()
{
    return d->m_bWidgetMovable;
}

bool FramelessHelper::widgetResizable()
{
    return d->m_bWidgetResizable;
}

bool FramelessHelper::rubberBandOnMove()
{
    return d->m_bRubberBandOnMove;
}

bool FramelessHelper::rubberBandOnResisze()
{
    return d->m_bRubberBandOnResize;
}

uint FramelessHelper::borderWidth()
{
    return CursorPosCalculator::m_nBorderWidth;
}

uint FramelessHelper::titleHeight()
{
    return CursorPosCalculator::m_nTitleHeight;
}
