// Copyright (C) 2022 The Qt Company Ltd.
// Copyright (C) 2019 Ford Motor Company
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#include "connectpage.h"
#include "ui_connectpage.h"
#include "bleiodevice.h"

#include <QtBluetooth/QBluetoothDeviceDiscoveryAgent>
#include <QtBluetooth/QLowEnergyController>
#include <QtBluetooth/QLowEnergyService>

using namespace Qt::Literals::StringLiterals;

ConnectPage::ConnectPage(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::ConnectPage)
    , m_discoveryAgent(new QBluetoothDeviceDiscoveryAgent(this))
{
    ui->setupUi(this);
    m_discoveryAgent->setLowEnergyDiscoveryTimeout(20000);
    connect(m_discoveryAgent,  &QBluetoothDeviceDiscoveryAgent::errorOccurred, this, [this]() {
        emit showMessage(m_discoveryAgent->errorString());
        ui->scanButton->setEnabled(true);
        ui->connectButton->setEnabled(false);
    });
    connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered,
            this, &ConnectPage::refreshDevices);
    connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, [this]() {
        ui->scanButton->setEnabled(true);
        emit showMessage("Scan finished"_L1);
        refreshDevices();
    });
    connect(ui->devicesListWidget, &QListWidget::clicked, this, [this](const QModelIndex &index) {
        ui->connectButton->setEnabled(index.isValid() && index.row() >= 0
                                      && index.row() < m_leDevices.size());
        showMessage("Scan stopped");
        ui->scanButton->setEnabled(true);
        m_discoveryAgent->stop();
    });
    connect(ui->connectButton, &QPushButton::clicked, this, [this]() {
        if (ui->devicesListWidget->currentRow() < 0
                || ui->devicesListWidget->currentRow() >= m_leDevices.size()) {
            return; // Should never happen, but better safe than sorry
        }
        auto device = m_leDevices[ui->devicesListWidget->currentRow()];
        qDebug() << "Connecting to:" << device.name() << device.address() << device.deviceUuid();
        connectToDevice(device);
    });

    connect(ui->scanButton, &QPushButton::clicked, this, &ConnectPage::startScanning);
}

ConnectPage::~ConnectPage()
{
    if (m_controller)
        m_controller->disconnect(); // Avoid possible stray signals
    if (m_discoveryAgent && m_discoveryAgent->isActive())
        m_discoveryAgent->stop();
    delete ui;
}

void ConnectPage::startScanning()
{
    ui->scanButton->setEnabled(false);
    ui->connectButton->setEnabled(false);
    emit showMessage("Scanning for BLE devices, please wait"_L1);
    m_discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
    ui->devicesListWidget->setEnabled(true);
    ui->devicesListWidget->clear();
    m_leDevices.clear();
}

void ConnectPage::disconnectFromDevice()
{
    if (m_controller)
        m_controller->disconnectFromDevice();
}

void ConnectPage::refreshDevices()
{
    ui->devicesListWidget->clear();
    m_leDevices.clear();
    // The LE device list is used to filter out classic bluetooth devices which the device
    // discovery method may find (even if using low energy device discovery method). This list
    // helps in matching the right device listwidget index the user has selected
    for (const auto &device : m_discoveryAgent->discoveredDevices()) {
        if (device.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) {
            m_leDevices.append(device);
#ifdef Q_OS_DARWIN
            // On macOS/iOS we don't have address available
            ui->devicesListWidget->addItem(device.name() + " " + device.deviceUuid().toString());
#else
            ui->devicesListWidget->addItem(device.name() + " " + device.address().toString());
#endif
        }
    }
}

