#include <stdio.h>

#include "sdkconfig.h"

/* FreeRTOS */
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"

/* IDF drivers */
#include "esp_log.h"
#include "esp_sleep.h"
#include "esp_spi_flash.h"
#include "esp_system.h"

/* EPD driver */
#include "epd_board_specific.h"
#include "epd_driver.h"
#include "epd_highlevel.h"

/* LVGL */
#include "lvgl.h"

/* Private */
#include "app_bkp_ram.h"
#include "app_lvgl.h"

#define LVGL_TASK_HEAP     4096
#define LVGL_TASK_INTERVAL 100

#define APP_LOG_TAG "APP_LVGL"

#define EPD_MAX_REFR_COUNT 8

static EpdiyHighlevelState s_epd_hl;
static int                 s_temp = 26;

static lv_disp_draw_buf_t s_disp_buf;
static lv_color_t        *s_screen_buf;
static lv_disp_drv_t      s_disp_drv;

static SemaphoreHandle_t s_lvgl_semphr;
static SemaphoreHandle_t s_lvgl_flush_cmd_semphr;   /* This semaphore is used to start the drawing */
static SemaphoreHandle_t s_lvgl_flush_ready_semphr; /* This semaphore is used to notify the drawing has completed */

static void app_lvgl_task(void *pvParameters);
static void app_lvgl_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
static void app_lvgl_set_px_cb(lv_disp_drv_t *disp_drv, uint8_t *buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y,
                               lv_color_t color, lv_opa_t opa);
static void app_lvgl_log_cb(const char *buf);

int app_lvgl_init(void) {
    /* Initialize EPDIY */
    epd_init(EPD_OPTIONS_DEFAULT);
    s_epd_hl     = epd_hl_init(EPD_BUILTIN_WAVEFORM);
    s_screen_buf = (lv_color_t *)epd_hl_get_framebuffer(&s_epd_hl);

    /* Initialize LVGL */
    lv_init();
    lv_log_register_print_cb(app_lvgl_log_cb);
    lv_disp_draw_buf_init(&s_disp_buf, s_screen_buf, NULL, 960 * 540);
    lv_disp_drv_init(&s_disp_drv);
    s_disp_drv.user_data    = &s_epd_hl;
    s_disp_drv.draw_buf     = &s_disp_buf;
    s_disp_drv.hor_res      = 960;
    s_disp_drv.ver_res      = 540;
    s_disp_drv.full_refresh = true;
    s_disp_drv.flush_cb     = app_lvgl_flush_cb;
    s_disp_drv.set_px_cb    = app_lvgl_set_px_cb;
    lv_disp_drv_register(&s_disp_drv);

    s_lvgl_semphr = xSemaphoreCreateMutex();
    if (s_lvgl_semphr == NULL) {
        ESP_LOGE(APP_LOG_TAG, "Failed to create LVGL semaphore.");
        return -1;
    }

    s_lvgl_flush_ready_semphr = xSemaphoreCreateBinary();
    if (s_lvgl_flush_ready_semphr == NULL) {
        ESP_LOGE(APP_LOG_TAG, "Failed to create flush semaphore.");
        return -2;
    }

    s_lvgl_flush_cmd_semphr = xSemaphoreCreateBinary();
    if (s_lvgl_flush_cmd_semphr == NULL) {
        ESP_LOGE(APP_LOG_TAG, "Failed to create flush command semaphore.");
        return -3;
    }

    if (xTaskCreate(app_lvgl_task, "LV_TASK", LVGL_TASK_HEAP, NULL, 2, NULL) != pdPASS) {
        ESP_LOGE(APP_LOG_TAG, "Failed to create LVGL task, available heap: %ld.", esp_get_free_heap_size());
        return -4;
    }

    ESP_LOGI(APP_LOG_TAG, "LVGL initialized.");

    return 0;
}

int app_lvgl_deinit(void) {
    epd_powerdown_lilygo_t5_47();
    epd_deinit();

    /* LVGL is not required to be de-init. */

    return 0;
}

int app_lvgl_lock(uint32_t ms) {
    if (xSemaphoreTake(s_lvgl_semphr, pdMS_TO_TICKS(ms)) != pdPASS) {
        return -1;
    }

    return 0;
}

int app_lvgl_unlock(void) {
    if (xSemaphoreGive(s_lvgl_semphr) != pdPASS) {
        return -1;
    }

    return 0;
}

int app_lvgl_start_flush(void) {
    if (xSemaphoreGive(s_lvgl_flush_cmd_semphr) != pdPASS) {
        return -1;
    }

    return 0;
}

int app_lvgl_wait_flush(uint32_t timeout_ms) {
    if (xSemaphoreTake(s_lvgl_flush_ready_semphr, pdMS_TO_TICKS(timeout_ms)) != pdPASS) {
        return -1;
    }

    return 0;
}

static void app_lvgl_task(void *pvParameters) {
    if (xSemaphoreTake(s_lvgl_flush_cmd_semphr, portMAX_DELAY) != pdPASS) {
        vTaskSuspend(NULL);
    }

    ESP_LOGI(APP_LOG_TAG, "Received flush GO signal, start flushing...");

    epd_poweron();
    epd_fullclear(&s_epd_hl, s_temp);
    epd_poweroff();

    for (;;) {
        if (app_lvgl_lock(50) == 0) {
            lv_timer_handler();

            ESP_LOGD(APP_LOG_TAG, "LVGL timer handler executed.");

            app_lvgl_unlock();
        }
        vTaskDelay(pdMS_TO_TICKS(LVGL_TASK_INTERVAL));
    }
}

static void app_lvgl_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
    EpdiyHighlevelState *hl = disp_drv->user_data;

    EpdRect rect = {
        .x      = area->x1,
        .y      = area->y1,
        .width  = area->x2 - area->x1 + 1,
        .height = area->y2 - area->y1 + 1,
    };

    ESP_LOGI(APP_LOG_TAG, "Flush called.");

    epd_poweron();
    epd_hl_update_area(hl, MODE_GL16, s_temp, rect);
    epd_poweroff();

    // Done flushing..
    xSemaphoreGive(s_lvgl_flush_ready_semphr);

    lv_disp_flush_ready(disp_drv);
}

static void app_lvgl_set_px_cb(lv_disp_drv_t *disp_drv, uint8_t *buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y,
                               lv_color_t color, lv_opa_t opa) {
    uint32_t px_offset = (buf_w * y / 2) + x / 2;

    uint8_t pix_y = lv_color_brightness(color);
    pix_y         = (pix_y / 16) & 0x0FU;

    if (x % 2 == 0) {
        buf[px_offset] &= 0xF0;
        buf[px_offset] |= pix_y;
    } else {
        buf[px_offset] &= 0x0F;
        buf[px_offset] |= pix_y << 4;
    }
}

static void app_lvgl_log_cb(const char *buf) {
    ESP_LOGI(APP_LOG_TAG, "LVGL: %s", buf);
}