/*! Starts the actual firmware update process.
 */
void DeRestPluginPrivate::updateFirmware()
{
    if (gwFirmwareNeedUpdate)
    {
        gwFirmwareNeedUpdate = false;
    }

    Q_ASSERT(apsCtrl);
    if (apsCtrl->getParameter(deCONZ::ParamFirmwareUpdateActive) == deCONZ::FirmwareUpdateIdle ||
        apsCtrl->getParameter(deCONZ::ParamDeviceConnected) == 1)
    {
        DBG_Printf(DBG_INFO, "GW firmware update conditions not met, abort\n");
        fwUpdateState = FW_Idle;
        fwUpdateTimer->start(FW_IDLE_TIMEOUT);
        updateEtag(gwConfigEtag);
        return;
    }

    QString gcfFlasherBin = qApp->applicationDirPath() + "/GCFFlasher";
#ifdef Q_OS_WIN
    gcfFlasherBin.append(".exe");
    QString bin = gcfFlasherBin;
#elif defined(Q_OS_LINUX) && !defined(ARCH_ARM) // on RPi a normal sudo is ok since we don't need password there
    QString bin = "pkexec";
    gcfFlasherBin = "/usr/bin/GCFFlasher_internal";
    fwProcessArgs.prepend(gcfFlasherBin);
#elif defined(Q_OS_OSX)
    // TODO
    // /usr/bin/osascript -e 'do shell script "make install" with administrator privileges'
    QString bin = "sudo";
    fwProcessArgs.prepend(gcfFlasherBin);
#else
    QString bin = "sudo";
    gcfFlasherBin = "/usr/bin/GCFFlasher_internal";
    fwProcessArgs.prepend(gcfFlasherBin);
#endif

    if (!fwProcess)
    {
        fwProcess = new QProcess(this);
    }

    fwProcessArgs << "-f" << fwUpdateFile;

    fwUpdateState = FW_UpdateWaitFinished;
    updateEtag(gwConfigEtag);
    fwUpdateTimer->start(250);

    fwProcess->start(bin, fwProcessArgs);
}
/*! Observes the firmware update process.
 */
void DeRestPluginPrivate::updateFirmwareWaitFinished()
{
    if (fwProcess)
    {
        if (fwProcess->bytesAvailable())
        {
            QByteArray data = fwProcess->readAllStandardOutput();
            DBG_Printf(DBG_INFO, "%s", qPrintable(data));

            if (apsCtrl->getParameter(deCONZ::ParamFirmwareUpdateActive) != deCONZ::FirmwareUpdateRunning)
            {
                if (data.contains("flashing"))
                {
                    apsCtrl->setParameter(deCONZ::ParamFirmwareUpdateActive, deCONZ::FirmwareUpdateRunning);
                }
            }
        }

        if (fwProcess->state() == QProcess::Starting)
        {
            DBG_Printf(DBG_INFO, "GW firmware update starting ..\n");
        }
        else if (fwProcess->state() == QProcess::Running)
        {
            DBG_Printf(DBG_INFO_L2, "GW firmware update running ..\n");
        }
        else if (fwProcess->state() == QProcess::NotRunning)
        {
            if (fwProcess->exitStatus() == QProcess::NormalExit)
            {
                DBG_Printf(DBG_INFO, "GW firmware update exit code %d\n", fwProcess->exitCode());
            }
            else if (fwProcess->exitStatus() == QProcess::CrashExit)
            {
                DBG_Printf(DBG_INFO, "GW firmware update crashed %s\n", qPrintable(fwProcess->errorString()));
            }

            fwProcess->deleteLater();
            fwProcess = 0;
        }
    }

    // done
    if (fwProcess == 0)
    {
        fwUpdateStartedByUser = false;
        gwFirmwareNeedUpdate = false;
        updateEtag(gwConfigEtag);
        apsCtrl->setParameter(deCONZ::ParamFirmwareUpdateActive, deCONZ::FirmwareUpdateIdle);
        fwUpdateState = FW_Idle;
        fwUpdateTimer->start(FW_IDLE_TIMEOUT_LONG);
    }
    else // recheck
    {
        fwUpdateTimer->start(250);
    }
}
/*! Starts the firmware update.
 */
bool DeRestPluginPrivate::startUpdateFirmware()
{
    fwUpdateStartedByUser = true;
    if (fwUpdateState == FW_WaitUserConfirm)
    {
        apsCtrl->setParameter(deCONZ::ParamFirmwareUpdateActive, deCONZ::FirmwareUpdateRunning);
        updateEtag(gwConfigEtag);
        fwUpdateState = FW_DisconnectDevice;
        fwUpdateTimer->start(100);
        return true;
    }

    return false;
}
/*! Delayed trigger to update the firmware.
 */
