/**
 * @file    led_ancs.c
 *
 * @author  PerL13
 * @date    2025-09-30
 * @version 1.2
 *
 * @note    Set the IO and LED strip length in menuconfig
 * supports strobe and burst mode also settable via menuconfig
 *
 */

#include "led_ancs.h"
#include "led_strip.h"
#include "esp_timer.h"
#include "esp_log.h"

#define LED_ANCS_TAG  "LED_ANCS"

static led_state_t led_state;
static led_strip_handle_t strip;
static uint8_t led_brightness[LED_STRIP_COUNT] = {0};

// --- millis helper ---
static inline uint32_t millis(void) {
    return (uint32_t)(esp_timer_get_time() / 1000ULL);
}

// --- Internal helpers ---
static void leds_clear(void) {
    esp_err_t err = led_strip_clear(strip);
    if (err != ESP_OK) {
        ESP_LOGE(LED_ANCS_TAG, "Failed to clear leds (err=0x%x)", err);
    }
}

void led_init(void)
{
    esp_err_t err;

    led_strip_config_t strip_config = {
        .strip_gpio_num = LED_STRIP_PIN,    // WS2812 data pin
        .max_leds = LED_STRIP_COUNT,        // number of pixels
//        .led_model = LED_MODEL_WS2812,
//        .color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_RGB,
        .flags.invert_out = false,
    };

    led_strip_rmt_config_t rmt_config = {
        .clk_src = RMT_CLK_SRC_DEFAULT,
        .resolution_hz = 10 * 1000 * 1000, // 10 MHz
        .mem_block_symbols = 64,
        .flags.with_dma = false,
    };

    err = led_strip_new_rmt_device(&strip_config, &rmt_config, &strip);
    if (err != ESP_OK) {
        ESP_LOGE(LED_ANCS_TAG, "Failed to configure led strip (err=0x%x)", err);
    }

    leds_clear();
    led_state.mode = LED_MODE_OFF;
    led_state.r = led_state.g = led_state.b = 0;
    led_state.start_time = 0;
    led_state.index = 0;
    led_state.left_position = 0;
    led_state.right_position = 0;

    ESP_LOGI(LED_ANCS_TAG, "Led strip initialized");
}

static void led_set_pxl(const uint8_t pxl, const uint8_t r, const uint8_t g, const uint8_t b) {
    esp_err_t err;
    if(pxl >= LED_STRIP_COUNT) {
        ESP_LOGE(LED_ANCS_TAG, "pxl strip out-of-range");
    }
    else {
        err = led_strip_set_pixel(strip, pxl, r, g, b);
        if (err != ESP_OK) {
            ESP_LOGE(LED_ANCS_TAG, "Failed to set pixel %d (err=0x%x)", pxl, err);
        }
    }
}

static void leds_solid(const uint8_t r, const uint8_t g, const uint8_t b) {
    esp_err_t err;

    for (int i = 0; i < LED_STRIP_COUNT; i++) {
        led_set_pxl(i, r, g, b);
    }
    err = led_strip_refresh(strip);
    if (err != ESP_OK) {
        ESP_LOGE(LED_ANCS_TAG, "Failed to refresh strip (err=0x%x)", err);
    }
}

static void led_strobe_run(void) {
    bool should_on = false;

    uint32_t elapsed = millis() - led_state.start_time;

    uint32_t burst_period = (LED_STROBE_BURSTS * LED_STROBE_PERIOD_MS + LED_STROBE_PAUSE_MS);
    uint32_t cycle = elapsed % burst_period;
    if (cycle < LED_STROBE_BURSTS * LED_STROBE_PERIOD_MS) {
        uint32_t phase = cycle % LED_STROBE_PERIOD_MS;
        should_on = (phase < LED_STROBE_PERIOD_MS / 2); //50% duty cycle
    } else {
        should_on = false; // we are in pause
    }

    if (should_on != led_state.strobe_on) {
        led_state.strobe_on = should_on;
        if (led_state.strobe_on) leds_solid(led_state.r, led_state.g, led_state.b);
        else leds_clear();
    }
}

static void led_chase_run(void) {
    esp_err_t err;

    // fade all LEDs
    for (int i = 0; i < LED_STRIP_COUNT; i++) {
        if (led_brightness[i] > LED_DECAY_STEP)
            led_brightness[i] -= LED_DECAY_STEP;
        else
            led_brightness[i] = 0;
    }

    // light head LED
    led_brightness[led_state.index] = LED_BRIGHTNESS_MAX;

    // update strip
    for (int i = 0; i < LED_STRIP_COUNT; i++) {
        led_set_pxl(i,(led_state.r * led_brightness[i]) / 255,
                      (led_state.g * led_brightness[i]) / 255,
                      (led_state.b * led_brightness[i]) / 255);
    }
    err = led_strip_refresh(strip);
    if (err != ESP_OK) {
        ESP_LOGE(LED_ANCS_TAG, "Failed to refresh strip (err=0x%x)", err);
    }
    // move head
    led_state.index = (led_state.index + 1) % LED_STRIP_COUNT;
}

static void led_status_run(void) {
    esp_err_t err;

    if (led_state.left_position < led_state.right_position) {
        leds_solid(0, 0, LED_BRIGHTNESS_MIN); //set all to blue, low brightness
        led_set_pxl(led_state.left_position, led_state.r, led_state.g, led_state.b);
        led_set_pxl(led_state.right_position, led_state.r, led_state.g, led_state.b);
        err = led_strip_refresh(strip);
        if (err != ESP_OK) {
            ESP_LOGE(LED_ANCS_TAG, "Failed to refresh strip (err=0x%x)", err);
        }
        led_state.left_position++;
        led_state.right_position--;
    }
}

