/*

LED MODULE

Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>

*/

#include "led.h"

#if LED_SUPPORT

#include <algorithm>

#include "mqtt.h"
#include "relay.h"
#include "rpc.h"
#include "ws.h"

#include "led_pattern.h"
#include "led_config.h"

void led_t::init() {
    pinMode(_pin, OUTPUT);
    status(false);
}

bool led_t::status() {
    bool result = digitalRead(_pin);
    return _inverse ? !result : result;
}

bool led_t::status(bool new_status) {
    digitalWrite(_pin, _inverse ? !new_status : new_status);
    return new_status;
}

bool led_t::toggle() {
    return status(!status());
}

LedPattern::LedPattern(const LedPattern::Delays& delays) :
    delays(delays),
    queue(),
    clock_last(ESP.getCycleCount()),
    clock_delay(delays.size() ? delays.back().on() : 0)
{}

bool LedPattern::started() {
    return queue.size() > 0;
}

bool LedPattern::ready() {
    return delays.size() > 0;
}

void LedPattern::start() {
    clock_last = ESP.getCycleCount();
    clock_delay = 0;
    queue = { delays.rbegin(), delays.rend() };
}

void LedPattern::stop() {
    queue.clear();
}

// For network-based modes, cycle ON & OFF (time in milliseconds)
// XXX: internals convert these to clock cycles, delay cannot be longer than 25000 / 50000 ms
static const LedDelay _ledDelays[] {
    {100, 4900},  // Connected
    {4900, 100},  // Connected (inverse)
    {100, 900},   // Config / AP
    {900, 100},   // Config / AP (inverse)
    {500, 500}    // Idle
};

enum class LedDelayName : int {
    NetworkConnected,
    NetworkConnectedInverse,
    NetworkConfig,
    NetworkConfigInverse,
    NetworkIdle
};

std::vector<led_t> _leds;
bool _led_update { false };

// -----------------------------------------------------------------------------

namespace settings {
namespace internal {

template <>
LedMode convert(const String& value) {
    if (value.length() == 1) {
        switch (*value.c_str()) {
        case '0':
            return LedMode::Manual;
        case '1':
            return LedMode::WiFi;
#if RELAY_SUPPORT
        case '2':
            return LedMode::Follow;
        case '3':
            return LedMode::FollowInverse;
        case '4':
            return LedMode::FindMe;
        case '5':
            return LedMode::FindMeWiFi;
#endif
        case '6':
            return LedMode::On;
        case '7':
            return LedMode::Off;
#if RELAY_SUPPORT
        case '8':
            return LedMode::Relay;
        case '9':
            return LedMode::RelayWiFi;
#endif
        }
    }

    return LedMode::Manual;
}

} // namespace internal
} // namespace settings

// -----------------------------------------------------------------------------

size_t ledCount() {
    return _leds.size();
}

bool _ledStatus(led_t& led) {
    return led.started() || led.status();
}

bool _ledStatus(led_t& led, bool status) {
    bool result = false;

    // when led has pattern, status depends on whether it's running
    auto& pattern = led.pattern();
    if (pattern.ready()) {
        if (status) {
            if (!pattern.started()) {
                pattern.start();
            }
            result = true;
        } else {
            pattern.stop();
            led.status(false);
            result = false;
        }
    // if not, simply proxy status directly to the led pin
    } else {
        result = led.status(status);
    }

    return result;
}

bool _ledToggle(led_t& led) {
    return _ledStatus(led, !_ledStatus(led));
}

bool ledStatus(size_t id, bool status) {
    if (id < ledCount()) {
        return _ledStatus(_leds[id], status);
    }

    return status;
}

bool ledStatus(size_t id) {
    if (id < ledCount()) {
        return _ledStatus(_leds[id]);
    }

    return false;
}

const LedDelay& _ledDelayFromName(LedDelayName name) {
    switch (name) {
    case LedDelayName::NetworkConnected:
    case LedDelayName::NetworkConnectedInverse:
    case LedDelayName::NetworkConfig:
    case LedDelayName::NetworkConfigInverse:
    case LedDelayName::NetworkIdle:
        return _ledDelays[static_cast<int>(name)];
    }

    return _ledDelays[static_cast<int>(LedDelayName::NetworkIdle)];
}

void _ledPattern(led_t& led) {
    const auto clock_current = ESP.getCycleCount();

    auto& pattern = led.pattern();
    if (clock_current - pattern.clock_last >= pattern.clock_delay) {
        const bool status = led.toggle();

        auto& current = pattern.queue.back();
        switch (current.mode()) {
        case LedDelayMode::Finite:
            if (status && current.repeat()) {
                pattern.queue.pop_back();
                if (!pattern.queue.size()) {
                    led.status(false);
                    return;
                }
            }
            break;
        case LedDelayMode::Infinite:
        case LedDelayMode::None:
            break;
        }

        pattern.clock_delay = status ? current.on() : current.off();
        pattern.clock_last = ESP.getCycleCount();
    }
}

