/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of maveod.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "maveod.h"
#include "loggingcategories.h"

#include <QSysInfo>
#include <QProcess>
#include <QFileInfo>

Maveod* Maveod::s_instance = nullptr;

Maveod *Maveod::instance()
{
    if (!s_instance)
        s_instance = new Maveod(nullptr);

    return s_instance;
}

void Maveod::destroy()
{
    if (s_instance) {
        qCDebug(dcMaveod()) << "Delete maveod instance";
        delete s_instance;
    }

    s_instance = nullptr;
}

QString Maveod::configurationFileName() const
{
    return m_configurationFileName;
}

void Maveod::setConfigurationFileName(const QString &configurationFileName)
{
    m_configurationFileName = configurationFileName;
}

QString Maveod::hostName() const
{
    return m_hostName;
}

QUuid Maveod::systemUuid() const
{
    return m_systemUuid;
}

Configuration *Maveod::configuration() const
{
    return m_configuration;
}

Maveod::Maveod(QObject *parent) :
    QObject(parent)
{
    m_hostName = readHostName();
    m_systemUuid = readSystemUuid();

    qCDebug(dcMaveod()) << "Hostname:" << m_hostName;
    qCDebug(dcMaveod()) << "System UUID:" << m_systemUuid.toString();

    // Configuration
    m_configuration = new Configuration(this);

    // Service for interacting with nymead
    m_nymeadService = new NymeadService(this);
    connect(m_nymeadService, &NymeadService::availableChanged, this, &Maveod::onNymeaServiceAvailableChanged);

    m_systemdService = new SystemdService(this);

    m_factoryResetHandler = new FactoryResetHandler(m_systemdService, this);

    m_debugServer = new DebugServer(this);
    connect(m_debugServer, &DebugServer::userButtonClicked, this, &Maveod::onUserButtonClicked);
    connect(m_debugServer, &DebugServer::userButtonLongPressed, this, &Maveod::onUserButtonLongPressed);
    connect(m_debugServer, &DebugServer::resetButtonClicked, this, &Maveod::onResetButtonClicked);
    connect(m_debugServer, &DebugServer::resetButtonLongPressed, this, &Maveod::onResetButtonLongPressed);

    // The networkmanager
    m_networkManager = new NetworkManager(this);
    connect(m_networkManager, &NetworkManager::availableChanged, this, &Maveod::onNetworkManagerAvailableChanged);
    connect(m_networkManager, &NetworkManager::stateChanged, this, &Maveod::onNetworkManagerStateChanged);
    connect(m_networkManager, &NetworkManager::connectivityStateChanged, this, &Maveod::onNetworkManagerConnectivityStateChanged);

    // The bluetooth server for the networkmanager
    m_bluetoothServer = new BluetoothServer(m_networkManager);
    connect(m_bluetoothServer, &BluetoothServer::runningChanged, this, &Maveod::onBluetoothServerRunningChanged);
    connect(m_bluetoothServer, &BluetoothServer::connectedChanged, this, &Maveod::onBluetoothServerClientConnectedChanged);

    // The hardware manager initializing the maveo hardware interaction
    m_hardwareManager = new HardwareManager(this);
    connect(m_hardwareManager, &HardwareManager::userButtonClicked, this, &Maveod::onUserButtonClicked);
    connect(m_hardwareManager, &HardwareManager::userButtonLongPressed, this, &Maveod::onUserButtonLongPressed);
    connect(m_hardwareManager, &HardwareManager::resetButtonClicked, this, &Maveod::onResetButtonClicked);
    connect(m_hardwareManager, &HardwareManager::resetButtonLongPressed, this, &Maveod::onResetButtonLongPressed);

    // The advertising timer
    m_advertisingTimer = new QTimer(this);
    m_advertisingTimer->setSingleShot(true);
    connect(m_advertisingTimer, &QTimer::timeout, this, &Maveod::onAdvertisingTimeout);
}

Maveod::~Maveod()
{
    qCDebug(dcMaveod()) << "Shutting down debug server";
    delete m_debugServer;
    m_debugServer = nullptr;

    qCDebug(dcMaveod()) << "Shutting down nymeas service";
    delete m_nymeadService;
    m_nymeadService = nullptr;

    qCDebug(dcMaveod()) << "Shutting down bluetooth server";
    delete m_bluetoothServer;
    m_bluetoothServer = nullptr;

    qCDebug(dcMaveod()) << "Shutting down networkmanager";
    delete m_networkManager;
    m_networkManager = nullptr;
}

QString Maveod::readHostName()
{
    return QSysInfo::machineHostName();
}

