/*#############################################################################
## Author: Shaun Reed                                                        ##
## Legal: All Content (c) 2025 Shaun Reed, all rights reserved               ##
##                                                                           ##
## Contact: shaunrd0@gmail.com  | URL: www.shaunreed.com                     ##
##############################################################################
*/
#ifndef DISPLAY_H
#define DISPLAY_H

#include <lv_init.h>
#include <widgets/label/lv_label.h>

#include "i2c.h"
#include "panel_device.h"

#include <esp_lcd_panel_ops.h>
#include <esp_lcd_types.h>

struct LCD
{
    esp_lcd_panel_handle_t esp_panel_;

    esp_lcd_panel_io_handle_t esp_io_;

    /// LVGL display handle.
    lv_display_t* lv_display_;

    lv_array_t lv_objs_;

    struct IPanelDevice* device_;
};

/**
 * Construct a new LCD using an object that implements IPanelDevice.
 *
 * @param device An object that implements the IPanelDevice interface.
 */
static struct LCD LCD_init(struct IPanelDevice* device)
{
    struct LCD display;
    ESP_LOGI(LCD_TAG, "Creating panel IO handle");
    display.esp_io_ = NULL;
    esp_lcd_panel_dev_config_t panel_config = {
        .reset_gpio_num = (gpio_num_t)(-1),
        .bits_per_pixel = 1,
        .vendor_config = device->vendor_config_cb(),
    };
    esp_lcd_panel_io_i2c_config_t io_config = {
        .dev_addr = I2C_HW_ADDR,
        // User data to pass to the LVGL flush_ready callback.
        // See IPanelDevice::lvgl_flush_ready_cb
        .user_ctx = NULL,
        .control_phase_bytes = 1,
        .dc_bit_offset = 6,
        .lcd_cmd_bits = LCD_CMD_BITS,
        .lcd_param_bits = LCD_PARAM_BITS,
        .scl_speed_hz = LCD_PIXEL_CLOCK_HZ,
    };
    ESP_ERROR_CHECK(
        esp_lcd_new_panel_io_i2c(I2C_get(), &io_config, &display.esp_io_));

    // If the passed handle is already allocated, delete it.
    if (display.esp_panel_ != NULL)
    {
        ESP_LOGI(LCD_TAG, "Removing unused panel");
        esp_lcd_panel_del(display.esp_panel_);
    }

    ESP_LOGI(LCD_TAG, "Installing vendor panel driver");
    device->init_panel_cb(&panel_config, display.esp_io_, &display.esp_panel_);

    ESP_LOGI(LCD_TAG, "Resetting panel display");
    ESP_ERROR_CHECK(esp_lcd_panel_reset(display.esp_panel_));
    ESP_LOGI(LCD_TAG, "Initializing panel display");
    ESP_ERROR_CHECK(esp_lcd_panel_init(display.esp_panel_));
    ESP_LOGI(LCD_TAG, "Turning on panel display");
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(display.esp_panel_, true));

    if (!lv_is_initialized())
    {
        ESP_LOGI(LCD_TAG, "Initialize LVGL");
        lv_init();
    }

    ESP_LOGI(LCD_TAG, "Creating LVGL display");
    display.lv_display_ = lv_display_create(device->width_, device->height_);
    assert(display.lv_display_);

    // associate the i2c panel handle to the display
    lv_display_set_user_data(display.lv_display_, display.esp_panel_);

    device->register_rendering_data_cb(display.lv_display_, display.esp_io_,
                                       device->lv_buf_, device->lv_buf_size_);
    device->register_lvgl_tick_timer_cb();
    ESP_LOGI(LCD_TAG, "Initializing LVGL array");
    lv_array_init(&display.lv_objs_, 1, sizeof(lv_obj_t*));
    return display;
}

/**
 * Create a LVGL label with some given text on the current display.
 * The name of the object can be reused to change text on this label later.
 *
 * @param display
 * @param text Text to write to the display.
 * @param long_mode LVGL long mode for text wider than the current display.
 * @param align LVGL alignment to use for placing the label on the display.
 * @return The index of the inserted label on the LVGL screen
 */