void ConnectPage::connectToDevice(const QBluetoothDeviceInfo &device)
{
    ui->scanButton->setEnabled(false);
    ui->connectButton->setEnabled(false);
    ui->devicesListWidget->setEnabled(false);

    QString deviceIdentifier;
    if (!device.name().isEmpty())
        deviceIdentifier = device.name();
    else if (!device.address().isNull())
        deviceIdentifier = device.address().toString();
    else
        deviceIdentifier = device.deviceUuid().toString(QUuid::WithoutBraces);
    emit showMessage("Connecting to %1"_L1.arg(deviceIdentifier));

    // If this is not the first time, release the previous low energy controller
    if (m_controller) {
        m_controller->disconnectFromDevice();
        delete m_controller;
        m_controller = nullptr;
    }

    m_controller = QLowEnergyController::createCentral(device, this);
    connect(m_controller, &QLowEnergyController::connected, this, [this]() {
        m_controller->discoverServices();
    });
    connect(m_controller, &QLowEnergyController::errorOccurred, this, [this]() {
        ui->devicesListWidget->setEnabled(true);
        ui->scanButton->setEnabled(!m_discoveryAgent->isActive());
        m_service = nullptr;
        emit showMessage("Error occurred: "_L1 + m_controller->errorString());
    });

    connect(m_controller, &QLowEnergyController::disconnected, this, [this]() {
        m_service = nullptr;
        ui->devicesListWidget->setEnabled(true);
        emit showMessage("Diconnected from remote"_L1);
        emit disconnected();
    });

    connect(m_controller, &QLowEnergyController::serviceDiscovered,
            this, [this](const QBluetoothUuid &newService) {
        if (newService != BLEIoDevice::SERVICE_UUID)
            return;
        emit showMessage("Service found"_L1);
        m_service = m_controller->createServiceObject(newService, m_controller);
        if (!m_service) {
            qWarning("BT LE Service couldn't be created");
            return;
        }
        if (m_service->state() == QLowEnergyService::RemoteService) {
            emit showMessage("Service found, scanning for details"_L1);
            connect(m_service, &QLowEnergyService::stateChanged,
                    this, [this](QLowEnergyService::ServiceState state) {

                if (state == QLowEnergyService::ServiceState::RemoteServiceDiscovered)
                    connectNode();
            });
            m_service->discoverDetails();
        } else {
            connectNode();
        }
    });

    connect(m_controller, &QLowEnergyController::discoveryFinished, this, [this]() {
        if (m_service)
            return;
        emit showMessage("Service was not found");
        ui->scanButton->setEnabled(true);
        ui->devicesListWidget->setEnabled(true);
    });

    m_controller->setRemoteAddressType(QLowEnergyController::PublicAddress);
    m_controller->connectToDevice();
}

void ConnectPage::connectNode()
{
    if (!m_service)
        return;

    // Below we subscribe to characteristic value change notifications.
    // If subscription fails, disconnect as there's no point in continuing
    QObject::connect(m_service.data(), &QLowEnergyService::descriptorWritten, this,
            [this](const QLowEnergyDescriptor &info,
            const QByteArray &value) {

        if (info.uuid() != QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration)
            return;

        if (value != QLowEnergyCharacteristic::CCCDEnableNotification) {
            // We failed to subscribe to characteristic change notifications
            m_controller->disconnectFromDevice();
        } else {
            emit showMessage("Connected to QtRO node"_L1);
            auto ioDevice = new BLEIoDevice(m_service,
                                            BLEIoDevice::CLIENT_RX_SERVER_TX_CHAR_UUID,
                                            BLEIoDevice::CLIENT_TX_SERVER_RX_CHAR_UUID,
                                            this);
            connect(this, &ConnectPage::disconnected, ioDevice, &BLEIoDevice::disconnected);
            connect(ioDevice, &BLEIoDevice::disconnected, ioDevice, &BLEIoDevice::deleteLater);
            emit connected(ioDevice);
        }
    });

    QObject::connect(m_service, &QLowEnergyService::errorOccurred, [this]
                                (QLowEnergyService::ServiceError error) {
            if (error == QLowEnergyService::ServiceError::DescriptorWriteError)
                m_controller->disconnectFromDevice();
    });

    m_service->writeDescriptor(
                  m_service->characteristic(BLEIoDevice::CLIENT_RX_SERVER_TX_CHAR_UUID).descriptor(
                  QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration),
                  QLowEnergyCharacteristic::CCCDEnableNotification);
}
