/*

ALEXA MODULE

Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>

*/

#include "alexa.h"

#if ALEXA_SUPPORT

#include <queue>

#include "api.h"
#include "light.h"
#include "mqtt.h"
#include "relay.h"
#include "rpc.h"
#include "web.h"
#include "ws.h"

#include <fauxmoESP.h>
#include <ArduinoJson.h>

namespace {

struct AlexaEvent {
    AlexaEvent() = delete;
    AlexaEvent(unsigned char id, bool state, unsigned char value) :
        _id(id),
        _state(state),
        _value(value)
    {}

    unsigned char id() const {
        return _id;
    }

    unsigned char value() const {
        return _value;
    }

    bool state() const {
        return _state;
    }

private:
    unsigned char _id;
    bool _state;
    unsigned char _value;
};

std::queue<AlexaEvent> _alexa_events;

fauxmoESP _alexa;

} // namespace

// -----------------------------------------------------------------------------
// ALEXA
// -----------------------------------------------------------------------------

bool _alexaWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
    return (strncmp(key, "alexa", 5) == 0);
}

void _alexaWebSocketOnConnected(JsonObject& root) {
    root["alexaEnabled"] = alexaEnabled();
    root["alexaName"] = getSetting("alexaName");
}

void _alexaConfigure() {
    _alexa.enable(wifiConnected() && alexaEnabled());
}

#if WEB_SUPPORT
    bool _alexaBodyCallback(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
        return _alexa.process(request->client(), request->method() == HTTP_GET, request->url(), String((char *)data));
    }

    bool _alexaRequestCallback(AsyncWebServerRequest *request) {
        String body = (request->hasParam("body", true)) ? request->getParam("body", true)->value() : String();
        return _alexa.process(request->client(), request->method() == HTTP_GET, request->url(), body);
    }
#endif

#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE

void _alexaUpdateLights() {
    _alexa.setState(static_cast<unsigned char>(0u), lightState(), lightState() ? 255u : 0u);

    auto channels = lightChannels();
    for (decltype(channels) channel = 0; channel < channels; ++channel) {
        auto value = lightChannel(channel);
        _alexa.setState(channel + 1, value > 0, value);
    }
}

#endif

#if RELAY_SUPPORT

void _alexaUpdateRelay(size_t id, bool status) {
    _alexa.setState(id, status, status ? 255 : 0);
}

#endif
// -----------------------------------------------------------------------------

bool alexaEnabled() {
    return getSetting("alexaEnabled", 1 == ALEXA_ENABLED);
}

void alexaLoop() {

    _alexa.handle();

    while (!_alexa_events.empty()) {
        auto& event = _alexa_events.front();
        DEBUG_MSG_P(PSTR("[ALEXA] Device #%hhu state=#%s value=%hhu\n"),
            event.id(), event.state() ? 't' : 'f', event.value());

#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
        if (0 == event.id()) {
            lightState(event.state());
        } else {
            lightState(event.id() - 1, event.state());
            lightChannel(event.id() - 1, event.value());
            lightUpdate();
        }
#else
        relayStatus(event.id(), event.state());
#endif

        _alexa_events.pop();
    }

}

constexpr bool _alexaCreateServer() {
    return !WEB_SUPPORT;
}

constexpr const char* _alexaHostname() {
    return ALEXA_HOSTNAME;
}


void _alexaSettingsMigrate(int version) {
    if (version && (version < 3)) {
        moveSetting("fauxmoEnabled", "alexaEnabled");
    }
}

void alexaSetup() {
    // Backwards compatibility
    _alexaSettingsMigrate(migrateVersion());

    // Basic fauxmoESP configuration
    _alexa.createServer(_alexaCreateServer());
    _alexa.setPort(80);

    // Use custom alexa hostname if defined, device hostname otherwise
    String hostname = getSetting("alexaName", _alexaHostname());
    if (!hostname.length()) {
        hostname = getSetting("hostname", getIdentifier());
    }

    auto deviceName = [&](size_t index) {
        auto name = hostname;
        name += ' ';
        name += index;
        return name;
    };

#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
    // 1st is the global state, the rest are mapped to channel values
    _alexa.addDevice(hostname.c_str());
    for (size_t channel = 1; channel <= lightChannels(); ++channel) {
        _alexa.addDevice(deviceName(channel).c_str());
    }

    // Relays are mapped 1-to-1
#elif RELAY_SUPPORT
    auto relays = relayCount();
    if (relays > 1) {
        for (decltype(relays) id = 1; id <= relays; ++id) {
            _alexa.addDevice(deviceName(id).c_str());
        }
    } else {
        _alexa.addDevice(hostname.c_str());
    }

#endif

    // Load & cache settings
    _alexaConfigure();

    // Websockets
    #if WEB_SUPPORT
        webBodyRegister(_alexaBodyCallback);
        webRequestRegister(_alexaRequestCallback);
        wsRegister()
            .onVisible([](JsonObject& root) { root["alexaVisible"] = 1; })
            .onConnected(_alexaWebSocketOnConnected)
            .onKeyCheck(_alexaWebSocketOnKeyCheck);
    #endif

    // Register wifi callback
    wifiRegister([](wifi::Event event) {
        if ((event == wifi::Event::StationConnected)
            || (event == wifi::Event::StationDisconnected)) {
            _alexaConfigure();
        }
    });

    // Callback
    _alexa.onSetState([&](unsigned char device_id, const char*, bool state, unsigned char value) {
        _alexa_events.emplace(device_id, state, value);
    });

    // Register main callbacks
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
    lightSetReportListener(_alexaUpdateLights);
#else
    relaySetStatusChange(_alexaUpdateRelay);
#endif

    espurnaRegisterReload(_alexaConfigure);
    espurnaRegisterLoop(alexaLoop);

}

#endif
