// 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 "bluezperipheralobjects_p.h"

#include "propertiesadaptor_p.h"
#include "gattservice1adaptor_p.h"
#include "gattcharacteristic1adaptor_p.h"
#include "gattdescriptor1adaptor_p.h"

#include <QtCore/QLoggingCategory>
#include <QtDBus/QDBusConnection>

QT_BEGIN_NAMESPACE

Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)

using namespace Qt::StringLiterals;

static constexpr auto characteristicPathTemplate{"%1/char%2"_L1};
static constexpr auto descriptorPathTemplate{"%1/desc%2"_L1};
static constexpr auto servicePathTemplate{"%1/service%2"_L1};

// The interface names and error values are from BlueZ "gatt-api" documentation
static constexpr auto bluezServiceInterface{"org.bluez.GattService1"_L1};
static constexpr auto bluezCharacteristicInterface{"org.bluez.GattCharacteristic1"_L1};
static constexpr auto bluezDescriptorInterface{"org.bluez.GattDescriptor1"_L1};

static constexpr auto bluezErrorInvalidValueLength{"org.bluez.Error.InvalidValueLength"_L1};
static constexpr auto bluezErrorInvalidOffset{"org.bluez.Error.InvalidOffset"_L1};
static constexpr auto bluezErrorNotAuthorized{"org.bluez.Error.NotAuthorized"_L1};
// Bluetooth Core v5.3, 3.2.9, Vol 3, Part F
static constexpr int maximumAttributeLength{512};


QtBluezPeripheralGattObject::QtBluezPeripheralGattObject(const QString& objectPath,
           const QString& uuid, QLowEnergyHandle handle, QObject* parent)
    : QObject(parent), objectPath(objectPath), uuid(uuid), handle(handle),
      propertiesAdaptor(new OrgFreedesktopDBusPropertiesAdaptor(this))
{}

QtBluezPeripheralGattObject::~QtBluezPeripheralGattObject()
{
    unregisterObject();
}

bool QtBluezPeripheralGattObject::registerObject()
{
    if (m_registered)
        return true;

    if (QDBusConnection::systemBus().registerObject(objectPath, this)) {
        qCDebug(QT_BT_BLUEZ) << "Registered object on DBus:" << objectPath << uuid;
        m_registered = true;
        return true;
    } else {
        qCWarning(QT_BT_BLUEZ) << "Failed to register object on DBus:" << objectPath << uuid;
        return false;
    }
}

void QtBluezPeripheralGattObject::unregisterObject()
{
    if (!m_registered)
        return;
    QDBusConnection::systemBus().unregisterObject(objectPath);
    qCDebug(QT_BT_BLUEZ) << "Unregistered object on DBus:" << objectPath << uuid;
    m_registered = false;
}

void QtBluezPeripheralGattObject::accessEvent(const QVariantMap& options)
{
    // Report this event for connection management purposes
    const auto remoteDevice = options.value("device"_L1).value<QDBusObjectPath>().path();
    if (!remoteDevice.isEmpty())
        emit remoteDeviceAccessEvent(remoteDevice, options.value("mtu"_L1).toUInt());
}

QtBluezPeripheralDescriptor::QtBluezPeripheralDescriptor(
                         const QLowEnergyDescriptorData& descriptorData,
                         const QString& characteristicPath, quint16 ordinal,
                         QLowEnergyHandle handle, QLowEnergyHandle characteristicHandle,
                         QObject* parent)
    : QtBluezPeripheralGattObject(descriptorPathTemplate.arg(characteristicPath).arg(ordinal),
                 descriptorData.uuid().toString(QUuid::WithoutBraces), handle, parent),
      m_adaptor(new OrgBluezGattDescriptor1Adaptor(this)),
      m_characteristicPath(characteristicPath),
      m_characteristicHandle(characteristicHandle)
{
    if (descriptorData.value().size() > maximumAttributeLength) {
        qCWarning(QT_BT_BLUEZ) << "Descriptor value is too large, cropping it to"
                               << maximumAttributeLength;
        m_value = descriptorData.value().sliced(0, maximumAttributeLength);
    } else {
        m_value = descriptorData.value();
    }
    initializeFlags(descriptorData);
}

