/*
GARLAND MODULE
Copyright (C) 2020 by Dmitry Blinov <dblinov76 at gmail dot com>

Inspired by https://github.com/Vasil-Pahomov/ArWs2812 (currently https://github.com/Vasil-Pahomov/Liana)

Tested on 300 led strip.

The most time consuming operation is actually showing leds by Adafruit Neopixel. It take about 1870 mcs.
More long strip can take more time to show.
Currently animation calculation, brightness calculation/transition and showing makes in one loop cycle.
Debug output shows timings. Overal timing should be not more that 3000 ms.

MQTT control:
Topic: DEVICE_NAME/garland/set
Message: {"command":"string", "enable":"string", "brightness":int, "speed":int, "animation":"string",
          "palette":"string"/int, "duration":int}
All parameters are optional.

"command:["immediate", "queue", "sequence", "reset"] - if not set, than "immediate" by default
    Commands priority:
        - "immediate" - executes immediately, braking current animation.
        - "queue" - if queue is not empty, than next queue command executes after current animation end.
                    after execution command removed from queue.
        - "sequence" - executes commands in sequence in cycle.
        - "reset" - clean queue and sequence, restore default settings, enable garland.
        - random if there are no commands in queue or sequence.

"enable":["true", "false"] - enable or disable garland
"brightness":[0-255] - set brightness
"speed":[30-60] - set animation speed
"animation":["PixieDust", "Sparkr", "Run", "Stars", "Spread", "R"andCyc", "Fly", "Comets", "Assemble", "Dolphins", "Salut"]
    - setup animation. If not set or not recognized, than setup previous anmation
"palette":["RGB", "Rainbow", "Stripe", "Party", "Heat", Fire", "Blue", "Sun", "Lime", "Pastel"]
    - can be one of listed above or can be one color palette.
    - one color palette can be set by string, that represents color in the format "0xRRGGBB" (0xFF0000 for red) or
      integer number, corresponding to it. Examples: "palette":"0x0000FF", "palette":255 equal to Blue color.
"duration":5000 - setup command duration in milliseconds. If not set, than infinite duration will setup.

If command contains animation, palette or duration, than it setup next animation, that will be shown for duration (infinite if
duration does not set), otherwise it just set scene parameters.

Infinite commands can be interrupted by immediate command or by reset command.
*/

#include "garland.h"

#if GARLAND_SUPPORT

#include <Adafruit_NeoPixel.h>

#include <memory>
#include <vector>

#include "garland/color.h"
#include "garland/palette.h"
#include "garland/scene.h"
#include "mqtt.h"
#include "ws.h"

const char* NAME_GARLAND_ENABLED        = "garlandEnabled";
const char* NAME_GARLAND_BRIGHTNESS     = "garlandBrightness";
const char* NAME_GARLAND_SPEED          = "garlandSpeed";

const char* NAME_GARLAND_SWITCH         = "garland_switch";
const char* NAME_GARLAND_SET_BRIGHTNESS = "garland_set_brightness";
const char* NAME_GARLAND_SET_SPEED      = "garland_set_speed";
const char* NAME_GARLAND_SET_DEFAULT    = "garland_set_default";

const char* MQTT_TOPIC_GARLAND          = "garland";

const char* MQTT_PAYLOAD_COMMAND        = "command";
const char* MQTT_PAYLOAD_ENABLE         = "enable";
const char* MQTT_PAYLOAD_BRIGHTNESS     = "brightness";
const char* MQTT_PAYLOAD_ANIM_SPEED     = "speed";
const char* MQTT_PAYLOAD_ANIMATION      = "animation";
const char* MQTT_PAYLOAD_PALETTE        = "palette";
const char* MQTT_PAYLOAD_DURATION       = "duration";

const char* MQTT_COMMAND_IMMEDIATE      = "immediate";
const char* MQTT_COMMAND_RESET          = "reset"; // reset queue
const char* MQTT_COMMAND_QUEUE          = "queue"; // enqueue command payload
const char* MQTT_COMMAND_SEQUENCE       = "sequence"; // place command to sequence

#define EFFECT_UPDATE_INTERVAL_MIN      7000  // 5 sec
#define EFFECT_UPDATE_INTERVAL_MAX      12000 // 10 sec

#define NUMLEDS_CAN_CAUSE_WDT_RESET     100

