/************************************************************************
 *
 * Copyright 2010-2011 Jakob Leben (jakob.leben@gmail.com)
 *
 * This file is part of SuperCollider Qt GUI.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 ************************************************************************/

#include "QWidgetProxy.h"
#include "painting.h"
#include "Common.h"
#include "hacks/hacks_qt.hpp"

#include <QApplication>
#include <QLayout>
#include <QMouseEvent>
#include <QKeyEvent>
#include <QPainter>
#include <QFontMetrics>
#include <QUrl>
#include <QMimeData>
#include <QDrag>

#ifdef Q_WS_X11
#    include "hacks/hacks_x11.hpp"
#    include <QX11Info>
#    include <X11/Xlib.h>
// X11 defines the following, clashing with QEvent::Type enum
#    undef KeyPress
#    undef KeyRelease
#endif

#ifdef Q_OS_MAC
#    include "./hacks/hacks_mac.hpp"
#endif

using namespace QtCollider;

QAtomicInt QWidgetProxy::_globalEventMask = 0;
QMimeData* QWidgetProxy::sDragData = 0;
QString QWidgetProxy::sDragLabel;

QWidgetProxy::QWidgetProxy(QWidget* w, PyrObject* po):
    QObjectProxy(w, po),
    _keyEventWidget(w),
    _mouseEventWidget(w),
    _performDrag(false) {}

void QWidgetProxy::setKeyEventWidget(QWidget* w) {
    if (w == 0 || w == _keyEventWidget)
        return;

    QWidget* me = widget();

    if (_keyEventWidget != me)
        _keyEventWidget->removeEventFilter(this);

    _keyEventWidget = w;

    if (_keyEventWidget != me) {
        _keyEventWidget->installEventFilter(this);
    }
}

void QWidgetProxy::setMouseEventWidget(QWidget* w) {
    if (w == 0 || w == _mouseEventWidget)
        return;

    QWidget* me = widget();

    if (_mouseEventWidget != me)
        _mouseEventWidget->removeEventFilter(this);

    _mouseEventWidget = w;

    if (_mouseEventWidget != me) {
        _mouseEventWidget->installEventFilter(this);
    }
}

bool QWidgetProxy::alwaysOnTop() {
    QWidget* w = widget();
    if (!w)
        return false;

    Qt::WindowFlags flags = w->windowFlags();
    if (flags & Qt::Window && flags & Qt::WindowStaysOnTopHint)
        return true;
    else
        return false;
}

void QWidgetProxy::refresh() {
    QWidget* w = widget();
    if (w)
        sendRefreshEventRecursive(w);
}

void QWidgetProxy::setLayout(QObjectProxy* layoutProxy) {
    QWidget* w = widget();
    QLayout* l = qobject_cast<QLayout*>(layoutProxy->object());
    if (!w || !l)
        return;

    QLayout* exLayout = w->layout();
    if (exLayout != l) {
        if (exLayout != 0) {
            qcDebugMsg(2, QStringLiteral("Deleting old layout."));
            delete exLayout;
        }
        qcDebugMsg(2, QStringLiteral("Setting layout."));
        w->setLayout(l);
        l->activate();
    } else {
        qcDebugMsg(2, QStringLiteral("Layout same as existing. Will do nothing."));
    }
}

bool QWidgetProxy::setParent(QObjectProxy* parentProxy) {
    QObject* parent = parentProxy->object();
    if (!parent || !widget())
        return true;

    if (parent->isWidgetType()) {
        QWidget* pw = qobject_cast<QWidget*>(parent);
        bool ok = pw->metaObject()->invokeMethod(pw, "addChild", Q_ARG(QWidget*, widget()));
        if (!ok)
            widget()->setParent(pw);
        return true;
    }
    return false;
}

void QWidgetProxy::setDragData(QMimeData* data, const QString& label) {
    if (data == 0)
        return;

    if (sDragData == 0) {
        sDragData = data;
        sDragLabel = label;
        _performDrag = true;
    } else {
        delete data;
        qcErrorMsg("QWidgetProxy: attempt at starting a drag while another one is in progress.");
    }
}

void QWidgetProxy::customEvent(QEvent* e) {
    int type = e->type();
    switch (type) {
    case QtCollider::Event_Proxy_BringFront:
        bringFrontEvent();
        return;
    case QtCollider::Event_Proxy_SetFocus:
        setFocusEvent(static_cast<SetFocusEvent*>(e));
        return;
    case QtCollider::Event_Proxy_SetAlwaysOnTop:
        setAlwaysOnTopEvent(static_cast<SetAlwaysOnTopEvent*>(e));
        return;
    default:
        QObjectProxy::customEvent(e);
    }
}


void QWidgetProxy::bringFrontEvent() {
    QWidget* w = widget();
    if (!w)
        return;

    w->setWindowState(w->windowState() & ~Qt::WindowMinimized | Qt::WindowActive);
    w->show();
    w->raise();

#ifdef Q_WS_X11
    raise_window(QX11Info::display(), w);
#endif

#ifdef Q_OS_MAC
    QtCollider::Mac::activateApp();
#endif
}

