/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qwindowstabletsupport.h"

#include "qwindowscontext.h"
#include "qwindowskeymapper.h"
#include "qwindowswindow.h"
#include "qwindowsscreen.h"

#include <qpa/qwindowsysteminterface.h>

#include <QtGui/QTabletEvent>
#include <QtGui/QScreen>
#include <QtGui/QGuiApplication>
#include <QtGui/QWindow>
#include <QtCore/QDebug>
#include <QtCore/QVarLengthArray>
#include <QtCore/QtMath>

#include <private/qguiapplication_p.h>
#include <QtCore/private/qsystemlibrary_p.h>

// Note: The definition of the PACKET structure in pktdef.h depends on this define.
#define PACKETDATA (PK_X | PK_Y | PK_BUTTONS | PK_NORMAL_PRESSURE | PK_TANGENT_PRESSURE | PK_ORIENTATION | PK_CURSOR | PK_Z | PK_TIME)
#include <pktdef.h>

QT_BEGIN_NAMESPACE

enum {
    PacketMode = 0,
    TabletPacketQSize = 128,
    DeviceIdMask = 0xFF6, // device type mask && device color mask
    CursorTypeBitMask = 0x0F06 // bitmask to find the specific cursor type (see Wacom FAQ)
};

extern "C" LRESULT QT_WIN_CALLBACK qWindowsTabletSupportWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) {
    case WT_PROXIMITY:
        if (QWindowsContext::instance()->tabletSupport()->translateTabletProximityEvent(wParam, lParam))
            return 0;
        break;
    case WT_PACKET:
        if (QWindowsContext::instance()->tabletSupport()->translateTabletPacketEvent())
            return 0;
        break;
    }
    return DefWindowProc(hwnd, message, wParam, lParam);
}


// Scale tablet coordinates to screen coordinates.

static inline int sign(int x)
{
    return x >= 0 ? 1 : -1;
}

inline QPointF QWindowsTabletDeviceData::scaleCoordinates(int coordX, int coordY, const QRect &targetArea) const
{
    const int targetX = targetArea.x();
    const int targetY = targetArea.y();
    const int targetWidth = targetArea.width();
    const int targetHeight = targetArea.height();

    const qreal x = sign(targetWidth) == sign(maxX) ?
        ((coordX - minX) * qAbs(targetWidth) / qAbs(qreal(maxX - minX))) + targetX :
        ((qAbs(maxX) - (coordX - minX)) * qAbs(targetWidth) / qAbs(qreal(maxX - minX))) + targetX;

    const qreal y = sign(targetHeight) == sign(maxY) ?
        ((coordY - minY) * qAbs(targetHeight) / qAbs(qreal(maxY - minY))) + targetY :
        ((qAbs(maxY) - (coordY - minY)) * qAbs(targetHeight) / qAbs(qreal(maxY - minY))) + targetY;

    return QPointF(x, y);
}

QWindowsWinTab32DLL QWindowsTabletSupport::m_winTab32DLL;

/*!
    \class QWindowsWinTab32DLL QWindowsTabletSupport
    \brief Functions from wintabl32.dll shipped with WACOM tablets used by QWindowsTabletSupport.

    \internal
    \ingroup qt-lighthouse-win
*/

bool QWindowsWinTab32DLL::init()
{
    if (wTInfo)
        return true;
    QSystemLibrary library(QStringLiteral("wintab32"));
    if (!library.load())
        return false;
    wTOpen = (PtrWTOpen)library.resolve("WTOpenW");
    wTClose = (PtrWTClose)library.resolve("WTClose");
    wTInfo = (PtrWTInfo)library.resolve("WTInfoW");
    wTEnable = (PtrWTEnable)library.resolve("WTEnable");
    wTOverlap = (PtrWTEnable)library.resolve("WTOverlap");
    wTPacketsGet = (PtrWTPacketsGet)library.resolve("WTPacketsGet");
    wTGet = (PtrWTGet)library.resolve("WTGetW");
    wTQueueSizeGet = (PtrWTQueueSizeGet)library.resolve("WTQueueSizeGet");
    wTQueueSizeSet = (PtrWTQueueSizeSet)library.resolve("WTQueueSizeSet");
    return wTOpen && wTClose && wTInfo && wTEnable && wTOverlap && wTPacketsGet && wTQueueSizeGet && wTQueueSizeSet;
}