static uint32_t LCD_set_text_with_mode(struct LCD* display, const char* text,
                                       lv_label_long_mode_t long_mode,
                                       lv_align_t align)
{
    // Lock the mutex due to the LVGL APIs are not thread-safe.
    _lock_acquire(&lv_lock_);

    ESP_LOGI(LCD_TAG, "Setting new text: %s", text);
    lv_obj_t* scr = lv_display_get_screen_active(display->lv_display_);

    lv_obj_t* new_object = lv_label_create(scr);

    // Set text and long mode.
    lv_label_set_long_mode(new_object, long_mode);
    lv_label_set_text(new_object, text);

    // Set the size of the screen.
    // If you use rotation 90 or 270 use lv_display_get_vertical_resolution.
    lv_obj_set_width(
        new_object, lv_display_get_horizontal_resolution(display->lv_display_));
    lv_obj_align(new_object, align, 0, 0);

    uint32_t index = lv_array_size(&display->lv_objs_);
    if (lv_array_push_back(&display->lv_objs_, &new_object) != LV_RESULT_OK)
    {
        ESP_LOGE(LCD_TAG, "Failed to add new object to array");
    }

    _lock_release(&lv_lock_);
    return 1;
}

/**
 * Create a LVGL label with some given text on the current display.
 * The name of the object can be reused to change text on this label later.
 *
 * @param display
 * @param text Text to write to the display.
 * @return The index of the inserted label on the LVGL screen
 */
static uint32_t LCD_set_text(struct LCD* display, const char* text)
{
    return LCD_set_text_with_mode(display, text, LV_LABEL_LONG_SCROLL_CIRCULAR,
                                  LV_ALIGN_TOP_MID);
}

static void LCD_set_text_at_with_mode(struct LCD* display, const char* text,
                                      uint32_t i,
                                      lv_label_long_mode_t long_mode,
                                      lv_align_t align)
{
    // Lock the mutex due to the LVGL APIs are not thread-safe.
    _lock_acquire(&lv_lock_);

    if (lv_array_is_empty(&display->lv_objs_))
    {
        ESP_LOGI(LCD_TAG, "Cannot set text at index %d; The array is empty.");
        _lock_release(&lv_lock_);
        return;
    }
    lv_obj_t** ptr = (lv_obj_t**)lv_array_at(&display->lv_objs_, i);
    lv_obj_t* label = (lv_obj_t*)*ptr;
    if (label == NULL)
    {
        ESP_LOGE(LCD_TAG, "Failed to set text at index %d; Label is null", i);
        _lock_release(&lv_lock_);
        return;
    }
    lv_label_set_text(label, text);

    ESP_LOGI(LCD_TAG, "Setting text at index %d:", i);
    // Set text and long mode.
    lv_label_set_long_mode(label, long_mode);
    lv_label_set_text(label, text);

    // Set the size of the screen.
    // If you use rotation 90 or 270 use lv_display_get_vertical_resolution.
    lv_obj_set_width(
        label, lv_display_get_horizontal_resolution(display->lv_display_));
    lv_obj_align(label, align, 0, 0);

    _lock_release(&lv_lock_);
}

static void LCD_set_text_at(struct LCD* display, const char* text, uint32_t i)
{
    // Lock the mutex due to the LVGL APIs are not thread-safe.
    _lock_acquire(&lv_lock_);

    if (lv_array_is_empty(&display->lv_objs_))
    {
        ESP_LOGI(LCD_TAG, "Cannot set text at index %d; The array is empty.");
        _lock_release(&lv_lock_);
        return;
    }
    lv_obj_t** ptr = (lv_obj_t**)lv_array_at(&display->lv_objs_, i);
    lv_obj_t* label = (lv_obj_t*)*ptr;
    if (label == NULL)
    {
        ESP_LOGE(LCD_TAG, "Failed to set text at index %d; Label is null", i);
        _lock_release(&lv_lock_);
        return;
    }
    lv_label_set_text(label, text);

    _lock_release(&lv_lock_);
}


static void LCD_clear(struct LCD* display)
{
    _lock_acquire(&lv_lock_);

    uint32_t size = lv_array_size(&display->lv_objs_);
    ESP_LOGI(LCD_TAG, "Clearing %d LVGL objects", size);
    for (uint32_t i = 0; i < size && size > 0; i++)
    {
        ESP_LOGI(LCD_TAG, "Checking array index %d", i);
        lv_obj_t** ptr_to_delete =
            (lv_obj_t**)lv_array_at(&display->lv_objs_, i);
        lv_obj_t* to_delete = (lv_obj_t*)*ptr_to_delete;
        if (to_delete == NULL)
        {
            ESP_LOGE(LCD_TAG, "Failed to clear all LVGL objects");
            continue;
        }
        if (lv_obj_is_valid(to_delete))
        {
            ESP_LOGI(LCD_TAG, "Removing LVGL object");
            lv_label_set_text(to_delete, "test");
            lv_obj_delete(to_delete);
        }
        else
        {
            ESP_LOGE(LCD_TAG, "Error: LVGL object is not valid");
        }
    }
    lv_array_clear(&display->lv_objs_);

    _lock_release(&lv_lock_);
}

#endif // DISPLAY_H
