/*
 * Copyright (c) 2023 Juan Manuel Cruz <jcruz@fi.uba.ar> <jcruz@frba.utn.edu.ar>.
 * Copyright (c) 2026 Martin Javier Klöckner <mklockner@fi.uba.ar>
 *
 * See file `LICENSE` for full details
 */

#include "main.h"

#include "logger.h"
#include "dwt.h"

#include "app.h"
#include "task_system_attribute.h"
#include "task_system_interface.h"
#include "task_leds_attribute.h"
#include "task_leds_interface.h"
#include "task_menu_attribute.h"
#include "task_menu_interface.h"
#include "task_buzzer_attribute.h"
#include "task_buzzer_interface.h"

#define G_TASK_SYS_CNT_INI              0ul
#define G_TASK_SYS_TICK_CNT_INI         0ul
#define G_TASK_SYS_EEPROM_START_ADDR    0x0801FC00
#define G_TASK_SYS_EEPROM_END_ADDR      0x0801FFF0
#define G_TASK_SYS_TIMER_UPDATE_RATE_MS 1000ul

#define DEL_SYS_MIN 0ul
#define DEL_SYS_MED 50ul
#define DEL_SYS_MAX 500ul

// If persistent values are lost these are the timer default values
#define TIMER_DEFAULT_START_HOUR 00ul
#define TIMER_DEFAULT_START_MIN  00ul
#define TIMER_DEFAULT_END_HOUR   12ul
#define TIMER_DEFAULT_END_MIN    00ul
#define TIMER_DEFAULT_STATUS     false

// Exported variables
volatile uint32_t g_task_system_tick_cnt; // Updated every ms by SysTick callback

// Private variables
static task_system_dta_t task_system_dta = {
    DEL_SYS_MIN, ST_SYS_IDLE, EV_SYS_IDLE, false
};

#define SYSTEM_DTA_QTY (sizeof(task_system_dta)/sizeof(task_system_dta_t))

static uint32_t g_task_system_timer_tick;
static task_system_timer_t task_system_timer = {
    TIMER_DEFAULT_START_HOUR, TIMER_DEFAULT_START_MIN,
    TIMER_DEFAULT_END_HOUR, TIMER_DEFAULT_END_MIN,
    TIMER_DEFAULT_STATUS
};

// Private function declaration
static void task_system_statechart(void *parameters);
static void task_system_temp_ctrl_enable(void *parameters);
static void task_system_timer_update(void *parameters);
static void task_system_rtc_init(void);
static void task_system_rtc_increment_hour(void);
static void task_system_rtc_increment_min(void);
static void task_system_eeprom_read_param(void *parameters);
static void task_system_eeprom_write_param(void *parameters);
static uint32_t task_system_eeprom_get_current_addr(void);

// Public function definition
void task_system_init(void *parameters)
{
    task_system_dta_t *p_task_system_dta;
    task_system_st_t state;

    LOGGER_INFO("Initializing `task_system`...");

    g_task_system_tick_cnt = G_TASK_SYS_TICK_CNT_INI;
    g_task_system_timer_tick = G_TASK_SYS_CNT_INI;

    task_system_interface_init_event_queue();
    task_system_rtc_init();
    task_system_eeprom_read_param(parameters);

    p_task_system_dta = &task_system_dta;

    state = ST_SYS_IDLE;
    p_task_system_dta->state = state;

    shared_data_type *p_shared_data = (shared_data_type *)parameters;
    p_shared_data->timer = &task_system_timer;

    LOGGER_INFO("Done initializing `task_system`");
}

void task_system_update(void *parameters)
{
    bool b_time_update_required = false;

    __asm("CPSID i");
    if (G_TASK_SYS_TICK_CNT_INI < g_task_system_tick_cnt)
    {
        g_task_system_tick_cnt--;
        b_time_update_required = true;
    }
    __asm("CPSIE i");

    while (b_time_update_required)
    {
        if (0 == g_task_system_timer_tick)
        {
            g_task_system_timer_tick = G_TASK_SYS_TIMER_UPDATE_RATE_MS;
            task_system_timer_update(parameters);
        }
        else
        {
            g_task_system_timer_tick--;
        }
        task_system_statechart(parameters);

        __asm("CPSID i");
        if (G_TASK_SYS_TICK_CNT_INI < g_task_system_tick_cnt)
        {
            g_task_system_tick_cnt--;
            b_time_update_required = true;
        }
        else
        {
            b_time_update_required = false;
        }
        __asm("CPSIE i");
    }
}