bool          _garland_enabled          = true;
unsigned long _lastTimeUpdate           = 0;
unsigned long _currentDuration          = ULONG_MAX;
unsigned int  _currentCommandInSequence = 0;
String        _immediate_command;
std::queue<String>  _command_queue;
std::vector<String> _command_sequence;

// Palette should
Palette pals[] = {
    // palettes below are taken from http://www.color-hex.com/color-palettes/ (and modified)
    // RGB: Red,Green,Blue sequence
    Palette("RGB", {0xFF0000, 0x00FF00, 0x0000FF}),

    // Rainbow: Rainbow colors
    Palette("Rainbow", {0xFF0000, 0xAB5500, 0xABAB00, 0x00FF00, 0x00AB55, 0x0000FF, 0x5500AB, 0xAB0055}),

    // RainbowStripe: Rainbow colors with alternating stripes of black
    Palette("Stripe", {0xFF0000, 0x000000, 0xAB5500, 0x000000, 0xABAB00, 0x000000, 0x00FF00, 0x000000,
                       0x00AB55, 0x000000, 0x0000FF, 0x000000, 0x5500AB, 0x000000, 0xAB0055, 0x000000}),

    // Party: Blue purple ping red orange yellow (and back). Basically, everything but the greens.
    // This palette is good for lighting at a club or party.
    Palette("Party", {0x5500AB, 0x84007C, 0xB5004B, 0xE5001B, 0xE81700, 0xB84700, 0xAB7700, 0xABAB00,
                      0xAB5500, 0xDD2200, 0xF2000E, 0xC2003E, 0x8F0071, 0x5F00A1, 0x2F00D0, 0x0007F9}),

    // Heat: Approximate "black body radiation" palette, akin to the FastLED 'HeatColor' function.
    // Recommend that you use values 0-240 rather than the usual 0-255, as the last 15 colors will be
    // 'wrapping around' from the hot end to the cold end, which looks wrong.
    Palette("Heat", {0x700070, 0xFF0000, 0xFFFF00, 0xFFFFCC}),

    // Fire:
    Palette("Fire", {0x000000, 0x220000, 0x880000, 0xFF0000, 0xFF6600, 0xFFCC00}),

    // Blue:
    Palette("Blue", {0xffffff, 0x0000ff, 0x00ffff}),

    // Sun: Slice Of The Sun
    Palette("Sun", {0xfff95b, 0xffe048, 0xffc635, 0xffad22, 0xff930f}),

    // Lime: yellow green mix
    Palette("Lime", {0x51f000, 0x6fff00, 0x96ff00, 0xc9ff00, 0xf0ff00}),

    // Pastel: Pastel Fruity Mixture
    Palette("Pastel", {0x75aa68, 0x5960ae, 0xe4be6c, 0xca5959, 0x8366ac})};

constexpr size_t palsSize() { return sizeof(pals)/sizeof(pals[0]); }

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(GARLAND_LEDS, GARLAND_D_PIN, NEO_GRB + NEO_KHZ800);
Scene scene(&pixels);

Anim* anims[] = {new AnimGlow(), new AnimStart(), new AnimPixieDust(), new AnimSparkr(), new AnimRun(), new AnimStars(), new AnimSpread(),
                 new AnimRandCyc(), new AnimFly(), new AnimComets(), new AnimAssemble(), new AnimDolphins(), new AnimSalut(), new AnimFountain(), new AnimWaves()};

constexpr size_t animsSize() { return sizeof(anims)/sizeof(anims[0]); }

#define START_ANIMATION  1
Anim* _currentAnim       = anims[1];
Palette* _currentPalette = &pals[0];
auto one_color_palette = std::unique_ptr<Palette>(new Palette("White", {0xffffff}));
//------------------------------------------------------------------------------
void garlandDisable() {
    pixels.clear();
}

//------------------------------------------------------------------------------
void garlandEnabled(bool enabled) {
    _garland_enabled = enabled;
    setSetting(NAME_GARLAND_ENABLED, _garland_enabled);
    if (!_garland_enabled) {
        schedule_function([]() {
            pixels.clear();
            pixels.show();
        });
    }
#if WEB_SUPPORT
    char buffer[128];
    snprintf_P(buffer, sizeof(buffer), PSTR("{\"garlandEnabled\": %s}"), enabled ? "true" : "false");
    wsSend(buffer);
#endif
}

