/*
 * advwidget.cpp - AdvancedWidget template class
 * Copyright (C) 2005-2007  Michail Pishchagin
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

#include "advwidget.h"

#include "psioptions.h"

#include <QApplication>
#include <QDebug>
#include <QDesktopWidget>
#include <QScreen>
#include <QTimer>
#include <QWidget>
#include <QtGlobal> // required to make mingw32 happy
#ifdef Q_OS_WIN
#include <windows.h>
#include <winuser.h>
#endif

// TODO: Make use of KDE taskbar flashing support

//----------------------------------------------------------------------------
// AdvancedWidgetShared
//----------------------------------------------------------------------------

class AdvancedWidgetShared : public QObject {
    Q_OBJECT
public:
    AdvancedWidgetShared();
    ~AdvancedWidgetShared();
};

AdvancedWidgetShared::AdvancedWidgetShared() : QObject(qApp) { }

AdvancedWidgetShared::~AdvancedWidgetShared() { }

static AdvancedWidgetShared *advancedWidgetShared = nullptr;

//----------------------------------------------------------------------------
// GAdvancedWidget::Private
//----------------------------------------------------------------------------

class GAdvancedWidget::Private : public QObject {
    Q_OBJECT
public:
    Private(QWidget *parent);

    static int  stickAt;
    static bool stickToWindows;
    static bool stickEnabled;

    QWidget *parentWidget_;
    bool     flashing_;
    QString  geometryOptionPath_;

    bool flashing() const;
    void doFlash(bool on);
    void posChanging(int *x, int *y, int *width, int *height);
    void moveEvent(QMoveEvent *e);

protected:
    // reimplemented
    bool eventFilter(QObject *obj, QEvent *e);

private:
    QTimer *saveGeometryTimer_;
    QRect   newGeometry_;
    QRect   normalGeometry_;

public slots:
    void saveGeometry();
    void restoreGeometry();

private slots:
    void updateGeometry();

    void restoreGeometry(QRect savedGeometry);
};

int  GAdvancedWidget::Private::stickAt        = 5;
bool GAdvancedWidget::Private::stickToWindows = true;
bool GAdvancedWidget::Private::stickEnabled   = true;

GAdvancedWidget::Private::Private(QWidget *parent) : QObject(parent), parentWidget_(parent), flashing_(false)
{
    if (!advancedWidgetShared)
        advancedWidgetShared = new AdvancedWidgetShared();

    saveGeometryTimer_ = new QTimer(this);
    saveGeometryTimer_->setInterval(100);
    saveGeometryTimer_->setSingleShot(true);
    connect(saveGeometryTimer_, SIGNAL(timeout()), SLOT(saveGeometry()));
}

void GAdvancedWidget::Private::posChanging(int *x, int *y, int *width, int *height)
{
    if (stickAt <= 0 || !stickEnabled || !parentWidget_->isTopLevel() || parentWidget_->isMaximized()
        || !parentWidget_->updatesEnabled()) {
        return;
    }

    QWidget *p = parentWidget_;
    if (p->pos() == QPoint(*x, *y) && p->frameSize() == QSize(*width, *height))
        return;

    bool resizing = p->frameSize() != QSize(*width, *height);

    QDesktopWidget *desktop = qApp->desktop();
    QWidgetList     list;

    if (stickToWindows)
        list = QApplication::topLevelWidgets();

    for (QWidget *w : qAsConst(list)) {
        QRect rect;
        bool  dockWidget = false;

        if (w->windowType() == Qt::Desktop)
            rect = (static_cast<QDesktopWidget *>(w))->availableGeometry(static_cast<QWidget *>(parent()));
        else {
            if (w == p || desktop->screenNumber(p) != desktop->screenNumber(w))
                continue;

            if (!w->isVisible())
                continue;

            // we want for widget to stick to outer edges of another widget, so
            // we'll change the rect to what it'll snap
            rect       = QRect(w->frameGeometry().topLeft(), w->frameGeometry().bottomRight());
            dockWidget = true;
        }

        if (*x != p->x())
            if (*x <= rect.left() + stickAt && *x > rect.left() - stickAt) {
                if (!dockWidget
                    || (p->frameGeometry().bottom() >= rect.bottom() && p->frameGeometry().top() <= rect.top())) {
                    *x = rect.left();
                    if (resizing)
                        *width = p->frameSize().width() + p->x() - *x;
                }
            }

        if (*x + *width >= rect.right() - stickAt && *x + *width <= rect.right() + stickAt) {
            if (!dockWidget
                || (p->frameGeometry().bottom() >= rect.bottom() && p->frameGeometry().top() <= rect.top())) {
                if (resizing)
                    *width = rect.right() - *x + 1;
                else
                    *x = rect.right() - *width + 1;
            }
        }

        if (*y != p->y())
            if (*y <= rect.top() + stickAt && *y > rect.top() - stickAt) {
                if (!dockWidget
                    || (p->frameGeometry().right() >= rect.right() && p->frameGeometry().left() <= rect.left())) {
                    *y = rect.top();
                    if (resizing)
                        *height = p->frameSize().height() + p->y() - *y;
                }
            }

        if (*y + *height >= rect.bottom() - stickAt && *y + *height <= rect.bottom() + stickAt) {
            if (!dockWidget
                || (p->frameGeometry().right() >= rect.right() && p->frameGeometry().left() <= rect.left())) {
                if (resizing)
                    *height = rect.bottom() - *y + 1;
                else
                    *y = rect.bottom() - *height + 1;
            }
        }
    }
}

bool GAdvancedWidget::Private::flashing() const { return flashing_; }

void GAdvancedWidget::Private::doFlash(bool yes)
{
    flashing_ = yes;
    if (parentWidget_->window() != parentWidget_)
        return;

#ifdef Q_OS_WIN

    FLASHWINFO fwi;

    fwi.cbSize = sizeof(fwi);
    fwi.hwnd   = HWND(parentWidget_->winId());

    if (yes) {
        fwi.dwFlags   = FLASHW_ALL | FLASHW_TIMER;
        fwi.dwTimeout = 0;
        fwi.uCount    = 5;
    } else {
        fwi.dwFlags = FLASHW_STOP;
        fwi.uCount  = 0;
    }

    FlashWindowEx(&fwi);

#else

    QApplication::alert(parentWidget_, yes ? 0 : 1);

#endif
}

void GAdvancedWidget::Private::moveEvent(QMoveEvent *)
{
    if (!parentWidget_->isTopLevel())
        return;
#ifdef Q_OS_MAC
    QRect r = qApp->desktop()->availableGeometry(parentWidget_);
    QRect g = parentWidget_->frameGeometry();

    int margin = 5;

    if (g.top() < r.top())
        g.moveTo(g.x(), r.top());

    if (g.right() < r.left() + margin)
        g.moveTo(r.left() + margin - g.width(), g.y());

    if (g.left() > r.right() - margin)
        g.moveTo(r.right() - margin, g.y());

    if (g.top() > r.bottom() - margin)
        g.moveTo(g.x(), r.bottom() - margin);

    newGeometry_ = g;
    QTimer::singleShot(0, this, SLOT(updateGeometry()));
#endif
}

void GAdvancedWidget::Private::updateGeometry()
{
    QWidget *w = static_cast<QWidget *>(parent());
    w->move(newGeometry_.topLeft());
}

void GAdvancedWidget::Private::saveGeometry()
{
    bool isMaximized = bool(parentWidget_->windowState() & Qt::WindowMaximized);
    // if window is maximized normalGeometry() returns null rect. So in this case we use cached geometry
    PsiOptions::instance()->setOption(geometryOptionPath_,
                                      isMaximized ? normalGeometry_ : parentWidget_->normalGeometry());
    PsiOptions::instance()->setOption(geometryOptionPath_ + "-frame", parentWidget_->frameGeometry());
    PsiOptions::instance()->setOption(geometryOptionPath_ + "-screen",
                                      QApplication::desktop()->screenNumber(parentWidget_));
    PsiOptions::instance()->setOption(geometryOptionPath_ + "-maximized", isMaximized);
    PsiOptions::instance()->setOption(geometryOptionPath_ + "-fullscreen",
                                      bool(parentWidget_->windowState() & Qt::WindowFullScreen));
}

void GAdvancedWidget::Private::restoreGeometry()
{
    PsiOptions *o = PsiOptions::instance();
    QVariant    v(o->getOption(geometryOptionPath_));

    if (v.type() == QVariant::ByteArray) {
        // migrate options back from format used for a short time before
        // 0.12-RC2. This can be removed later.
        parentWidget_->restoreGeometry(v.toByteArray());
    } else if (o->getOption(geometryOptionPath_ + "-frame").toRect() != QRect(0, 0, 0, 0)) {
        // this is at least as safe as saving this kind of binary blob into the options file.
        // if future Qt versions drop support for restoring from this format old options files
        // would break anyway. If we want to (e.g. to add other features) we can also reimplement
        // restoring any other way without breaking the options format at all.
        // and this way we are sure no Qt version the user happens to have installed writes some
        // newer version of the format that older Qts can't restore from.

        QByteArray  array;
        QDataStream stream(&array, QIODevice::WriteOnly);
        stream.setVersion(QDataStream::Qt_4_0);
        const quint32 magicNumber           = 0x1D9D0CB;
        quint16       majorVersion          = 1;
        quint16       minorVersion          = 0;
        QRect         restoredFrameGeometry = o->getOption(geometryOptionPath_ + "-frame").toRect();
        normalGeometry_                     = o->getOption(geometryOptionPath_).toRect();

        stream << magicNumber << majorVersion << minorVersion << restoredFrameGeometry << normalGeometry_
               << qint32(o->getOption(geometryOptionPath_ + "-screen").toInt())
               << quint8(o->getOption(geometryOptionPath_ + "-maximized").toBool())
               << quint8(o->getOption(geometryOptionPath_ + "-fullscreen").toBool());

        parentWidget_->restoreGeometry(array);
        return;
    } else {
        // used for bootstrapping and for pre 0.12-RC1 options files.
        QRect savedGeometry = o->getOption(geometryOptionPath_).toRect();
        if (!savedGeometry.isEmpty()) {
            restoreGeometry(savedGeometry);
        }
    }
}

// FIXME: should use frameGeometry
void GAdvancedWidget::Private::restoreGeometry(QRect savedGeometry)
{
    QRect geom = savedGeometry;
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
    QScreen *screen;
    if (!(screen = QApplication::screenAt(geom.topLeft()))) {
        qWarning("Failed to find screen for coords %dx%d", geom.topLeft().x(), geom.topLeft().y());
        return;
    }
    QRect r = screen->geometry();
#else
    QDesktopWidget *pdesktop = QApplication::desktop();
    int             nscreen  = pdesktop->screenNumber(geom.topLeft());
    QRect           r        = pdesktop->screenGeometry(nscreen);
#endif
    // if the coordinates are out of the desktop bounds, reset to the top left
    int pad = 10;
    if (geom.left() < r.left())
        geom.moveLeft(r.left());
    if (geom.right() >= r.right())
        geom.moveRight(r.right() - 1);
    if (geom.top() < r.top())
        geom.moveTop(r.top());
    if (geom.bottom() >= r.bottom())
        geom.moveBottom(r.bottom() - 1);
    if ((geom.width() + pad * 2) > r.width())
        geom.setWidth(r.width() - pad * 2);
    if ((geom.height() + pad * 2) > r.height())
        geom.setHeight(r.height() - pad * 2);

    parentWidget_->move(geom.topLeft());
    parentWidget_->resize(geom.size());
}

bool GAdvancedWidget::Private::eventFilter(QObject *obj, QEvent *e)
{
    if (obj == parentWidget_) {
        if (e->type() == QEvent::Move || e->type() == QEvent::Resize) {
            Qt::WindowStates ws = parentWidget_->windowState();
            if (!(ws & Qt::WindowMaximized) && !(ws & Qt::WindowFullScreen)) {
                normalGeometry_ = parentWidget_->normalGeometry();
            }
            saveGeometryTimer_->start();
        }

        return false;
    }

    return QObject::eventFilter(obj, e);
}

//----------------------------------------------------------------------------
// GAdvancedWidget
//----------------------------------------------------------------------------

GAdvancedWidget::GAdvancedWidget(QWidget *parent) : QObject(parent) { d = new Private(parent); }

#ifdef Q_OS_WIN
bool GAdvancedWidget::nativeEvent(const QByteArray &eventType, MSG *msg, long *result)
{
    Q_UNUSED(eventType);
    if (msg->message == WM_WINDOWPOSCHANGING) {
        WINDOWPOS *wpos = (WINDOWPOS *)msg->lParam;

        d->posChanging(&wpos->x, &wpos->y, &wpos->cx, &wpos->cy);

        *result = 0;
        return true;
    }

    return false;
}
#endif

QString GAdvancedWidget::geometryOptionPath() const { return d->geometryOptionPath_; }

void GAdvancedWidget::setGeometryOptionPath(const QString &optionPath)
{
    Q_ASSERT(d->geometryOptionPath_.isEmpty());
    Q_ASSERT(!optionPath.isEmpty());
    d->geometryOptionPath_ = optionPath;
    d->restoreGeometry();
    d->parentWidget_->installEventFilter(d);
}

bool GAdvancedWidget::flashing() const { return d->flashing(); }

void GAdvancedWidget::doFlash(bool on) { d->doFlash(on); }
/* looks like the next part of the code is no longer relevant
#ifdef Q_OS_WIN
// http://groups.google.ru/group/borland.public.cppbuilder.winapi/msg/6eb6f1832d68686d?hl=ru&
bool ForceForegroundWindow(HWND hwnd)
{
    // Code from Thomas Stutz @ delphi3000.com
    // Converted to Borland C++ Builder Code by Wolfgang Frisch
    bool Result;
    // #define SPI_GETFOREGROUNDLOCKTIMEOUT (0x2000);
    // #define SPI_SETFOREGROUNDLOCKTIMEOUT (0x2001);
    DWORD nullvalue = 0;

    DWORD ForegroundThreadID;
    DWORD ThisThreadID;
    DWORD timeout;

    if (IsIconic(hwnd)) {
        ShowWindow(hwnd, SW_RESTORE);
    }

    if (GetForegroundWindow() == hwnd) {
        return true;
    }
    else {
        OSVERSIONINFO osvi;
        osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
        GetVersionEx(&osvi);

        // Windows 98/2000 doesn't want to foreground a window when some other
        // window has keyboard focus
        if (((osvi.dwPlatformId == VER_PLATFORM_WIN32_NT) && (osvi.dwMajorVersion > 4))
            || ((osvi.dwPlatformId  == VER_PLATFORM_WIN32_WINDOWS)
                && ((osvi.dwMajorVersion  > 4) || ((osvi.dwMajorVersion == 4) &&
                                                   (osvi.dwMinorVersion > 0))))) {
            // Code from Karl E. Peterson, www.mvps.org/vb/sample.htm
            // Converted to Delphi by Ray Lischner
            // Published in The Delphi Magazine 55, page 16

            Result = false;
            ForegroundThreadID = GetWindowThreadProcessId(GetForegroundWindow(), nullptr);
            ThisThreadID = GetWindowThreadProcessId(hwnd, nullptr);
            if (AttachThreadInput(ThisThreadID, ForegroundThreadID, true)) {
                BringWindowToTop(hwnd); // IE 5.5 related hack
                SetForegroundWindow(hwnd);
                AttachThreadInput(ThisThreadID, ForegroundThreadID, false);
                Result = (GetForegroundWindow() == hwnd);
            }

            if (!Result) {
                // Code by Daniel P. Stasinski
                SystemParametersInfo(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, &timeout, 0);
                SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, &nullvalue, SPIF_SENDCHANGE);
                BringWindowToTop(hwnd); // IE 5.5 related hack
                SetForegroundWindow(hwnd);
                SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, &timeout, SPIF_SENDCHANGE);
            }
        }
        else {
            BringWindowToTop(hwnd); // IE 5.5 related hack
            SetForegroundWindow(hwnd);
        }
        Result = (GetForegroundWindow() == hwnd);
    }
    return Result;
}
#endif
*/
/**
 * http://trolltech.com/developer/task-tracker/index_html?id=202971&method=entry
 * There's a bug in Qt that prevents us to show a window on Windows
 * without it being activated in the process. This is not good in some cases.
 * We try our best to work-around this on Windows.
 */