// Private function definition
static void task_system_rtc_init(void)
{
    HAL_PWR_EnableBkUpAccess();

    // Set RTC Time only if RTC has been reset
    if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) != RTC_BACKUP_MAGIC_NUMBER)
    {
        s_time.Hours   = 16;
        s_time.Minutes = 41;
        s_time.Seconds = 20;
        HAL_RTC_SetTime(&hrtc, &s_time, RTC_FORMAT_BIN);

        s_date.WeekDay = RTC_WEEKDAY_FRIDAY;
        s_date.Month   = RTC_MONTH_MARCH;
        s_date.Date    = 20;
        s_date.Year    = 26; // 2026
        HAL_RTC_SetDate(&hrtc, &s_date, RTC_FORMAT_BIN);
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, RTC_BACKUP_MAGIC_NUMBER);
    }
}

static void task_system_rtc_increment_hour()
{
    HAL_RTC_GetTime(&hrtc, &s_time, RTC_FORMAT_BIN);
    s_time.Hours = (s_time.Hours + 1) % 24;
    HAL_RTC_SetTime(&hrtc, &s_time, RTC_FORMAT_BIN);
}

static void task_system_rtc_increment_min()
{
    HAL_RTC_GetTime(&hrtc, &s_time, RTC_FORMAT_BIN);
    s_time.Minutes = (s_time.Minutes + 1) % 60;
    HAL_RTC_SetTime(&hrtc, &s_time, RTC_FORMAT_BIN);
}

static uint32_t task_system_eeprom_get_current_addr(void)
{
    uint32_t addr = G_TASK_SYS_EEPROM_START_ADDR;

    while (G_TASK_SYS_EEPROM_END_ADDR > addr)
    {
        if (0xFFFF == *(uint16_t*)addr)
        {
            // Empty value found
            return (addr - sizeof(uint16_t));
        }
        addr += sizeof(uint16_t);
    }

    // Page is full
    return 0;
}

static void task_system_eeprom_read_param(void *parameters)
{
    shared_data_type *p_shared_data = (shared_data_type *)parameters;

    uint32_t addr = task_system_eeprom_get_current_addr();
    if (0 == addr)
    {
        addr = G_TASK_SYS_EEPROM_END_ADDR;
    }

    p_shared_data->temp_set_point = *(uint16_t*)addr;
}

// To reduce the wear of flash its better to write the new variables on the same
// page but on contiguous addresses every time the variables are updated, this
// also has the benefit that there is no need to erase the full page all the
// time but only once the page fills which after 512 writes (2 bytes on a page
// of 1024 bytes).
static void task_system_eeprom_write_param(void *parameters)
{
    uint32_t new_addr, curr_addr;
    shared_data_type *p_shared_data = (shared_data_type *)parameters;

    curr_addr = task_system_eeprom_get_current_addr();

    if(p_shared_data->temp_set_point == *(uint16_t *)curr_addr)
    {
        // No need to update if the value is the same
        return;
    }

    uint16_t data[] = {
        p_shared_data->temp_set_point
    };

    HAL_FLASH_Unlock();

    if (0 == curr_addr)
    {
        // Memory page is full
        LOGGER_INFO("Memory page is full. Erasing full page...");

        FLASH_EraseInitTypeDef erase = {0};
        uint32_t sector_error;

        erase.TypeErase   = FLASH_TYPEERASE_PAGES;
        erase.PageAddress = G_TASK_SYS_EEPROM_START_ADDR;
        erase.NbPages     = 1; // 1024 bytes

        if(HAL_FLASHEx_Erase(&erase, &sector_error) != HAL_OK)
        {
            HAL_FLASH_Lock();
            LOGGER_ERROR("Could erase flash at address %d",
                    G_TASK_SYS_EEPROM_START_ADDR);
            return;
        }

        new_addr = G_TASK_SYS_EEPROM_START_ADDR;
    }
    else
    {
        new_addr = (curr_addr + sizeof(uint16_t));
    }

    HAL_FLASH_Program(TYPEPROGRAM_HALFWORD, new_addr, *(uint16_t *)data);
    HAL_FLASH_Lock();
    return;
}