QUuid Maveod::readSystemUuid()
{
    QUuid systemUuid;
    QFile systemUuidFile("/etc/machine-id");
    if (systemUuidFile.open(QFile::ReadOnly)) {
        QString tmpId = QString::fromLatin1(systemUuidFile.readAll()).trimmed();
        tmpId.insert(8, "-");
        tmpId.insert(13, "-");
        tmpId.insert(18, "-");
        tmpId.insert(23, "-");
        systemUuid = QUuid(tmpId);
    } else {
        qWarning(dcNetworkManagerBluetoothServer()) << "Failed to open /etc/machine-id for reading the system uuid as device information serialnumber.";
    }
    systemUuidFile.close();

    return systemUuid;
}

void Maveod::setSystemStatus(Maveod::SystemStatus systemStatus, bool force)
{
    if (m_systemStatus == systemStatus && !force)
        return;

    if (force) {
        qCDebug(dcMaveod()) << "System status set forced to" << systemStatus;
    } else {
        qCDebug(dcMaveod()) << "System status changed from" << m_systemStatus << "to" << systemStatus;
    }

    m_systemStatus = systemStatus;

    bool setLedSuccess = false;
    switch (m_systemStatus) {
    case SystemStatusUninitialized:
    case SystemStatusIdle:
        setLedSuccess = m_hardwareManager->ledController()->setLed(LedController::LedCommandSnooze, LedController::LedAnimationLogistic, 2000, Qt::blue);
        break;
    case SystemStatusError:
        setLedSuccess = m_hardwareManager->ledController()->setLed(LedController::LedCommandSetColor, LedController::LedAnimationNone, 0, Qt::red);
        break;
    case SystemStatusOffline:
        setLedSuccess = m_hardwareManager->ledController()->setLed(LedController::LedCommandSnooze, LedController::LedAnimationLogistic, 2000, Qt::red);
        break;
    case SystemStatusPoorSignal:
        setLedSuccess = m_hardwareManager->ledController()->setLed(LedController::LedCommandCycleBlueRed, LedController::LedAnimationNone, 400, Qt::blue);
        break;
    case SystemStatusBluetoothAdvertising:
        setLedSuccess = m_hardwareManager->ledController()->setLed(LedController::LedCommandBlink, LedController::LedAnimationNone, 500, Qt::blue);
        break;
    case SystemStatusBluetoothConnected:
        setLedSuccess = m_hardwareManager->ledController()->setLed(LedController::LedCommandSetColor, LedController::LedAnimationNone, 0, Qt::blue);
        break;
    case SystemStatusFactoryResetting:
        // Constant purple like during recovery since it might be a recovery
        setLedSuccess = m_hardwareManager->ledController()->setLed(LedController::LedCommandSetColor, LedController::LedAnimationNone, 0, QColor(255, 0, 255));
        break;
    }

    if (setLedSuccess) {
        qCDebug(dcMaveod()) << "Successfully configured LED.";
    } else {
        qCWarning(dcMaveod()) << "Could not configure the LED.";
    }
}

void Maveod::evaluateSystemStatus()
{
    if (m_initializing) {
        qCDebug(dcMaveod()) << "Not evaluating system status. The application is still initializing...";
        return;
    }

    if (m_factoryResetHandler->factoryResetRunning()) {
        qCWarning(dcMaveod()) << "Factory reset is running. Not evaluating system status...";
        return;
    }

    qCDebug(dcMaveod()) << "Evaluating system status...";
    // First check any possible system error
    if (QBluetoothLocalDevice::allDevices().isEmpty()) {
        qCWarning(dcMaveod()) << "There is no bluetooth adapter available.";
        setSystemStatus(SystemStatusError);
        return;
    }

    if (!m_networkManager->available()) {
        qCWarning(dcMaveod()) << "The networkmanager is not available.";
        setSystemStatus(SystemStatusError);
        return;
    }

    if (!m_networkManager->wirelessAvailable()) {
        qCWarning(dcMaveod()) << "There is no wireless network adapter available.";
        setSystemStatus(SystemStatusError);
        return;
    }

    // Note: nymea takes some time to start, so lets's give a grace persiod of 30 seconds before marking it as error
    if (!m_nymeadService->available()) {
        if (m_nymeaGracePeriodTimer->isActive()) {
            qCDebug(dcMaveod()) << "Nymea seems not to be available yet. Reevaluating after grace period";
        } else {
            qCWarning(dcMaveod()) << "Nymea seems not to be available.";
            setSystemStatus(SystemStatusError);
        }
        return;
    }

    // Check the bluetooth status
    if (!m_bluetoothServer->running() && m_bluetoothServer->connected()) {
        qCDebug(dcMaveod()) << "The bluetooth server is still shutting down. Waiting for the disconnected signal.";
        return;
    }

    if (m_bluetoothServer->running() && !m_bluetoothServer->connected()) {
        qCDebug(dcMaveod()) << "The bluetooth server is advertising and no client is connected";
        setSystemStatus(SystemStatusBluetoothAdvertising);
        return;
    }

    if (m_bluetoothServer->running() && m_bluetoothServer->connected()) {
        qCDebug(dcMaveod()) << "The bluetooth server is running and a client is connected";
        setSystemStatus(SystemStatusBluetoothConnected);
        return;
    }

    // Check the network connectivity
    if (m_networkManager->connectivityState() != NetworkManager::NetworkManagerConnectivityStateFull) {
        if (m_networkManager->state() == NetworkManager::NetworkManagerStateConnecting) {
            qCDebug(dcMaveod()) << "The networkmanager is currently connecting. Lets wait before marking the system as offline" << m_networkManager->state();
        } else {
            qCDebug(dcMaveod()) << "The system is not in the Full connected state. The system seems to be offline" << m_networkManager->state() << m_networkManager->connectivityState();
            setSystemStatus(SystemStatusOffline);
            return;
        }
    }

    // Everything is fine, set status to idle
    qCDebug(dcMaveod()) << "Evaluation done. System is in idle mode.";
    setSystemStatus(SystemStatusIdle, true);
}