void GAdvancedWidget::showWithoutActivation()
{
    if (d->parentWidget_->isVisible())
        return;

    // TODO: look at Qt::WA_ShowWithoutActivating that was introduced
    // in Qt 4.4.0, maybe it'll provide a simpler alternative to this
    // windows-specific code

    /*#ifdef Q_OS_WIN
        HWND foregroundWindow = GetForegroundWindow();
    #endif*/

    bool showWithoutActivating = d->parentWidget_->testAttribute(Qt::WA_ShowWithoutActivating);
    d->parentWidget_->setAttribute(Qt::WA_ShowWithoutActivating, true);
    d->parentWidget_->show();
    d->parentWidget_->setAttribute(Qt::WA_ShowWithoutActivating, showWithoutActivating);

    /*#ifdef Q_OS_WIN
        if (foregroundWindow) {
            // the first step is to make sure we're the topmost window
            // otherwise step two doesn't seem to have any effect at all
            ForceForegroundWindow((HWND)d->parentWidget_->winId());
            ForceForegroundWindow(foregroundWindow);
        }
    #endif*/
}

void GAdvancedWidget::changeEvent(QEvent *event)
{
    if (event->type() == QEvent::ActivationChange || event->type() == QEvent::WindowStateChange) {
        if (d->parentWidget_->isActiveWindow()) {
            doFlash(false);
        }
    }
}

int GAdvancedWidget::stickAt() { return Private::stickAt; }

void GAdvancedWidget::setStickAt(int val) { Private::stickAt = val; }

bool GAdvancedWidget::stickToWindows() { return Private::stickToWindows; }

void GAdvancedWidget::setStickToWindows(bool val) { Private::stickToWindows = val; }

bool GAdvancedWidget::stickEnabled() { return Private::stickEnabled; }

void GAdvancedWidget::setStickEnabled(bool val) { Private::stickEnabled = val; }

void GAdvancedWidget::moveEvent(QMoveEvent *e) { d->moveEvent(e); }

#include "advwidget.moc"
