/*
 * led program bullshit
 * all programs are called at 100hz
 */

//esp_random()


#include <SPIFFS.h>

#include "ledprog.h"
#include "led.h"


LedProgState progu[6];
LedProgState *prog;


// helpers
static uint8_t _ledprog_check_all_lit(uint16_t *led)
{
    int i;

    for (i = 0; i < 10; i++) {
        if (led[i]) {
            return 1;
        }
    }

    return 0;
}

static uint16_t _ledprog_rnd()
{
    return esp_random() & 0xffff;
}

static uint16_t _ledprog_scale(uint16_t val, uint16_t in_min, uint16_t in_max, uint16_t out_min, uint16_t out_max)
{
    uint16_t scale;

    scale = (uint16_t)map(val, in_min, in_max, out_min, out_max);
    
    return scale;
}

void ledprog_init(LedProgState *s)
{
    s->ledidx = 0;
    s->flags = 0;
    s->pwait = 1;
    s->speedtick = 1;
}

/*
trail mod - fade up to value, then down
stars - low twinkling animation with more downtime in letter A
random - obvious, with fade in and out values and rate of turn on
waves - fade up in middle, and move outward
vertical fade - fade in, from top to bottom
  global fade - vert fade with 0 offset
horiz fade - same, left to right or right to left
r-shot - R lights and spreads light
circular - animation goes in circle, clockwise or counter clockwise, do it X times
alternate - every other lit s-a-e -o-c-
*/

// programs
void ledprog_trail(LedProgState *s)
{
    int i;
    uint16_t *led;

    // option[0] = target pwm value (0-1000)
    // option[1] = pwm fade off value (0-1000)
    // option[2] = delay ticks to next character (lower = faster)
    // option[4] = bottom offset, n4 - n1 = preceed, 0 = sync, 1-5 = trail, 6 = sync next
    //      TODO: option[4] is not yet implemented
    // option[5] = delay ticks after pattern done (lower = faster)

    s->speedtick--;

    if (s->speedtick == 0) {
        s->speedtick = s->speed + 1;

        led = led_getptr();

        switch (s->flags) {
            case 0: {   // light and scroll phase
                s->pwait--;
                // ready for next character
                if (s->pwait == 0) {
                    s->ledidx++;
                    // last character? wait for dimcycle to complete
                    if (s->ledidx == 10) {
                        s->flags = 1;
                        break;
                    }
                    // nope, just next character
                    s->pwait = s->option[2];
                }
                break;
            }
            case 1: {   // cooldown phase
                if (_ledprog_check_all_lit(led)) {
                    // not cooled down yet
                    break;
                }
                // cooled down
                s->flags = 2;
                s->pwait = s->option[5];
            } // no-break
            case 2: {   // delay phase
                s->pwait--;
                if (s->pwait == 0) {
                    s->flags = 0;
                    s->ledidx = 0;
                    s->pwait = s->option[2];
                }
                return;
            }
        }

        // dim LEDs
        for (i = 0; i < 10; i++) {
            if (led[i] >= s->option[1]) {
                led[i] -= s->option[1];
            } else {
                led[i] = 0;
            }
        }

        // set LED
        if (s->ledidx < 10) {
            led[s->ledidx] = s->option[0];
        }
    }
}

