/* MIT License
 *
 * Copyright (c) 2019 - 2023 Andreas Merkle <web@blue-andi.de>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/*******************************************************************************
    DESCRIPTION
*******************************************************************************/
/**
 * @brief  Gets CTA bus arrival times
 * @author queenkjuul <queenkjuul69@gmail.com>
 */

/******************************************************************************
 * Includes
 *****************************************************************************/
#include "AsyncHttpClient.h"
#include "CTABusTrackerPlugin.h"
#include "FileSystem.h"

#include <ArduinoJson.h>
#include <Logging.h>
#include <JsonFile.h>
#include <Util.h>

/******************************************************************************
 * Compiler Switches
 *****************************************************************************/

/******************************************************************************
 * Macros
 *****************************************************************************/

/******************************************************************************
 * Types and classes
 *****************************************************************************/

/******************************************************************************
 * Prototypes
 *****************************************************************************/

/******************************************************************************
 * Local Variables
 *****************************************************************************/

/* Initialize image path. */
const char *CTABusTrackerPlugin::CTA_LOGO_ICON_PATH = "/plugins/CTABusTrackerPlugin/ctapix.bmp";

const char *CTABusTrackerPlugin::CTA_BUS_BASE_URI = "http://ctabustracker.com/bustime/api/v2";

const char *CTABusTrackerPlugin::TOPIC_CONFIG = "/ctabus";

const char *CTABusTrackerPlugin::TOPIC_ROUTES = "/ctabusroutes";

/******************************************************************************
 * Public Methods
 *****************************************************************************/

void CTABusTrackerPlugin::getTopics(JsonArray &topics) const
{
    (void)topics.add(TOPIC_CONFIG);
    (void)topics.add(TOPIC_ROUTES);
}

bool CTABusTrackerPlugin::getTopic(const String &topic, JsonObject &value) const
{
    bool isSuccessful = false;

    if (0U != topic.equals(TOPIC_CONFIG))
    {
        getConfiguration(value);
        isSuccessful = true;
    }
    if (0U != topic.equals(TOPIC_ROUTES))
    {
        const_cast<CTABusTrackerPlugin*>(this)->getRoutes(value);
        isSuccessful = true;
    }

    return isSuccessful;
}

bool CTABusTrackerPlugin::setTopic(const String &topic, const JsonObject &value)
{
    bool isSuccessful = false;

    if (0U != topic.equals(TOPIC_CONFIG))
    {
        const size_t JSON_DOC_SIZE = 1024U;
        DynamicJsonDocument jsonDoc(JSON_DOC_SIZE);
        JsonObject jsonCfg = jsonDoc.to<JsonObject>();
        JsonVariantConst jsonApiKey = value["apiKey"];
        JsonVariantConst jsonRte = value["rte"];
        JsonVariantConst jsonDir = value["dir"];
        JsonVariantConst jsonStpid = value["stpid"];
        JsonVariantConst jsonOrig = value["orig"];
        JsonVariantConst jsonDest = value["dest"];
        JsonVariantConst jsonTwo = value["two"];

        /* The received configuration may not contain all single key/value pair.
         * Therefore read first the complete internal configuration and
         * overwrite them with the received ones.
         */
        getConfiguration(jsonCfg);

        /* Note:
         * Check only for the key/value pair availability.
         * The type check will follow in the setConfiguration().
         */

        if (false == jsonApiKey.isNull())
        {
            jsonCfg["apiKey"] = jsonApiKey.as<String>();
            isSuccessful = true;
        }
        if (false == jsonRte.isNull())
        {
            jsonCfg["rte"] = jsonRte.as<String>();
            isSuccessful = true;
        }
        if (false == jsonDir.isNull())
        {
            jsonCfg["dir"] = jsonDir.as<String>();
            isSuccessful = true;
        }
        if (false == jsonStpid.isNull())
        {
            jsonCfg["stpid"] = jsonStpid.as<String>();
            isSuccessful = true;
        }
        if (false == jsonOrig.isNull())
        {
            jsonCfg["orig"] = jsonOrig.as<String>();
            isSuccessful = true;
        }
        if (false == jsonDest.isNull())
        {
            jsonCfg["dest"] = jsonDest.as<String>();
            isSuccessful = true;
        }
        if (false == jsonTwo.isNull())
        {
            jsonCfg["two"] = jsonTwo.as<String>();
            isSuccessful = true;
        }

        if (true == isSuccessful)
        {
            JsonObjectConst jsonCfgConst = jsonCfg;

            isSuccessful = setConfiguration(jsonCfgConst);

            if (true == isSuccessful)
            {
                requestStoreToPersistentMemory();
            }
        }
    }

    return isSuccessful;
}