void Maveod::onNetworkManagerAvailableChanged(bool available)
{
    if (m_factoryResetHandler->factoryResetRunning()) {
        qCDebug(dcMaveod()) << "Networkmanager available changed to" << available << "but a factory reset is currently running. Ignoring.";
        return;
    }

    if (available) {
        qCDebug(dcMaveod()) << "Network manager is now available.";

        // Make sure networking is enabled
        if (!m_networkManager->networkingEnabled()) {
            if (!m_networkManager->enableNetworking(true)) {
                qCWarning(dcMaveod()) << "Could not enable networking";
            }
        }

        // Make sure wireless networking is enabled
        if (!m_networkManager->wirelessEnabled()) {
            if (!m_networkManager->enableWireless(true)) {
                qCWarning(dcMaveod()) << "Could not enable wireless networking";
            }
        }
    } else {
        qCWarning(dcMaveod()) << "Network manager is not available any more.";
    }

    // Update system status
    evaluateSystemStatus();
}

void Maveod::onNetworkManagerStateChanged(const NetworkManager::NetworkManagerState &state)
{
    qCDebug(dcMaveod()) << "Network manager state changed" << state;
    // Update system status
    evaluateSystemStatus();
}

void Maveod::onNetworkManagerConnectivityStateChanged(const NetworkManager::NetworkManagerConnectivityState &connectivityState)
{
    qCDebug(dcMaveod()) << "Network manager connectivity state changed" << connectivityState;

    // Update system status
    evaluateSystemStatus();
}

void Maveod::onUserButtonClicked()
{
    if (m_factoryResetHandler->factoryResetRunning()) {
        qCDebug(dcMaveod()) << "User button clicked while the factory reset is running. Ignoring.";
        return;
    }

    qCDebug(dcMaveod()) << "User button clicked. Sending push button authentication to nymead";
    m_nymeadService->pushButtonPressed();
}

void Maveod::onUserButtonLongPressed()
{
    if (m_factoryResetHandler->factoryResetRunning()) {
        qCDebug(dcMaveod()) << "User button long pressed while the factory reset is running. Ignoring.";
        return;
    }

    // Disable bluetooth hardware resource on nymead
    m_nymeadService->enableBluetooth(false);

    // Start blutooth server
    qCDebug(dcMaveod()) << "User button long pressed. Starting Bluetooth LE server with" << m_advertisingTimeout / 1000 << "[s] timeout.";
    m_bluetoothServer->start();
}

void Maveod::onResetButtonClicked()
{
    if (m_factoryResetHandler->factoryResetRunning()) {
        qCDebug(dcMaveod()) << "Reset button clicked while the factory reset is running. Ignoring.";
        return;
    }

    if (!m_systemdService->rebootSystem()) {
        qCWarning(dcMaveod()) << "Could not restart the system.";
    }
}

void Maveod::onResetButtonLongPressed()
{
    if (m_factoryResetHandler->factoryResetRunning()) {
        qCDebug(dcMaveod()) << "Reset button long pressed while the factory reset is running. Ignoring.";
        return;
    }

    qCDebug(dcMaveod()) << "Start system factory reset mechanism...";
    setSystemStatus(SystemStatusFactoryResetting, true);
    m_factoryResetHandler->startFactoryReset();
}