void ledprog_stars(LedProgState *s)
{
    uint16_t i;
    uint16_t *led;
    uint16_t rnd;
    uint16_t set;

    // option[0] = target max pwm value (0-1000)
    // option[1] = target min initial pwm value (0-1000)
    // option[2] = density % (doing any action) (0-0xffff)
    // option[3] = set chance % (light vs dim) (0-0xffff)
    // option[4] = max decay rate (0-1000)
    // option[5] = twinkle min target (0-1000)

    s->speedtick--;

    if (s->speedtick == 0) {
        s->speedtick = s->speed + 1;

        // validation
        // set decay rate
        if (!s->option[4]) {
            s->option[4] = s->option[1] >> 1;
        }
        // initial pwm must be greater than twinkle target
        if (s->option[1] < s->option[5]) {
            s->option[1] = s->option[5] + 1;
        }


        led = led_getptr();

        for (i = 0; i < 10; i++) {
            // are we doing anything?
            rnd = _ledprog_rnd();
            if (rnd < s->option[2]) {
                uint8_t lit;
                
                // our "setting" value
                rnd = _ledprog_rnd();
                set = _ledprog_scale(rnd, 0, 0xffff, 0, s->option[0]);
                
                // we are "lit" when we are above min thresh
                lit = led[i] > s->option[1] ? 1 : 0;

                // higher chance to light (brighten) when already lit
                uint16_t chk = s->option[3];
                if (lit) {
                    chk >>= 1;
                }

                // and our check value
                rnd = _ledprog_rnd();

                // main lighting step
                if (rnd < chk) {
                    // ok, we are lighting or brightening this shit
                    if (set < s->option[1]) {
                        set = s->option[1];
                    }

                    led[i] += set;
                    
                    if (led[i] > s->option[0]) {
                        led[i] = s->option[0];
                    }
                } else {
                    // don't decay too fast
                    while (set > s->option[4]) {
                        set >>= 1;
                    }

                    // start dimming
                    if (led[i] < set) {
                        led[i] = 0;
                    } else {
                        led[i] -= set;
                    }
                }

                // finally, twinkle
                if (s->option[5]) {
                    if (led[i] < s->option[5]) {
                        rnd = _ledprog_rnd();
                        while (rnd > s->option[5]) {
                            rnd >>= 1;
                        }

                        led[i] += rnd;
                        led[i] %= s->option[5];
                    }
                }
            }
        }
    }
}

void ledprog_random(LedProgState *s)
{
    uint16_t i;
    uint16_t *led;
    uint16_t rnd;
    uint16_t set;

    // option[0] = target lit pwm value, (0-1000)
    // option[1] = light rate (0-1000)
    // option[2] = decay rate (0-1000)
    // option[3] = chance (lighting) (0-0xffff)

    s->speedtick--;

    if (s->speedtick == 0) {
        s->speedtick = s->speed + 1;

        led = led_getptr();

        for (i = 0; i < 10; i++) {
            rnd = _ledprog_rnd();
            set = _ledprog_scale(rnd, 0, 0xffff, 0, s->option[1]);

            // continue any ramping in progress
            if (s->flags & (1 << i)) {
                led[i] += set;
                if (led[i] >= s->option[0]) {
                    led[i] = s->option[0];
                    s->flags &= ~(1 << i);
                }
                continue;
            }

            // are we doing anything?
            rnd = _ledprog_rnd();
            if (rnd < s->option[3]) {
                // ok, we are lighting this shit
                led[i] = set;
                s->flags |= (1 << i);
            } else {
                // we always decay when not doing light ramping
                if (led[i] > s->option[1]) {
                    led[i] -= s->option[1];
                } else {
                    led[i] = 0;
                }
            }
        }
    }
}

void ledprog_waves(LedProgState *s)
{
    int i;
    uint16_t *led;
    uint16_t val;

    // option[0] = target pwm value (0-1000)
    // option[1] = pwm fade value (0-1000)
    // option[2] = delay ticks to next character (lower = faster)
    // option[5] = delay ticks after pattern done (lower = faster)
    // TODO: fade in rather than insta-turn on

    s->speedtick--;

    if (s->speedtick == 0) {
        s->speedtick = s->speed + 1;

        led = led_getptr();

        switch (s->flags) {
            case 0: {   // light and scroll phase
                s->pwait--;
                // ready for next character
                if (s->pwait == 0) {
                    s->ledidx++;
                    // last character? wait for dimcycle to complete
                    if (s->ledidx == 3) {
                        s->flags = 1;
                        break;
                    }
                    // nope, just next character
                    s->pwait = s->option[2];
                }
                break;
            }
            case 1: {   // cooldown phase
                if (_ledprog_check_all_lit(led)) {
                    // not cooled down yet
                    break;
                }
                // cooled down
                s->flags = 2;
                s->pwait = s->option[5];
            } // no-break
            case 2: {   // delay phase
                s->pwait--;
                if (s->pwait == 0) {
                    s->flags = 0;
                    s->ledidx = 0;
                    s->pwait = s->option[2];
                }
                return;
            }
        }

        // dim LEDs
        for (i = 0; i < 10; i++) {
            if (led[i] >= s->option[1]) {
                led[i] -= s->option[1];
            } else {
                led[i] = 0;
            }
        }

        // set LED
        switch (s->ledidx) {
            case 0: {
                led[2] = led[7] = s->option[0];
                break;
            }
            case 1: {
                val = s->option[0] - (s->option[0] >> 3);
                led[1] = led[8] = val;
                led[3] = led[6] = val;
                break;
            }
            case 2: {
                val = s->option[0] - (s->option[0] >> 2);
                led[0] = led[9] = val;
                led[4] = led[5] = val;
                break;
            }
        }
    }
}

