#pragma once
#include "wled.h"

/*
 * Usermod for analog clock
 */
extern Timezone* tz;

class AnalogClockUsermod : public Usermod {
private:
    static constexpr uint32_t refreshRate = 50; // per second
    static constexpr uint32_t refreshDelay = 1000 / refreshRate;

    struct Segment {
        // config
        int16_t firstLed  = 0;
        int16_t lastLed   = 59;
        int16_t centerLed = 0;

        // runtime
        int16_t size;

        Segment() {
            update();
        }

        void validateAndUpdate() {
            if (firstLed < 0 || firstLed >= strip.getLengthTotal() ||
                    lastLed < firstLed || lastLed >= strip.getLengthTotal()) {
                *this = {};
                return;
            }
            if (centerLed < firstLed || centerLed > lastLed) {
                centerLed = firstLed;
            }
            update();
        }

        void update() {
            size = lastLed - firstLed + 1;
        }
    };

    // configuration (available in API and stored in flash)
    bool     enabled          = false;
    Segment  mainSegment;
    bool     hourMarksEnabled = true;
    uint32_t hourMarkColor    = 0xFF0000;
    uint32_t hourColor        = 0x0000FF;
    uint32_t minuteColor      = 0x00FF00;
    bool     secondsEnabled   = true;
    Segment  secondsSegment;
    uint32_t secondColor      = 0xFF0000;
    bool     blendColors      = true;
    uint16_t secondsEffect    = 0;

    // runtime
    bool     initDone         = false;
    uint32_t lastOverlayDraw  = 0;

    void validateAndUpdate() {
        mainSegment.validateAndUpdate();
        secondsSegment.validateAndUpdate();
        if (secondsEffect < 0 || secondsEffect > 1) {
            secondsEffect = 0;
        }
    }

    int16_t adjustToSegment(double progress, Segment const& segment) {
        int16_t led = segment.centerLed + progress * segment.size;
        return led > segment.lastLed
                ? segment.firstLed + led - segment.lastLed - 1
                : led;
    }

    void setPixelColor(uint16_t n, uint32_t c) {
        if (!blendColors) {
            strip.setPixelColor(n, c);
        } else {
            uint32_t oldC = strip.getPixelColor(n);
            strip.setPixelColor(n, qadd32(oldC, c));
        }
    }

    String colorToHexString(uint32_t c) {
        char buffer[9];
        sprintf(buffer, "%06X", c);
        return buffer;
    }

    bool hexStringToColor(String const& s, uint32_t& c, uint32_t def) {
        char *ep;
        unsigned long long r = strtoull(s.c_str(), &ep, 16);
        if (*ep == 0) {
            c = r;
            return true;
        } else {
            c = def;
            return false;
        }
    }

    void secondsEffectSineFade(int16_t secondLed, Toki::Time const& time) {
        uint32_t ms = time.ms % 1000;
        uint8_t b0 = (cos8(ms * 64 / 1000) - 128) * 2;
        setPixelColor(secondLed, gamma32(scale32(secondColor, b0)));
        uint8_t b1 = (sin8(ms * 64 / 1000) - 128) * 2;
        setPixelColor(inc(secondLed, 1, secondsSegment), gamma32(scale32(secondColor, b1)));
    }

    static inline uint32_t qadd32(uint32_t c1, uint32_t c2) {
        return RGBW32(
            qadd8(R(c1), R(c2)),
            qadd8(G(c1), G(c2)),
            qadd8(B(c1), B(c2)),
            qadd8(W(c1), W(c2))
        );
    }

    static inline uint32_t scale32(uint32_t c, fract8 scale) {
        return RGBW32(
            scale8(R(c), scale),
            scale8(G(c), scale),
            scale8(B(c), scale),
            scale8(W(c), scale)
        );
    }

    static inline int16_t dec(int16_t n, int16_t i, Segment const& seg) {
        return n - seg.firstLed >= i
                ? n - i
                : seg.lastLed - seg.firstLed - i + n + 1;
    }

    static inline int16_t inc(int16_t n, int16_t i, Segment const& seg) {
        int16_t r = n + i;
        if (r > seg.lastLed) {
            return seg.firstLed + n - seg.lastLed;
        }
        return r;
    }

public:
    AnalogClockUsermod() {
    }

    void setup() override {
        initDone = true;
        validateAndUpdate();
    }

    void loop() override {
        if (millis() - lastOverlayDraw > refreshDelay) {
            strip.trigger();
        }
    }