/*!
    \class QWindowsTabletSupport
    \brief Tablet support for Windows.

    Support for WACOM tablets.

    \sa http://www.wacomeng.com/windows/docs/Wintab_v140.htm

    \internal
    \since 5.2
    \ingroup qt-lighthouse-win
*/

QWindowsTabletSupport::QWindowsTabletSupport(HWND window, HCTX context)
    : m_window(window)
    , m_context(context)
    , m_absoluteRange(20)
    , m_tiltSupport(false)
    , m_currentDevice(-1)
{
     AXIS orientation[3];
    // Some tablets don't support tilt, check if it is possible,
    if (QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_DEVICES, DVC_ORIENTATION, &orientation))
        m_tiltSupport = orientation[0].axResolution && orientation[1].axResolution;
}

QWindowsTabletSupport::~QWindowsTabletSupport()
{
    QWindowsTabletSupport::m_winTab32DLL.wTClose(m_context);
    DestroyWindow(m_window);
}

QWindowsTabletSupport *QWindowsTabletSupport::create()
{
    if (!m_winTab32DLL.init())
        return 0;
    const HWND window = QWindowsContext::instance()->createDummyWindow(QStringLiteral("TabletDummyWindow"),
                                                                       L"TabletDummyWindow",
                                                                       qWindowsTabletSupportWndProc);
    if (!window) {
        qCWarning(lcQpaTablet) << __FUNCTION__ << "Unable to create window for tablet.";
        return 0;
    }
    LOGCONTEXT lcMine;
    // build our context from the default context
    QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_DEFSYSCTX, 0, &lcMine);
    // Go for the raw coordinates, the tablet event will return good stuff
    lcMine.lcOptions |= CXO_MESSAGES | CXO_CSRMESSAGES;
    lcMine.lcPktData = lcMine.lcMoveMask = PACKETDATA;
    lcMine.lcPktMode = PacketMode;
    lcMine.lcOutOrgX = 0;
    lcMine.lcOutExtX = lcMine.lcInExtX;
    lcMine.lcOutOrgY = 0;
    lcMine.lcOutExtY = -lcMine.lcInExtY;
    const HCTX context = QWindowsTabletSupport::m_winTab32DLL.wTOpen(window, &lcMine, true);
    if (!context) {
        qCDebug(lcQpaTablet) << __FUNCTION__ << "Unable to open tablet.";
        DestroyWindow(window);
        return 0;

    }
    // Set the size of the Packet Queue to the correct size
    const int currentQueueSize = QWindowsTabletSupport::m_winTab32DLL.wTQueueSizeGet(context);
    if (currentQueueSize != TabletPacketQSize) {
        if (!QWindowsTabletSupport::m_winTab32DLL.wTQueueSizeSet(context, TabletPacketQSize)) {
            if (!QWindowsTabletSupport::m_winTab32DLL.wTQueueSizeSet(context, currentQueueSize))  {
                qWarning("Unable to set queue size on tablet. The tablet will not work.");
                QWindowsTabletSupport::m_winTab32DLL.wTClose(context);
                DestroyWindow(window);
                return 0;
            } // cannot restore old size
        } // cannot set
    } // mismatch
    qCDebug(lcQpaTablet) << "Opened tablet context " << context << " on window "
        <<  window << "changed packet queue size " << currentQueueSize
        << "->" <<  TabletPacketQSize;
    return new QWindowsTabletSupport(window, context);
}

unsigned QWindowsTabletSupport::options() const
{
    UINT result = 0;
    m_winTab32DLL.wTInfo(WTI_INTERFACE, IFC_CTXOPTIONS, &result);
    return result;
}