//------------------------------------------------------------------------------
bool garlandEnabled() {
    return _garland_enabled;
}

//------------------------------------------------------------------------------
// Setup
//------------------------------------------------------------------------------
void _garlandConfigure() {
    _garland_enabled = getSetting(NAME_GARLAND_ENABLED, true);
    DEBUG_MSG_P(PSTR("[GARLAND] _garland_enabled = %d\n"), _garland_enabled);

    byte brightness = getSetting(NAME_GARLAND_BRIGHTNESS, 255);
    scene.setBrightness(brightness);
    DEBUG_MSG_P(PSTR("[GARLAND] brightness = %d\n"), brightness);

    float speed = getSetting(NAME_GARLAND_SPEED, 50);
    scene.setSpeed(speed);
}

//------------------------------------------------------------------------------
void _garlandReload() {
    _garlandConfigure();
}

//------------------------------------------------------------------------------
void setDefault() {
    scene.setDefault();
    byte brightness = scene.getBrightness();
    setSetting(NAME_GARLAND_BRIGHTNESS, brightness);
    byte speed = scene.getSpeed();
    setSetting(NAME_GARLAND_SPEED, speed);
#if WEB_SUPPORT
    char buffer[128];
    snprintf_P(buffer, sizeof(buffer), PSTR("{\"garlandBrightness\": %d, \"garlandSpeed\": %d}"), brightness, speed);
    wsSend(buffer);
#endif
}

#if WEB_SUPPORT
//------------------------------------------------------------------------------
void _garlandWebSocketOnConnected(JsonObject& root) {
    root[NAME_GARLAND_ENABLED] = garlandEnabled();
    root[NAME_GARLAND_BRIGHTNESS] = scene.getBrightness();
    root[NAME_GARLAND_SPEED] = scene.getSpeed();
    root["garlandVisible"] = 1;
}

//------------------------------------------------------------------------------
bool _garlandWebSocketOnKeyCheck(const char* key, JsonVariant& value) {
    if (strncmp(key, NAME_GARLAND_ENABLED, strlen(NAME_GARLAND_ENABLED)) == 0) return true;
    if (strncmp(key, NAME_GARLAND_BRIGHTNESS, strlen(NAME_GARLAND_BRIGHTNESS)) == 0) return true;
    if (strncmp(key, NAME_GARLAND_SPEED, strlen(NAME_GARLAND_SPEED)) == 0) return true;
    return false;
}

//------------------------------------------------------------------------------
void _garlandWebSocketOnAction(uint32_t client_id, const char* action, JsonObject& data) {
    if (strcmp(action, NAME_GARLAND_SWITCH) == 0) {
        if (data.containsKey("status") && data.is<int>("status")) {
            garlandEnabled(1 == data["status"].as<int>());
        }
    }

    if (strcmp(action, NAME_GARLAND_SET_BRIGHTNESS) == 0) {
        if (data.containsKey("brightness")) {
            byte new_brightness = data.get<byte>("brightness");
            DEBUG_MSG_P(PSTR("[GARLAND] new brightness = %d\n"), new_brightness);
            setSetting(NAME_GARLAND_BRIGHTNESS, new_brightness);
            scene.setBrightness(new_brightness);
        }
    }

    if (strcmp(action, NAME_GARLAND_SET_SPEED) == 0) {
        if (data.containsKey("speed")) {
            byte new_speed = data.get<byte>("speed");
            DEBUG_MSG_P(PSTR("[GARLAND] new speed = %d\n"), new_speed);
            setSetting(NAME_GARLAND_SPEED, new_speed);
            scene.setSpeed(new_speed);
        }
    }

    if (strcmp(action, NAME_GARLAND_SET_DEFAULT) == 0) {
        setDefault();
    }
}
#endif

