/****************************************************************************
**
** Copyright (C) 2016 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 "osxbtledeviceinquiry_p.h"
#include "qbluetoothdeviceinfo.h"
#include "osxbtnotifier_p.h"
#include "qbluetoothuuid.h"
#include "osxbtutility_p.h"

#include <QtCore/qloggingcategory.h>
#include <QtCore/qdebug.h>
#include <QtCore/qendian.h>

#include <algorithm>

QT_BEGIN_NAMESPACE

namespace OSXBluetooth {

QBluetoothUuid qt_uuid(NSUUID *nsUuid)
{
    if (!nsUuid)
        return QBluetoothUuid();

    uuid_t uuidData = {};
    [nsUuid getUUIDBytes:uuidData];
    quint128 qtUuidData = {};
    std::copy(uuidData, uuidData + 16, qtUuidData.data);
    return QBluetoothUuid(qtUuidData);
}

const int timeStepMS = 100;
const int powerOffTimeoutMS = 30000;

struct AdvertisementData {
    // That's what CoreBluetooth has:
    // CBAdvertisementDataLocalNameKey
    // CBAdvertisementDataTxPowerLevelKey
    // CBAdvertisementDataServiceUUIDsKey
    // CBAdvertisementDataServiceDataKey
    // CBAdvertisementDataManufacturerDataKey
    // CBAdvertisementDataOverflowServiceUUIDsKey
    // CBAdvertisementDataIsConnectable
    // CBAdvertisementDataSolicitedServiceUUIDsKey

    // For now, we "parse":
    QString localName;
    QVector<QBluetoothUuid> serviceUuids;
    QHash<quint16, QByteArray> manufacturerData;
    // TODO: other keys probably?
    AdvertisementData(NSDictionary *AdvertisementData);
};

AdvertisementData::AdvertisementData(NSDictionary *advertisementData)
{
    if (!advertisementData)
        return;

    // ... constant CBAdvertisementDataLocalNameKey ...
    // NSString containing the local name of a peripheral.
    NSObject *value = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
    if (value && [value isKindOfClass:[NSString class]])
        localName = QString::fromNSString(static_cast<NSString *>(value));

    // ... constant CBAdvertisementDataServiceUUIDsKey ...
    // A list of one or more CBUUID objects, representing CBService UUIDs.

    value = [advertisementData objectForKey:CBAdvertisementDataServiceUUIDsKey];
    if (value && [value isKindOfClass:[NSArray class]]) {
        NSArray *uuids = static_cast<NSArray *>(value);
        for (CBUUID *cbUuid in uuids)
            serviceUuids << qt_uuid(cbUuid);
    }

    value = [advertisementData objectForKey:CBAdvertisementDataManufacturerDataKey];
    if (value && [value isKindOfClass:[NSData class]]) {
        QByteArray data = QByteArray::fromNSData(static_cast<NSData *>(value));
        manufacturerData.insert(qFromLittleEndian<quint16>(data.constData()), data.mid(2));
    }
}

}

QT_END_NAMESPACE

QT_USE_NAMESPACE

@interface  QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry)(PrivateAPI)
- (void)stopScanSafe;
- (void)stopNotifier;
@end

@implementation QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry)
{
    LECBManagerNotifier *notifier;
    ObjCScopedPointer<CBCentralManager> manager;

    QList<QBluetoothDeviceInfo> devices;
    LEInquiryState internalState;
    int inquiryTimeoutMS;

    QT_PREPEND_NAMESPACE(OSXBluetooth)::GCDTimer elapsedTimer;
}

-(id)initWithNotifier:(LECBManagerNotifier *)aNotifier
{
    if (self = [super init]) {
        Q_ASSERT(aNotifier);
        notifier = aNotifier;
        internalState = InquiryStarting;
        inquiryTimeoutMS = OSXBluetooth::defaultLEScanTimeoutMS;
    }

    return self;
}

- (void)dealloc
{
    [self stopScanSafe];
    [manager setDelegate:nil];
    [elapsedTimer cancelTimer];
    [self stopNotifier];
    [super dealloc];
}

- (void)timeout:(id)sender
{
    Q_UNUSED(sender)

    if (internalState == InquiryActive) {
        [self stopScanSafe];
        [manager setDelegate:nil];
        internalState = InquiryFinished;
        Q_ASSERT(notifier);
        emit notifier->discoveryFinished();
    } else if (internalState == InquiryStarting) {
        // This is interesting on iOS only, where the system shows an alert
        // asking to enable Bluetooth in the 'Settings' app. If not done yet
        // (after 30 seconds) - we consider this as an error.
        [manager setDelegate:nil];
        internalState = ErrorPoweredOff;
        Q_ASSERT(notifier);
        emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
    }
}

- (void)startWithTimeout:(int)timeout
{
    dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue());
    Q_ASSERT(leQueue);
    inquiryTimeoutMS = timeout;
    manager.reset([[CBCentralManager alloc] initWithDelegate:self queue:leQueue]);
}

- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability-new"

    if (central != manager)
        return;

    if (internalState != InquiryActive && internalState != InquiryStarting)
        return;

    Q_ASSERT(notifier);

    using namespace OSXBluetooth;

    const auto state = central.state;
#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13)
    if (state == CBManagerStatePoweredOn) {
#else
    if (state == CBCentralManagerStatePoweredOn) {
#endif
        if (internalState == InquiryStarting) {
            internalState = InquiryActive;

            if (inquiryTimeoutMS > 0) {
                [elapsedTimer cancelTimer];
                elapsedTimer.resetWithoutRetain([[GCDTimerObjC alloc] initWithDelegate:self]);
                [elapsedTimer startWithTimeout:inquiryTimeoutMS step:timeStepMS];
            }

            // ### Qt 6.x: remove the use of env. variable, as soon as a proper public API is in place.
            bool envOk = false;
            const int env = qEnvironmentVariableIntValue("QT_BLUETOOTH_SCAN_ENABLE_DUPLICATES", &envOk);
            if (envOk && env) {
                [manager scanForPeripheralsWithServices:nil
                 options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES}];
            } else {
                [manager scanForPeripheralsWithServices:nil options:nil];
            }
        } // Else we ignore.
#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13)
    } else if (state == CBManagerStateUnsupported || state == CBManagerStateUnauthorized) {
#else
    } else if (state == CBCentralManagerStateUnsupported || state == CBCentralManagerStateUnauthorized) {
#endif
        if (internalState == InquiryActive) {
            [self stopScanSafe];
            // Not sure how this is possible at all,
            // probably, can never happen.
            internalState = ErrorPoweredOff;
            emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
        } else {
            internalState = ErrorLENotSupported;
            emit notifier->LEnotSupported();
        }

        [manager setDelegate:nil];
#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13)
    } else if (state == CBManagerStatePoweredOff) {
#else
    } else if (state == CBCentralManagerStatePoweredOff) {
#endif

#ifndef Q_OS_MACOS
        if (internalState == InquiryStarting) {
            // On iOS a user can see at this point an alert asking to
            // enable Bluetooth in the "Settings" app. If a user does so,
            // we'll receive 'PoweredOn' state update later.
            // No change in internalState. Wait for 30 seconds.
            [elapsedTimer cancelTimer];
            elapsedTimer.resetWithoutRetain([[GCDTimerObjC alloc] initWithDelegate:self]);
            [elapsedTimer startWithTimeout:powerOffTimeoutMS step:300];
            return;
        }
#else
            Q_UNUSED(powerOffTimeoutMS)
#endif // Q_OS_MACOS
        [elapsedTimer cancelTimer];
        [self stopScanSafe];
        [manager setDelegate:nil];
        internalState = ErrorPoweredOff;
        // On macOS we report PoweredOffError and our C++ owner will delete us
        // (here we're kwnon as 'self'). Connection is Qt::QueuedConnection so we
        // are apparently safe to call -stopNotifier after the signal.
        emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
        [self stopNotifier];
    } else {
        // The following two states we ignore (from Apple's docs):
        //"
        // -CBCentralManagerStateUnknown
        // The current state of the central manager is unknown;
        // an update is imminent.
        //
        // -CBCentralManagerStateResetting
        // The connection with the system service was momentarily
        // lost; an update is imminent. "
        // Wait for this imminent update.
    }

#pragma clang diagnostic pop
}

- (void)stopScanSafe
{
    // CoreBluetooth warns about API misused if we call stopScan in a state
    // other than powered on. Hence this 'Safe' ...
    if (!manager)
        return;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability-new"

    if (internalState == InquiryActive) {
        const auto state = manager.data().state;
    #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13)
        if (state == CBManagerStatePoweredOn)
    #else
        if (state == CBCentralManagerStatePoweredOn)
    #endif
            [manager stopScan];
    }

#pragma clang diagnostic pop
}

- (void)stopNotifier
{
    if (notifier) {
        notifier->disconnect();
        notifier->deleteLater();
        notifier = nullptr;
    }
}

- (void)stop
{
    [self stopScanSafe];
    [manager setDelegate:nil];
    [elapsedTimer cancelTimer];
    [self stopNotifier];
    internalState = InquiryCancelled;
}

- (void)centralManager:(CBCentralManager *)central
        didDiscoverPeripheral:(CBPeripheral *)peripheral
        advertisementData:(NSDictionary *)advertisementData
        RSSI:(NSNumber *)RSSI
{
    using namespace OSXBluetooth;

    if (central != manager)
        return;

    if (internalState != InquiryActive)
        return;

    if (!notifier)
        return;

    QBluetoothUuid deviceUuid;

    if (!peripheral.identifier) {
        qCWarning(QT_BT_OSX) << "peripheral without NSUUID";
        return;
    }

    deviceUuid = OSXBluetooth::qt_uuid(peripheral.identifier);

    if (deviceUuid.isNull()) {
        qCWarning(QT_BT_OSX) << "no way to address peripheral, QBluetoothUuid is null";
        return;
    }

    const AdvertisementData qtAdvData(advertisementData);
    QString name(qtAdvData.localName);
    if (!name.size() && peripheral.name)
        name = QString::fromNSString(peripheral.name);

    // TODO: fix 'classOfDevice' (0 for now).
    QBluetoothDeviceInfo newDeviceInfo(deviceUuid, name, 0);
    if (RSSI)
        newDeviceInfo.setRssi([RSSI shortValue]);

    if (qtAdvData.serviceUuids.size())
        newDeviceInfo.setServiceUuids(qtAdvData.serviceUuids);

    const QList<quint16> keys = qtAdvData.manufacturerData.keys();
    for (quint16 key : keys)
        newDeviceInfo.setManufacturerData(key, qtAdvData.manufacturerData.value(key));

    // CoreBluetooth scans only for LE devices.
    newDeviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration);
    emit notifier->deviceDiscovered(newDeviceInfo);
}

@end