bool CTABusTrackerPlugin::hasTopicChanged(const String &topic)
{
    MutexGuard<MutexRecursive> guard(m_mutex);
    bool hasTopicChanged = m_hasTopicChanged;

    // only a single topic
    PLUGIN_NOT_USED(topic);

    m_hasTopicChanged = false;

    return hasTopicChanged;
}

void CTABusTrackerPlugin::setSlot(const ISlotPlugin *slotInterf)
{
    m_slotInterf = slotInterf;
}

void CTABusTrackerPlugin::start(uint16_t width, uint16_t height)
{
    MutexGuard<MutexRecursive> guard(m_mutex);

    m_routeCanvas.setPosAndSize(0, 0, RTE_SECTION_WIDTH, RTE_SECTION_HEIGHT);

    (void)m_routeCanvas.addWidget(m_routeTextWidget);

    /* The text canvas is left aligned to the icon canvas and it spans over
     * the whole display height.
     */
    m_arrivalsCanvas.setPosAndSize(RTE_SECTION_WIDTH + 1, 0, width - RTE_SECTION_WIDTH, height);
    (void)m_arrivalsCanvas.addWidget(m_arrivalsTextWidget);

    /* Choose font. */
    m_arrivalsTextWidget.setFont(Fonts::getFontByType(m_fontType));
    m_routeTextWidget.setFont(Fonts::getFontByType(m_fontType));

    /* The text widget inside the text canvas is left aligned on x-axis and
     * aligned to the center of y-axis.
     */
    if (height > m_arrivalsTextWidget.getFont().getHeight())
    {
        uint16_t diffY = height - m_arrivalsTextWidget.getFont().getHeight();
        uint16_t offsY = diffY / 2U;

        m_arrivalsTextWidget.move(0, offsY);
        m_routeTextWidget.move(0, offsY);
    }

    if (false == loadConfiguration())
    {
        if (false == saveConfiguration())
        {
            LOG_WARNING("Failed to create initial config file %s", getFullPathToConfiguration().c_str());
        }
    }
    else
    {
        updateTimestampLastUpdate();
    }

    m_cfgReloadTimer.start(CFG_RELOAD_PERIOD);
    m_routeReloadTimer.start(7000U);

    initHttpClient();
}

void CTABusTrackerPlugin::stop()
{
    String configurationFilename = getFullPathToConfiguration();
    MutexGuard<MutexRecursive> guard(m_mutex);

    m_requestTimer.stop();
    m_cfgReloadTimer.stop();
    m_routeReloadTimer.stop();

    if (false != FILESYSTEM.remove(configurationFilename.c_str()))
        ;
}