//------------------------------------------------------------------------------
void setupScene(Anim* new_anim, Palette* new_palette, unsigned long new_duration) {
    unsigned long currentAnimRunTime = millis() - _lastTimeUpdate;
    _lastTimeUpdate = millis();

    int numShows = scene.getNumShows();
    int frameRate = currentAnimRunTime > 0 ? numShows * 1000 / currentAnimRunTime : 0;

    static String palette_name = "Start";
    DEBUG_MSG_P(PSTR("[GARLAND] Anim: %-10s Pal: %-8s timings: calc: %4d pixl: %3d show: %4d frate: %d\n"),
                _currentAnim->name(), palette_name.c_str(),
                scene.getAvgCalcTime(), scene.getAvgPixlTime(), scene.getAvgShowTime(), frameRate);

    _currentDuration = new_duration;
    _currentAnim     = new_anim;
    _currentPalette  = new_palette;
    palette_name = _currentPalette->name();
    DEBUG_MSG_P(PSTR("[GARLAND] Anim: %-10s Pal: %-8s Inter: %d\n"),
                _currentAnim->name(), palette_name.c_str(), _currentDuration);

    scene.setAnim(_currentAnim);
    scene.setPalette(_currentPalette);
    scene.setup();
}

//------------------------------------------------------------------------------
bool executeCommand(const String& command) {
    DEBUG_MSG_P(PSTR("[GARLAND] Executing command \"%s\"\n"), command.c_str());
    // Parse JSON input
    DynamicJsonBuffer jsonBuffer;
    JsonObject& root = jsonBuffer.parseObject(command);
    if (!root.success()) {
        DEBUG_MSG_P(PSTR("[GARLAND] Error parsing command\n"));
        return false;
    }

    bool scene_setup_required = false;

    if (root.containsKey(MQTT_PAYLOAD_ENABLE)) {
        auto enable = root[MQTT_PAYLOAD_ENABLE].as<String>();
        garlandEnabled(enable != "false");
    }

    if (root.containsKey(MQTT_PAYLOAD_BRIGHTNESS)) {
        auto brightness = root[MQTT_PAYLOAD_BRIGHTNESS].as<byte>();
        scene.setBrightness(brightness);
    }

    if (root.containsKey(MQTT_PAYLOAD_ANIM_SPEED)) {
        auto speed = root[MQTT_PAYLOAD_ANIM_SPEED].as<byte>();
        scene.setSpeed(speed);
    }

    Anim* newAnim = _currentAnim;
    if (root.containsKey(MQTT_PAYLOAD_ANIMATION)) {
        auto animation = root[MQTT_PAYLOAD_ANIMATION].as<const char*>();
        for (size_t i = 0; i < animsSize(); ++i) {
            auto anim_name = anims[i]->name();
            if (strcmp(animation, anim_name) == 0) {
                newAnim = anims[i];
                scene_setup_required = true;
                break;
            }
        }
    }

    Palette* newPalette = _currentPalette;
    if (root.containsKey(MQTT_PAYLOAD_PALETTE)) {
        if (root.is<int>(MQTT_PAYLOAD_PALETTE)) {
            one_color_palette.reset(new Palette("Color", {root[MQTT_PAYLOAD_PALETTE].as<uint32_t>()}));
            newPalette = one_color_palette.get();
        } else {
            auto palette = root[MQTT_PAYLOAD_PALETTE].as<const char*>();
            bool palette_found = false;
            for (size_t i = 0; i < palsSize(); ++i) {
                auto pal_name = pals[i].name();
                if (strcmp(palette, pal_name) == 0) {
                    newPalette = &pals[i];
                    palette_found = true;
                    scene_setup_required = true;
                    break;
                }
            }
            if (!palette_found) {
                uint32_t color = (uint32_t)strtoul(palette, NULL, 0);
                if (color != 0) {
                    one_color_palette.reset(new Palette("Color", {color}));
                    newPalette = one_color_palette.get();
                }
            }
        }
    }

    unsigned long newAnimDuration = LONG_MAX;
    if (root.containsKey(MQTT_PAYLOAD_DURATION)) {
        newAnimDuration = root[MQTT_PAYLOAD_DURATION].as<unsigned long>();
        scene_setup_required = true;
    }

    if (scene_setup_required) {
        setupScene(newAnim, newPalette, newAnimDuration);
        return true;
    }
    return false;
}

