/*
 * true's GAT Nametag
 * Rushed SUPERCON 8 EDITION
 * main MCU firmware
 * 2024 true
 *
 * Hastily designed and rushed to completion for Supercon 8 on Nov 1, 2024.
 *
 * The original GAT Nametag used an 8-bit 8051 core MCU.
 * Original code was hastily ported to the WP DC29 minibadge.
 * This code is again hastily ported, and is based on the DC29 minibadge codebase.
 * this is why things are so convoluted.
 *
 *
 * the main MCU is responsible for the following:
 * - rendering OLED UI, LED programs
 * - storing fonts in flash
 * - communicating with I2C devices. includes oled, accelerometer, eeprom, sub MCU, RGBLED
 * - implementing USB interface (once I get time)
 * - implementing BLE (maybe)
 *
 * todo:
 * - decouple oled render routine from interrupt off time. not an issue for now...
 *
 */

#include <hw/ch32sub.h>
#include "CH59x_common.h"

#include "comm/i2c.h"

#include "hw/gat/gat_gpio.h"

#include "led/rgbled.h"

#include "misc/accel.h"
#include "misc/tinymt.h"

#include "ui/menu.h"
#include "ui/oled.h"

#include "global.h"
#include "port_intr.h"
#include "user_config.h"



// global settings
#define OLED_UPDATE_RATE        13      // framerate of OLED; { (256*(8/8)) / OLED_UPDATE_RATE }; 13 = 19FPS, 15 = 17FPS

// flags
#define FLAG_OLED_UPDATE        (1 << 0)
#define FLAG_RGBLED_RUN         (1 << 1)
#define FLAG_ACCEL_POLL         (1 << 2)
#define FLAG_CH32V_RESEND_CONF  (1 << 3)
#define FLAG_RGBLED_SEND        (1 << 5)

#define PROG_TICK_RATE          ((32768-8192-4096) / 256)   // not sure why this value can't be 32768/256
                                                            // this was checked with a stopwatch and is close enough
                                                            // this value IS FRAMERATE DEPENDENT for some reason... figure it out later
#define IDLE_SLEEP_START        (1 << 0)
#define IDLE_MENU_START         (1 << 1)
#define IDLE_DIM_START          (1 << 2)
#define IDLE_SLEEP_READY        (1 << 4)
#define IDLE_MENU_READY         (1 << 5)
#define IDLE_DIM_READY          (1 << 6)



const uint8_t vers[] = "241026b";

uint32_t cpu_use = 0;
uint32_t cpu_max = 0;
uint16_t cpu_pct = 0;
uint16_t cpu_pct_max = 0;

uint32_t uptime = 0;
uint16_t uptime_hour;
uint8_t  uptime_min;
uint8_t  uptime_sec;

uint32_t idle_time = 0;
uint8_t  idle_flags;


static volatile uint8_t flags_lo = 0;
//static uint8_t flags_hi = FLAG_RESET_SYSTICK;


static uint8_t st_tick = 0;             // program tick counter
static uint8_t oled_tick = 0;           // oled framerate counter



void ch59x_xtal_conf()
{
    HSECFG_Current(HSE_RCur_125);
    HSECFG_Capacitance(HSECap_12p);
}

/*
 * the CPU will change clockspeed depending on what it is doing.
 * all hardware timers are tied to system clock and cannot be derived from
 * the crystal directly. so we can't rely on timers or systick for timing.
 *
 * this leaves the RTC as the only periodic interrupt source which uses LSI clk.
 * RTC has the capability to trigger an interrupt when count bits match a mask.
 * we're not using the RTC for anything else, so RTC interrupt should suffice
 * for our main tick interrupt.
 */
__HIGH_CODE
void rtc_reset_trig()
{
    sys_safe_access_enable();
    R8_RTC_FLAG_CTRL |= RB_RTC_TRIG_CLR;

    R32_RTC_TRIG = 0;
    R8_RTC_MODE_CTRL |= RB_RTC_LOAD_HI;
    sys_safe_access_disable();
    while((R32_RTC_TRIG & 0x3FFF) != (R32_RTC_CNT_DAY & 0x3FFF));
    sys_safe_access_enable();
    R32_RTC_TRIG = 0;
    R8_RTC_MODE_CTRL |= RB_RTC_LOAD_LO;
    sys_safe_access_disable();

    RTC_TRIGFunCfg(PROG_TICK_RATE);
}

void rtcisr_init()
{
    LClk32K_Cfg(Clk32K_LSI, ENABLE);        // enable internal oscillator
    Calibration_LSI(Level_128);             // calibrate LSI fromm HSE

    rtc_reset_trig();

    PFIC_SetPriority(RTC_IRQn, 0x00);       // rtc is highest priority interrupt
    PFIC_EnableIRQ(RTC_IRQn);
}

/*
 * used only for rough cpu usage calculation stuff
 */