void _ledBlink(led_t& led, const LedDelay& delays) {
    static auto clock_last = ESP.getCycleCount();
    static auto delay_for = delays.on();

    const auto clock_current = ESP.getCycleCount();
    if (clock_current - clock_last >= delay_for) {
        delay_for = led.toggle() ? delays.on() : delays.off();
        clock_last = clock_current;
    }
}

inline void _ledBlink(led_t& led, LedDelayName name) {
    _ledBlink(led, _ledDelayFromName(name));
}

#if WEB_SUPPORT

bool _ledWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
    return (strncmp(key, "led", 3) == 0);
}

void _ledWebSocketOnVisible(JsonObject& root) {
    if (ledCount() > 0) {
        root["ledVisible"] = 1;
    }
}

void _ledWebSocketOnConnected(JsonObject& root) {
    if (!ledCount()) {
        return;
    }

    JsonObject& config = root.createNestedObject("ledConfig");

    {
        static constexpr const char* const schema_keys[] PROGMEM = {
            "ledGpio",
            "ledInv",
            "ledMode"
#if RELAY_SUPPORT
            ,"ledRelay"
#endif
        };

        JsonArray& schema = config.createNestedArray("schema");
        schema.copyFrom(schema_keys, sizeof(schema_keys) / sizeof(*schema_keys));
    }

    JsonArray& leds = config.createNestedArray("leds");

    for (size_t index = 0; index < ledCount(); ++index) {
        JsonArray& led = leds.createNestedArray();
        led.add(getSetting({"ledGpio", index}, led::build::pin(index)));
        led.add(static_cast<int>(getSetting({"ledInv", index}, led::build::inverse(index))));
        led.add(static_cast<int>(getSetting({"ledMode", index}, led::build::mode(index))));
#if RELAY_SUPPORT
        led.add(getSetting({"ledRelay", index}, led::build::relay(index)));
#endif
    }
}

#endif

#if MQTT_SUPPORT
void _ledMQTTCallback(unsigned int type, const char* topic, const char* payload) {
    if (type == MQTT_CONNECT_EVENT) {
        char buffer[strlen(MQTT_TOPIC_LED) + 3];
        snprintf_P(buffer, sizeof(buffer), PSTR("%s/+"), MQTT_TOPIC_LED);
        mqttSubscribe(buffer);
        return;
    }

    // Only want `led/+/<MQTT_SETTER>`
    // We get the led ID from the `+`
    if (type == MQTT_MESSAGE_EVENT) {
        const String magnitude = mqttMagnitude((char *) topic);
        if (!magnitude.startsWith(MQTT_TOPIC_LED)) {
            return;
        }

        size_t ledID;
        if (!tryParseId(magnitude.substring(strlen(MQTT_TOPIC_LED) + 1).c_str(), ledCount, ledID)) {
            return;
        }

        auto& led = _leds[ledID];
        if (led.mode() != LED_MODE_MANUAL) {
            return;
        }

        const auto value = rpcParsePayload(payload);
        switch (value) {
        case PayloadStatus::On:
        case PayloadStatus::Off:
            _ledStatus(led, (value == PayloadStatus::On));
            break;
        case PayloadStatus::Toggle:
            _ledToggle(led);
            break;
        case PayloadStatus::Unknown:
        default:
            _ledLoadPattern(led, payload);
            _ledStatus(led, true);
            break;
        }
    }
}

#endif

#if RELAY_SUPPORT
std::vector<size_t> _led_relays;
#endif

void _ledConfigure() {
#if RELAY_SUPPORT
    _led_relays.resize(relayCount(), RelaysMax);
#endif

    for (size_t id = 0; id < _leds.size(); ++id) {
#if RELAY_SUPPORT
        _led_relays[id] = getSetting({"ledRelay", id}, led::build::relay(id));
#endif
        _leds[id].mode(getSetting({"ledMode", id}, led::build::mode(id)));
        _leds[id].stop();
        _ledLoadPattern(_leds[id], getSetting({"ledPattern", id}).c_str());
    }
    _led_update = true;
}

// -----------------------------------------------------------------------------

void ledUpdate(bool do_update) {
    _led_update = do_update;
}