InterfaceList QtBluezPeripheralDescriptor::properties() const
{
    InterfaceList properties;
    properties.insert(bluezDescriptorInterface,
                      {
                          {"UUID"_L1, uuid},
                          {"Characteristic"_L1, QDBusObjectPath(m_characteristicPath)},
                          {"Flags"_L1, m_flags}
                      });
    return  properties;
}

// org.bluez.GattDescriptor1
// This function is invoked when remote device reads the value
QByteArray QtBluezPeripheralDescriptor::ReadValue(const QVariantMap &options, QString& error)
{
    accessEvent(options);
    // Offset is set by Bluez when the value size is more than MTU size.
    // Bluez deduces the value size from the first ReadValue. If the
    // received data size is larger than MTU, Bluez will take the first MTU bytes and
    // issue more ReadValue calls with the 'offset' set
    const quint16 offset = options.value("offset"_L1).toUInt();
    const quint16 mtu = options.value("mtu"_L1).toUInt();

    if (offset > m_value.length() - 1) {
        qCWarning(QT_BT_BLUEZ) << "Invalid offset" << offset << ", value len:" << m_value.length();
        error = bluezErrorInvalidOffset;
        return {};
    }

    if (offset > 0)
        return m_value.mid(offset, mtu);
    else
        return m_value;
}

// org.bluez.GattDescriptor1
// This function is invoked when remote device writes a value
QString QtBluezPeripheralDescriptor::WriteValue(const QByteArray &value,
                                                const QVariantMap &options)
{
    accessEvent(options);

    if (options.value("prepare-authorize"_L1).toBool()) {
        // Qt API doesn't provide the means for application to authorize
        qCWarning(QT_BT_BLUEZ) << "Descriptor write requires authorization."
                               << "The client device needs to be trusted beforehand";
        return bluezErrorNotAuthorized;
    }

    if (value.size() > maximumAttributeLength) {
        qCWarning(QT_BT_BLUEZ) << "Descriptor value is too large:" << value.size();
        return bluezErrorInvalidValueLength;
    }
    m_value = value;
    emit valueUpdatedByRemote(m_characteristicHandle, handle, value);
    return {};
}

// This function is called when the value has been updated locally (server-side)
bool QtBluezPeripheralDescriptor::localValueUpdate(const QByteArray& value)
{
    if (value.size() > maximumAttributeLength) {
        qCWarning(QT_BT_BLUEZ) << "Descriptor value is too large:" << value.size();
        return false;
    }
    m_value = value;
    return true;
}

void QtBluezPeripheralDescriptor::initializeFlags(const QLowEnergyDescriptorData& data)
{
    // Flag tokens are from org.bluez.GattDescriptor1 documentation
    if (data.isReadable())
        m_flags.append("read"_L1);
    if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired)
        m_flags.append("encrypt-read"_L1);
    if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired)
        m_flags.append("encrypt-authenticated-read"_L1);

    if (data.isWritable())
        m_flags.append("write"_L1);
    if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired)
        m_flags.append("encrypt-write"_L1);
    if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired)
        m_flags.append("encrypt-authenticated-write"_L1);

    if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired
            || data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired)
        m_flags.append("authorize"_L1);

    if (m_flags.isEmpty()) {
        qCWarning(QT_BT_BLUEZ) << "Descriptor property flags not set" << uuid
                               << "Peripheral may fail to register";
    }
}

QtBluezPeripheralCharacteristic::QtBluezPeripheralCharacteristic(
        const QLowEnergyCharacteristicData& characteristicData,
        const QString& servicePath, quint16 ordinal,
        QLowEnergyHandle handle, QObject* parent)
    : QtBluezPeripheralGattObject(characteristicPathTemplate.arg(servicePath).arg(ordinal),
                 characteristicData.uuid().toString(QUuid::WithoutBraces), handle, parent),
      m_adaptor(new OrgBluezGattCharacteristic1Adaptor(this)),
      m_servicePath(servicePath),
      m_minimumValueLength(std::min(characteristicData.minimumValueLength(),
                                    maximumAttributeLength)),
      m_maximumValueLength(std::min(characteristicData.maximumValueLength(),
                                    maximumAttributeLength))
{
    initializeFlags(characteristicData);
    initializeValue(characteristicData.value());
}