void systick_init(void)
{
    SysTick->CNT = 0;                                       // clear counter
    SysTick->CTLR = SysTick_CTLR_STCLK | SysTick_CTLR_STE;  // enable counter in /1 mode
}


static void idle_clear()
{
    if (idle_flags & IDLE_SLEEP_READY) {
        // wake up things
        ssd1306_set_display(1);

        uconf.flags &= ~UCONF_FLAGS_LEDS_DISABLE;
        rgbled_send();
    }

    if (idle_flags & IDLE_DIM_READY) {
        rgbled_set_imax_normal();
    }

    idle_flags = 0;
    idle_time = 0;
}

__HIGH_CODE
void oled_update_done()
{
    int8_t rot;
    uint8_t oldsys;

    // reset oled callback, clear screen, and set default pixel mode and font size
    oled.callback = 0;
    if (!(menu->flags & MENU_FLAG_NO_AUTOCLS)) {
        ssd1306_cls(&oled);
    }
    ssd1306fb_set_color(SSD1306_STATE_SET_PIXEL);
    oled.state &= ~SSD1306_STATE_STR_HALFWIDTH;


    // orientation / flipping flags
    rot = accel_get_rotation(&accel);

    oldsys = sysflags;
    if ((rot > (96+4)) || (rot < (32-4))) {
        sysflags &= ~SYS_OLED_ROTATE_X;
    } else if ((rot > (32+4)) && (rot < (96-4))) {
        sysflags |=  SYS_OLED_ROTATE_X;
    }

    if ((rot > (64+4)) && (rot < 124)) {
        sysflags &= ~SYS_OLED_ROTATE_Y;
    } else if ((rot > 4) && (rot < (64-4))) {
        sysflags |=  SYS_OLED_ROTATE_Y;
    }

    if (sysflags != oldsys) {
        sysflags |= SYS_OLED_ROTATE_CHANGED;
    }

    if ((rot < 21) || (rot > (64 + 24))) {
        sysflags &= ~SYS_OLED_REVERSE_CHARS;
    } else if (rot > 24 && rot < (64 + 21)){
        sysflags |=  SYS_OLED_REVERSE_CHARS;
    }

    // power saving
    // todo: don't hardcode the movement values
    if (accel_get_movement() > 3) {
        idle_clear();
    }

    // start going to sleep if the option is enabled and if we've been still for enough time
    if (uconf.sleep_timeout && (idle_time == uconf.sleep_timeout)) {
        idle_flags |= IDLE_SLEEP_START;
    }
    if (uconf.menu_timeout && (idle_time == uconf.menu_timeout)) {
        idle_flags |= IDLE_MENU_START;
    }
    if (uconf.dim_timeout && (idle_time == uconf.dim_timeout)) {
        rgbled_set_imax_dim();
        idle_flags |= IDLE_DIM_START | IDLE_DIM_READY;
    }


#ifdef MENU_TIMEOUT_TO_NAMETAG
    // root menu idle counting
    // note: untested.
    if (menu == &menu_0) {
        if (!idle_time_menu) {
            idle_time_menu = uptime;
        } else if ((uptime - idle_time_menu) >= MENU_TIMEOUT_TO_NAMETAG) {
            // been at the root menu too long.
            // return to nametag
            menu_stop(0);
            idle_time_menu = 0;
        }
    } else {
        idle_time_menu = 0;
    }
#endif

    // render menu
    menu_tick();

    // calculate CPU usage
    cpu_pct = ((cpu_use * 100) / OLED_UPDATE_RATE) / cpu_max;
    if (cpu_pct > cpu_pct_max) cpu_pct_max = cpu_pct;
}


