/*
 * Copyright (C) 2019 by Multi-Tech Systems
 *
 * This file is part of libmts-io.
 *
 * libmts-io is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * libmts-io is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with libmts-io.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
#include <fstream>
#include <mts/MTS_Text.h>
#include <mts/MTS_Logger.h>
#include <mts/MTS_Thread.h>
#include <mts/MTS_Timer.h>
#include <mts/MTS_IO_ME910C1WWRadio.h>

using namespace MTS::IO;

const std::string ME910C1WWRadio::MODEL_NAME("ME910C1-WW");

const std::string ME910C1WWRadio::KEY_FUMO_PDPID("pdpid");         // optional (default : "3")
const std::string ME910C1WWRadio::KEY_FUMO_PDPTYPE("pdptype");     // optional (default : "IPV4V6")
const std::string ME910C1WWRadio::KEY_FUMO_APN("apn");             // optional (default : "")
const std::string ME910C1WWRadio::KEY_FUMO_ADDRESS("address");
const std::string ME910C1WWRadio::KEY_FUMO_DIR("dir");
const std::string ME910C1WWRadio::KEY_FUMO_FILE("file");
const std::string ME910C1WWRadio::KEY_FUMO_USER("user");
const std::string ME910C1WWRadio::KEY_FUMO_PASSWORD("password");
const std::string ME910C1WWRadio::KEY_FUMO_DRYRUN("dryrun");

ME910C1WWRadio::ME910C1WWRadio(const std::string& sPort)
: ME910Radio(MODEL_NAME, sPort)
{

}

ICellularRadio::CODE ME910C1WWRadio::setActiveFirmware(const Json::Value& jArgs) {
    ICellularRadio::CODE rc;

    //    Set command allows enabling a specific firmware image on products
    //    embedding 2 different firmware images:
    //
    //    "AT#FWSWITCH=<image_number>[,<storage_conf>]"
    //    <image_number> - Firmware Image To Be Enabled
    //    0 – Image 1 (Default)
    //    1 – Image 2
    //    <storage_conf> - Setting Storage Configuration
    //    0 – Save the <image_number> value in RAM
    //    1 – Save the <image_number> value in NVM

    printTrace("%s| Set Active Firmware Image Number", getName().c_str());

    if(!jArgs["fwid"].isString()) {
        return INVALID_ARGS;
    }

    //  ME910C1-WW uses code "2" for World-Wide mode
    if (jArgs["fwid"].asString() != "2" && jArgs["fwid"].asString() != "1" && jArgs["fwid"].asString() != "0") {
            return INVALID_ARGS;
    }

    //  Test command to check if firmware switch is supported
    rc = sendBasicCommand("AT#FWSWITCH=?");
    if (rc == ERROR) {
        printTrace("%s| FWSWITCH is not supported", getName().c_str());
        return NOT_APPLICABLE;
    }
    else if (rc != SUCCESS) {
        return rc;
    }

    std::string sCmd = "AT#FWSWITCH=";
    sCmd += jArgs["fwid"].asString();
    sCmd += ",1";

    printTrace("%s| Issuing %s command", getName().c_str(), sCmd.c_str());

    return sendBasicCommand(sCmd, 5000);
}

ICellularRadio::CODE ME910C1WWRadio::getActiveFirmware(std::string& sFwId) {
    std::string sCmd;
    ICellularRadio::CODE rc;
    //
    // Read command reports the current active firmware image:
    // AT#FWSWITCH?
    // #FWSWITCH: 1
    //
    // OK
    //
    printTrace("%s| Get Active Firmware Image Number", getName().c_str());

    //  Test command to check if firmware switch is supported
    sCmd = "AT#FWSWITCH=?";
    rc = sendBasicCommand(sCmd);
    if (rc == ERROR) {
        printTrace("%s| FWSWITCH is not supported", getName().c_str());
        return NOT_APPLICABLE;
    }
    else if (rc != SUCCESS) {
        return rc;
    }

    sCmd = "AT#FWSWITCH?";
    std::string sResult = sendCommand(sCmd);
    size_t end = sResult.find(ICellularRadio::RSP_OK);
    if (end == std::string::npos) {
        printWarning("%s| Unable to get active image number from radio using command [%s]",
                getName().c_str(),
                sCmd.c_str());
        return FAILURE;
    }

    size_t start = sResult.find("#FWSWITCH:") + sizeof("#FWSWITCH:");
    sFwId = MTS::Text::trim(sResult.substr(start, end-start));
    if(sFwId.size() == 0) {
        printWarning("%s| Firmware Image Number is empty", getName().c_str());
        return FAILURE;
    }

    return SUCCESS;
}

ICellularRadio::CODE ME910C1WWRadio::doGetFirmwareNumbers(std::string &sFirmware, std::string &sFirmwareBuild) {
    ICellularRadio::CODE rc = FAILURE;

    rc = getFirmware(sFirmware);
    if (rc != SUCCESS){
        return rc;
    }

    rc = getFirmwareBuild(sFirmwareBuild);
    if (rc != SUCCESS){
        return rc;
    }

    return rc;
}

ICellularRadio::CODE ME910C1WWRadio::doFumoReadConfig(const Json::Value& jArgs, Json::Value &jConfig) {
    ICellularRadio::CODE rc = INVALID_ARGS;
    std::string sPath;

    do
    {
        if (!jArgs["config-file"].isString()) {
            rc = INVALID_ARGS;
            break;
        }

        sPath = jArgs["config-file"].asString();

        std::ifstream file(sPath.c_str());
        if (!file.is_open()) {
            printError("Failed to open file [%s]", sPath.c_str());
            break;
        }

        file.seekg(0, std::ios::end);
        size_t size = file.tellg();
        std::string buffer(size, ' ');
        file.seekg(0);
        file.read(&buffer[0], size);
        file.close();

        Json::Features features = Json::Features::strictMode();
        Json::Reader reader(features);
        if (!reader.parse(buffer, jConfig)) {
            printError("Error parsing FOTA configuration file");
            break;
        }

        //
        // set default values if missing
        //
        if (!jConfig.isMember(KEY_FUMO_PDPID)) {
            jConfig[KEY_FUMO_PDPID] = std::string("3");
        }

        if (!jConfig.isMember(KEY_FUMO_PDPTYPE)) {
            jConfig[KEY_FUMO_PDPTYPE] = std::string("IPV4V6");
        }

        if (!jConfig.isMember(KEY_FUMO_APN)) {
            jConfig[KEY_FUMO_APN] = std::string("");
        }

        //
        // validate
        //
        if (!jConfig[KEY_FUMO_PDPID].isString()) {
            printError("Error loading FOTA configuration: PDP context id is not set");
            break;
        }

        if (jConfig[KEY_FUMO_PDPID].asString().empty()) {
            printError("Error loading FOTA configuration: context id is empty");
            break;
        }

        if (!jConfig[KEY_FUMO_PDPTYPE].isString()) {
            printError("Error loading FOTA configuration: PDP type is not set");
            break;
        }

        if (jConfig[KEY_FUMO_PDPTYPE].asString().empty()) {
            printError("Error loading FOTA configuration: PDP type is empty");
            break;
        }

        // Note : allow empty APN
        if (!jConfig[KEY_FUMO_APN].isString()) {
            printError("Error loading FOTA configuration: APN is not set");
            break;
        }

        if (!jConfig[KEY_FUMO_ADDRESS].isString()) {
            printError("Error loading FOTA configuration: address is not set");
            break;
        }

        if (jConfig[KEY_FUMO_ADDRESS].asString().empty()) {
            printError("Error loading FOTA configuration: address is empty");
            break;
        }

        // Note: allow empty dir
        if (!jConfig[KEY_FUMO_DIR].isString()) {
            printError("Error loading FOTA configuration: directory is not set");
            break;
        }

        if (!jConfig[KEY_FUMO_FILE].isString()) {
            printError("Error loading FOTA configuration: filename is not set");
            break;
        }

        if (jConfig[KEY_FUMO_FILE].asString().empty()) {
            printError("Error loading FOTA configuration: filename is empty");
            break;
        }

        // Note: allow empty username/password
        if (!jConfig[KEY_FUMO_USER].isString()) {
            printError("Error loading FOTA configuration: username is not set");
            break;
        }

        if (!jConfig[KEY_FUMO_PASSWORD].isString()) {
            printError("Error loading FOTA configuration: password is not set");
            break;
        }

        rc = SUCCESS;
    }
    while(0);

    return rc;
}

ICellularRadio::CODE ME910C1WWRadio::doFumoSetup(const Json::Value &jConfig, UpdateCb& stepCb) {
    ICellularRadio::CODE rc = FAILURE;
    std::string sCmd;

    std::string sContextId = jConfig[KEY_FUMO_PDPID].asString();
    std::string sApn = jConfig[KEY_FUMO_APN].asString();
    std::string sPdpType = jConfig[KEY_FUMO_PDPTYPE].asString();


    do
    {
        //
        // Execution command is used to activate or deactivate either the GSM
        // context or the specified PDP context.
        //
        // AT#SGACT=<cid>,<stat>[,<userId>,<pwd>]
        //
        sCmd = "AT#SGACT=" + sContextId + ",0";
        rc = sendBasicCommand(sCmd);
        if (rc != SUCCESS) {
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: Failed to deactivate PDP context"));
            }
            break;
        }

        //
        // Read current Firmware numbers (let it be after AT#SGACT not to confuse users)
        //
        rc = doGetFirmwareNumbers(m_sFw, m_sFwBuild);
        if (rc != SUCCESS) {
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: Failed to obtain current firmware version"));
            }
            break;
        }

        //
        // Set command specifies PDP context parameter values for a PDP context identified by
        // the (local) context identification parameter <cid>.
        //
        // AT+CGDCONT= [<cid>[,<PDP_type>[,<APN>[,<PDP_addr>[,<d_comp>[,<h_comp>[,<pd1>[,...[,pdN]]]]]]]]]
        //
        sCmd = "AT+CGDCONT=" + sContextId + ",\"" + sPdpType + "\"";
        if (!sApn.empty()) {
            sCmd += ",\"" + sApn + "\"";
        }
        rc = sendBasicCommand(sCmd, 1000);
        if (rc != SUCCESS) {
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: Failed to setup PDP context"));
            }
            break;
        }

        //
        // Set command sets the socket configuration parameters.
        //
        // AT#SCFG==<connId>,<cid>,<pktSz>,<maxTo>,<connTo>,<txTo>
        //    <connId> - socket connection identifier
        //    <cid> - PDP context identifier
        //    <pktSz> - packet size to be used by the TCP/UDP/IP stack for data sending.
        //    <maxTo> - exchange timeout (or socket inactivity timeout); if there’s no
        //             data exchange within this timeout period the connection is closed (timeout value in seconds).
        //    <connTo> - connection timeout; if we can’t establish a connection to the
        //             remote within this timeout period, an error is raised timeout value in hundreds of milliseconds.
        //    <txTo> - data sending timeout; after this period data are sent also if they’re
        //             less than max packet size (timeout value in hundreds of milliseconds).
        //
        sCmd = "AT#SCFG=1," + sContextId + ",300,90,600,50";
        rc = sendBasicCommand(sCmd);
        if (rc != SUCCESS) {
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: Failed to set connection configuration parameters"));
            }
            break;
        }

        //
        // Activate PDP context
        //
        sCmd = "AT#SGACT=" + sContextId + ",1";
        rc = sendBasicCommand(sCmd, 60 * 1000);
        if (rc != SUCCESS) {
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: Failed to activate PDP context"));
            }
            break;
        }
    }
    while (0);

    return rc;
}

ICellularRadio::CODE ME910C1WWRadio::doFumoFtp(const Json::Value &jConfig, UpdateCb& stepCb) {
    ICellularRadio::CODE rc = FAILURE;
    std::string sCmd;
    std::string sResult;

    //
    // Set command sets the time-out used when opening either the FTP control
    // channel or the FTP traffic channel.
    //
    // AT#FTPTO= [<tout>]
    // <tout> - time-out in 100 ms units
    //
    rc = sendBasicCommand("AT#FTPTO=2400");
    if (rc != SUCCESS) {
        if(stepCb) {
            stepCb(Json::Value("FUMO Error: Failed to setup connection timeout"));
        }
        return rc;
    }

    //
    // Execution command opens an FTP connection toward the FTP server.
    //
    // AT#FTPOPEN=[<server:port>,<username>,<password>[,<mode>]]
    //
    sCmd = "AT#FTPOPEN=";
    sCmd += "\"" + jConfig[KEY_FUMO_ADDRESS].asString() +  "\",";
    sCmd += "\"" + jConfig[KEY_FUMO_USER].asString() +  "\",";
    sCmd += "\"" + jConfig[KEY_FUMO_PASSWORD].asString() +  "\",1";
    rc = sendBasicCommand(sCmd, 60 * 1000);
    if (rc != SUCCESS) {
        if(stepCb) {
            stepCb(Json::Value("FUMO Error: Failed to open connection"));
        }
        return rc;
    }

    if (stepCb) {
        stepCb(Json::Value("FUMO Info: connection opened"));
    }

    do
    {
        //
        // Set command, issued during an FTP connection, sets the file transfer type.
        //
        // AT#FTPTYPE=[<type>]
        //  <type> - file transfer type:
        //    0 - binary
        //    1 - ascii
        //
        rc = sendBasicCommand("AT#FTPTYPE=0", 1000);
        if (rc != SUCCESS) {
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: failed to set file transfer type"));
            }
            break;
        }

        //
        // Execution command, issued during an FTP connection, changes the
        //  working directory on FTP server.
        //
        // AT#FTPCWD=[<dirname>]
        //
        sCmd = "AT#FTPCWD=\"/";
        if (!jConfig[KEY_FUMO_DIR].asString().empty()) {
            sCmd += jConfig[KEY_FUMO_DIR].asString() + "/";
        }
        sCmd += "\"";
        rc = sendBasicCommand(sCmd, 60 * 1000);
        if (rc != SUCCESS) {
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: failed to change working directory on the server"));
            }
            break;
        }

        if (stepCb) {
            stepCb(Json::Value("FUMO Info: downloading the firmware"));
        }

        //
        // Start FTP transfer
        //
        sCmd = "AT#FTPGETOTA=";
        sCmd += "\"" + jConfig[KEY_FUMO_FILE].asString() + "\",1,1";
        sendBasicCommand(sCmd);

        //
        // Noticed that after successful AT#FTPGETOTA the radio resets the connection.
        // and the response code (OK, ERROR.. ) not always reach the host. Therefore
        // we send the AT#FTPGETOTA with relatively small timeout and then poll with AT
        // until we get valid response. After that, using AT#FTPMSG we can check the
        // result of the last FTP command (which is AT#FTPGETOTA in our case).
        MTS::Timer oTimer;

        oTimer.start();

        while (oTimer.getSeconds() < (30 * 60)) // 30 min
        {
            MTS::Thread::sleep(5000);

            rc = sendBasicCommand("AT");
            if (rc == SUCCESS) {
                break;
            }

            resetConnection(1);
        }
        oTimer.stop();

        if (rc != SUCCESS) {
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: unable to obtain radio after reset"));
            }
            break;
        }

        //
        // Now check the FTP status
        //
        std::string sResult = sendCommand("AT#FTPMSG");

        printTrace("RADIO| AT#FTPMSG result [%s]", sResult.c_str());

        if (sResult.find(ICellularRadio::RSP_OK) == std::string::npos) {
            rc = FAILURE;
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: failed to download the firmware file"));
            }
            break;
        }
        if (sResult.find("#FTPMSG: 550") != std::string::npos) {
            // FTP(550) : File not found
            rc = FAILURE;
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: file not found"));
            }
            break;
        }
        if (sResult.find("#FTPMSG: 226") == std::string::npos) {
            // FTP(226) : Successfully transferred
            rc = FAILURE;
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: failed to download the firmware file"));
            }
            break;
        }
    }
    while (0);

    //
    // Execution command closes an FTP connection.
    //
    // AT#FTPCLOSE
    //
    ICellularRadio::CODE rcclose = sendBasicCommand("AT#FTPCLOSE", 60 * 1000);
    if (rcclose != SUCCESS && rc == SUCCESS) {
        if(stepCb) {
            // Only one "FUMO Error" message should be sent
            stepCb(Json::Value("FUMO Error: Failed to close FTP connection"));
        }
        rc = rcclose;
    }

    if (rc == SUCCESS) {
        if(stepCb) {
            stepCb(Json::Value("FUMO Info: firmware downloaded successfully"));
        }
    }

    return rc;
}

ICellularRadio::CODE ME910C1WWRadio::doFumoCleanup(const Json::Value &jConfig, UpdateCb& stepCb) {
    ICellularRadio::CODE rc = FAILURE;
    std::string sCmd;

    std::string sContextId = jConfig[KEY_FUMO_PDPID].asString();

    //
    // Deactivate PDP context
    //
    sCmd = "AT#SGACT=" + sContextId + ",0";
    rc = sendBasicCommand(sCmd, 10000);
    if (rc != SUCCESS) {
        if(stepCb) {
            stepCb(Json::Value("FUMO Error: Failed to deactivate PDP context"));
        }
    }
    return rc;
}

ICellularRadio::CODE ME910C1WWRadio::doFumoApplyFirmware(const Json::Value &jConfig, UpdateCb& stepCb) {
    ICellularRadio::CODE rc = FAILURE;

    if (jConfig.isMember(KEY_FUMO_DRYRUN)) {
        if(stepCb) {
            stepCb(Json::Value("FUMO Info: applying the radio firmware"));
        }
        return SUCCESS;
    }

    rc = sendBasicCommand("AT#OTAUP=0", 10000);
    if (rc != SUCCESS) {
        if(stepCb) {
            stepCb(Json::Value("FUMO Error: failed to apply the firmware"));
        }
        return rc;
    }

    if(stepCb) {
        stepCb(Json::Value("FUMO Info: applying the radio firmware"));
    }

    return rc;
}

ICellularRadio::CODE ME910C1WWRadio::doFumoWaitNewFirmware(const Json::Value &jConfig, UpdateCb& stepCb) {
    std::string sFirmware;
    std::string sFirmwareBuild;
    ICellularRadio::CODE rc = FAILURE;

    if (jConfig.isMember(KEY_FUMO_DRYRUN)) {
        if(stepCb) {
            stepCb(Json::Value("FUMO done: radio firmware applied successfully"));
        }
        return SUCCESS;
    }

    // The radio is expected to send "#OTAEV: Module Upgraded To New Fw" unsolicited message
    // on success. However, for some reason, we do not see this message.

    MTS::Timer oTimer;
    oTimer.start();

    while (oTimer.getSeconds() < (5 * 60)) { // 5 minutes

        MTS::Thread::sleep(10000);

        if (doGetFirmwareNumbers(sFirmware, sFirmwareBuild) != SUCCESS) {
            // The radio is probably unavailable
            resetConnection(100);
            continue;
        }

        if (sFirmware == m_sFw && sFirmwareBuild == m_sFwBuild) {
            // Have the same firmware. The radio resets several time
            // before the firmware is actually get upgraded. So keep polling.
            continue;
        }

        // The firmware numbers have changed
        rc = SUCCESS;
        break;
    }
    oTimer.stop();

    if (rc == SUCCESS) {
        if(stepCb) {
            stepCb(Json::Value("FUMO done: radio firmware applied successfully"));
        }
    }
    else {
        if(stepCb) {
            stepCb(Json::Value("FUMO error: radio firmware has not been updated"));
        }
    }

    return rc;
}


ICellularRadio::CODE ME910C1WWRadio::doFumoPerform(const Json::Value &jConfig, UpdateCb& stepCb) {
    ICellularRadio::CODE rc = FAILURE;

    UpdateCb dummyCb;

    // Set the PDP context for the FOTA
    rc = doFumoSetup(jConfig, stepCb);
    if (rc != SUCCESS) {
        return rc;
    }

    // Download FW over FTP
    rc = doFumoFtp(jConfig, stepCb);
    if (rc != SUCCESS) {
        doFumoCleanup(jConfig, dummyCb);
        return rc;
    }

    // Clean up before applying the FW file
    rc = doFumoCleanup(jConfig, stepCb);
    if (rc != SUCCESS) {
        return rc;
    }

    // Apply the FW file
    rc = doFumoApplyFirmware(jConfig, stepCb);
    if (rc != SUCCESS) {
        return rc;
    }

    rc = doFumoWaitNewFirmware(jConfig, stepCb);

    return rc;
}

ICellularRadio::CODE ME910C1WWRadio::updateFumo(const Json::Value& jArgs, UpdateCb& stepCb) {
    Json::Value jConfig(Json::objectValue);
    ICellularRadio::CODE rc = FAILURE;
    std::string sFwId;

    do
    {
        rc = getActiveFirmware(sFwId);
        if (rc != SUCCESS) {
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: failed to obtain current active firmware id"));
            }
            break;
        }

        // For Verizon Only
        if (sFwId != "1") {
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: fumo is not supported"));
            }
            break;
        }

        rc = doFumoReadConfig(jArgs, jConfig);
        if (rc != SUCCESS) {
            if(stepCb) {
                stepCb(Json::Value("FUMO Error: bad configuration parameters"));
            }
            break;
        }

        rc = doFumoPerform(jConfig, stepCb);
    }
    while(0);

    return rc;
}