void QWidgetProxy::setFocusEvent(QtCollider::SetFocusEvent* e) {
    if (!widget())
        return;

    if (e->focus)
        widget()->setFocus(Qt::OtherFocusReason);
    else
        widget()->clearFocus();
}

void QWidgetProxy::setAlwaysOnTopEvent(QtCollider::SetAlwaysOnTopEvent* e) {
    QWidget* w = widget();
    if (!w)
        return;

    Qt::WindowFlags flags = w->windowFlags();
    if (flags & Qt::Window) {
        if (e->alwaysOnTop)
            flags |= Qt::WindowStaysOnTopHint;
        else
            flags &= ~Qt::WindowStaysOnTopHint;

        // record the initial state to restore it later
        QPoint pos = w->pos();
        bool visible = w->isVisible();

        w->setWindowFlags(flags);

        // setting window flags will move the window to (0,0) and hide it,
        // so restore the initial state
        w->move(pos);
        if (visible)
            w->show();
    }
}

void QWidgetProxy::performDrag() {
    Q_ASSERT(sDragData);

    QFont f;
    const QString& label = sDragLabel;
    QFontMetrics fm(f);
    QSize size = fm.size(0, label) + QSize(8, 4);

    QPixmap pix(size);
    QPainter p(&pix);
    p.setBrush(QColor(255, 255, 255));
    QRect r(pix.rect());
    p.drawRect(r.adjusted(0, 0, -1, -1));
    p.drawText(r, Qt::AlignCenter, label);
    p.end();

    QDrag* drag = new QDrag(widget());
    drag->setMimeData(sDragData);
    drag->setPixmap(pix);
    drag->setHotSpot(QPoint(0, +r.height() + 2));
    drag->exec();

    sDragData = 0;
    sDragLabel.clear();
}

bool QWidgetProxy::preProcessEvent(QObject* o, QEvent* e, EventHandlerData& eh, QList<QVariant>& args) {
    // NOTE We assume that qObject need not be checked here, as we wouldn't get events if
    // it wasn't existing
    int acquired_globalEventMask = _globalEventMask.loadRelaxed();

    switch (eh.type) {
    case QEvent::KeyPress:
        return ((acquired_globalEventMask & KeyPress) || eh.enabled) && interpretKeyEvent(o, e, args);

    case QEvent::KeyRelease:
        return ((acquired_globalEventMask & KeyRelease) || eh.enabled) && interpretKeyEvent(o, e, args);

    case QEvent::MouseButtonPress:
    case QEvent::MouseMove:
    case QEvent::MouseButtonRelease:
    case QEvent::MouseButtonDblClick:
    case QEvent::Enter:
    case QEvent::Leave:
        return eh.enabled && interpretMouseEvent(o, e, args);

    case QEvent::Wheel:
        return eh.enabled && interpretMouseWheelEvent(o, e, args);

    case QEvent::DragEnter:
    case QEvent::DragMove:
    case QEvent::Drop:
        return eh.enabled && interpretDragEvent(o, e, args);

    default:
        return eh.enabled;
    }
}

bool QWidgetProxy::interpretMouseEvent(QObject* o, QEvent* e, QList<QVariant>& args) {
    if (o != _mouseEventWidget || !_mouseEventWidget->isEnabled())
        return false;

    QWidget* w = widget();

    QEvent::Type etype = e->type();

    if (etype == QEvent::Enter || etype == QEvent::Leave)
        return true;

    QMouseEvent* mouse = static_cast<QMouseEvent*>(e);
    QPoint pt = (_mouseEventWidget == w ? mouse->pos() : _mouseEventWidget->mapTo(w, mouse->pos()));
    args << pt.x();
    args << pt.y();

    args << (int)mouse->modifiers();

    if (etype == QEvent::MouseMove) {
        int buttons = mouse->buttons();

        if (buttons == 0) {
            // Special treatment of mouse-tracking events.

            QWidget* win = w->window();

            // Only accept if window has a special property enabled.
            if (!(win && win->property("_qc_win_mouse_tracking").toBool()))
                return false;

            // Reject the events when mouse pointer leaves the window,
            // resulting in out-of-bounds coordinates
            if (win == w) {
                if (pt.x() < 0 || pt.x() >= w->width() || pt.y() < 0 || pt.y() >= w->height())
                    return false;
            }
        }

        args << (int)mouse->buttons();
    } else {
        // MouseButtonPress, MouseButtonDblClick, MouseButtonRelease

        int button;

        switch (mouse->button()) {
        case Qt::LeftButton:
            button = 0;
            break;
        case Qt::RightButton:
            button = 1;
            break;
        case Qt::MiddleButton:
            button = 2;
            break;
        default:
            button = -1;
        }

        args << button;

        if (etype == QEvent::MouseButtonPress)
            args << 1;
        else if (etype == QEvent::MouseButtonDblClick)
            args << 2;
    }

    return true;
}