void ledprog_fade_vert(LedProgState *s)
{
    int i;
    uint16_t *led;
    uint16_t off;

    // option[0] = target pwm value (0-1000)
    // option[1] = pwm fade value (0-1000)
    // option[2] = tick delay to other active row, 0 = both at same time (higher = more delay to next)
    // option[3] = set: bottom to top, unset: top to bottom
    // option[5] = delay after pattern done

    s->speedtick--;

    if (s->speedtick == 0) {
        s->speedtick = s->speed + 1;

        led = led_getptr();

        // active led offset
        off = s->option[3] ? 5 : 0;

        switch (s->flags) {
            case 0: {   // fade in phase
                // fade in main row
                if ((s->ledidx & 0x01) == 0) {
                    led[off] += s->option[1];
                    if (led[off] >= s->option[0]) {
                        led[off] = s->option[0];
                        s->ledidx |= 0x01;
                    }
                    
                    for (i = 1; i < 5; i++) {
                        led[i + off] = led[off];
                    }
                }

                if (s->pwait < s->option[2]) {
                    s->pwait++;
                    break;
                }

                // fade in other row
                off = 5 - off;

                if ((s->ledidx & 0x02) == 0) {
                    led[off] += s->option[1];
                    if (led[off] >= s->option[0]) {
                        led[off] = s->option[0];
                        s->ledidx |= 0x02;
                    }
                    
                    for (i = 1; i < 5; i++) {
                        led[i + off] = led[off];
                    }
                }

                // are we done?
                if (s->ledidx == 0x03) {
                    // both rows are on, go to cooldown wait
                    s->flags = 1;
                }

                break;
            }
            case 1: {   // cooldown phase
                if (_ledprog_check_all_lit(led)) {
                    // not cooled down yet
                    break;
                }
                // cooled down
                s->flags = 2;
                s->pwait = s->option[5];
            } // no-break
            case 2: {   // delay phase
                s->pwait--;
                if (s->pwait == 0) {
                    s->flags = 0;
                    s->ledidx = 0;
                    s->pwait = 0;
                }
                return;
            }
        }

        // dim LEDs
        off = s->option[3] ? 5 : 0;
        if (s->ledidx & 0x01) {
            if (led[off] >= s->option[1]) {
                led[off] -= s->option[1];
            } else {
                led[off] = 0;
            }

            for (i = off + 1; i < off + 5; i++) {
                led[i] = led[off];
            }
        }

        off = 5 - off;
        if (s->ledidx & 0x02) {
            if (led[off] >= s->option[1]) {
                led[off] -= s->option[1];
            } else {
                led[off] = 0;
            }

            for (i = off + 1; i < off + 5; i++) {
                led[i] = led[off];
            }
        }
    }
}

void ledprog_fade_horiz(LedProgState *s)
{

}

void ledprog_circular(LedProgState *s)
{
    int i;
    uint16_t *led;
    int idx;

    // option[0] = target pwm value (0-1000)
    // option[1] = pwm fade value (0-1000)
    // option[2] = delay ticks to next character (lower = faster)
    // option[3] = direction (set = ccw, unset = cw)

    s->speedtick--;

    if (s->speedtick == 0) {
        s->speedtick = s->speed + 1;

        led = led_getptr();

        idx = s->ledidx;

        // change direction?
        if (s->option[3]) {
            idx = 9 - idx;
        }

        // fix offset for bottom row
        if (idx >= 5) {
            idx = 5 + (9 - idx);
        }

        s->pwait--;
        // ready for next character
        if (s->pwait == 0) {
            s->ledidx++;

            // last character? loop that shit
            if (s->ledidx == 10) {
                s->ledidx = 0;
            }

            // set led
            led[idx] = s->option[0];

            // next character delay
            s->pwait = s->option[2];
        }

        // dim LEDs
        for (i = 0; i < 10; i++) {
            if (led[i] >= s->option[1]) {
                led[i] -= s->option[1];
            } else {
                led[i] = 0;
            }
        }
    }
}