    void handleOverlayDraw() override {
        if (!enabled) {
            return;
        }

        lastOverlayDraw = millis();

        auto time = toki.getTime();
        double secondP = second(localTime) / 60.0;
        double minuteP = minute(localTime) / 60.0;
        double hourP = (hour(localTime) % 12) / 12.0 + minuteP / 12.0;

        if (hourMarksEnabled)         {
            for (int Led = 0; Led <= 55; Led = Led + 5)
            {
                int16_t hourmarkled = adjustToSegment(Led / 60.0, mainSegment);
                setPixelColor(hourmarkled, hourMarkColor);
            }
        }

        if (secondsEnabled) {
            int16_t secondLed = adjustToSegment(secondP, secondsSegment);

            switch (secondsEffect) {
                case 0: // no effect
                    setPixelColor(secondLed, secondColor);
                    break;

                case 1: // fading seconds
                    secondsEffectSineFade(secondLed, time);
                    break;
            }

            // TODO: move to secondsTrailEffect
            // for (uint16_t i = 1; i < secondsTrail + 1; ++i) {
            //     uint16_t trailLed = dec(secondLed, i, secondsSegment);
            //     uint8_t trailBright = 255 / (secondsTrail + 1) * (secondsTrail - i + 1);
            //     setPixelColor(trailLed, gamma32(scale32(secondColor, trailBright)));
            // }
        }

        setPixelColor(adjustToSegment(minuteP, mainSegment), minuteColor);
        setPixelColor(adjustToSegment(hourP, mainSegment), hourColor);
    }

    void addToConfig(JsonObject& root) override {
        validateAndUpdate();

        JsonObject top = root.createNestedObject(F("Analog Clock"));
        top[F("Overlay Enabled")]               = enabled;
        top[F("First LED (Main Ring)")]         = mainSegment.firstLed;
        top[F("Last LED (Main Ring)")]          = mainSegment.lastLed;
        top[F("Center/12h LED (Main Ring)")]    = mainSegment.centerLed;
        top[F("Hour Marks Enabled")]            = hourMarksEnabled;
        top[F("Hour Mark Color (RRGGBB)")]      = colorToHexString(hourMarkColor);
        top[F("Hour Color (RRGGBB)")]           = colorToHexString(hourColor);
        top[F("Minute Color (RRGGBB)")]         = colorToHexString(minuteColor);
        top[F("Show Seconds")]                  = secondsEnabled;
        top[F("First LED (Seconds Ring)")]      = secondsSegment.firstLed;
        top[F("Last LED (Seconds Ring)")]       = secondsSegment.lastLed;
        top[F("Center/12h LED (Seconds Ring)")] = secondsSegment.centerLed;
        top[F("Second Color (RRGGBB)")]         = colorToHexString(secondColor);
        top[F("Seconds Effect (0-1)")]          = secondsEffect;
        top[F("Blend Colors")]                  = blendColors;
    }

    bool readFromConfig(JsonObject& root) override {
        JsonObject top = root[F("Analog Clock")];

        bool configComplete = !top.isNull();

        String color;
        configComplete &= getJsonValue(top[F("Overlay Enabled")], enabled, false);
        configComplete &= getJsonValue(top[F("First LED (Main Ring)")], mainSegment.firstLed, 0);
        configComplete &= getJsonValue(top[F("Last LED (Main Ring)")], mainSegment.lastLed, 59);
        configComplete &= getJsonValue(top[F("Center/12h LED (Main Ring)")], mainSegment.centerLed, 0);
        configComplete &= getJsonValue(top[F("Hour Marks Enabled")], hourMarksEnabled, false);
        configComplete &= getJsonValue(top[F("Hour Mark Color (RRGGBB)")], color, F("161616")) && hexStringToColor(color, hourMarkColor, 0x161616);
        configComplete &= getJsonValue(top[F("Hour Color (RRGGBB)")], color, F("0000FF")) && hexStringToColor(color, hourColor, 0x0000FF);
        configComplete &= getJsonValue(top[F("Minute Color (RRGGBB)")], color, F("00FF00")) && hexStringToColor(color, minuteColor, 0x00FF00);
        configComplete &= getJsonValue(top[F("Show Seconds")], secondsEnabled, true);
        configComplete &= getJsonValue(top[F("First LED (Seconds Ring)")], secondsSegment.firstLed, 0);
        configComplete &= getJsonValue(top[F("Last LED (Seconds Ring)")], secondsSegment.lastLed, 59);
        configComplete &= getJsonValue(top[F("Center/12h LED (Seconds Ring)")], secondsSegment.centerLed, 0);
        configComplete &= getJsonValue(top[F("Second Color (RRGGBB)")], color, F("FF0000")) && hexStringToColor(color, secondColor, 0xFF0000);
        configComplete &= getJsonValue(top[F("Seconds Effect (0-1)")], secondsEffect, 0);
        configComplete &= getJsonValue(top[F("Blend Colors")], blendColors, true);

        if (initDone) {
            validateAndUpdate();
        }

        return configComplete;
    }

    uint16_t getId() override {
        return USERMOD_ID_ANALOG_CLOCK;
    }
};