InterfaceList QtBluezPeripheralCharacteristic::properties() const
{
    InterfaceList properties;
    properties.insert(bluezCharacteristicInterface,
                      {
                          {"UUID"_L1, uuid},
                          {"Service"_L1, QDBusObjectPath(m_servicePath)},
                          {"Flags"_L1, m_flags}
                      });
    return properties;
}

// org.bluez.GattCharacteristic1
// This function is invoked when remote device reads the value
QByteArray QtBluezPeripheralCharacteristic::ReadValue(const QVariantMap &options, QString& error)
{
    accessEvent(options);
    // Offset is set by Bluez when the value size is more than MTU size.
    // Bluez deduces the value size from the first ReadValue. If the
    // received data size is larger than MTU, Bluez will take the first MTU bytes and
    // issue more ReadValue calls with the 'offset' set
    const quint16 offset = options.value("offset"_L1).toUInt();
    const quint16 mtu = options.value("mtu"_L1).toUInt();

    if (offset > m_value.length() - 1) {
        qCWarning(QT_BT_BLUEZ) << "Invalid offset" << offset << ", value len:" << m_value.length();
        error = bluezErrorInvalidOffset;
        return {};
    }

    if (offset > 0)
        return m_value.mid(offset, mtu);
    else
        return m_value;
}

// org.bluez.GattCharacteristic1
// This function is invoked when remote device writes a value
QString QtBluezPeripheralCharacteristic::WriteValue(const QByteArray &value,
                                                    const QVariantMap &options)
{
    accessEvent(options);

    if (options.value("prepare-authorize"_L1).toBool()) {
        // Qt API doesn't provide the means for application to authorize
        qCWarning(QT_BT_BLUEZ) << "Characteristic write requires authorization."
                               << "The client device needs to be trusted beforehand";
        return bluezErrorNotAuthorized;
    }

    if (value.size() < m_minimumValueLength || value.size() > m_maximumValueLength) {
        qCWarning(QT_BT_BLUEZ) << "Characteristic value has invalid length" << value.size()
                               << "min:" << m_minimumValueLength
                               << "max:" << m_maximumValueLength;
        return bluezErrorInvalidValueLength;
    }
    m_value = value;
    emit valueUpdatedByRemote(handle, value);
    return {};
}

// This function is called when the value has been updated locally (server-side)
bool QtBluezPeripheralCharacteristic::localValueUpdate(const QByteArray& value)
{
    if (value.size() < m_minimumValueLength || value.size() > m_maximumValueLength) {
        qCWarning(QT_BT_BLUEZ) << "Characteristic value has invalid length" << value.size()
                               << "min:" << m_minimumValueLength
                               << "max:" << m_maximumValueLength;
        return false;
    }
    m_value = value;
    if (m_notifying) {
        emit propertiesAdaptor->PropertiesChanged(
                    bluezCharacteristicInterface, {{"Value"_L1, m_value}}, {});
    }
    return true;
}

// org.bluez.GattCharacteristic1
// These are called when remote client enables or disables NTF/IND
void QtBluezPeripheralCharacteristic::StartNotify()
{
    qCDebug(QT_BT_BLUEZ) << "NTF or IND enabled for characteristic" << uuid;
    m_notifying = true;
}

void QtBluezPeripheralCharacteristic::StopNotify()
{
    qCDebug(QT_BT_BLUEZ) << "NTF or IND disabled for characteristic" << uuid;
    m_notifying = false;
}


void QtBluezPeripheralCharacteristic::initializeValue(const QByteArray& value)
{
    const auto valueSize = value.size();
    if (valueSize < m_minimumValueLength || valueSize > m_maximumValueLength) {
        qCWarning(QT_BT_BLUEZ) << "Characteristic value has invalid length" << valueSize
                               << "min:" << m_minimumValueLength
                               << "max:" << m_maximumValueLength;
        m_value = QByteArray(m_minimumValueLength, 0);
    } else {
        m_value = value;
    }
}