//------------------------------------------------------------------------------
// Loop
//------------------------------------------------------------------------------
void garlandLoop(void) {
    if (!_immediate_command.isEmpty()) {
        executeCommand(_immediate_command);
        _immediate_command.clear();
    }

    if (!garlandEnabled())
        return;

    scene.run();

    unsigned long currentAnimRunTime = millis() - _lastTimeUpdate;
    if (currentAnimRunTime > _currentDuration && scene.finishedAnimCycle()) {
        bool scene_setup_done = false;
        if (!_command_queue.empty()) {
            scene_setup_done = executeCommand(_command_queue.front());
            _command_queue.pop();
        } else if (!_command_sequence.empty()) {
            scene_setup_done = executeCommand(_command_sequence[_currentCommandInSequence]);
            ++_currentCommandInSequence;
            if (_currentCommandInSequence >= _command_sequence.size())
                _currentCommandInSequence = 0;
        }

        if (!scene_setup_done) {
            Anim* newAnim = _currentAnim;
            while (newAnim == _currentAnim) {
                newAnim = anims[secureRandom(START_ANIMATION + 1, animsSize())];
            }

            Palette* newPalette = _currentPalette;
            while (newPalette == _currentPalette) {
                newPalette = &pals[secureRandom(palsSize())];
            }

            unsigned long newAnimDuration = secureRandom(EFFECT_UPDATE_INTERVAL_MIN, EFFECT_UPDATE_INTERVAL_MAX);

            setupScene(newAnim, newPalette, newAnimDuration);
        }
    }
}

//------------------------------------------------------------------------------
void garlandMqttCallback(unsigned int type, const char * topic, const char * payload) {
    if (type == MQTT_CONNECT_EVENT) {
        mqttSubscribe(MQTT_TOPIC_GARLAND);
    }

    if (type == MQTT_MESSAGE_EVENT) {
        // Match topic
        String t = mqttMagnitude((char*)topic);

        if (t.equals(MQTT_TOPIC_GARLAND)) {
            // Parse JSON input
            DynamicJsonBuffer jsonBuffer;
            JsonObject& root = jsonBuffer.parseObject(payload);
            if (!root.success()) {
                DEBUG_MSG_P(PSTR("[GARLAND] Error parsing mqtt data\n"));
                return;
            }

            String command = MQTT_COMMAND_IMMEDIATE;
            if (root.containsKey(MQTT_PAYLOAD_COMMAND)) {
                command = root[MQTT_PAYLOAD_COMMAND].as<String>();
            }

            if (command == MQTT_COMMAND_IMMEDIATE) {
                _immediate_command = payload;
            } else if (command == MQTT_COMMAND_RESET) {
                std::queue<String> empty_queue;
                std::swap(_command_queue, empty_queue);
                std::vector<String> empty_sequence;
                std::swap(_command_sequence, empty_sequence);
                _immediate_command.clear();
                _currentDuration = 0;
                setDefault();
                garlandEnabled(true);
            } else if (command == MQTT_COMMAND_QUEUE) {
                _command_queue.push(payload);
            } else if (command == MQTT_COMMAND_SEQUENCE) {
                _command_sequence.push_back(payload);
            }
        }
    }
}

//------------------------------------------------------------------------------
void garlandSetup() {
    _garlandConfigure();

    mqttRegister(garlandMqttCallback);
// Websockets
#if WEB_SUPPORT
    wsRegister()
        .onConnected(_garlandWebSocketOnConnected)
        .onKeyCheck(_garlandWebSocketOnKeyCheck)
        .onAction(_garlandWebSocketOnAction);
#endif

    espurnaRegisterLoop(garlandLoop);
    espurnaRegisterReload(_garlandReload);

    pixels.begin();
    scene.setAnim(_currentAnim);
    scene.setPalette(_currentPalette);
    scene.setup();

    _currentDuration = secureRandom(EFFECT_UPDATE_INTERVAL_MIN, EFFECT_UPDATE_INTERVAL_MAX);
}

/*#######################################################################
  _____
 / ____|
| (___     ___    ___   _ __     ___
 \___ \   / __|  / _ \ | '_ \   / _ \
 ____) | | (__  |  __/ | | | | |  __/
|_____/   \___|  \___| |_| |_|  \___|
#######################################################################*/

#define GARLAND_SCENE_TRANSITION_MS      1000    // transition time between animations, ms
#define GARLAND_SCENE_SPEED_MAX          70
#define GARLAND_SCENE_SPEED_FACTOR       10
#define GARLAND_SCENE_DEFAULT_SPEED      50
#define GARLAND_SCENE_DEFAULT_BRIGHTNESS 255