void ledprog_alternate(LedProgState *s)
{
    int i;
    uint16_t *led;
    int idx;
    uint16_t *val;

    // option[0] = target pwm value (0-1000)
    // option[1] = pwm fade up value (0-1000)
    // option[2] = pwm fade down value (0-1000)
    // option[3] = stay on ticks (lower = faster)
    // option[4] = stay off ticks (lower = faster)

    // pwait = even tick counter

    s->speedtick--;

    if (s->speedtick == 0) {
        s->speedtick = s->speed + 1;

        led = led_getptr();

        idx = 0;
        val = &s->pwait;
        
        if ((s->flags & (1 << idx)) == 0) {
            if (*val < s->option[4]) {
                (*val)++;
            } else {
                // ramp up
                led[idx] += s->option[1];

                if (led[idx] >= s->option[0]) {
                    led[idx] = s->option[0];
                    s->flags |= (1 << idx);
                    s->pwait = 0;
                }

                for (i = idx + 2; i < 10; i += 2) {
                    led[i] = led[idx];
                }
            }
        } else {
            // check flags
            if (*val < s->option[3]) {
                (*val)++;
            } else {
                // ramp down
                if (led[idx] <= s->option[1]) {
                    led[idx] = 0;
                    s->flags &= ~(1 << idx);
                    s->pwait = 0;
                } else {
                    led[idx] -= s->option[2];
                }

                for (i = idx + 2; i < 10; i += 2) {
                    led[i] = led[idx];
                }
            }
        }

        // copy odd bank
        for (i = 0; i < 10; i += 2) {
            led[i + 1] = s->option[0] - led[i];
        }
    }
}

void ledprog_bounce(LedProgState *s)
{
    int i;
    uint16_t *led;
    int idx;

    // option[0] = target pwm value (0-1000)
    // option[1] = pwm fade value (0-1000)
    // option[2] = delay ticks to next character (lower = faster)
    // option[3] = dwell ticks at the end bounds (lower = faster)

    s->speedtick--;

    if (s->speedtick == 0) {
        s->speedtick = s->speed + 1;

        led = led_getptr();

        idx = s->ledidx;

        // flip direction?
        if (s->flags & 1) {
            idx = 4 - idx;
        }

        switch (s->flags) {
            case 0:
            case 1: {   // light and scroll phase
                s->pwait--;
                // ready for next character
                if (s->pwait == 0) {
                    s->ledidx++;

                    // last character? loop that shit
                    if (s->ledidx == 5) {
                        s->ledidx--;
                        s->flags += 2;
                        s->pwait = s->option[3];
                        break;
                    }

                    // set led
                    led[idx]     = s->option[0];
                    led[idx + 5] = s->option[0];

                    // next character delay
                    s->pwait = s->option[2];
                }
            }
            case 2:
            case 3: {   // delay phase
                led[idx]     = s->option[0];
                led[idx + 5] = s->option[0];

                s->pwait--;
                if (s->pwait == 0) {
                    if (s->flags == 2) {
                        s->flags = 1;
                    } else {
                        s->flags = 0;
                    }

                    s->ledidx = 0;
                    s->pwait = 1;
                }

                break;
            }
        }

        // dim LEDs
        for (i = 0; i < 10; i++) {
            if (led[i] >= s->option[1]) {
                led[i] -= s->option[1];
            } else {
                led[i] = 0;
            }
        }
    }
}

void ledprog_beam(LedProgState *s)
{
    // eh I'll do this some other year
}

void ledprog_strobe(LedProgState *s)
{
    int i;
    uint16_t *led;

    int idx;
    int offset;
    uint16_t *rpt, *dly;
    
    // recommended speed 1
    // option[0] = target pwm value (0-1000)
    // option[1] = pattern1 repeats (0-100, default 8)
    // option[2] = pattern2 repeats (0-100, default 4)
    // option[3] = pattern1 delay ticks (0-1000, default 2)
    // option[4] = pattern2 delay ticks (0-1000, default 4)
    
    s->speedtick--;

    if (s->speedtick == 0) {
        s->speedtick = s->speed + 1;

        led = led_getptr();

        // validation
        s->flags %= 16;

        // which pattern speed info?
        if (s->flags < 8) {
            rpt = &s->option[1];
            dly = &s->option[3];
        } else {
            rpt = &s->option[2];
            dly = &s->option[4];
        }

        // get offset
        offset = s->flags / 8;

        s->pwait++;
        if (s->pwait > *dly) {
            s->pwait = 0;

            // increment and check
            s->flags++;

            if (s->flags % 8 == 0) {
                s->flags -= 8;
                // pattern done? flip to other
                s->ledidx++;
                if (s->ledidx >= *rpt) {
                    s->flags += 8;
                    s->ledidx = 0;
                }
            }

            s->flags %= 16;
        }

        // which pattern step
        idx = s->flags % 8;
        // which led offset
        offset = s->ledidx & 1;

        // clear before doing it, then do it
        for (i = 0; i < 10; i++) {
            led[i] = 0;
        }


        switch (idx) {
            case 1:
            case 3: {
                // no lighting
                return;
            }
            case 5: {
                if (s->flags < 8) {
                    return;
                }
            }
        }

        // do lighting
        for (i = offset; i < 10; i += 2) {
            led[i] = s->option[0];
        }
    }
}



