/*
 * Created on: Jul 27, 2024
 *
 * super basic interface for the buttons
 * renderer for the cursor (always 50% duty cycle)
 * interface for the rgb programs
 */

#include <stdint.h>

#include "adc.h"
#include "btn.h"
#include "config.h"
#include "led.h"
#include "led_rgbprog.h"
#include "rand.h"


#define MODE_RUN                0
#define MODE_PROGRAM            1
#define MODE_PARAMETER          2

#define MIN_GC                  2
#define MAX_GC                  26

#define UI_CONF_SAVE_TIMEOUT    192

#define UI_PROG_RUNTIME_MIN     (15)    // 15 seconds
#define UI_PROG_RUNTIME_MAX     (120)   // 120 seconds


static uint8_t mode = MODE_RUN;
static uint8_t tick = 0;

static uint16_t editor_timeout_timer = 0;

static uint8_t target_gc = 0;

static uint8_t cursor_flash = 0;
static uint8_t cursor_state = 0;
const uint16_t cursor_flash_rates[8] = {      // off-to-on flash rates in 1/256 second increments
        255,                                        //                  (0.5 on-off/second)
        191,                                        //                  (0.75 on-off/second)
        127,                                        //                  (1.0 on-off/second)
        85,                                         // C64 rate         (1.5 on-off/second)
        68,                                         // Windows default  (530ms on-off)
        41,                                         // PC MDA           (3.114 on-off/second)
        34,                                         // PC CGA/EGA       (3.745 on-off/second)
        29,                                         // PC VGA text mode (4.38 on-off/second)
};

static uint8_t  rgb_prog_idx = 0xff;        // currently running rgbled program index
static uint16_t rgb_prog_timer = 9;         // timeout until this program is done and switches
static uint8_t  rgb_prog_is_editing = 0;    // currently editing a program's parameters
static uint8_t  preview_idx = 0;            // currently selected program preview index

static uint8_t  config_save_timer;



void ui_btn_push_cb(uint8_t idx)
{
    uint8_t i, w;
    // are both buttons pushed?
    if ((btn[0]._mask & BTN_PUSH) && (btn[1]._mask & BTN_PUSH)) {
        // are none held?
        if (!(btn[0]._mask & BTN_HOLD) && !(btn[1]._mask & BTN_HOLD)) {
            // can only enter programming mode if the cursor is flashing
            if (userconf.cursor_color != CONF_CURSOR_OFF) {
                // put both buttons into ignore mode, so hold and release events
                // do not fire
                btn[0]._mask |= BTN_IGNORE;
                btn[1]._mask |= BTN_IGNORE;

                // both buttons are pushed at the same time quickly, and not held
                // this will toggle modes
                mode++;

                // parameter mode only works if a program is enabled to adjust
                if (mode == MODE_PARAMETER) {
                    if (!userconf.ledprog_ena_mask) mode = MODE_RUN;
                }
                // parameter mode is the last mode
                if (mode > MODE_PARAMETER) {
                    mode = MODE_RUN;
                }

                // ensure a valid program is selected
                if (mode == MODE_PARAMETER) {
                    w = preview_idx;
                    for (i = 0; i < 8; i++) {
                        w &= 0x7;
                        if (userconf.ledprog_ena_mask & (1 << w)) {
                            preview_idx = w;
                            break;
                        }
                        w++;
                    }
                }

                // reset any LED program
                led_rgb_firstrun();

                // configure the preview index to be the first available
                // depending on the mode
                if (mode == MODE_PROGRAM) preview_idx = 0;
                if (mode == MODE_PARAMETER) {
                    /*
                    preview_idx = 0;
                    for (i = 0; i < 5; i++) {
                        if (userconf.ledprog_ena_mask & (1 << preview_idx)) {
                            preview_idx = i;
                            break;
                        }
                    }
                    */
                }

                return;
            }
        }
    }

    // there are no on-push events.
    switch (mode) {
        case MODE_RUN: {

        }
    }
}

void ui_btn_hold_cb(uint8_t idx)
{
    switch (mode) {
        case MODE_PARAMETER: {
            switch (idx) {
                case 0: {
                    if (rgb_prog_is_editing) {
                        userconf.ledprog_setting[preview_idx][0]++;
                    }
                }
            }
        }
    }
}