void CTABusTrackerPlugin::process(bool isConnected)
{
    Msg msg;
    MutexGuard<MutexRecursive> guard(m_mutex);

    /* Configuration in persistent memory updated? */
    if ((true == m_cfgReloadTimer.isTimerRunning()) &&
        (true == m_cfgReloadTimer.isTimeout()))
    {
        if (true == isConfigurationUpdated())
        {
            m_reloadConfigReq = true;
        }

        m_cfgReloadTimer.restart();
    }

    if ((true == m_routeReloadTimer.isTimerRunning()) && 
        (true == m_routeReloadTimer.isTimeout()))
    {
        if (true == isConnected && m_routes.empty())
        {
            if (false == startRoutesRequest())
            {
                // hopefully never accidentally overlap 30s data timer
                m_routeReloadTimer.start(17000U);
            }
            else
            {
                m_routeReloadTimer.start(SIMPLE_TIMER_HOURS(2));
            }
        }
        else if (false == isConnected || false == m_routes.empty())
        {
            m_routeReloadTimer.stop();
        }
    }

    if (true == m_storeConfigReq)
    {
        if (false == saveConfiguration())
        {
            LOG_WARNING("Failed to save configuration: %s", getFullPathToConfiguration().c_str());
        }

        m_storeConfigReq = false;
    }
    else if (true == m_reloadConfigReq)
    {
        LOG_INFO("Reload configuration: %s", getFullPathToConfiguration().c_str());

        if (true == loadConfiguration())
        {
            updateTimestampLastUpdate();
        }

        m_reloadConfigReq = false;
    }
    else
    {
        ;
    }

    /* Only if a network connection is established the required information
     * shall be periodically requested via REST API.
     */
    if (false == m_requestTimer.isTimerRunning())
    {
        if (true == isConnected)
        {
            if (false == startHttpRequest())
            {
                m_requestTimer.start(UPDATE_PERIOD_SHORT);
            }
            else
            {
                m_requestTimer.start(UPDATE_PERIOD);
            }
        }
    }
    else
    {
        /* If the connection is lost, stop periodically requesting information
         * via REST API.
         */
        if (false == isConnected)
        {
            m_requestTimer.stop();
        }
        /* Network connection is available and next request may be necessary for
         * information update.
         */
        else if (true == m_requestTimer.isTimeout())
        {
            if (false == startHttpRequest())
            {
                m_requestTimer.start(UPDATE_PERIOD_SHORT);
            }
            else
            {
                m_requestTimer.start(UPDATE_PERIOD);
            }
        }
    }

    if (true == m_taskProxy.receive(msg))
    {
        switch (msg.type)
        {
        case MSG_TYPE_INVALID:
            /* Should never happen. */
            break;

        case MSG_TYPE_RSP:
            if (nullptr != msg.rsp)
            {
                handleWebResponse(*msg.rsp);
                delete msg.rsp;
                msg.rsp = nullptr;
            }
            break;

        default:
            /* Should never happen. */
            break;
        }
    }
}

void CTABusTrackerPlugin::update(YAGfx &gfx)
{
    MutexGuard<MutexRecursive> guard(m_mutex);

    gfx.fillScreen(ColorDef::BLACK);
    m_routeCanvas.update(gfx);
    m_arrivalsCanvas.update(gfx);
}

/******************************************************************************
 * Protected Methods
 *****************************************************************************/

/******************************************************************************
 * Private Methods
 *****************************************************************************/

void CTABusTrackerPlugin::requestStoreToPersistentMemory()
{
    MutexGuard<MutexRecursive> guard(m_mutex);

    m_storeConfigReq = true;
}

bool CTABusTrackerPlugin::startRoutesRequest()
{
    bool status = false;

    LOG_INFO("Started routes request");

    m_apiKey = "jt57PwLX6cCCWVVtnjLNXG2bc";

    if ((false == m_apiKey.isEmpty()))
    {
        String url = CTA_BUS_BASE_URI;
        url += "/getroutes?";
        url += "key=";
        url += m_apiKey;
        url += "&format=json";

        if (true == m_client.begin(url))
        {
            if (false == m_client.GET())
            {
                LOG_WARNING("GET failed %s .", url.c_str());
            }
            else
            {
                LOG_INFO("GET success %s .", url.c_str());
                status = true;
            }
        }
    }

    return status;
}

bool CTABusTrackerPlugin::startHttpRequest()
{
    bool status = false;

    m_apiKey = "jt57PwLX6cCCWVVtnjLNXG2bc";

    if ((false == m_apiKey.isEmpty()) && (false == m_rte.isEmpty()) && (false == m_dir.isEmpty()) && (false == m_stpid.isEmpty()))
    {
        String url = CTA_BUS_BASE_URI;

        url += "/getpredictions?";
        url += "key=";
        url += m_apiKey;
        url += "&format=json";
        url += "&rt=";
        url += m_rte;
        url += "&dir=";
        url += m_dir;
        url += "&stpid=";
        url += m_stpid;
        url += "&top=2";

        if (true == m_client.begin(url))
        {
            if (false == m_client.GET())
            {
                LOG_WARNING("GET failed %s .", url.c_str());
            }
            else
            {
                LOG_INFO("GET success %s .", url.c_str());
                status = true;
            }
        }
    }

    return status;
}