Scene::Scene(Adafruit_NeoPixel* pixels)
    : _pixels(pixels),
      _numLeds(pixels->numPixels()),
      _leds1(_numLeds),
      _leds2(_numLeds),
      _ledstmp(_numLeds),
      _seq(_numLeds) {
}

void Scene::setPalette(Palette* palette) {
    _palette = palette;
    if (setUpOnPalChange) {
        setupImpl();
    }
}

void Scene::setBrightness(byte brightness) {
    DEBUG_MSG_P(PSTR("[GARLAND] Scene::setBrightness = %d\n"), brightness);
    this->brightness = brightness;
}

byte Scene::getBrightness() {
    DEBUG_MSG_P(PSTR("[GARLAND] Scene::getBrightness = %d\n"), brightness);
    return brightness;
}

// Speed is reverse to cycleFactor and 10x
void Scene::setSpeed(byte speed) {
    this->speed = speed;
    cycleFactor = (float)(GARLAND_SCENE_SPEED_MAX - speed) / GARLAND_SCENE_SPEED_FACTOR;
    DEBUG_MSG_P(PSTR("[GARLAND] Scene::setSpeed %d cycleFactor = %d\n"), speed, (int)(cycleFactor * 1000));
}

byte Scene::getSpeed() {
    DEBUG_MSG_P(PSTR("[GARLAND] Scene::getSpeed %d cycleFactor = %d\n"), speed, (int)(cycleFactor * 1000));
    return speed;
}

void Scene::setDefault() {
    speed = GARLAND_SCENE_DEFAULT_SPEED;
    cycleFactor = (float)(GARLAND_SCENE_SPEED_MAX - speed) / GARLAND_SCENE_SPEED_FACTOR;
    brightness = GARLAND_SCENE_DEFAULT_BRIGHTNESS;
    DEBUG_MSG_P(PSTR("[GARLAND] Scene::setDefault speed = %d cycleFactor = %d brightness = %d\n"), speed, (int)(cycleFactor * 1000), brightness);
}

void Scene::run() {
    unsigned long iteration_start_time = micros();

    if (state == Calculate || cyclesRemain < 1) {
        // Calculate number of cycles for this animation iteration
        float cycleSum = cycleFactor * (_anim ? _anim->getCycleFactor() : 1.0) + cycleTail;
        cyclesRemain = cycleSum;
        if (cyclesRemain < 1) {
            cyclesRemain = 1;
            cycleSum = 0;
            cycleTail = 0;
        } else {
            cycleTail = cycleSum - cyclesRemain;
        }

        if (_anim) {
            _anim->Run();
        }

        sum_calc_time += (micros() - iteration_start_time);
        iteration_start_time = micros();
        ++calc_num;
        state = Transition;
    }

    if (state == Transition && cyclesRemain < 3) {
        // transition coef, if within 0..1 - transition is active
        // changes from 1 to 0 during transition, so we interpolate from current
        // color to previous
        float transc = (float)((long)transms - (long)millis()) / GARLAND_SCENE_TRANSITION_MS;
        Color* leds_prev = (_leds == &_leds1[0]) ? &_leds2[0] : &_leds1[0];

        if (transc > 0) {
            for (int i = 0; i < _numLeds; i++) {
                // transition is in progress
                Color c = _leds[i].interpolate(leds_prev[i], transc);
                byte r = (int)(bri_lvl[c.r]) * brightness / 256;
                byte g = (int)(bri_lvl[c.g]) * brightness / 256;
                byte b = (int)(bri_lvl[c.b]) * brightness / 256;
                _pixels->setPixelColor(i, _pixels->Color(r, g, b));
            }
        } else {
            for (int i = 0; i < _numLeds; i++) {
                // regular operation
                byte r = (int)(bri_lvl[_leds[i].r]) * brightness / 256;
                byte g = (int)(bri_lvl[_leds[i].g]) * brightness / 256;
                byte b = (int)(bri_lvl[_leds[i].b]) * brightness / 256;
                _pixels->setPixelColor(i, _pixels->Color(r, g, b));
            }
        }

        sum_pixl_time += (micros() - iteration_start_time);
        iteration_start_time = micros();
        ++pixl_num;
        state = Show;
    }

    if (state == Show && cyclesRemain < 2) {
        /* Showing pixels (actually transmitting their RGB data) is most time consuming operation in the
        garland workflow. Using 800 kHz gives 1.25 μs per bit. -> 30 μs (0.03 ms) per RGB LED.
        So for example 3 ms for 100 LEDs. Unfortunately it can't be postponed and resumed later as it
        will lead to reseting the transmition operation. From other hand, long operation can cause
        Soft WDT reset. To avoid wdt reset we need to switch soft wdt off for long strips.
        It is not best practice, but assuming that it is only garland, it can be acceptable.
        Tested up to 300 leds. */
        if (_numLeds > NUMLEDS_CAN_CAUSE_WDT_RESET) {
            ESP.wdtDisable();
        }
        _pixels->show();
        if (_numLeds > NUMLEDS_CAN_CAUSE_WDT_RESET) {
            ESP.wdtEnable(5000);
        }
        sum_show_time += (micros() - iteration_start_time);
        ++show_num;
        state = Calculate;
        ++numShows;
    }
    --cyclesRemain;
}

