/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtBluetooth module 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 "qbluetoothservicediscoveryagent.h"
#include "qbluetoothservicediscoveryagent_p.h"

#include <QtCore/QByteArray>
#include <QtCore/QLibrary>
#include <QtCore/QLoggingCategory>
#include <QtCore/QThread>
#include <QtCore/QUrl>

#include <initguid.h>
#include <winsock2.h>
#include <qt_windows.h>

#if defined(Q_CC_MINGW)
// Workaround for MinGW headers declaring BluetoothSdpGetElementData incorrectly.
# define BluetoothSdpGetElementData _BluetoothSdpGetElementData_notok
# include <bluetoothapis.h>
# undef BluetoothSdpGetElementData
  extern "C" DWORD WINAPI BluetoothSdpGetElementData(LPBYTE, ULONG, PSDP_ELEMENT_DATA);
#else
# include <bluetoothapis.h>
#endif

#include <ws2bth.h>
#include <iostream>

QT_BEGIN_NAMESPACE

struct FindServiceResult {
    QBluetoothServiceInfo info;
    Qt::HANDLE hSearch = INVALID_HANDLE_VALUE;
    int systemError = NO_ERROR;
};

Q_DECLARE_LOGGING_CATEGORY(QT_BT_WINDOWS)

static QList<QVariant> spdContainerToVariantList(LPBYTE containerStream, ULONG containerLength);

static QVariant spdElementToVariant(const SDP_ELEMENT_DATA &element)
{
    QVariant variant;

    switch (element.type) {
    case SDP_TYPE_UINT: {
        switch (element.specificType) {
        case SDP_ST_UINT128:
            //Not supported!!!
            break;
        case SDP_ST_UINT64:
            variant =  QVariant::fromValue<quint64>(element.data.uint64);
            break;
        case SDP_ST_UINT32:
            variant =  QVariant::fromValue<quint32>(element.data.uint32);
            break;
        case SDP_ST_UINT16:
            variant =  QVariant::fromValue<quint16>(element.data.uint16);
            break;
        case SDP_ST_UINT8:
            variant =  QVariant::fromValue<quint8>(element.data.uint8);
            break;
        default:
            break;
        }
        break;
    }
    case SDP_TYPE_INT: {
        switch (element.specificType) {
        case SDP_ST_INT128: {
            //Not supported!!!
            break;
        }
        case SDP_ST_INT64:
            variant = QVariant::fromValue<qint64>(element.data.int64);
            break;
        case SDP_ST_INT32:
            variant = QVariant::fromValue<qint32>(element.data.int32);
            break;
        case SDP_ST_INT16:
            variant = QVariant::fromValue<qint16>(element.data.int16);
            break;
        case SDP_ST_INT8:
            variant = QVariant::fromValue<qint8>(element.data.int8);
            break;
        default:
            break;
        }
        break;
    }
    case SDP_TYPE_UUID: {
        switch (element.specificType) {
        case SDP_ST_UUID128:
            variant = QVariant::fromValue(QBluetoothUuid(element.data.uuid128));
            break;
        case SDP_ST_UUID32:
            variant = QVariant::fromValue(QBluetoothUuid(quint32(element.data.uuid32)));
            break;
        case SDP_ST_UUID16:
            variant = QVariant::fromValue(QBluetoothUuid(quint16(element.data.uuid16)));
            break;
        default:
            break;
        }
        break;
    }
    case SDP_TYPE_STRING: {
        const QByteArray stringBuffer(reinterpret_cast<const char*>(element.data.string.value), element.data.string.length);
        variant = QVariant::fromValue<QString>(QString::fromLocal8Bit(stringBuffer));
        break;
    }
    case SDP_TYPE_URL: {
        const QString urlString = QString::fromLocal8Bit(reinterpret_cast<const char*>(element.data.url.value),
                                                   int(element.data.url.length));
        const QUrl url(urlString);
        if (url.isValid())
            variant = QVariant::fromValue<QUrl>(url);
        break;
    }
    case SDP_TYPE_SEQUENCE: {
        const QList<QVariant> sequenceList = spdContainerToVariantList(element.data.sequence.value,
                                                                 element.data.sequence.length);
        const QBluetoothServiceInfo::Sequence sequence(sequenceList);
        variant = QVariant::fromValue(sequence);
        break;
    }
    case SDP_TYPE_ALTERNATIVE: {
        const QList<QVariant> alternativeList = spdContainerToVariantList(element.data.alternative.value,
                                                                    element.data.alternative.length);
        const QBluetoothServiceInfo::Alternative alternative(alternativeList);
        variant = QVariant::fromValue(alternative);
        break;
    }
    case SDP_TYPE_BOOLEAN:
        variant = QVariant::fromValue<bool>(bool(element.data.booleanVal));
        break;
    case SDP_TYPE_NIL:
        break;
    default:
        break;
    }

    return variant;
}