void DeRestPluginPrivate::firmwareUpdateTimerFired()
{
    if (otauLastBusyTimeDelta() < OTA_LOW_PRIORITY_TIME)
    {
        fwUpdateState = FW_Idle;
        fwUpdateTimer->start(FW_IDLE_TIMEOUT);
    }
    else if (fwUpdateState == FW_Idle)
    {
        if (gwFirmwareNeedUpdate)
        {
            gwFirmwareNeedUpdate = false;
            updateEtag(gwConfigEtag);
        }
        fwUpdateState = FW_CheckDevices;
        fwUpdateTimer->start(0);
    }
    else if (fwUpdateState == FW_CheckDevices)
    {
        checkFirmwareDevices();
    }
    else if (fwUpdateState == FW_CheckVersion)
    {
        queryFirmwareVersion();
    }
    else if (fwUpdateState == FW_DisconnectDevice)
    {
        updateFirmwareDisconnectDevice();
    }
    else if (fwUpdateState == FW_Update)
    {
        updateFirmware();
    }
    else if (fwUpdateState == FW_UpdateWaitFinished)
    {
        updateFirmwareWaitFinished();
    }
    else if (fwUpdateState == FW_WaitUserConfirm)
    {
        fwUpdateState = FW_Idle;
        fwUpdateTimer->start(FW_IDLE_TIMEOUT);
    }
    else
    {
        fwUpdateState = FW_Idle;
        fwUpdateTimer->start(FW_IDLE_TIMEOUT);
    }
}
/*! Extracts the update channels version info about the deCONZ/WebApp.

    \param reply which holds the version info in JSON format
 */
void DeRestPluginPrivate::internetDiscoveryExtractVersionInfo(QNetworkReply *reply)
{
    bool ok;
    QByteArray content = reply->readAll();
    QVariant var = Json::parse(content, ok);
    QVariantMap map = var.toMap();

    if (!ok || map.isEmpty())
    {
        DBG_Printf(DBG_ERROR, "discovery couldn't extract version info from reply\n");
    }

    if (map.contains("versions") && (map["versions"].type() == QVariant::Map))
    {
        QString version;
        QVariantMap versions = map["versions"].toMap();

        if (versions.contains(gwUpdateChannel) && (versions[gwUpdateChannel].type() == QVariant::String))
        {
            version = versions[gwUpdateChannel].toString();

            if (!version.isEmpty())
            {
                if (gwUpdateVersion != version)
                {
                    DBG_Printf(DBG_INFO, "discovery found version %s for update channel %s\n", qPrintable(version), qPrintable(gwUpdateChannel));
                    gwUpdateVersion = version;
                    updateEtag(gwConfigEtag);
                }
            }
            else
            {
                DBG_Printf(DBG_ERROR, "discovery reply doesn't contain valid version info for update channel %s\n", qPrintable(gwUpdateChannel));
            }
        }
        else
        {
            DBG_Printf(DBG_ERROR, "discovery reply doesn't contain version info for update channel %s\n", qPrintable(gwUpdateChannel));
        }
    }
    else
    {
        DBG_Printf(DBG_ERROR, "discovery reply doesn't contain valid version info\n");
    }
}
/*! Starts the device disconnect so that the serial port is released.
 */
void DeRestPluginPrivate::updateFirmwareDisconnectDevice()
{
    Q_ASSERT(apsCtrl);
//    if (apsCtrl->getParameter(deCONZ::ParamFirmwareUpdateActive) == deCONZ::FirmwareUpdateIdle)
//    {
//        if (apsCtrl->getParameter(deCONZ::ParamDeviceConnected) == 1)
//        {
//            DBG_Printf(DBG_INFO, "GW firmware disconnect device before update\n");
//        }
//    }

    if (apsCtrl->getParameter(deCONZ::ParamDeviceConnected) == 1)
    {
        fwUpdateTimer->start(100); // recheck
    }
    else
    {
        DBG_Printf(DBG_INFO, "GW firmware start update (device not connected)\n");
        fwUpdateState = FW_Update;
        fwUpdateTimer->start(0);
        updateEtag(gwConfigEtag);
    }
}
/*! Lazy query of firmware version.
    Because the device might not be connected at first optaining the
    firmware version must be delayed.

    If the firmware is older then the min required firmware for the platform
    and a proper firmware update file exists, the API will announce that a
    firmware update is available.
 */