void Scene::setupImpl() {
    transms = millis() + GARLAND_SCENE_TRANSITION_MS;

    // switch operation buffers (for transition to operate)
    if (_leds == &_leds1[0]) {
        _leds = &_leds2[0];
    } else {
        _leds = &_leds1[0];
    }

    if (_anim) {
        _anim->Setup(_palette, _numLeds, _leds, &_ledstmp[0], &_seq[0]);
    }
}

void Scene::setup() {
    sum_calc_time = 0;
    sum_pixl_time = 0;
    sum_show_time = 0;
    calc_num = 0;
    pixl_num = 0;
    show_num = 0;
    numShows = 0;

    if (!setUpOnPalChange) {
        setupImpl();
    }
}

unsigned long Scene::getAvgCalcTime() { return sum_calc_time / calc_num; }
unsigned long Scene::getAvgPixlTime() { return sum_pixl_time / pixl_num; }
unsigned long Scene::getAvgShowTime() { return sum_show_time / show_num; }

/*#######################################################################
                    _                       _     _
    /\             (_)                     | |   (_)
   /  \     _ __    _   _ __ ___     __ _  | |_   _    ___    _ __
  / /\ \   | '_ \  | | | '_ ` _ \   / _` | | __| | |  / _ \  | '_ \
 / ____ \  | | | | | | | | | | | | | (_| | | |_  | | | (_) | | | | |
/_/    \_\ |_| |_| |_| |_| |_| |_|  \__,_|  \__| |_|  \___/  |_| |_|
#######################################################################*/

Anim::Anim(const char* name) : _name(name) {}

void Anim::Setup(Palette* palette, uint16_t numLeds, Color* leds, Color* ledstmp, byte* seq) {
    this->palette = palette;
    this->numLeds = numLeds;
    this->leds = leds;
    this->ledstmp = ledstmp;
    this->seq = seq;
    SetupImpl();
}

void Anim::initSeq() {
    for (int i = 0; i < numLeds; ++i)
        seq[i] = i;
}

void Anim::shuffleSeq() {
    for (int i = 0; i < numLeds; ++i) {
        byte ind = (unsigned int)(rngb() * numLeds / 256);
        if (ind != i) {
            std::swap(seq[ind], seq[i]);
        }
    }
}

void Anim::glowSetUp() {
    braPhaseSpd = secureRandom(4, 13);
    if (braPhaseSpd > 8) {
        braPhaseSpd = braPhaseSpd - 17;
    }
    braFreq = secureRandom(20, 60);
}

void Anim::glowForEachLed(int i) {
    int8 bra = braPhase + i * braFreq;
    bra = BRA_OFFSET + (abs(bra) >> BRA_AMP_SHIFT);
    leds[i] = leds[i].brightness(bra);
}

void Anim::glowRun() { braPhase += braPhaseSpd; }

bool operator== (const Color &c1, const Color &c2)
{
    return (c1.r == c2.r && c1.g == c2.g && c1.b == c2.b);
}

unsigned int rng() {
    static unsigned int y = 0;
    y += micros();  // seeded with changing number
    y ^= y << 2;
    y ^= y >> 7;
    y ^= y << 7;
    return (y);
}

// Ranom numbers generator in byte range (256) much faster than secureRandom.
// For usage in time-critical places.
byte rngb() { return (byte)rng(); }

#endif  // GARLAND_SUPPORT