const LedProgList ledprog_list[11] = {
    {(const char *)"Trail",         &ledprog_trail},
    {(const char *)"Twinkle",       &ledprog_stars},
    {(const char *)"Random",        &ledprog_random},
    {(const char *)"Waves",         &ledprog_waves},
    {(const char *)"Fader (Vert)",  &ledprog_fade_vert},
    {NULL, NULL}, //{(const char *)"Fader (Horiz)", &ledprog_fade_horiz},
    {(const char *)"Circular",      &ledprog_circular},
    {(const char *)"Alternator",    &ledprog_alternate},
    {(const char *)"Bounce",        &ledprog_bounce},
    {NULL, NULL}, // {(const char *)"Beam",          &ledprog_beam}
    {(const char *)"Strobe",        &ledprog_strobe}
};


void ledprog_save()
{
    if (SPIFFS.exists("/sf-prog.conf")) {
        SPIFFS.remove("/sf-prog.conf");
    }

    File f = SPIFFS.open("/sf-prog.conf", "w");
    if (f) {
        // storage time
        f.write((const uint8_t *)progu, sizeof(progu));
        f.close();    
    }
}

void ledprog_load(uint8_t force_default)
{
    uint8_t *dst;
    
    if (!force_default) {
        if (SPIFFS.exists("/sf-prog.conf")) {
		    File f = SPIFFS.open("/sf-prog.conf", "r");
            if (f) {
                // ok, we can copy some data
                dst = (uint8_t *)progu;
                f.read(dst, sizeof(progu));
                f.close();
                return;
            }
        }
	}

	// ok we couldn't load data. so here's some fun defaults.
	progu[0].prog_idx = 0;
    progu[0].runena = 1;
	progu[0].speed = 2;
	progu[0].option[0] = 600;
	progu[0].option[1] = 20;
	progu[0].option[2] = 15;
	progu[0].option[3] = 0;
	progu[0].option[4] = 0;
	progu[0].option[5] = 50;

    progu[1].prog_idx = 1;
    progu[1].runena = 1;
	progu[1].speed = 6;
	progu[1].option[0] = 250;
	progu[1].option[1] = 150;
	progu[1].option[2] = 30000;
	progu[1].option[3] = 3000;
	progu[1].option[4] = 40;
	progu[1].option[5] = 56;

    progu[2].prog_idx = 2;
    progu[2].runena = 1;
	progu[2].speed = 5;
	progu[2].option[0] = 500;
	progu[2].option[1] = 250;
	progu[2].option[2] = 125;
	progu[2].option[3] = 1337;
	progu[2].option[4] = 0;
	progu[2].option[5] = 0;

    progu[3].prog_idx = 4;
    progu[3].runena = 1;
	progu[3].speed = 2;
	progu[3].option[0] = 500;
	progu[3].option[1] = 5;
	progu[3].option[2] = 50;
	progu[3].option[3] = 0;
	progu[3].option[4] = 0;
	progu[3].option[5] = 20;

    progu[4].prog_idx = 7;
    progu[4].runena = 1;
	progu[4].speed = 3;
	progu[4].option[0] = 420;
	progu[4].option[1] = 25;
	progu[4].option[2] = 25;
	progu[4].option[3] = 14;
	progu[4].option[4] = 14;
	progu[4].option[5] = 0;

    progu[5].prog_idx = 8;
    progu[5].runena = 1;
	progu[5].speed = 2;
	progu[5].option[0] = 400;
	progu[5].option[1] = 13;
	progu[5].option[2] = 12;
	progu[5].option[3] = 42;
	progu[5].option[4] = 0;
	progu[5].option[5] = 0;
}