static void led_test_1_run(void) {
    uint32_t elapsed = millis() - led_state.start_time;
    uint32_t led_period = (LED_TEST_1_ON_MS + LED_TEST_1_OFF_MS);
    uint32_t step_num = elapsed / led_period;
    uint32_t step_pos = elapsed % led_period;
    uint16_t category_colors_size = sizeof(category_colors) / sizeof(rgb_t);
    uint8_t new_index = step_num % category_colors_size;

    if (new_index != led_state.index) {
        led_state.index = new_index;
        ESP_LOGI(LED_ANCS_TAG, "Set the category %d on Leds", led_state.index);
    }
    if (step_pos < LED_TEST_1_ON_MS) {
        const rgb_t *c;
        c = &category_colors[led_state.index];
        leds_solid(c->r, c->g, c->b );
    } else {
        leds_clear();
    }
}

static void led_set_strobe(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t brightness, const uint32_t duration_ms) {
    led_state.mode = LED_MODE_STROBE;
    led_state.r = (uint16_t)r * brightness / 255;
    led_state.g = (uint16_t)g * brightness / 255;
    led_state.b = (uint16_t)b * brightness / 255;
    led_state.duration_ms = duration_ms;
    led_state.start_time = millis();
    led_state.strobe_on = false;
}

static void led_set_chase(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t brightness, const uint32_t duration_ms) {
    led_state.mode = LED_MODE_CHASE;
    led_state.r = (uint16_t)r * brightness / 255;
    led_state.g = (uint16_t)g * brightness / 255;
    led_state.b = (uint16_t)b * brightness / 255;
    led_state.duration_ms = duration_ms;
    led_state.start_time = millis();
    led_state.index = 0;
}

static void led_set_status(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t brightness, const uint32_t duration_ms) {
    led_state.mode = LED_MODE_STATUS;
    led_state.r = (uint16_t)r * brightness / 255;
    led_state.g = (uint16_t)g * brightness / 255;
    led_state.b = (uint16_t)b * brightness / 255;
    led_state.duration_ms = duration_ms;
    led_state.start_time = millis();
    led_state.left_position = 0;
    led_state.right_position = LED_STRIP_COUNT-1;
}

// --- Public API ---
void led_set_solid(const uint8_t r, const uint8_t g, const uint8_t b) {
    led_state.mode = LED_MODE_SOLID;
    led_state.r = r;
    led_state.g = g;
    led_state.b = b;
    led_state.start_time = millis();
    led_state.duration_ms = LED_SOLID_TIME_MS;
    leds_solid(r, g, b);
}

void led_set_by_category(const uint8_t cid) {
    //set or override existing animation, if its not solid
    if (led_state.mode != LED_MODE_SOLID) {
        const rgb_t *c;

        if (cid < sizeof(category_colors)/sizeof(category_colors[0])) {
            c = &category_colors[cid];
        } else {
             c = &category_colors[12];
        }

#if CONFIG_ILLUMINANCS_STROBE_MODE_ON
        led_set_strobe(c->r, c->g, c->b, LED_BRIGHTNESS_MAX, LED_STROBE_TIME_MS);
#else
        led_set_chase(c->r, c->g, c->b, LED_BRIGHTNESS_MAX, LED_CHASE_TIME_MS);
#endif
    }
}

void led_set_test_1(void) {
    led_state.mode = LED_MODE_TEST_1;
    led_state.start_time = millis();
    led_state.duration_ms = LED_TEST_1_TIME_MS;
    led_state.index = 0;
}

// --- Non-blocking update (call frequently) ---
void led_update(void) {
    uint32_t now = millis();
    uint32_t elapsed;

    switch (led_state.mode) {
        case LED_MODE_SOLID: {
            elapsed = now - led_state.start_time;
            if (elapsed > led_state.duration_ms) {
                led_state.mode  = LED_MODE_OFF;
                ESP_LOGI(LED_ANCS_TAG, "Ended LED_MODE_SOLID");
                break;
            }
            //in led solid there is no runner since it is already done in led_set_solid()
            break;
        }
        case LED_MODE_STROBE: {
            elapsed = now - led_state.start_time;
            if (elapsed > led_state.duration_ms) {
                led_set_status(led_state.r, led_state.g, led_state.b, LED_BRIGHTNESS_MIN, LED_STATUS_TIME_MS); // go to status
                ESP_LOGI(LED_ANCS_TAG, "Ended LED_MODE_STROBE");
                break;
            }
            led_strobe_run();
            break;
        }
        case LED_MODE_CHASE: {
            elapsed = now - led_state.start_time;
            if (elapsed > led_state.duration_ms) {
                led_set_status(led_state.r, led_state.g, led_state.b, LED_BRIGHTNESS_MIN, LED_STATUS_TIME_MS); // go to status
                ESP_LOGI(LED_ANCS_TAG, "Ended LED_MODE_CHASE");
                break;
            }
            led_chase_run();
            break;
        }
        case LED_MODE_STATUS:
            elapsed = now - led_state.start_time;
            if (elapsed > led_state.duration_ms) {
                led_set_solid(0, 0, LED_BRIGHTNESS_MIN); //set all to blue, low brightness
                //led_state.mode  = LED_MODE_OFF;
                ESP_LOGI(LED_ANCS_TAG, "Ended LED_MODE_STATUS");
                break;
            }
            led_status_run();
            break;
        case LED_MODE_TEST_1: {
            elapsed = now - led_state.start_time;
            if (elapsed > led_state.duration_ms) {
                led_state.mode  = LED_MODE_OFF;
                ESP_LOGI(LED_ANCS_TAG, "Ended LED_MODE_TEST_1");
                break;
            }
            led_test_1_run();
            break;
        }
        default:
            break;
    }
}
