// SPDX-License-Identifier: Apache-2.0
#include <drivers/esp_epaper.h>
#include <esp_epaper_module.h>
#include "esp_epaper_rotate.h"

#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/esp32_spi.h>
#include <tactility/drivers/spi_controller.h>
#include <tactility/error.h>
#include <tactility/log.h>

#include <epaper.h>

#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>

#include <cstdlib>
#include <cstring>

constexpr auto* TAG = "esp_epaper";
#define GET_CONFIG(device) (static_cast<const EspEpaperConfig*>((device)->config))

/** Width/height overrides above this are rejected as nonsense. */
constexpr uint16_t MAX_PANEL_DIMENSION = 2048;

struct EspEpaperInternal {
    /** Opaque esp_epaper device, owns the panel's pins and SPI device. */
    epd_handle_t epd;
    epd_panel_info_t info;
    /** Scratch buffer in native panel layout, for rotated frames (rotation != 0). */
    uint8_t* rotate_buffer;
    /** Serializes panel/SPI access between draw_bitmap and power state changes. */
    SemaphoreHandle_t panel_mutex;
    /** disp_on_off state; the panel is in deep sleep while false. */
    bool display_on;
};

static uint16_t esp_epaper_get_display_width(const EspEpaperInternal* internal, uint8_t rotation) {
    return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.height : internal->info.width;
}

static uint16_t esp_epaper_get_display_height(const EspEpaperInternal* internal, uint8_t rotation) {
    return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.width : internal->info.height;
}

static bool resolve_panel_type(const char* name, epd_panel_type_t* out_type) {
    struct PanelMapping {
        const char* name;
        epd_panel_type_t type;
    };
    static constexpr PanelMapping kPanelMappings[] = {
        { "gdey0154d67", EPD_PANEL_GDEY0154D67 },
        { "gdep073e01", EPD_PANEL_GDEP073E01 },
        { "gdey037f51", EPD_PANEL_GDEY037F51 },
        { "gdey029t71h", EPD_PANEL_GDEY029T71H },
        { "ssd16xx-154", EPD_PANEL_SSD16XX_154 },
        { "ssd16xx-213", EPD_PANEL_SSD16XX_213 },
        { "ssd16xx-266", EPD_PANEL_SSD16XX_266 },
        { "ssd16xx-270", EPD_PANEL_SSD16XX_270 },
        { "ssd16xx-290", EPD_PANEL_SSD16XX_290 },
        { "ssd16xx-370", EPD_PANEL_SSD16XX_370 },
        { "ssd16xx-420", EPD_PANEL_SSD16XX_420 },
    };
    for (const auto& mapping : kPanelMappings) {
        if (std::strcmp(name, mapping.name) == 0) {
            *out_type = mapping.type;
            return true;
        }
    }
    return false;
}

static error_t esp_epaper_reset(Device* device) {
    auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
    xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
    // epd_wake() toggles the reset pin and re-runs the full init sequence.
    const esp_err_t ret = epd_wake(internal->epd);
    // epd_wake() re-inits the panel, so it is awake (and drawable) again.
    if (ret == ESP_OK) {
        internal->display_on = true;
    }
    xSemaphoreGive(internal->panel_mutex);
    return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}

static error_t esp_epaper_init(Device* device) {
    auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
    xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
    const esp_err_t ret = epd_wake(internal->epd);
    if (ret == ESP_OK) {
        internal->display_on = true;
    }
    xSemaphoreGive(internal->panel_mutex);
    return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}