static void task_system_temp_ctrl_enable(void *parameters)
{
    shared_data_type *p_shared_data = (shared_data_type *)parameters;

    p_shared_data->temp_ctrl_enabled = !p_shared_data->temp_ctrl_enabled;
    task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
    task_buzzer_interface_put_event(EV_BUZ_XX_LONG_BEEP, ID_BUZ_A);

    if (true == p_shared_data->temp_ctrl_enabled)
    {
        task_leds_interface_put_event(EV_LED_XX_BLINK, ID_LED_A);
        task_leds_interface_put_event(EV_LED_XX_BLINK, ID_LED_B);
    }
    else
    {
        task_leds_interface_put_event(EV_LED_XX_OFF, ID_LED_A);
        task_leds_interface_put_event(EV_LED_XX_OFF, ID_LED_B);
    }
}

static void task_system_statechart(void *parameters)
{
    task_system_dta_t *p_task_system_dta = &task_system_dta;
    shared_data_type *p_shared_data = (shared_data_type *)parameters;

    if (true == task_system_interface_any_event())
    {
        p_task_system_dta->event_flag = true;
        p_task_system_dta->event = task_system_interface_get_event();

        // Events which are common to every state
        switch (p_task_system_dta->event)
        {

            case EV_SYS_REM_TIMER_TOGGLE:
                p_task_system_dta->event_flag = false;
                p_shared_data->timer->enabled = !p_shared_data->timer->enabled;
                task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
            break;

            case EV_SYS_REM_TEMP_CTRL_ENABLED:
                p_task_system_dta->event_flag = false;
                task_system_temp_ctrl_enable(parameters);
                task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
            break;

            case EV_SYS_REM_TEMP_SET:
                p_task_system_dta->event_flag = false;
                task_system_eeprom_write_param(parameters);
                task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
            break;

            default:
            break;
        }
    }

    switch (p_task_system_dta->state)
    {
        case ST_SYS_IDLE:
            if (true == p_task_system_dta->event_flag)
            {
                p_task_system_dta->event_flag = false;
                switch (p_task_system_dta->event)
                {
                    case EV_SYS_BTN_B_ACTIVE:
                    break;

                    case EV_SYS_BTN_C_ACTIVE:
                        task_system_temp_ctrl_enable(parameters);
                    break;

                    case EV_SYS_BTN_A_ACTIVE:
                    case EV_SYS_BTN_D_ACTIVE:
                        p_task_system_dta->state = ST_SYS_CLOCK_SET;
                        task_menu_interface_put_event(EV_MEN_ST_CLOCK_SET);
                    break;

                    default: break;
                }
            }
        break;

        case ST_SYS_CLOCK_SET:
            if (true == p_task_system_dta->event_flag)
            {
                p_task_system_dta->event_flag = false;
                switch (p_task_system_dta->event)
                {
                    case EV_SYS_BTN_A_ACTIVE:
                    case EV_SYS_BTN_D_ACTIVE:
                        p_task_system_dta->state = ST_SYS_TEMP_SET;
                        task_menu_interface_put_event(EV_MEN_ST_TEMP_SET);
                    break;
                    case EV_SYS_BTN_B_ACTIVE:
                        task_system_rtc_increment_hour();
                        task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
                    break;
                    case EV_SYS_BTN_C_ACTIVE:
                        task_system_rtc_increment_min();
                        task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
                    break;
                    default: break;
                }
            }
        break;

        case ST_SYS_TEMP_SET:
            if (true == p_task_system_dta->event_flag)
            {
                p_task_system_dta->event_flag = false;
                switch (p_task_system_dta->event)
                {
                    case EV_SYS_BTN_A_ACTIVE:
                    case EV_SYS_BTN_D_ACTIVE:
                        p_task_system_dta->state = ST_SYS_TIMER_SET_STATUS;
                        task_menu_interface_put_event(EV_MEN_ST_TIMER_SET_STATUS);
                        task_system_eeprom_write_param(parameters);
                    break;

                    case EV_SYS_BTN_B_ACTIVE:
                        p_shared_data->temp_set_point =
                            ((p_shared_data->temp_set_point - 5) % 85);
                        task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
                    break;

                    case EV_SYS_BTN_C_ACTIVE:
                        p_shared_data->temp_set_point =
                            ((p_shared_data->temp_set_point + 5) % 85);

                        task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
                    break;

                    default: break;
                }
            }
        break;

        case ST_SYS_TIMER_SET_STATUS:
            if (true == p_task_system_dta->event_flag)
            {
                p_task_system_dta->event_flag = false;
                switch (p_task_system_dta->event)
                {

                    case EV_SYS_BTN_C_ACTIVE:
                        p_shared_data->timer->enabled = !p_shared_data->timer->enabled;
                        task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
                    break;

                    case EV_SYS_BTN_A_ACTIVE:
                    case EV_SYS_BTN_B_ACTIVE:
                        p_task_system_dta->state = ST_SYS_IDLE;
                        task_menu_interface_put_event(EV_MEN_ST_IDLE);
                        break;

                    case EV_SYS_BTN_D_ACTIVE:
                        p_task_system_dta->state = ST_SYS_TIMER_SET_START;
                        task_menu_interface_put_event(EV_MEN_ST_TIMER_SET_START);
                    break;

                    default: break;
                }
            }
        break;

        case ST_SYS_TIMER_SET_START:
            if (true == p_task_system_dta->event_flag)
            {
                p_task_system_dta->event_flag = false;
                switch (p_task_system_dta->event)
                {
                    case EV_SYS_BTN_B_ACTIVE:
                        p_shared_data->timer->start_hour =
                            ((p_shared_data->timer->start_hour + 1) % 24);
                        task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
                    break;

                    case EV_SYS_BTN_C_ACTIVE:
                        p_shared_data->timer->start_min =
                            ((p_shared_data->timer->start_min + 1) % 60);
                        task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
                    break;

                    case EV_SYS_BTN_A_ACTIVE:
                    case EV_SYS_BTN_D_ACTIVE:
                        p_task_system_dta->state = ST_SYS_TIMER_SET_END;
                        task_menu_interface_put_event(EV_MEN_ST_TIMER_SET_END);
                    break;

                    default: break;
                }
            }
        break;

        case ST_SYS_TIMER_SET_END:
            if (true == p_task_system_dta->event_flag)
            {
                p_task_system_dta->event_flag = false;
                switch (p_task_system_dta->event)
                {
                    case EV_SYS_BTN_B_ACTIVE:
                        p_shared_data->timer->end_hour =
                            ((p_shared_data->timer->end_hour + 1) % 24);
                        task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
                    break;

                    case EV_SYS_BTN_C_ACTIVE:
                        p_shared_data->timer->end_min =
                            ((p_shared_data->timer->end_min + 1) % 60);
                        task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
                    break;

                    case EV_SYS_BTN_A_ACTIVE:
                    case EV_SYS_BTN_D_ACTIVE:
                        p_task_system_dta->state = ST_SYS_IDLE;
                        task_menu_interface_put_event(EV_MEN_ST_IDLE);
                    break;

                    default: break;
                }
            }
        break;

        default: 
            p_task_system_dta->state = ST_SYS_IDLE;
            p_task_system_dta->event_flag = false;
            p_shared_data->temp_ctrl_enabled = false;
            task_menu_interface_put_event(EV_MEN_ST_IDLE);
            task_leds_interface_put_event(EV_LED_XX_OFF, ID_LED_A);
            task_leds_interface_put_event(EV_LED_XX_OFF, ID_LED_B);
        break;
    }
}