void DeRestPluginPrivate::queryFirmwareVersion()
{
    Q_ASSERT(apsCtrl);
    if (!apsCtrl)
    {
        return;
    }

    { // check for GCFFlasher binary
        QString gcfFlasherBin = qApp->applicationDirPath() + "/GCFFlasher";
#ifdef Q_OS_WIN
        gcfFlasherBin.append(".exe");
#elif defined(Q_OS_LINUX) && !defined(ARCH_ARM) // on RPi a normal sudo is ok since we don't need password there
        gcfFlasherBin = "/usr/bin/GCFFlasher_internal";
#elif defined(Q_OS_OSX)
        // TODO
#else
        gcfFlasherBin = "/usr/bin/GCFFlasher_internal";
#endif

        if (!QFile::exists(gcfFlasherBin))
        {
            DBG_Printf(DBG_INFO, "GW update firmware failed, %s doesn't exist\n", qPrintable(gcfFlasherBin));
            fwUpdateState = FW_Idle;
            fwUpdateTimer->start(FW_IDLE_TIMEOUT_LONG);
            return;
        }
    }

    // does the update file exist?
    if (fwUpdateFile.isEmpty())
    {
        QString fileName;
        fileName.sprintf("deCONZ_Rpi_0x%08x.bin.GCF", GW_MIN_RPI_FW_VERSION);

        // search in different locations
        std::vector<QString> paths;
#ifdef Q_OS_LINUX
        paths.push_back(QLatin1String("/usr/share/deCONZ/firmware/"));
#endif
        paths.push_back(deCONZ::getStorageLocation(deCONZ::ApplicationsDataLocation) + QLatin1String("/firmware/"));
        paths.push_back(deCONZ::getStorageLocation(deCONZ::HomeLocation) + QLatin1String("/raspbee_firmware/"));
#ifdef Q_OS_OSX
        QDir dir(qApp->applicationDirPath());
        dir.cdUp();
        dir.cd("Resources");
        paths.push_back(dir.path() + "/");
#endif

        std::vector<QString>::const_iterator i = paths.begin();
        std::vector<QString>::const_iterator end = paths.end();
        for (; i != end; ++i)
        {
            if (QFile::exists(*i + fileName))
            {
                fwUpdateFile = *i + fileName;
                DBG_Printf(DBG_INFO, "GW update firmware found: %s\n", qPrintable(fwUpdateFile));
                break;
            }
        }
    }

    if (fwUpdateFile.isEmpty())
    {
        DBG_Printf(DBG_ERROR, "GW update firmware not found: %s\n", qPrintable(fwUpdateFile));
        fwUpdateState = FW_Idle;
        fwUpdateTimer->start(FW_IDLE_TIMEOUT);
        return;
    }

    uint8_t devConnected = apsCtrl->getParameter(deCONZ::ParamDeviceConnected);
    uint32_t fwVersion = apsCtrl->getParameter(deCONZ::ParamFirmwareVersion);

    Q_ASSERT(!gwFirmwareNeedUpdate);

    if (devConnected == 0 || fwVersion == 0)
    {
        // if even after some time no firmware was detected
        // ASSUME that a device is present and reachable but might not have firmware installed
//        if (getUptime() >= FW_WAIT_UPDATE_READY)
        {
            QString str;
            str.sprintf("0x%08x", GW_MIN_RPI_FW_VERSION);

            gwFirmwareVersion = "0x00000000"; // unknown
            gwFirmwareVersionUpdate = str;
            gwConfig["fwversion"] = gwFirmwareVersion;
            gwFirmwareNeedUpdate = true;
            updateEtag(gwConfigEtag);

            fwUpdateState = FW_WaitUserConfirm;
            fwUpdateTimer->start(FW_WAIT_USER_TIMEOUT);
            apsCtrl->setParameter(deCONZ::ParamFirmwareUpdateActive, deCONZ::FirmwareUpdateReadyToStart);

            if (fwUpdateStartedByUser)
            {
                startUpdateFirmware();
            }
        }
        return;
    }
    else if (devConnected)
    {
        QString str;
        str.sprintf("0x%08x", fwVersion);

        if (gwFirmwareVersion != str)
        {
            gwFirmwareVersion = str;
            gwConfig["fwversion"] = str;
            updateEtag(gwConfigEtag);
        }

        DBG_Printf(DBG_INFO, "GW firmware version: %s\n", qPrintable(gwFirmwareVersion));

        // if the device is detected check that the firmware version is >= min version
        if ((fwVersion & FW_PLATFORM_MASK) == FW_PLATFORM_RPI)
        {
            if (fwVersion < GW_MIN_RPI_FW_VERSION)
            {
                gwFirmwareVersionUpdate.sprintf("0x%08x", GW_MIN_RPI_FW_VERSION);
                gwFirmwareNeedUpdate = true;
                updateEtag(gwConfigEtag);

                DBG_Printf(DBG_INFO, "GW firmware version shall be updated to: 0x%08x\n", GW_MIN_RPI_FW_VERSION);
                fwUpdateState = FW_WaitUserConfirm;
                fwUpdateTimer->start(FW_WAIT_USER_TIMEOUT);
                apsCtrl->setParameter(deCONZ::ParamFirmwareUpdateActive, deCONZ::FirmwareUpdateReadyToStart);
                return;
            }
            else
            {
                DBG_Printf(DBG_INFO, "GW firmware version is up to date: 0x%08x\n", fwVersion);
                fwUpdateState = FW_Idle;
                fwUpdateTimer->start(FW_IDLE_TIMEOUT_LONG);
                return;
            }
        }

        if (!gwFirmwareVersionUpdate.isEmpty())
        {
            gwFirmwareVersionUpdate.clear();
            updateEtag(gwConfigEtag);
        }
    }

    fwUpdateState = FW_Idle;
    fwUpdateTimer->start(FW_IDLE_TIMEOUT);
}