void ui_btn_release_cb(uint8_t idx)
{
    uint8_t i;
    uint8_t w;

    switch (mode) {
        /* button logic flow:
         *
         * mode changes happen by pressing both buttons.
         * this is handled in the push event handler.
         *
         * if cursor is off, mode changing cannot occur.
         */

        /*
         * NORMAL MODE: cursor is flashing (or off if disabled).
         *
         * cursor changes are committed to EEPROM about a second after changing is done.
         *
         * any program which is enabled will run. the active program randomly changes after random delays.
         */
        case MODE_RUN: {
            switch (idx) {
                case 0: {   // - top button: changes cursor color (white, green, orange, off)
                    userconf.cursor_color++;
                    userconf.cursor_color &= 0x3;

                    // force cursor to change immediately, force cursor on
                    cursor_flash = 0;
                    cursor_state = 0;

                    config_save_timer = UI_CONF_SAVE_TIMEOUT;

                    break;
                }

                case 1: {   // - bot button: changes cursor flash rate. press and hold
                            //               while cursor is not off to enter program change mode.
                    userconf.cursor_flash++;
                    userconf.cursor_flash &= 0x7;

                    config_save_timer = UI_CONF_SAVE_TIMEOUT;

                    break;
                }
            }

            break;
        }

        /*
        * PROGRAM CHANGE MODE: cursor is on.
        *
        * by default, all are disabled. selected program will flash.
        * cursor will light on the selected program if it is enabled.
        *
        * letters in TECH preview the currently selected program.
        * letters in RETRO will each preview their own program (any flash / twinkle will always have idle light)
        */
        case MODE_PROGRAM: {
            switch (idx) {
                case 0: {   // - top button: tap enables or disables the program. push and hold does nothing.
                    w = userconf.ledprog_ena_mask;
                    if (userconf.ledprog_ena_mask & (1 << preview_idx)) {
                        userconf.ledprog_ena_mask &= ~(1 << preview_idx);
                    } else {
                        userconf.ledprog_ena_mask |=  (1 << preview_idx);
                    }

                    break;
                }
                case 1: {   // - bot button: tap selects the next program index. push and hold does nothing.
                    preview_idx++;
                    if (preview_idx >= 5) preview_idx = 0;

                    break;
                }
            }

            break;
        }

        /*
         * PARAMETER CHANGE MODE: cursor is off (or rather, very dimly on).
         *
         * letters in RETRO only show enabled programs. dark for disabled programs.
         * RETRO letters will flash to indicate currently selected program.
         *
         * letters in TECH preview the current program, at some fixed brightness.
         *
         * - top button: change some parameter
         * -       knob: change some parameter (color/hue, etc)
         */
        case MODE_PARAMETER: {
            switch (idx) {
                case 0: {   // - top button: when editing disabled: selects the next program
                            //               when editing enabled: tapped or held: increments a value, with loop
                    if (rgb_prog_is_editing) {
                        userconf.ledprog_setting[preview_idx][0]++;
                    } else {
                        w = preview_idx;
                        for (i = 0; i < 8; i++) {
                            w++;
                            w &= 0x7;
                            if (userconf.ledprog_ena_mask & (1 << w)) {
                                preview_idx = w;
                                break;
                            }
                        }
                    }
                    break;
                }
                case 1: {   // - bot button: enables / disables editing of selected program
                    rgb_prog_is_editing = !rgb_prog_is_editing;

                    break;
                }
            }

            break;
        }
    }
}

static void ui_cursor_flash()
{
    uint8_t color, flash;
    uint8_t level = 0;

    color = userconf.cursor_color;
    flash = userconf.cursor_flash;

    switch (mode) {
        case MODE_RUN: {
            if (rgb_prog_idx != 4) {
                if (!cursor_flash) {
                    // toggle on/off
                    cursor_state++;
                    cursor_state &= 1;

                    // set new cursor rate, force cursor on
                    cursor_flash = cursor_flash_rates[flash] >> 1;

                    // set all colors off
                    cursor[0] = cursor[1] = cursor[2] = 0;
                }

                // wind down counter
                cursor_flash--;

                // set brightness of led, if we're in an on state
                if (color < CONF_CURSOR_OFF) {
                    if (cursor_state) {
                        // first frame of on = not quite full brightness
                        level = (cursor_flash == cursor_flash_rates[flash] - 1) ? 160 : 255;

                        // at final frames, dim
                        if (cursor_flash <= 4) {
                            level >>= (4 - cursor_flash);
                        }
                    }

                    // set the level on the cursor
                    if (cursor[color] != level) {
                        cursor[color] = led_gamma(level);
                        led_is_updated();
                    }
                } else {
                    // clear any cursors that may have been left on
                    cursor[0] = 0;
                    cursor[1] = 0;
                    cursor[2] = 0;
                }
            }

            break;
        }
        case MODE_PROGRAM: {
            // cursor is on if this program is flagged as on
            cursor[0] = (userconf.ledprog_ena_mask & (1 << preview_idx)) ? 127 : 0;
            cursor[1] = 0;
            cursor[2] = 0;

            break;
        }
        case MODE_PARAMETER: {
            // cursor is on when program is being edited
            cursor[0] = rgb_prog_is_editing ? 127 : 0;
            cursor[1] = 0;
            cursor[2] = 0;

            break;
        }
    }
}