static QList<QVariant> spdContainerToVariantList(LPBYTE containerStream, ULONG containerLength)
{
    HBLUETOOTH_CONTAINER_ELEMENT iter = nullptr;
    SDP_ELEMENT_DATA element = {};

    QList<QVariant> sequence;

    for (;;) {
        const DWORD result = ::BluetoothSdpGetContainerElementData(containerStream,
                                                           containerLength,
                                                           &iter,
                                                           &element);

        if (result == ERROR_SUCCESS) {
            const QVariant variant = spdElementToVariant(element);
            sequence.append(variant);
        } else if (result == ERROR_NO_MORE_ITEMS) {
            break;
        } else if (result == ERROR_INVALID_PARAMETER) {
            break;
        }
    }

    return sequence;
}

#if defined(Q_CC_MINGW)
# define SDP_CALLBACK
#else
# define SDP_CALLBACK QT_WIN_CALLBACK
#endif
static BOOL SDP_CALLBACK bluetoothSdpCallback(ULONG attributeId, LPBYTE valueStream, ULONG streamSize, LPVOID param)
{
    QBluetoothServiceInfo *result = static_cast<QBluetoothServiceInfo*>(param);

    SDP_ELEMENT_DATA element = {};

    if (::BluetoothSdpGetElementData(valueStream, streamSize, &element) == ERROR_SUCCESS) {
        switch (element.type) {
        case SDP_TYPE_UINT:
        case SDP_TYPE_INT:
        case SDP_TYPE_UUID:
        case SDP_TYPE_STRING:
        case SDP_TYPE_URL:
        case SDP_TYPE_BOOLEAN:
        case SDP_TYPE_SEQUENCE:
        case SDP_TYPE_ALTERNATIVE: {
            const QVariant variant = spdElementToVariant(element);

            result->setAttribute(attributeId, variant);
            break;
        }
        case SDP_TYPE_NIL:
            break;
        default:
            break;
        }
    }
    return true;
}

enum {
    WSAControlFlags = LUP_FLUSHCACHE
          | LUP_RETURN_NAME
          | LUP_RETURN_TYPE
          | LUP_RETURN_ADDR
          | LUP_RETURN_BLOB
          | LUP_RETURN_COMMENT
};

static FindServiceResult findNextService(HANDLE hSearch)
{
    FindServiceResult result;
    result.hSearch = hSearch;

    QByteArray resultBuffer(2048, 0);
    WSAQUERYSET *resultQuery = reinterpret_cast<WSAQUERYSET*>(resultBuffer.data());
    DWORD resultBufferSize = DWORD(resultBuffer.size());
    const int resultCode = ::WSALookupServiceNext(hSearch,
                                                WSAControlFlags,
                                                &resultBufferSize,
                                                resultQuery);

    if (resultCode == SOCKET_ERROR) {
        result.systemError = ::WSAGetLastError();
        if (result.systemError == WSA_E_NO_MORE)
            ::WSALookupServiceEnd(hSearch);
        return result;
     }

    if (resultQuery->lpBlob
            && ::BluetoothSdpEnumAttributes(resultQuery->lpBlob->pBlobData,
                                          resultQuery->lpBlob->cbSize,
                                          bluetoothSdpCallback,
                                          &result.info)) {
        return result;
    } else {
        result.systemError = GetLastError();
    }
    return result;
}

static FindServiceResult findFirstService(const QBluetoothAddress &address)
{
    WSAData wsadata = {};
    FindServiceResult result;

    // IPv6 requires Winsock v2.0 or better.
    if (::WSAStartup(MAKEWORD(2, 0), &wsadata) != 0) {
        result.systemError = ::WSAGetLastError();
        return result;
    }

    const QString addressAsString = QStringLiteral("(%1)").arg(address.toString());
    QVector<WCHAR> addressAsWChar(addressAsString.size() + 1);
    addressAsString.toWCharArray(addressAsWChar.data());

    GUID protocol = L2CAP_PROTOCOL_UUID; //Search for L2CAP and RFCOMM services

    WSAQUERYSET serviceQuery = {};
    serviceQuery.dwSize = sizeof(WSAQUERYSET);
    serviceQuery.lpServiceClassId = &protocol;
    serviceQuery.dwNameSpace = NS_BTH;
    serviceQuery.dwNumberOfCsAddrs = 0;
    serviceQuery.lpszContext = addressAsWChar.data();

    HANDLE hSearch = nullptr;
    const int resultCode = ::WSALookupServiceBegin(&serviceQuery,
                                                 WSAControlFlags,
                                                 &hSearch);
    if (resultCode == SOCKET_ERROR) {
        result.systemError = ::WSAGetLastError();
        ::WSALookupServiceEnd(hSearch);
        return result;
    }
    return findNextService(hSearch);
}