QString QWindowsTabletSupport::description() const
{
    const unsigned size = m_winTab32DLL.wTInfo(WTI_INTERFACE, IFC_WINTABID, 0);
    if (!size)
        return QString();
    QVarLengthArray<TCHAR> winTabId(size + 1);
    m_winTab32DLL.wTInfo(WTI_INTERFACE, IFC_WINTABID, winTabId.data());
    WORD implementationVersion = 0;
    m_winTab32DLL.wTInfo(WTI_INTERFACE, IFC_IMPLVERSION, &implementationVersion);
    WORD specificationVersion = 0;
    m_winTab32DLL.wTInfo(WTI_INTERFACE, IFC_SPECVERSION, &specificationVersion);
    const unsigned opts = options();
    QString result = QString::fromLatin1("%1 specification: v%2.%3 implementation: v%4.%5 options: 0x%6")
        .arg(QString::fromWCharArray(winTabId.data()))
        .arg(specificationVersion >> 8).arg(specificationVersion & 0xFF)
        .arg(implementationVersion >> 8).arg(implementationVersion & 0xFF)
        .arg(opts, 0, 16);
    if (opts & CXO_MESSAGES)
        result += QLatin1String(" CXO_MESSAGES");
    if (opts & CXO_CSRMESSAGES)
        result += QLatin1String(" CXO_CSRMESSAGES");
    if (m_tiltSupport)
        result += QLatin1String(" tilt");
    return result;
}

void QWindowsTabletSupport::notifyActivate()
{
    // Cooperate with other tablet applications, but when we get focus, I want to use the tablet.
    const bool result = QWindowsTabletSupport::m_winTab32DLL.wTEnable(m_context, true)
        && QWindowsTabletSupport::m_winTab32DLL.wTOverlap(m_context, true);
   qCDebug(lcQpaTablet) << __FUNCTION__ << result;
}

static inline int indexOfDevice(const QVector<QWindowsTabletDeviceData> &devices, qint64 uniqueId)
{
    for (int i = 0; i < devices.size(); ++i)
        if (devices.at(i).uniqueId == uniqueId)
            return i;
    return -1;
}

static inline QTabletEvent::TabletDevice deviceType(const UINT cursorType)
{
    if (((cursorType & 0x0006) == 0x0002) && ((cursorType & CursorTypeBitMask) != 0x0902))
        return QTabletEvent::Stylus;
    if (cursorType == 0x4020) // Surface Pro 2 tablet device
        return QTabletEvent::Stylus;
    switch (cursorType & CursorTypeBitMask) {
    case 0x0802:
        return QTabletEvent::Stylus;
    case 0x0902:
        return QTabletEvent::Airbrush;
    case 0x0004:
        return QTabletEvent::FourDMouse;
    case 0x0006:
        return QTabletEvent::Puck;
    case 0x0804:
        return QTabletEvent::RotationStylus;
    default:
        break;
    }
    return QTabletEvent::NoDevice;
}

static inline QTabletEvent::PointerType pointerType(unsigned currentCursor)
{
    switch (currentCursor % 3) { // %3 for dual track
    case 0:
        return QTabletEvent::Cursor;
    case 1:
        return QTabletEvent::Pen;
    case 2:
        return QTabletEvent::Eraser;
    default:
        break;
    }
    return QTabletEvent::UnknownPointer;
}

#ifndef QT_NO_DEBUG_STREAM
QDebug operator<<(QDebug d, const QWindowsTabletDeviceData &t)
{
    QDebugStateSaver saver(d);
    d.nospace();
    d << "TabletDevice id:" << t.uniqueId << " pressure: " << t.minPressure
      << ".." << t.maxPressure << " tan pressure: " << t.minTanPressure << ".."
      << t.maxTanPressure << " area:" << t.minX << t.minY <<t.minZ
      << ".." << t.maxX << t.maxY << t.maxZ << " device " << t.currentDevice
      << " pointer " << t.currentPointerType;
    return d;
}
#endif // !QT_NO_DEBUG_STREAM

QWindowsTabletDeviceData QWindowsTabletSupport::tabletInit(qint64 uniqueId, UINT cursorType) const
{
    QWindowsTabletDeviceData result;
    result.uniqueId = uniqueId;
    /* browse WinTab's many info items to discover pressure handling. */
    AXIS axis;
    LOGCONTEXT lc;
    /* get the current context for its device variable. */
    QWindowsTabletSupport::m_winTab32DLL.wTGet(m_context, &lc);
    /* get the size of the pressure axis. */
    QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_DEVICES + lc.lcDevice, DVC_NPRESSURE, &axis);
    result.minPressure = int(axis.axMin);
    result.maxPressure = int(axis.axMax);

    QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_DEVICES + lc.lcDevice, DVC_TPRESSURE, &axis);
    result.minTanPressure = int(axis.axMin);
    result.maxTanPressure = int(axis.axMax);

    LOGCONTEXT defaultLc;
    /* get default region */
    QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_DEFCONTEXT, 0, &defaultLc);
    result.maxX = int(defaultLc.lcInExtX) - int(defaultLc.lcInOrgX);
    result.maxY = int(defaultLc.lcInExtY) - int(defaultLc.lcInOrgY);
    result.maxZ = int(defaultLc.lcInExtZ) - int(defaultLc.lcInOrgZ);
    result.currentDevice = deviceType(cursorType);
    return result;
}