// TODO: Only check timer conditions every second (not on every tick)
static void task_system_timer_update(void *parameters)
{
    shared_data_type *p_shared_data = (shared_data_type *)parameters;
    uint16_t start_24, end_24, clock_24;
    bool new_temp_ctrl_state;

    if (false == p_shared_data->timer->enabled)
    {
        return;
    }

    HAL_RTC_GetTime(&hrtc, &s_time, RTC_FORMAT_BIN);

    clock_24 = (s_time.Hours*100) + s_time.Minutes;
    start_24 = (p_shared_data->timer->start_hour*100) +
                p_shared_data->timer->start_min;
    end_24 = (p_shared_data->timer->end_hour*100) +
              p_shared_data->timer->end_min;

    new_temp_ctrl_state = ((end_24 > start_24)
                ? ((clock_24 >= start_24) && (clock_24 < end_24))
                : ((clock_24 >= start_24) || (clock_24 < end_24)));

    if (new_temp_ctrl_state != p_shared_data->temp_ctrl_enabled)
    {
        p_shared_data->temp_ctrl_enabled = new_temp_ctrl_state;

        task_menu_interface_put_event(EV_MEN_DISPLAY_UPDATE);
        task_buzzer_interface_put_event(EV_BUZ_XX_LONG_BEEP, ID_BUZ_A);

        if (true == p_shared_data->temp_ctrl_enabled)
        {
            task_leds_interface_put_event(EV_LED_XX_BLINK, ID_LED_A);
            task_leds_interface_put_event(EV_LED_XX_BLINK, ID_LED_B);
        }
        else
        {
            task_leds_interface_put_event(EV_LED_XX_OFF, ID_LED_A);
            task_leds_interface_put_event(EV_LED_XX_OFF, ID_LED_B);
        }
    }
}
