#include "lv_setup.hpp"

#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/task.h"
#include "lodepng_alloc.hpp"

#include "types.hpp"

internal const char *kTagLvgl = "LVGL";

SemaphoreHandle_t g_LvglMutex = nullptr;
lv_display_t *g_LvglDisplay = nullptr;
uint8_t *g_LvglDrawBuffer = nullptr;

internal void lvgl_tick_task(void *arg)
{
  while (true)
  {
    vTaskDelay(pdMS_TO_TICKS(10));

    if (xSemaphoreTake(g_LvglMutex, portMAX_DELAY) == pdTRUE)
    {
      lv_timer_handler();
      xSemaphoreGive(g_LvglMutex);
    }
  }
}

internal uint32_t my_tick_get_cb()
{
  return (uint32_t)(esp_timer_get_time() / 1000);
}

internal void lv_dummy_flush_cb(lv_display_t *disp, const lv_area_t *area,
                                uint8_t *px_map)
{
  // Headless display, so we don't actually flush to SPI/I2C.
  // We just tell LVGL that the "flush" is completed so it unblocks
  // wait_for_flushing.
  lv_display_flush_ready(disp);
}

internal void lv_draw_sample_ui()
{
  lv_obj_t *scr = lv_screen_active();
  // Default background to white for the grayscale PNG
  lv_obj_set_style_bg_color(scr, lv_color_white(), 0);

  lv_obj_t *label = lv_label_create(scr);
  lv_label_set_text(label, "Calendink Provider\nLVGL Headless Renderer");
  lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

  static lv_style_t style;
  lv_style_init(&style);

  lv_style_set_line_color(&style, lv_palette_main(LV_PALETTE_GREY));
  lv_style_set_line_width(&style, 6);
  lv_style_set_line_rounded(&style, true);

  /*Create an object with the new style*/
  lv_obj_t *obj = lv_line_create(scr);
  lv_obj_add_style(obj, &style, 0);

  static lv_point_precise_t p[] = {{10, 30}, {30, 50}, {100, 0}};
  lv_line_set_points(obj, p, 3);

  lv_obj_center(obj);
}

void setup_lvgl()
{
  ESP_LOGI(kTagLvgl, "Initializing LVGL");

  g_LvglMutex = xSemaphoreCreateMutex();

  lv_init();
  lv_tick_set_cb(my_tick_get_cb);

  uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH;
  uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT;

  // Create a virtual display
  g_LvglDisplay = lv_display_create(width, height);
  lv_display_set_flush_cb(g_LvglDisplay, lv_dummy_flush_cb);

  // Initialize LodePNG custom bump allocator
  lodepng_allocator_init();

  // Allocate draw buffers in PSRAM
  // Using LV_COLOR_FORMAT_RGB565 (2 bytes per pixel)
  size_t buf_size = LV_DRAW_BUF_SIZE(width, height, LV_COLOR_FORMAT_RGB565);

  // Fallback to MALLOC_CAP_DEFAULT if we can't get SPIRAM (for debugging
  // without it)
  void *buf1 = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM);
  if (!buf1)
  {
    ESP_LOGW(kTagLvgl, "Failed to allocate LVGL draw buffer in PSRAM, falling "
                       "back to internal RAM");
    buf1 = heap_caps_malloc(buf_size, MALLOC_CAP_DEFAULT);
  }

  if (!buf1)
  {
    ESP_LOGE(kTagLvgl, "Failed to allocate LVGL draw buffer entirely.");
    return;
  }

  g_LvglDrawBuffer = (uint8_t *)buf1;

  // Explicitly set the color format of the display FIRST
  // so that stride and byte-per-pixel calculations align with our buffer.
  lv_display_set_color_format(g_LvglDisplay, LV_COLOR_FORMAT_RGB565);

  lv_display_set_buffers(g_LvglDisplay, buf1, nullptr, buf_size,
                         LV_DISPLAY_RENDER_MODE_FULL);

  // Create the background task for the LVGL timer
  xTaskCreate(lvgl_tick_task, "LVGL Tick", 4096, nullptr, 5, nullptr);

  // Draw the sample UI
  if (xSemaphoreTake(g_LvglMutex, portMAX_DELAY) == pdTRUE)
  {
    lv_draw_sample_ui();
    xSemaphoreGive(g_LvglMutex);
  }

  ESP_LOGI(kTagLvgl, "LVGL fully initialized. Display %lux%lu created.", width,
           height);
}