void ledLoop() {
    for (size_t id = 0; id < _leds.size(); ++id) {
        auto& led = _leds[id];

        switch (led.mode()) {

        case LED_MODE_MANUAL:
            break;

        case LED_MODE_WIFI:
            if (wifiConnected()) {
                _ledBlink(led, LedDelayName::NetworkConnected);
            } else if (wifiConnectable()) {
                _ledBlink(led, LedDelayName::NetworkConfig);
            } else {
                _ledBlink(led, LedDelayName::NetworkIdle);
            }
            break;

#if RELAY_SUPPORT

        case LED_MODE_FINDME_WIFI:
            if (wifiConnected()) {
                if (relayStatus(_led_relays[id])) {
                    _ledBlink(led, LedDelayName::NetworkConnected);
                } else {
                    _ledBlink(led, LedDelayName::NetworkConnectedInverse);
                }
            } else if (wifiConnectable()) {
                if (relayStatus(_led_relays[id])) {
                    _ledBlink(led, LedDelayName::NetworkConfig);
                } else {
                    _ledBlink(led, LedDelayName::NetworkConfigInverse);
                }
            } else {
                _ledBlink(led, LedDelayName::NetworkIdle);
            }
                break;

        case LED_MODE_RELAY_WIFI:
            if (wifiConnected()) {
                if (relayStatus(_led_relays[id])) {
                    _ledBlink(led, LedDelayName::NetworkConnected);
                } else {
                    _ledBlink(led, LedDelayName::NetworkConnectedInverse);
                }
            } else if (wifiConnectable()) {
                if (relayStatus(_led_relays[id])) {
                    _ledBlink(led, LedDelayName::NetworkConfig);
                } else {
                    _ledBlink(led, LedDelayName::NetworkConfigInverse);
                }
            } else {
                _ledBlink(led, LedDelayName::NetworkIdle);
            }
            break;

        case LED_MODE_FOLLOW:
            if (_led_update) {
                _ledStatus(led, relayStatus(_led_relays[id]));
            }
            break;

        case LED_MODE_FOLLOW_INVERSE:
            if (_led_update) {
                led.status(!relayStatus(_led_relays[id]));
                _ledStatus(led, !relayStatus(_led_relays[id]));
            }
            break;

        case LED_MODE_FINDME:
            if (_led_update) {
                bool status = true;
                for (size_t relayId = 0; relayId < relayCount(); ++relayId) {
                    if (relayStatus(relayId)) {
                        status = false;
                        break;
                    }
                }
                _ledStatus(led, status);
            }
            break;

        case LED_MODE_RELAY:
            if (_led_update) {
                bool status = false;
                for (size_t relayId = 0; relayId < relayCount(); ++relayId) {
                    if (relayStatus(relayId)) {
                        status = true;
                        break;
                    }
                }
                _ledStatus(led, status);
            }
            break;

#endif // RELAY_SUPPORT == 1

        case LED_MODE_ON:
            if (_led_update) {
                _ledStatus(led, true);
            }
            break;

        case LED_MODE_OFF:
            if (_led_update) {
                _ledStatus(led, false);
            }
            break;

        }

        if (led.started()) {
            _ledPattern(led);
            continue;
        }

    }

    _led_update = false;

}

void _ledSettingsMigrate(int version) {
    if (!version || (version >= 5)) {
        return;
    }

    delSettingPrefix({
        "ledGPIO",
        "ledLogic"
    });
}

void ledSetup() {
    _ledSettingsMigrate(migrateVersion());
    _leds.reserve(led::build::preconfiguredLeds());

    for (size_t index = 0; index < LedsMax; ++index) {
        const auto pin = getSetting({"ledGpio", index}, led::build::pin(index));
        if (!gpioLock(pin)) {
            break;
        }

        _leds.emplace_back(pin,
            getSetting({"ledInv", index}, led::build::inverse(index)),
            getSetting({"ledMode", index}, led::build::mode(index)));
    }

    auto leds = _leds.size();

    DEBUG_MSG_P(PSTR("[LED] Number of leds: %u\n"), leds);
    if (leds) {
        _ledConfigure();

#if MQTT_SUPPORT
        mqttRegister(_ledMQTTCallback);
#endif

#if WEB_SUPPORT
        wsRegister()
            .onVisible(_ledWebSocketOnVisible)
            .onConnected(_ledWebSocketOnConnected)
            .onKeyCheck(_ledWebSocketOnKeyCheck);
#endif

#if RELAY_SUPPORT
        relaySetStatusNotify([](size_t, bool) {
            ledUpdate(true);
        });
#endif

        espurnaRegisterLoop(ledLoop);
        espurnaRegisterReload(_ledConfigure);
    }
}


#endif // LED_SUPPORT