// LVGL only ever calls this with the full frame: DISPLAY_COLOR_FORMAT_MONOCHROME forces
// LV_DISPLAY_RENDER_MODE_FULL in the generic kernel LVGL bridge (lvgl_display.c), and FULL mode
// only presents (calls draw_bitmap) once per render cycle, with the complete 0,0..hres,vres rect.
// hres/vres are the rotated (display) resolution reported by get_resolution_*; color_data is
// row-major, MSB-first 1bpp (LVGL's LV_COLOR_FORMAT_I1 with the palette header already stripped by
// the caller); bit 1 = white, bit 0 = black. epd_update() expects exactly that layout and polarity.
static error_t esp_epaper_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
    auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
    const auto* config = GET_CONFIG(device);

    const uint16_t display_width = esp_epaper_get_display_width(internal, config->rotation);
    const uint16_t display_height = esp_epaper_get_display_height(internal, config->rotation);
    if (x_start != 0 || y_start != 0 || x_end != display_width || y_end != display_height) {
        LOG_W(TAG, "draw_bitmap: only full-frame draws are supported (got %ld,%ld..%ld,%ld)", (long)x_start, (long)y_start, (long)x_end, (long)y_end);
        return ERROR_NOT_SUPPORTED;
    }

    xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);

    if (!internal->display_on) {
        // Display is off (deep sleep). Drop the frame; the mismatch is fine because the next
        // power-on triggers a full refresh of the current render anyway.
        xSemaphoreGive(internal->panel_mutex);
        return ERROR_NONE;
    }

    const uint8_t* source;
    if (config->rotation == 0) {
        source = static_cast<const uint8_t*>(color_data);
    } else {
        memset(internal->rotate_buffer, 0, internal->info.buffer_size);
        esp_epaper_rotate_frame(static_cast<const uint8_t*>(color_data), internal->rotate_buffer,
                                internal->info.width, internal->info.height, config->rotation);
        source = internal->rotate_buffer;
    }

    const esp_err_t ret = epd_update(internal->epd, source, config->update_mode);
    xSemaphoreGive(internal->panel_mutex);
    if (ret != ESP_OK) {
        LOG_E(TAG, "epd_update failed: %s", esp_err_to_name(ret));
        return ERROR_RESOURCE;
    }
    return ERROR_NONE;
}

static error_t esp_epaper_disp_on_off(Device* device, bool on_off) {
    auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));

    xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
    if (on_off == internal->display_on) {
        xSemaphoreGive(internal->panel_mutex);
        return ERROR_NONE;
    }

    bool ok = true;
    if (on_off) {
        if (epd_wake(internal->epd) != ESP_OK) {
            LOG_E(TAG, "epd_wake failed");
            ok = false;
        }
    } else {
        if (epd_sleep(internal->epd) != ESP_OK) {
            LOG_E(TAG, "epd_sleep failed");
            ok = false;
        }
    }

    if (ok) {
        internal->display_on = on_off;
    }
    xSemaphoreGive(internal->panel_mutex);
    return ok ? ERROR_NONE : ERROR_RESOURCE;
}

static DisplayColorFormat esp_epaper_get_color_format(Device*) {
    return DISPLAY_COLOR_FORMAT_MONOCHROME;
}

static uint16_t esp_epaper_get_resolution_x(Device* device) {
    auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
    return esp_epaper_get_display_width(internal, GET_CONFIG(device)->rotation);
}

static uint16_t esp_epaper_get_resolution_y(Device* device) {
    auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
    return esp_epaper_get_display_height(internal, GET_CONFIG(device)->rotation);
}

static void esp_epaper_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
    *out_buffer = nullptr;
}

static uint8_t esp_epaper_get_frame_buffer_count(Device*) {
    return 0;
}

static const DisplayApi esp_epaper_display_api = {
    .capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH,
    .reset = esp_epaper_reset,
    .init = esp_epaper_init,
    .draw_bitmap = esp_epaper_draw_bitmap,
    .mirror = nullptr,
    .swap_xy = nullptr,
    .get_swap_xy = nullptr,
    .get_mirror_x = nullptr,
    .get_mirror_y = nullptr,
    .set_gap = nullptr,
    .get_gap_x = nullptr,
    .get_gap_y = nullptr,
    .invert_color = nullptr,
    .disp_on_off = esp_epaper_disp_on_off,
    .disp_sleep = nullptr,
    .get_color_format = esp_epaper_get_color_format,
    .get_resolution_x = esp_epaper_get_resolution_x,
    .get_resolution_y = esp_epaper_get_resolution_y,
    .get_frame_buffer = esp_epaper_get_frame_buffer,
    .get_frame_buffer_count = esp_epaper_get_frame_buffer_count,
    .get_backlight = nullptr,
    .has_capability = nullptr,
};

static void free_internal(EspEpaperInternal* internal) {
    if (internal->epd != nullptr) {
        epd_deinit(internal->epd);
    }
    if (internal->rotate_buffer != nullptr) {
        free(internal->rotate_buffer);
    }
    if (internal->panel_mutex != nullptr) {
        vSemaphoreDelete(internal->panel_mutex);
    }
    free(internal);
}