__HIGH_CODE
void lowprio_task() {
    uint32_t interrupt_flags;

    while (1) {
        // sleep when we're doing nothing
        if (!flags_lo) {
            __WFI();
        }

        // disable all IRQs when doing any I2C related stuff, as interrupts
        // can corrupt transactions
        SYS_DisableAllIrq(&interrupt_flags);

        if (flags_lo & FLAG_ACCEL_POLL) {
            flags_lo &= ~FLAG_ACCEL_POLL;
            accel_poll();
        }

        // todo: implement proper sleep mode that saves power
        //       and wakes up on interrupt, instead of this
        if (idle_flags & IDLE_SLEEP_START) {
            if (!(idle_flags & IDLE_SLEEP_READY)) {
                idle_flags |= IDLE_SLEEP_READY;

                // just entered the mode? then turn things off
                ssd1306_set_display(0);

                uconf.flags |= UCONF_FLAGS_LEDS_DISABLE;
                rgbled_send();
            } else {
                // check to see if we should wake
                if (accel_get_movement() > 3) {
                    idle_clear();
                }
            }
        }

        // run normal program
        else {
            // temporary: re-send sub interrupts, sub button holds, enable rgb_hwen
            if (flags_lo & FLAG_CH32V_RESEND_CONF) {
                flags_lo &= ~FLAG_CH32V_RESEND_CONF;
                ch32sub_intr_defaults();
                btn_commit_hold();
                ch32sub_rgb_hwen(1);
            } else {
                // send button holds any time the orientation of the OLED changes
                if (sysflags & SYS_OLED_ROTATE_CHANGED) {
                    sysflags &= ~SYS_OLED_ROTATE_CHANGED;
                    btn_commit_hold();
                }
            }

            // process sub MCU interrupt, if pending
            ch32sub_process();

            if (flags_lo & FLAG_RGBLED_SEND) {
                flags_lo &= ~FLAG_RGBLED_SEND;
                rgbled_send();
            }

            // send the last oled frame data
            if (flags_lo & FLAG_OLED_UPDATE) {
                flags_lo &= ~FLAG_OLED_UPDATE;

                if (oled.callback && !(oled.state & SSD1306_STATE_BUSY)) {
                    if (ssd1306_cb_get()) {
                        oled.callback();
                    }
                }

                // update
                // only update this frame if we're not in the middle of starting up
                if (uptime) {
                    // process other tasks
                    if (oled.state & SSD1306_STATE_INITIALIZED) {
                        oled.callback = oled_update_done;
                    }
                    ssd1306_update();
                }

                cpu_use = SysTick->CNT;
            }

            // re-enable IRQs
            SYS_RecoverIrq(interrupt_flags);

            // render new rgbled frame
            // this is not included in cpu calcs :/
            if (flags_lo & FLAG_RGBLED_RUN) {
                flags_lo &= ~FLAG_RGBLED_RUN;

                // make sure a valid program is selected
                if (uconf.ledprog_rgb_idx > (sizeof(rgb_pgm) / sizeof(rgb_pgm[0]))) {
                    uconf.ledprog_rgb_idx = 0;
                }

                // now run a program
                rgbled_runprog(st_tick >> 2);
            }
        }

        // re-enable IRQs
        SYS_RecoverIrq(interrupt_flags);

        // drop into lower clock rate
        SetSysClock(SYSCLK_FREQ_IDLE);
    }
}


int main()
{
    // configure clock
    ch59x_xtal_conf();
    SetSysClock(SYSCLK_FREQ_USEI2C);
    // note that system clock speed is decreased after every use of I2C.

    PWR_DCDCCfg(ENABLE);                // enable DC-DC converter; brings significant power saving

    i2c_init();                         // get i2c initialized since most stuff will need it

    uconf_load();                       // read the user config

    ch32sub_init();                     // initialize aux MCU
    ch32sub_rgb_hwen(1);                // and enable RGBLED controller hardware pin so the controller can wake up

    tinymt32_init(&tinymt32_s, 1337);   // let's get random

    accel_init();                       // initialize accelerometer
    rgbled_init();                      // initialize RGBLED controller

    gat_gpio_init();                    // configure GAT aux GPIOs, get gat ID
    // todo: implement                  // configure GAT i2c slave

    ssd1306fb_set_target(&oled);        // start up the OLED and menu system
    ssd1306_init(1);                    // we'll try to init later too, since sometimes OLED fails to init
    menu_stop(0);                       // lol. yes, this "starts" the "menu" in nametag mode

    systick_init();                     // start up the system tick, used for CPU percentage calculation
    cpu_max = GetSysClock() / PROG_TICK_RATE;

    port_intr_init();                   // configure port-based interrupts (used for ch32sub interrupt)
    ch32sub_intr_defaults();            // configure interrupt sources on sub MCU - buttons, etc

    rtcisr_init();                      // configure main program tick interrupt

    lowprio_task();                     // start the low priority task loop
}


__INTERRUPT
__HIGH_CODE
void RTC_IRQHandler(void)
{
    // clear interrupt flag and reset RTC
    rtc_reset_trig();

    // speed up
    SetSysClock(SYSCLK_FREQ_NORMAL);

    // manual uptime counter
    st_tick++;
    if (!st_tick) {
        uptime++;
        uptime_hour = (uint16_t)(uptime / 3600);
        uptime_min  = (uint8_t)((uptime /   60) % 60);
        uptime_sec  = (uint8_t)((uptime       ) % 60);

        // in case the sub MCU crashes or whatever, make sure state is consistent
        if (uptime & 1) {
            flags_lo |= FLAG_CH32V_RESEND_CONF;
        }

        idle_time++;
    }

    oled_tick++;
    if (oled_tick >= OLED_UPDATE_RATE) {
        oled_tick = 0;
        SysTick->CNT = 0;
        flags_lo |= FLAG_OLED_UPDATE;
    }

    // operations
    switch (st_tick & 0x3) {
        case 0: {
            // send RGBLEDs, render new program
            flags_lo |= FLAG_RGBLED_SEND;
            flags_lo |= FLAG_RGBLED_RUN;
            break;
        }
        case 1: {
            flags_lo |= FLAG_ACCEL_POLL;
            // no break
        }
    }
}