void QtBluezPeripheralCharacteristic::initializeFlags(const QLowEnergyCharacteristicData& data)
{
    // Flag tokens are from org.bluez.GattCharacteristic1 documentation
    if (data.properties() & QLowEnergyCharacteristic::PropertyType::Broadcasting)
        m_flags.append("broadcast"_L1);
    if (data.properties() & QLowEnergyCharacteristic::PropertyType::WriteNoResponse)
        m_flags.append("write-without-response"_L1);
    if (data.properties() & QLowEnergyCharacteristic::PropertyType::Read)
        m_flags.append("read"_L1);
    if (data.properties() & QLowEnergyCharacteristic::PropertyType::Write)
        m_flags.append("write"_L1);
    if (data.properties() & QLowEnergyCharacteristic::PropertyType::Notify)
        m_flags.append("notify"_L1);
    if (data.properties() & QLowEnergyCharacteristic::PropertyType::Indicate)
        m_flags.append("indicate"_L1);
    if (data.properties() & QLowEnergyCharacteristic::PropertyType::WriteSigned)
        m_flags.append("authenticated-signed-writes"_L1);
    if (data.properties() & QLowEnergyCharacteristic::PropertyType::ExtendedProperty) {
        // If extended properties property is set, check if we have the descriptor
        // describing them. Bluez will generate the actual descriptor based on these
        // flags. For clarity: the 'extended-properties' token mentioned in the Bluez
        // API is implied by these flags.
        for (const auto& descriptor : data.descriptors()) {
            // Core Bluetooth v5.3 Vol 3, Part G, 3.3.3.1
            if (descriptor.uuid()
                    == QBluetoothUuid::DescriptorType::CharacteristicExtendedProperties
                    && descriptor.value().size() == 2) {
                const auto properties = descriptor.value().at(0);
                if (properties & 0x01)
                    m_flags.append("reliable-write"_L1);
                if (properties & 0x02)
                    m_flags.append("writable-auxiliaries"_L1);
            }
        }
    }

    if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired)
        m_flags.append("encrypt-read"_L1);
    if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired)
        m_flags.append("encrypt-authenticated-read"_L1);
    if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired)
        m_flags.append("encrypt-write"_L1);
    if (data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthenticationRequired)
        m_flags.append("encrypt-authenticated-write"_L1);

    if (data.readConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired
            || data.writeConstraints() & QBluetooth::AttAccessConstraint::AttAuthorizationRequired)
        m_flags.append("authorize"_L1);

    if (m_flags.isEmpty()) {
        qCWarning(QT_BT_BLUEZ) << "Characteristic property flags not set" << uuid
                               << "Peripheral may fail to register";
    }
}


QtBluezPeripheralService::QtBluezPeripheralService(const QLowEnergyServiceData &serviceData,
                      const QString& applicationPath, quint16 ordinal,
                      QLowEnergyHandle handle, QObject* parent)
    : QtBluezPeripheralGattObject(servicePathTemplate.arg(applicationPath).arg(ordinal),
                 serviceData.uuid().toString(QUuid::WithoutBraces), handle, parent),
      m_isPrimary(serviceData.type() == QLowEnergyServiceData::ServiceTypePrimary),
      m_adaptor(new OrgBluezGattService1Adaptor(this))
{
}

void QtBluezPeripheralService::addIncludedService(const QString& objectPath) {
    qCDebug(QT_BT_BLUEZ) << "Adding included service" << objectPath << "for" << uuid;
    m_includedServices.append(QDBusObjectPath(objectPath));
}

InterfaceList QtBluezPeripheralService::properties() const {
    InterfaceList interfaces;
    interfaces.insert(bluezServiceInterface,{
                          {"UUID"_L1, uuid},
                          {"Primary"_L1, m_isPrimary},
                          {"Includes"_L1, QVariant::fromValue(m_includedServices)}
                      });
    return interfaces;
};

QT_END_NAMESPACE

#include "moc_bluezperipheralobjects_p.cpp"