bool QWindowsTabletSupport::translateTabletProximityEvent(WPARAM /* wParam */, LPARAM lParam)
{
    PACKET proximityBuffer[1]; // we are only interested in the first packet in this case
    const int totalPacks = QWindowsTabletSupport::m_winTab32DLL.wTPacketsGet(m_context, 1, proximityBuffer);

    if (!LOWORD(lParam)) {
        qCDebug(lcQpaTablet) << "leave proximity for device #" << m_currentDevice;
        if (totalPacks > 0) {
            QWindowSystemInterface::handleTabletLeaveProximityEvent(proximityBuffer[0].pkTime,
                                                                    m_devices.at(m_currentDevice).currentDevice,
                                                                    m_devices.at(m_currentDevice).currentPointerType,
                                                                    m_devices.at(m_currentDevice).uniqueId);
        } else {
            QWindowSystemInterface::handleTabletLeaveProximityEvent(m_devices.at(m_currentDevice).currentDevice,
                                                                    m_devices.at(m_currentDevice).currentPointerType,
                                                                    m_devices.at(m_currentDevice).uniqueId);

        }
        return true;
    }

    if (!totalPacks)
        return false;

    const UINT currentCursor = proximityBuffer[0].pkCursor;
    UINT physicalCursorId;
    QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_CURSORS + currentCursor, CSR_PHYSID, &physicalCursorId);
    UINT cursorType;
    QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_CURSORS + currentCursor, CSR_TYPE, &cursorType);
    const qint64 uniqueId = (qint64(cursorType & DeviceIdMask) << 32L) | qint64(physicalCursorId);
    // initializing and updating the cursor should be done in response to
    // WT_CSRCHANGE. We do it in WT_PROXIMITY because some wintab never send
    // the event WT_CSRCHANGE even if asked with CXO_CSRMESSAGES
    m_currentDevice = indexOfDevice(m_devices, uniqueId);
    if (m_currentDevice < 0) {
        m_currentDevice = m_devices.size();
        m_devices.push_back(tabletInit(uniqueId, cursorType));
    }
    m_devices[m_currentDevice].currentPointerType = pointerType(currentCursor);
    qCDebug(lcQpaTablet) << "enter proximity for device #"
        << m_currentDevice << m_devices.at(m_currentDevice);
    QWindowSystemInterface::handleTabletEnterProximityEvent(proximityBuffer[0].pkTime,
                                                            m_devices.at(m_currentDevice).currentDevice,
                                                            m_devices.at(m_currentDevice).currentPointerType,
                                                            m_devices.at(m_currentDevice).uniqueId);
    return true;
}