void CTABusTrackerPlugin::initHttpClient()
{
    /* Note: All registered callbacks are running in a different task context!
     *       Therefore it is not allowed to access a member here directly.
     *       The processing must be deferred via task proxy.
     */
    m_client.regOnResponse(
        [this](const HttpResponse &rsp)
        {
            // big for routes request (~13kB)
            const size_t JSON_DOC_SIZE = 65536U;
            DynamicJsonDocument *jsonDoc = new (std::nothrow) DynamicJsonDocument(JSON_DOC_SIZE);

            if (nullptr != jsonDoc)
            {
                size_t payloadSize = 0U;
                const char *payload = reinterpret_cast<const char *>(rsp.getPayload(payloadSize));
                const size_t FILTER_SIZE = 128U;
                StaticJsonDocument<FILTER_SIZE> filter;
                DeserializationError error;

                if (true == filter.overflowed())
                {
                    LOG_ERROR("Less memory for filter available.");
                }

                error = deserializeJson(*jsonDoc, payload, payloadSize);

                if (DeserializationError::Ok != error.code())
                {
                    LOG_ERROR("Invalid JSON message received: %s", error.c_str());
                }
                else
                {
                    Msg msg;

                    msg.type = MSG_TYPE_RSP;
                    msg.rsp = jsonDoc;

                    if (false == this->m_taskProxy.send(msg))
                    {
                        delete jsonDoc;
                        jsonDoc = nullptr;
                    }
                }
            }
        });
}

void CTABusTrackerPlugin::handleRoutesResponse(DynamicJsonDocument &jsonDoc) {
    JsonArray routeArray = jsonDoc["bustime-response"]["routes"].as<JsonArray>();
    LOG_INFO("Handling route response");
    LOG_INFO("Route response length %s", String(routeArray.size()));
    int i = 0;
    m_routes.clear();
    for (JsonObject bustime_response_route : jsonDoc["bustime-response"]["routes"].as<JsonArray>()) 
    {
        Route newroute;
        newroute.rt = bustime_response_route["rt"].as<String>(); // "1", "2", "3", "4", "X4", "5", ...
        newroute.rtnm = bustime_response_route["rtnm"].as<String>(); // "Bronzeville/Union ...
        newroute.rtclr = bustime_response_route["rtclr"].as<String>(); // "#336633", "#993366", ...
        // newroute.rtdd = bustime_response_route["rtdd"].as<String>(); // "1", "2", "3", "4", "X4", ...
        m_routes.push_back(newroute);
        i++;
    }
    i = 0;
}

void CTABusTrackerPlugin::handleWebResponse(DynamicJsonDocument &jsonDoc)
{
    if (jsonDoc["bustime-response"]["routes"].as<JsonArray>().size() > 0)
    {
        handleRoutesResponse(jsonDoc);
        return;
    }
    JsonVariantConst routesResponse = jsonDoc["bustime-response"]["routes"];
    JsonVariantConst response = jsonDoc["bustime-response"]["prd"];
    JsonVariantConst currentEstimate = response[0];
    JsonVariantConst nextEstimate = response[1];
    String rte = currentEstimate["rt"].as<String>();
    String est = currentEstimate["prdctdn"].as<String>();
    String dir = currentEstimate["rtdir"].as<String>();
    String des = currentEstimate["des"].as<String>();
    String stpnm = currentEstimate["stpnm"].as<String>();
    String nxt = nextEstimate["prdctdn"].as<String>();

    if (des != "null" && des != "" && true == m_dest)
    {
        m_routeResponsePart = m_displayColor + rte + " to " + des;
    }
    else if (rte == "null" || rte == "")
    {
        m_routeResponsePart = m_delayColor + m_rte;
    }
    else
    {
        m_routeResponsePart = m_displayColor + rte;
    }

    if (true == m_orig && stpnm != "" && stpnm != "null")
    {
        m_routeResponsePart += " from " + stpnm;
    }

    if (est == "DUE")
    {
        m_relevantResponsePart = m_dueColor + est;
    }
    else if (est == "DLY")
    {
        if (nxt != "null" && nxt != "")
        {
            m_relevantResponsePart = m_delayColor + est + m_displayColor + " / " + nxt + " min";
        }
        else
        {
            m_relevantResponsePart = m_delayColor + est;
        }
    }
    else if (est == "null" || est == "")
    {
        m_relevantResponsePart = m_delayColor + " NO DATA ";
    }
    else
    {
        m_relevantResponsePart = m_displayColor + est + " min";
    }

    if (true == m_two && nxt != "null" && nxt != "")
    {
        m_relevantResponsePart += m_displayColor + " / " + nxt + " min";
    }

    LOG_INFO("Time prediction to print %s", m_relevantResponsePart.c_str());
    LOG_INFO("Route number received %s", m_routeResponsePart.c_str());

    m_arrivalsTextWidget.setFormatStr(m_relevantResponsePart);
    m_routeTextWidget.setFormatStr(m_routeResponsePart);
}