void Maveod::onBluetoothServerRunningChanged(bool running)
{
    if (m_factoryResetHandler->factoryResetRunning()) {
        qCDebug(dcMaveod()) << "Bluetooth server running changed to" << running << "while the factory reset is running. Ignoring.";
        return;
    }

    qCDebug(dcMaveod()) << "Bluetooth server" << (running ? "up" : "down");
    if (running) {
        m_advertisingTimer->start(m_advertisingTimeout);
        // Update system status
        evaluateSystemStatus();
    } else {
        m_advertisingTimer->stop();

        if (m_bluetoothServer->connected()) {
            qCDebug(dcMaveod()) << "Bluetooth server still shutting down..";
        } else {
            // Reenable bluetooth hardware resource on nymead
            m_nymeadService->enableBluetooth(true);
            evaluateSystemStatus();
        }
    }
}

void Maveod::onBluetoothServerClientConnectedChanged(bool connected)
{
    if (m_factoryResetHandler->factoryResetRunning()) {
        qCDebug(dcMaveod()) << "Bluetooth client connected changed to" << connected << "while the factory reset is running. Ignoring.";
        return;
    }

    qCDebug(dcMaveod()) << "Bluetooth client" << (connected ? "connected" : "disconnected");

    // Note: a client connected, no advertise timeout required any more.
    m_advertisingTimer->stop();

    // If client disconnected and the bluetooth server is not running any more, enable bluetooth on nymea
    if (!connected && !m_bluetoothServer->running()) {
        m_nymeadService->enableBluetooth(true);
    }

    evaluateSystemStatus();
}

void Maveod::onNymeaServiceAvailableChanged(bool available)
{
    if (m_factoryResetHandler->factoryResetRunning()) {
        qCDebug(dcMaveod()) << "The nymea service changed to" << (available ? "available" : "not available") << "while the factory reset is running. Ignoring.";
        return;
    }

    qCDebug(dcMaveod()) << "Nymea service" << (available ? "available" : "not available any more");
    if (available) {
        m_nymeaGracePeriodTimer->stop();
    }

    evaluateSystemStatus();
}

void Maveod::onAdvertisingTimeout()
{
    qCDebug(dcMaveod()) << "Bluetooth server advertising timeout after" << m_advertisingTimeout / 1000 << "[s]. Stopping bluetooth server...";
    m_bluetoothServer->stop();
}

void Maveod::postRun()
{
    qCDebug(dcMaveod()) << "Application is now running.";
    // Start the timer if nymea is not ready yet
    if (!m_nymeadService->available()) {
        m_nymeaGracePeriodTimer->start();
    }

    m_initializing = false;
    evaluateSystemStatus();
}

void Maveod::run()
{
    qCDebug(dcMaveod()) << "Run the application...";

    // Load configurations
    m_configuration->loadConfiguration(m_configurationFileName);
    qCDebug(dcMaveod()) << "Loading configuration from" << m_configurationFileName << "...";
    qCDebug(dcMaveod()) << "[Bluetooth]";
    qCDebug(dcMaveod()) << "    Advertise name:" << m_configuration->advertiseName();
    qCDebug(dcMaveod()) << "    Model name:" << m_configuration->modelName();
    qCDebug(dcMaveod()) << "    Hardware version:" << m_configuration->hardwareVersion();
    qCDebug(dcMaveod()) << "    Advertising timeout:" << m_configuration->advertisingTimeout() << "[ms]";
    qCDebug(dcMaveod()) << "[Hardware]";
    qCDebug(dcMaveod()) << "    User button GPIO:" << m_configuration->userGpio();
    qCDebug(dcMaveod()) << "    Reset button GPIO:" << m_configuration->resetGpio();

    // Configure Bluetooth server
    qCDebug(dcMaveod()) << "Configure bluetooth server...";
    m_bluetoothServer->setAdvertiseName(m_configuration->advertiseName(), true);
    m_bluetoothServer->setModelName(m_configuration->modelName());
    m_bluetoothServer->setHardwareVersion(m_configuration->hardwareVersion());
    // FIXME: check if we should use the system version or the maveod version
    m_bluetoothServer->setSoftwareVersion(MAVEOD_VERSION_STRING);

    qCDebug(dcMaveod()) << "Enable hardware manager...";
    m_hardwareManager->enable();

    qCDebug(dcMaveod()) << "Enable network manager...";
    m_networkManager->start();

    // Note: nymea takes some time to start, so lets's give a grace persiod of 30 seconds before marking it as error
    m_nymeaGracePeriodTimer = new QTimer(this);
    m_nymeaGracePeriodTimer->setSingleShot(true);
    m_nymeaGracePeriodTimer->setInterval(30000);
    connect(m_nymeaGracePeriodTimer, &QTimer::timeout, this, [this](){
        qCWarning(dcMaveod()) << "Nymea did not show up within 30 seconds after starting.";
        evaluateSystemStatus();
    });

    // Evaluate the system in the next event loop
    QTimer::singleShot(0, this, &Maveod::postRun);
}