bool QWindowsTabletSupport::translateTabletPacketEvent()
{
    static PACKET localPacketBuf[TabletPacketQSize];  // our own tablet packet queue.
    const int packetCount = QWindowsTabletSupport::m_winTab32DLL.wTPacketsGet(m_context, TabletPacketQSize, &localPacketBuf);
    if (!packetCount || m_currentDevice < 0)
        return false;

    const int currentDevice = m_devices.at(m_currentDevice).currentDevice;
    const int currentPointer = m_devices.at(m_currentDevice).currentPointerType;
    const qint64 uniqueId = m_devices.at(m_currentDevice).uniqueId;

    // The tablet can be used in 2 different modes, depending on it settings:
    // 1) Absolute (pen) mode:
    //    The coordinates are scaled to the virtual desktop (by default). The user
    //    can also choose to scale to the monitor or a region of the screen.
    //    When entering proximity, the tablet driver snaps the mouse pointer to the
    //    tablet position scaled to that area and keeps it in sync.
    // 2) Relative (mouse) mode:
    //    The pen follows the mouse. The constant 'absoluteRange' specifies the
    //    manhattanLength difference for detecting if a tablet input device is in this mode,
    //    in which case we snap the position to the mouse position.
    // It seems there is no way to find out the mode programmatically, the LOGCONTEXT orgX/Y/Ext
    // area is always the virtual desktop.
    const QRect virtualDesktopArea = QGuiApplication::primaryScreen()->virtualGeometry();

    qCDebug(lcQpaTablet) << __FUNCTION__ << "processing " << packetCount
        << "target:" << QGuiApplicationPrivate::tabletDevicePoint(uniqueId).target;

    const Qt::KeyboardModifiers keyboardModifiers = QWindowsKeyMapper::queryKeyboardModifiers();

    for (int i = 0; i < packetCount ; ++i) {
        const PACKET &packet = localPacketBuf[i];

        const int z = currentDevice == QTabletEvent::FourDMouse ? int(packet.pkZ) : 0;

        // This code is to delay the tablet data one cycle to sync with the mouse location.
        QPointF globalPosF = m_oldGlobalPosF;
        m_oldGlobalPosF = m_devices.at(m_currentDevice).scaleCoordinates(packet.pkX, packet.pkY, virtualDesktopArea);

        QWindow *target = QGuiApplicationPrivate::tabletDevicePoint(uniqueId).target; // Pass to window that grabbed it.
        QPoint globalPos = globalPosF.toPoint();

        // Get Mouse Position and compare to tablet info
        const QPoint mouseLocation = QWindowsCursor::mousePosition();

        // Positions should be almost the same if we are in absolute
        // mode. If they are not, use the mouse location.
        if ((mouseLocation - globalPos).manhattanLength() > m_absoluteRange) {
            globalPos = mouseLocation;
            globalPosF = globalPos;
        }

        if (!target)
            target = QWindowsScreen::windowAt(globalPos, CWP_SKIPINVISIBLE | CWP_SKIPTRANSPARENT);
        if (!target)
            continue;

        const QPoint localPos = target->mapFromGlobal(globalPos);

        const qreal pressureNew = packet.pkButtons && (currentPointer == QTabletEvent::Pen || currentPointer == QTabletEvent::Eraser) ?
            m_devices.at(m_currentDevice).scalePressure(packet.pkNormalPressure) :
            qreal(0);
        const qreal tangentialPressure = currentDevice == QTabletEvent::Airbrush ?
            m_devices.at(m_currentDevice).scaleTangentialPressure(packet.pkTangentPressure) :
            qreal(0);

        int tiltX = 0;
        int tiltY = 0;
        qreal rotation = 0;
        if (m_tiltSupport) {
            // Convert from azimuth and altitude to x tilt and y tilt. What
            // follows is the optimized version. Here are the equations used:
            // X = sin(azimuth) * cos(altitude)
            // Y = cos(azimuth) * cos(altitude)
            // Z = sin(altitude)
            // X Tilt = arctan(X / Z)
            // Y Tilt = arctan(Y / Z)
            const double radAzim = (packet.pkOrientation.orAzimuth / 10.0) * (M_PI / 180);
            const double tanAlt = std::tan((std::abs(packet.pkOrientation.orAltitude / 10.0)) * (M_PI / 180));

            const double degX = std::atan(std::sin(radAzim) / tanAlt);
            const double degY = std::atan(std::cos(radAzim) / tanAlt);
            tiltX = int(degX * (180 / M_PI));
            tiltY = int(-degY * (180 / M_PI));
            rotation = 360.0 - (packet.pkOrientation.orTwist / 10.0);
            if (rotation > 180.0)
                rotation -= 360.0;
        }

        if (QWindowsContext::verbose > 1)  {
            qCDebug(lcQpaTablet)
                << "Packet #" << i << '/' << packetCount << "button:" << packet.pkButtons
                << globalPosF << z << "to:" << target << localPos << "(packet" << packet.pkX
                << packet.pkY << ") dev:" << currentDevice << "pointer:"
                << currentPointer << "P:" << pressureNew << "tilt:" << tiltX << ','
                << tiltY << "tanP:" << tangentialPressure << "rotation:" << rotation;
        }

        QWindowSystemInterface::handleTabletEvent(target, packet.pkTime, QPointF(localPos), globalPosF,
                                                  currentDevice, currentPointer,
                                                  static_cast<Qt::MouseButtons>(packet.pkButtons),
                                                  pressureNew, tiltX, tiltY,
                                                  tangentialPressure, rotation, z,
                                                  uniqueId,
                                                  keyboardModifiers);
    }
    return true;
}

QT_END_NAMESPACE