bool QWidgetProxy::interpretMouseWheelEvent(QObject* o, QEvent* e, QList<QVariant>& args) {
    // NOTE: There seems to be a bug in wheel event propagation:
    // the event is propagated to parent twice!
    // Therefore we do not let the propagated events through to SC,
    // (we only let the "spontaneous" ones).

    if (o != _mouseEventWidget || !e->spontaneous() || !_mouseEventWidget->isEnabled())
        return false;

    QWheelEvent* we = static_cast<QWheelEvent*>(e);

    QWidget* w = widget();
    QPointF pt = _mouseEventWidget == w ? we->position() : _mouseEventWidget->mapTo(w, we->position().toPoint());

    QPointF delta;
    // only hi-res trackpads return pixelDelta everything else uses angleDelta..
    if (we->pixelDelta().isNull()) {
        delta = we->angleDelta();
        delta *= 0.125f; // scaled to return steps of 15
    } else {
        delta = we->pixelDelta();
        delta *= 0.25f; // this matches old scaling of delta
    }

    args << pt.x();
    args << pt.y();
    args << (int)we->modifiers();
    args << delta.x();
    args << delta.y();

    return true;
}

bool QWidgetProxy::interpretKeyEvent(QObject* o, QEvent* e, QList<QVariant>& args) {
    if (o != _keyEventWidget || !_keyEventWidget->isEnabled())
        return false;

    QKeyEvent* ke = static_cast<QKeyEvent*>(e);

    int key = ke->key();

    int mods = ke->modifiers();

    QChar character;

#ifdef Q_OS_MAC
    bool isLetter = key >= Qt::Key_A && key <= Qt::Key_Z;
    if (mods & Qt::MetaModifier && isLetter) {
        character = QChar(key - Qt::Key_A + 1);
    } else if (mods & Qt::AltModifier && isLetter) {
        character = (mods & Qt::ShiftModifier) ? QChar(key) : QChar(key - Qt::Key_A + 97);
    } else
#endif
    {
        QString text(ke->text());
        if (text.count())
            character = text[0];
    }

    int unicode = character.unicode();

#ifdef Q_WS_X11
    KeySym sym = ke->nativeVirtualKey();
    int keycode = XKeysymToKeycode(QX11Info::display(), sym);
#else
    // FIXME: On Mac OS X, this does not work for modifier keys
    int keycode = ke->nativeVirtualKey();
#endif

    args << character;
    args << mods;
    args << unicode;
    args << keycode;
    args << key;
    args << ke->spontaneous();

    return true;
}

static QString urlAsString(const QUrl& url) {
    if (QURL_IS_LOCAL_FILE(url))
        return url.toLocalFile();
    else
        return url.toString();
}

static bool interpretMimeData(const QMimeData* data, QList<QVariant>& args) {
    if (data->hasUrls()) {
        QList<QUrl> urls = data->urls();
        if (urls.count() > 1) {
            QVariantList list;
            Q_FOREACH (QUrl url, urls)
                list << urlAsString(url);
            args << QVariant(list);
        } else {
            args << urlAsString(urls[0]);
        }
    } else if (data->hasText()) {
        args << data->text();
    } else {
        return false;
    }

    return true;
}

bool QWidgetProxy::interpretDragEvent(QObject* o, QEvent* e, QList<QVariant>& args) {
    if (o != _mouseEventWidget)
        return false;

    QDropEvent* dnd = static_cast<QDropEvent*>(e);

    const QMimeData* data = dnd->mimeData();

    if (dnd->type() == QEvent::DragEnter) {
        bool internal = data->hasFormat("application/supercollider");
        args << internal;
        if (!internal)
            interpretMimeData(data, args);
    } else {
        QPoint pos = dnd->pos();
        args << pos.x() << pos.y();
    }

    return true;
}

bool QWidgetProxy::postProcessEvent(QObject* object, QEvent* event, bool handled) {
    if (_performDrag) {
        _performDrag = false;
        performDrag();
        return true;
    }

    return handled;
}

void QWidgetProxy::customPaint(QPainter* painter) {
    if (QtCollider::paintingAnnounced()) {
        qcDebugMsg(1, "WARNING: Custom painting already in progress. Will not paint.");
        return;
    }

    QtCollider::announcePainting();

    QtCollider::lockLang();

    if (QtCollider::beginPainting(painter, this)) {
        invokeScMethod(SC_SYM(doDrawFunc), QList<QVariant>(), 0, true);
        QtCollider::endPainting();
    }

    QtCollider::unlockLang();
}

void QWidgetProxy::sendRefreshEventRecursive(QWidget* w) {
    QEvent event(static_cast<QEvent::Type>(QtCollider::Event_Refresh));
    QApplication::sendEvent(w, &event);

    const QObjectList& children = w->children();
    Q_FOREACH (QObject* child, children) {
        if (child->isWidgetType())
            sendRefreshEventRecursive(static_cast<QWidget*>(child));
    }
}