static error_t start(Device* device) {
    auto* parent = device_get_parent(device);
    check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);

    const auto* spi_config = static_cast<const Esp32SpiConfig*>(parent->config);
    const auto* config = GET_CONFIG(device);

    epd_panel_type_t panel_type;
    if (!resolve_panel_type(config->panel_type, &panel_type)) {
        LOG_E(TAG, "Unknown panel type: %s", config->panel_type);
        return ERROR_NOT_SUPPORTED;
    }

    if (config->clock_speed_hz == 0) {
        LOG_E(TAG, "Invalid clock_speed_hz (0)");
        return ERROR_NOT_SUPPORTED;
    }

    if (config->update_mode > EPD_UPDATE_PARTIAL) {
        LOG_E(TAG, "Invalid update_mode %d (expected %d-%d)", (int)config->update_mode, (int)EPD_UPDATE_FULL, (int)EPD_UPDATE_PARTIAL);
        return ERROR_NOT_SUPPORTED;
    }

    if (config->width > MAX_PANEL_DIMENSION || config->height > MAX_PANEL_DIMENSION) {
        LOG_E(TAG, "Invalid panel size %ux%u (maximum %ux%u)", (unsigned)config->width, (unsigned)config->height, (unsigned)MAX_PANEL_DIMENSION, (unsigned)MAX_PANEL_DIMENSION);
        return ERROR_NOT_SUPPORTED;
    }

    epd_config_t epd_config = EPD_CONFIG_DEFAULT();
    epd_config.pins.busy = config->pin_busy.pin;
    epd_config.pins.rst = config->pin_reset.pin;
    epd_config.pins.dc = config->pin_dc.pin;
    epd_config.pins.cs = config->pin_cs.pin;
    epd_config.pins.sck = spi_config->pin_sclk.pin;
    epd_config.pins.mosi = spi_config->pin_mosi.pin;
    epd_config.spi.host = spi_config->host;
    epd_config.spi.speed_hz = config->clock_speed_hz;
    epd_config.panel.type = panel_type;
    epd_config.panel.width = config->width;
    epd_config.panel.height = config->height;

    epd_handle_t epd = nullptr;
    if (epd_init(&epd_config, &epd) != ESP_OK) {
        LOG_E(TAG, "epd_init failed");
        return ERROR_RESOURCE;
    }

    auto* internal = static_cast<EspEpaperInternal*>(calloc(1, sizeof(EspEpaperInternal)));
    if (internal == nullptr) {
        epd_deinit(epd);
        return ERROR_OUT_OF_MEMORY;
    }

    internal->epd = epd;
    internal->panel_mutex = xSemaphoreCreateMutex();
    if (internal->panel_mutex == nullptr) {
        free_internal(internal);
        return ERROR_OUT_OF_MEMORY;
    }

    if (epd_get_info(epd, &internal->info) != ESP_OK) {
        LOG_E(TAG, "epd_get_info failed");
        free_internal(internal);
        return ERROR_RESOURCE;
    }

    if (internal->info.color_mode != EPD_COLOR_BW) {
        LOG_E(TAG, "Panel color mode %d is not supported by the kernel display bridge (monochrome only)", internal->info.color_mode);
        free_internal(internal);
        return ERROR_NOT_SUPPORTED;
    }

    if (config->rotation > 3) {
        LOG_E(TAG, "Invalid rotation %u (expected 0-3)", config->rotation);
        free_internal(internal);
        return ERROR_NOT_SUPPORTED;
    }

    if (config->rotation != 0) {
        internal->rotate_buffer = static_cast<uint8_t*>(malloc(internal->info.buffer_size));
        if (internal->rotate_buffer == nullptr) {
            LOG_E(TAG, "Failed to allocate %lu-byte rotation buffer", internal->info.buffer_size);
            free_internal(internal);
            return ERROR_OUT_OF_MEMORY;
        }
    }

    internal->display_on = true;
    device_set_driver_data(device, internal);

    LOG_I(TAG, "Started %ux%u panel (buffer %lu bytes, rotation %u)", internal->info.width, internal->info.height, internal->info.buffer_size, config->rotation);
    return ERROR_NONE;
}

static error_t stop(Device* device) {
    auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));

    xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
    if (internal->display_on) {
        // Leave the panel in deep sleep.
        epd_sleep(internal->epd);
        internal->display_on = false;
    }
    xSemaphoreGive(internal->panel_mutex);

    free_internal(internal);
    device_set_driver_data(device, nullptr);
    return ERROR_NONE;
}

Driver esp_epaper_driver = {
    .name = "esp_epaper",
    .compatible = (const char*[]) { "tuanpmt,esp-epaper", nullptr },
    .start_device = start,
    .stop_device = stop,
    .api = &esp_epaper_display_api,
    .device_type = &DISPLAY_TYPE,
    .owner = &esp_epaper_module,
    .internal = nullptr
};