void ui_init()
{
    btn[0].hold = 420 >> 1;
    btn[0].repeat = (1000 / 20) >> 1;
    btn[0].cb_push = ui_btn_push_cb;
    btn[0].cb_hold = ui_btn_hold_cb;
    btn[0].cb_release = ui_btn_release_cb;

    btn[1].hold = 420 >> 1;
    btn[1].repeat = 0;
    btn[1].cb_push = ui_btn_push_cb;
    btn[1].cb_hold = ui_btn_hold_cb;
    btn[1].cb_release = ui_btn_release_cb;
}

static void rgb_prog_random_timer_generate() {
    uint32_t t;

    t = prng_get16();
    t *= (UI_PROG_RUNTIME_MAX - UI_PROG_RUNTIME_MIN) * 32;
    t >>= 16;
    t += UI_PROG_RUNTIME_MIN * 32;

    rgb_prog_timer = t;
}

static uint8_t rgb_prog_random_select()
{
    uint8_t i;
    uint8_t w;

    uint8_t new;

    w = prng_get8();
    w &= 0x7;
    for (i = 0; i < 8; i++) {
        new = (w + i) & 0x7;
        if (userconf.ledprog_ena_mask & (1 << new)) {
            // bias selecting a new program
            if (rgb_prog_idx != new) {
                rgb_prog_idx = new;
                led_rgb_firstrun();
                break;
            }
        }
    }

    if (i == 8) return 0;
    return 1;
}