void CTABusTrackerPlugin::clearQueue()
{
    Msg msg;

    while (true == m_taskProxy.receive(msg))
    {
        if (MSG_TYPE_RSP == msg.type)
        {
            delete msg.rsp;
            msg.rsp = nullptr;
        }
    }
}

void CTABusTrackerPlugin::getRoutes(JsonObject &jsonRtes)
{
    MutexGuard<MutexRecursive> guard(m_mutex);
    if (m_routes.empty())
    {
        startRoutesRequest(); 
    }
    jsonRtes["routes"] = JsonArray();
    for (int i = 0; i < m_routes.size(); i++)
    {
        jsonRtes["routes"][i]["rt"] = m_routes.at(i).rt;
        jsonRtes["routes"][i]["rtnm"] = m_routes.at(i).rtnm;
        jsonRtes["routes"][i]["rtclr"] = m_routes.at(i).rtclr;
    }
}

void CTABusTrackerPlugin::getConfiguration(JsonObject &jsonCfg) const
{
    MutexGuard<MutexRecursive> guard(m_mutex);

    jsonCfg["apiKey"] = m_apiKey;
    jsonCfg["rte"] = m_rte;
    jsonCfg["dir"] = m_dir;
    jsonCfg["stpid"] = m_stpid;
    jsonCfg["orig"] = m_orig;
    jsonCfg["dest"] = m_dest;
    jsonCfg["two"] = m_two;
}

bool CTABusTrackerPlugin::setConfiguration(JsonObjectConst &jsonCfg)
{
    bool status = false;
    JsonVariantConst jsonApiKey = jsonCfg["apiKey"];
    JsonVariantConst jsonRte = jsonCfg["rte"];
    JsonVariantConst jsonDir = jsonCfg["dir"];
    JsonVariantConst jsonStpid = jsonCfg["stpid"];
    JsonVariantConst jsonOrig = jsonCfg["orig"];
    JsonVariantConst jsonDest = jsonCfg["dest"];
    JsonVariantConst jsonTwo = jsonCfg["two"];

    if (false == jsonApiKey.is<String>())
    {
        LOG_WARNING("No API key");
    }
    else if (false == jsonRte.is<String>())
    {
        LOG_WARNING("No route provided");
    }
    else if (false == jsonDir.is<String>())
    {
        LOG_WARNING("No direction provided");
    }
    else if (false == jsonStpid.is<String>())
    {
        LOG_WARNING("No stop ID");
    }
    else
    {
        MutexGuard<MutexRecursive> guard(m_mutex);

        m_apiKey = jsonApiKey.as<String>();
        m_rte = jsonRte.as<String>();
        m_dir = jsonDir.as<String>();
        m_stpid = jsonStpid.as<String>();

        if (jsonOrig.as<String>() == "true")
        {
            m_orig = true;
        }
        else
        {
            m_orig = false;
        }

        if (jsonDest.as<String>() == "false")
        {
            m_dest = false;
        }
        else
        {
            m_dest = true;
        }

        if (jsonTwo.as<String>() == "true")
        {
            m_two = true;
        }
        else
        {
            m_two = false;
        }

        m_requestTimer.start(UPDATE_PERIOD_SHORT);

        status = true;
    }
    return status;
}

/******************************************************************************
 * External Functions
 *****************************************************************************/

/******************************************************************************
 * Local Functions
 *****************************************************************************/