QBluetoothServiceDiscoveryAgentPrivate::QBluetoothServiceDiscoveryAgentPrivate(
        QBluetoothServiceDiscoveryAgent *qp, const QBluetoothAddress &deviceAdapter)
    :  error(QBluetoothServiceDiscoveryAgent::NoError),
      state(Inactive),
      deviceDiscoveryAgent(0),
      mode(QBluetoothServiceDiscoveryAgent::MinimalDiscovery),
      singleDevice(false),
      pendingStop(false),
      pendingFinish(false),
      q_ptr(qp)
{
    Q_UNUSED(deviceAdapter);

    threadFind = new QThread;
    threadWorkerFind = new ThreadWorkerFind;
    threadWorkerFind->moveToThread(threadFind);
    connect(threadWorkerFind, &ThreadWorkerFind::findFinished, this, &QBluetoothServiceDiscoveryAgentPrivate::_q_nextSdpScan);
    connect(threadFind, &QThread::finished, threadWorkerFind, &ThreadWorkerFind::deleteLater);
    connect(threadFind, &QThread::finished, threadFind, &QThread::deleteLater);
    threadFind->start();
}

QBluetoothServiceDiscoveryAgentPrivate::~QBluetoothServiceDiscoveryAgentPrivate()
{
    if (pendingFinish)
        stop();
    if (threadFind)
        threadFind->quit();
}

void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &address)
{
    if (!pendingFinish) {
        pendingFinish = true;
        pendingStop = false;

        const auto threadWorker = threadWorkerFind;
        QMetaObject::invokeMethod(threadWorkerFind, [threadWorker, address]()
        {
            const FindServiceResult result = findFirstService(address);
            emit threadWorker->findFinished(QVariant::fromValue(result));
        }, Qt::QueuedConnection);
    }
}

void QBluetoothServiceDiscoveryAgentPrivate::stop()
{
    pendingStop = true;
}

bool QBluetoothServiceDiscoveryAgentPrivate::serviceMatches(const QBluetoothServiceInfo &info)
{
    if (uuidFilter.isEmpty())
        return true;

    if (uuidFilter.contains(info.serviceUuid()))
        return true;

    const QList<QBluetoothUuid> serviceClassUuids = info.serviceClassUuids();
    for (const QBluetoothUuid &uuid : serviceClassUuids)
        if (uuidFilter.contains(uuid))
            return true;

    return false;
}

void QBluetoothServiceDiscoveryAgentPrivate::_q_nextSdpScan(const QVariant &input)
{
    Q_Q(QBluetoothServiceDiscoveryAgent);
    auto result = input.value<FindServiceResult>();

    if (pendingStop) {
        ::WSALookupServiceEnd(result.hSearch);
        pendingStop = false;
        pendingFinish = false;
        emit q->canceled();
    } else {
        if (result.systemError == WSA_E_NO_MORE) {
            result.systemError = NO_ERROR;
        } else if (result.systemError != NO_ERROR) {
            if (result.hSearch != INVALID_HANDLE_VALUE)
                ::WSALookupServiceEnd(result.hSearch);
            error = (result.systemError == ERROR_INVALID_HANDLE) ?
                        QBluetoothServiceDiscoveryAgent::InvalidBluetoothAdapterError
                      : QBluetoothServiceDiscoveryAgent::InputOutputError;
            errorString = qt_error_string(result.systemError);
            qCWarning(QT_BT_WINDOWS) << errorString;
            emit q->error(this->error);
        } else {

            if (serviceMatches(result.info)) {
                result.info.setDevice(discoveredDevices.at(0));
                if (result.info.isValid()) {
                    if (!isDuplicatedService(result.info)) {
                        discoveredServices.append(result.info);
                        emit q->serviceDiscovered(result.info);
                    }
                }
            }

            const auto threadWorker = threadWorkerFind;
            const auto hSearch = result.hSearch;
            QMetaObject::invokeMethod(threadWorkerFind, [threadWorker, hSearch]()
            {
                FindServiceResult result = findNextService(hSearch);
                emit threadWorker->findFinished(QVariant::fromValue(result));
            }, Qt::QueuedConnection);
            return;
        }
        pendingFinish = false;
        _q_serviceDiscoveryFinished();
    }
}

QT_END_NAMESPACE

Q_DECLARE_METATYPE(FindServiceResult)