void ui_render()
{
    uint8_t i;

    uint16_t w;

    uint8_t flash;

    uint8_t out[16];


    // deal with eeprom
    if (config_save_timer) {
        config_save_timer--;
        if (!config_save_timer) {
            userconf_save();
        }
    }

    // flash rate at 2 times/second
    flash = tick >> 5;
    flash &= 0x3;

    if (editor_timeout_timer) {
        editor_timeout_timer--;
        if (!editor_timeout_timer) {
            mode = MODE_RUN;

            // save settings to eeprom
            userconf_save();
        }
    }

    tick++;

    switch (mode) {
        case MODE_RUN: {    // render an existing program
            // ensure rear LED is not on
            GPIOD->BCR = GPIO_Pin_0;

            // set brightness from knob 32 times/second
            // (the knob value updates less frequently, but we need to fade nicely)
            if (tick & 7 == 7) {
                w = adc_get_pot();
                if (w < 2) w = 0;
                else {
                    w *= MAX_GC - MIN_GC;
                    w >>= 8;
                    w += MIN_GC;
                }

                if (!w) {
                    if (target_gc) target_gc--;             // full off = full off.
                } else if (w < target_gc) {
                    if (target_gc > MIN_GC) target_gc--;    // dim to min possible
                } else if (w > target_gc) {
                    if (target_gc < MAX_GC) target_gc++;    // dim to max possible
                }

                is31fl3729_set_global_current(FL3729_ADDR, target_gc);
            }

            if (userconf.ledprog_ena_mask) {
                // fade and change programs depending on the timer

                // actually run the program
                if (rgb_prog_timer) {
                    if (led_rgbprog[rgb_prog_idx] && (rgb_prog_idx < LED_RGBPROG_COUNT)) {
                        led_rgbprog[rgb_prog_idx](LED_RGBPROG_NORMAL, tick);
                    }
                }

                if ((tick & 0x3) == 0) {
                    rgb_prog_timer--;

                    if (rgb_prog_timer <= 17) {
                        led_is_updated();

                        if (rgb_prog_timer == 17) {
                            // if no new program was selected (likely only one program selected?)
                            // just reset the timer
                            if (!rgb_prog_random_select()) {
                                rgb_prog_random_timer_generate();
                            }
                        }

                        else if (rgb_prog_timer >= 9) {
                            // fade out current program
                            w = 7 - (rgb_prog_timer - 9);
                            for (i = 0; i < 16; i++) {
                                out[i] = 0xff >> w;
                            }
                            is31fl3729_set_scaling_current_multi(FL3729_ADDR, out, 15);
                        }

                        else if (rgb_prog_timer > 7) {
                            // clear out for now, since new program hasn't actually been run
                            for (i = 0; i < 9; i++) {
                                rgb[i][0] = 0;
                                rgb[i][1] = 0;
                                rgb[i][2] = 0;
                            }
                            // try to select a new program if we haven't set one before
                            if (rgb_prog_idx == 0xff) {
                                rgb_prog_random_select();
                            }
                        }

                        else if (rgb_prog_timer > 0) {
                            // fade in new program
                            w = (rgb_prog_timer & 0x7);
                            for (i = 0; i < 16; i++) {
                                out[i] = 0xff >> w;
                            }
                            is31fl3729_set_scaling_current_multi(FL3729_ADDR, out, 15);
                        }

                        else { // 0
                            // randomize next program timing
                            for (i = 0; i < 16; i++) {
                                out[i] = 0xff;
                            }
                            is31fl3729_set_scaling_current_multi(FL3729_ADDR, out, 15);

                            rgb_prog_random_timer_generate();
                        }
                    }
                }
            }

            break;
        }

        case MODE_PROGRAM: {
            // always postpone config saving
            config_save_timer = UI_CONF_SAVE_TIMEOUT;

            // rapidly flash lsens
            if ((tick >> 3) & 1) {
                GPIOD->OUTDR ^= GPIO_Pin_0;
            }

            // always increase brightness of LEDs while editing
            is31fl3729_set_global_current(FL3729_ADDR, FL3729_GCC_MAX);

            // always force rendering
            led_is_updated();

            for (i = 0; i < 5; i++) {
                // render
                if (led_rgbprog[i]) {
                    led_rgbprog[i](LED_RGBPROG_PREVIEW | preview_idx, tick);
                }

                if (preview_idx == i) {
                    // flash the selected output
                    if (flash & 1) {
                        rgb[i][0] = 16;
                         rgb[i][1] = 16;
                         rgb[i][2] = 16;
                        //rgb[i][0] >>= 3;
                        //rgb[i][1] >>= 3;
                        //rgb[i][2] >>= 3;
                    }
                } else {
                    // dim inactive outputs
                    //if (!(userconf.ledprog_ena_mask & (1 << i))) {
                    //    rgb[i][0] >>= 1;
                    //    rgb[i][1] >>= 1;
                    //    rgb[i][2] >>= 1;
                    //}
                }
            }

            break;
        }

        case MODE_PARAMETER: {
            // always postpone config saving
            config_save_timer = UI_CONF_SAVE_TIMEOUT;

            // slowly flash lsnes
            if ((tick >> 5) & 1) {
                GPIOD->OUTDR ^= GPIO_Pin_0;
            }

            // always increase brightness of LEDs while editing
            is31fl3729_set_global_current(FL3729_ADDR, FL3729_GCC_MAX);

            // always force rendering
            led_is_updated();

            // deal with knob if editing is active
            if (rgb_prog_is_editing) {
                userconf.ledprog_setting[preview_idx][1] = adc_get_pot();
            }

            for (i = 0; i < 5; i++) {
                // render
                if (userconf.ledprog_ena_mask & (1 << i)) {
                    if (led_rgbprog[i]) {
                        led_rgbprog[i](LED_RGBPROG_PREVIEW | preview_idx, tick);
                    }

                    if (preview_idx == i) {
                        // flash the selected output
                        if (flash & 1) {
                            rgb[i][0] = 32;
                            rgb[i][1] = 32;
                            rgb[i][2] = 32;
                            //rgb[i][0] >>= 5;
                            //rgb[i][1] >>= 5;
                            //rgb[i][2] >>= 5;
                        }
                    } else {
                        // dim the other outputs
                        // rgb[i][0] >>= 2;
                        // rgb[i][1] >>= 2;
                        // rgb[i][2] >>= 2;
                    }
                } else {
                    // clear the output if it isn't enabled
                    rgb[i][0] = 0;
                    rgb[i][1] = 0;
                    rgb[i][2] = 0;
                }
            }

            break;
        }
    }

    ui_cursor_flash();
}
