// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qlowenergycharacteristicdata.h"
#include "qbluetoothaddress.h"
#include "qbluetoothuuid.h"
#include "btutility_p.h"

#include <QtCore/qoperatingsystemversion.h>
#include <QtCore/qendian.h>
#include <QtCore/qstring.h>

#ifndef QT_IOS_BLUETOOTH

#import <IOBluetooth/objc/IOBluetoothSDPUUID.h>
#import <CoreFoundation/CoreFoundation.h>
#import <CoreBluetooth/CBUUID.h>

#endif

#include <algorithm>
#include <limits>

QT_BEGIN_NAMESPACE

Q_LOGGING_CATEGORY(QT_BT_DARWIN, "qt.bluetooth.darwin")

namespace DarwinBluetooth {

const int defaultLEScanTimeoutMS = 40000;
// We use it only on iOS for now:
const int maxValueLength = 512;

const int defaultMtu = 23;

QString qt_address(NSString *address)
{
    if (address && address.length) {
        NSString *const fixed = [address stringByReplacingOccurrencesOfString:@"-" withString:@":"];
        return QString::fromNSString(fixed);
    }

    return QString();
}

#ifndef QT_IOS_BLUETOOTH


QBluetoothAddress qt_address(const BluetoothDeviceAddress *a)
{
    if (a) {
        // TODO: can a byte order be different in BluetoothDeviceAddress?
        const quint64 qAddress = a->data[5] |
                                 qint64(a->data[4]) << 8  |
                                 qint64(a->data[3]) << 16 |
                                 qint64(a->data[2]) << 24 |
                                 qint64(a->data[1]) << 32 |
                                 qint64(a->data[0]) << 40;
        return QBluetoothAddress(qAddress);
    }

    return QBluetoothAddress();
}

BluetoothDeviceAddress iobluetooth_address(const QBluetoothAddress &qAddress)
{
    BluetoothDeviceAddress a = {};
    if (!qAddress.isNull()) {
        const quint64 val = qAddress.toUInt64();
        a.data[0] = (val >> 40) & 0xff;
        a.data[1] = (val >> 32) & 0xff;
        a.data[2] = (val >> 24) & 0xff;
        a.data[3] = (val >> 16) & 0xff;
        a.data[4] = (val >> 8) & 0xff;
        a.data[5] = val & 0xff;
    }

    return a;
}

ObjCStrongReference<IOBluetoothSDPUUID> iobluetooth_uuid(const QBluetoothUuid &uuid)
{
    const unsigned nBytes = 128 / std::numeric_limits<unsigned char>::digits;
    const QUuid::Id128Bytes intVal(uuid.toBytes());

    const ObjCStrongReference<IOBluetoothSDPUUID> iobtUUID([IOBluetoothSDPUUID uuidWithBytes:intVal.data
                                                           length:nBytes], RetainPolicy::doInitialRetain);
    return iobtUUID;
}

QBluetoothUuid qt_uuid(IOBluetoothSDPUUID *uuid)
{
    QBluetoothUuid qtUuid;
    if (!uuid || [uuid length] != 16) // TODO: issue any diagnostic?
        return qtUuid;

    // TODO: ensure the correct byte-order!!!
    QUuid::Id128Bytes uuidVal = {};
    const quint8 *const source = static_cast<const quint8 *>([uuid bytes]);
    std::copy(source, source + 16, uuidVal.data);
    return QBluetoothUuid(uuidVal);
}

QString qt_error_string(IOReturn errorCode)
{
    switch (errorCode) {
    case kIOReturnSuccess:
        // NoError in many classes == an empty string description.
        return QString();
    case kIOReturnNoMemory:
        return QString::fromLatin1("memory allocation failed");
    case kIOReturnNoResources:
        return QString::fromLatin1("failed to obtain a resource");
    case kIOReturnBusy:
        return QString::fromLatin1("device is busy");
    case kIOReturnStillOpen:
        return QString::fromLatin1("device(s) still open");
    // Others later ...
    case kIOReturnError: // "general error" (IOReturn.h)
    default:
        return QString::fromLatin1("unknown error");
    }
}

void qt_test_iobluetooth_runloop()
{
    // IOBluetooth heavily relies on a CFRunLoop machinery in a way it dispatches
    // its callbacks. Technically, having a QThread with CFRunLoop-based event
    // dispatcher would suffice. At the moment of writing we do not have such
    // event dispatcher, so we only can work on the main thread.
    if (CFRunLoopGetMain() != CFRunLoopGetCurrent()) {
        qCWarning(QT_BT_DARWIN) << "IOBluetooth works only on the main thread or a"
                             << "thread with a running CFRunLoop";
    }
}

#endif // !QT_IOS_BLUETOOTH

// Apple has: CBUUID, NSUUID, CFUUID, IOBluetoothSDPUUID
// and it's handy to have several converters:

QBluetoothUuid qt_uuid(CBUUID *uuid)
{
    // Apples' docs say "128 bit" and "16-bit UUIDs are implicitly
    // pre-filled with the Bluetooth Base UUID."
    // But Core Bluetooth can return CBUUID objects of length 2, 4, and 16.

    if (!uuid)
        return QBluetoothUuid();

    QT_BT_MAC_AUTORELEASEPOOL;

    if (uuid.data.length == 2) {
        // CBUUID's docs say nothing about byte-order.
        // Seems to be in big-endian.
        const uchar *const src = static_cast<const uchar *>(uuid.data.bytes);
        return QBluetoothUuid(qFromBigEndian<quint16>(src));
    } else if (uuid.data.length == 4) {
        const uchar *const src = static_cast<const uchar *>(uuid.data.bytes);
        return QBluetoothUuid(qFromBigEndian<quint32>(src));
    } else if (uuid.data.length == 16) {
        QUuid::Id128Bytes qtUuidData = {};
        const quint8 *const source = static_cast<const quint8 *>(uuid.data.bytes);
        std::copy(source, source + 16, qtUuidData.data);

        return QBluetoothUuid(qtUuidData);
    }

    qCDebug(QT_BT_DARWIN) << "qt_uuid, invalid CBUUID, 2, 4, or 16 bytes expected, but got "
                          << uuid.data.length << " bytes length";
    return QBluetoothUuid();
}

ObjCStrongReference<CBUUID> cb_uuid(const QBluetoothUuid &qtUuid)
{
    bool ok = false;
    const auto asUInt16 = qToBigEndian(qtUuid.toUInt16(&ok));
    const auto asUInt128 = qtUuid.toBytes();

    const NSUInteger length = ok ? sizeof asUInt16 : sizeof asUInt128;
    const void *bytes = &asUInt128;
    if (ok)
        bytes = &asUInt16;

    NSData *uuidData = [NSData dataWithBytes:bytes length:length];
    ObjCStrongReference<CBUUID> cbUuid([CBUUID UUIDWithData:uuidData], RetainPolicy::doInitialRetain);
    return cbUuid;
}

bool equal_uuids(const QBluetoothUuid &qtUuid, CBUUID *cbUuid)
{
    const QBluetoothUuid qtUuid2(qt_uuid(cbUuid));
    return qtUuid == qtUuid2;
}

bool equal_uuids(CBUUID *cbUuid, const QBluetoothUuid &qtUuid)
{
    return equal_uuids(qtUuid, cbUuid);
}

QByteArray qt_bytearray(NSData *data)
{
    QByteArray value;
    if (!data || !data.length)
        return value;

    value.resize(data.length);
    const char *const src = static_cast<const char *>(data.bytes);
    std::copy(src, src + data.length, value.data());

    return value;
}

template<class Integer>
QByteArray qt_bytearray(Integer n)
{
    QByteArray value;
    value.resize(sizeof n);
    const char *const src = reinterpret_cast<char *>(&n);
    std::copy(src, src + sizeof n, value.data());

    return value;
}

QByteArray qt_bytearray(NSString *string)
{
    if (!string)
        return QByteArray();

    QT_BT_MAC_AUTORELEASEPOOL;
    NSData *const utf8Data = [string dataUsingEncoding:NSUTF8StringEncoding];

    return qt_bytearray(utf8Data);
}

QByteArray qt_bytearray(NSObject *obj)
{
    // descriptor.value has type 'id'.
    // While the Apple's docs say this about descriptors:
    //
    // - CBUUIDCharacteristicExtendedPropertiesString
    //   The string representation of the UUID for the extended properties descriptor.
    //   The corresponding value for this descriptor is an NSNumber object.
    //
    // - CBUUIDCharacteristicUserDescriptionString
    //   The string representation of the UUID for the user description descriptor.
    //   The corresponding value for this descriptor is an NSString object.
    //
    //   ... etc.
    //
    // This is not true. On OS X, they all seem to be NSData (or derived from NSData),
    // and they can be something else on iOS (NSNumber, NSString, etc.)
    if (!obj)
        return QByteArray();

    QT_BT_MAC_AUTORELEASEPOOL;

    if ([obj isKindOfClass:[NSData class]]) {
        return qt_bytearray(static_cast<NSData *>(obj));
    } else if ([obj isKindOfClass:[NSString class]]) {
        return qt_bytearray(static_cast<NSString *>(obj));
    } else if ([obj isKindOfClass:[NSNumber class]]) {
        NSNumber *const nsNumber = static_cast<NSNumber *>(obj);
        return qt_bytearray([nsNumber unsignedShortValue]);
    }
    // TODO: Where can be more types, but Core Bluetooth does not support them,
    // or at least it's not documented.

    return QByteArray();
}

ObjCStrongReference<NSData> data_from_bytearray(const QByteArray & qtData)
{
    if (!qtData.size())
        return ObjCStrongReference<NSData>([[NSData alloc] init], RetainPolicy::noInitialRetain);

    ObjCStrongReference<NSData> result([NSData dataWithBytes:qtData.constData() length:qtData.size()], RetainPolicy::doInitialRetain);
    return result;
}

ObjCStrongReference<NSMutableData> mutable_data_from_bytearray(const QByteArray &qtData)
{
    using MutableData = ObjCStrongReference<NSMutableData>;

    if (!qtData.size())
        return MutableData([[NSMutableData alloc] init], RetainPolicy::noInitialRetain);

    MutableData result([[NSMutableData alloc] initWithLength:qtData.size()], RetainPolicy::noInitialRetain);
    [result replaceBytesInRange:NSMakeRange(0, qtData.size())
                      withBytes:qtData.constData()];
    return result;
}

// A small RAII class for a dispatch queue.
class SerialDispatchQueue
{
public:
    explicit SerialDispatchQueue(const char *label)
    {
        Q_ASSERT(label);

        queue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL);
        if (!queue) {
            qCCritical(QT_BT_DARWIN) << "failed to create dispatch queue with label"
                                     << label;
        }
    }
    ~SerialDispatchQueue()
    {
        if (queue)
            dispatch_release(queue);
    }

    dispatch_queue_t data() const
    {
        return queue;
    }
private:
    dispatch_queue_t queue;

    Q_DISABLE_COPY(SerialDispatchQueue)
};

dispatch_queue_t qt_LE_queue()
{
    static const SerialDispatchQueue leQueue("qt-bluetooth-LE-queue");
    return leQueue.data();
}

} // namespace DarwinBluetooth

QT_END_NAMESPACE
