/**
 * @file display_ssd1306_lvgl.c
 * @brief SSD1306 + LVGL9: 内部帧缓冲 L8, flush 时转为 1bpp (SSD1306 页格式)
 */

#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/lock.h>
#include <sys/param.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_attr.h"
#include "esp_mac.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_ssd1306.h"
#include "driver/i2c_master.h"


#define CONFIG_LV_ATTRIBUTE_LARGE_CONST EXT_RAM_BSS_ATTR	// 将 LVGL bitmap 放在外部 RAM 中
#include "lvgl.h"
#include "src/font/lv_binfont_loader.h"

#include "display_ssd1306_lvgl.h"
#include "fusion_12_font.h"
#include "bsp.h"
#include "main.h"

extern const lv_font_t matrix_5x7;

static const char* TAG = "oled_lvgl";
// static const lv_font_t* s_oled_font = &matrix_5x7;
#define OLED_FONT_NAME "matrix_5x7"
#define OLED_FONT_LINE_H        8
#define OLED_CJK_FONT_PX        12   /* 12×12 字体 */
#define OLED_CJK_FONT_TOP_BLANK 1    /* 字模格内顶部留白 1px */
#define OLED_CJK_ROW_GAP        1    /* 行间空白（最后一行之后不需要） */
#define OLED_CJK_ROW_PITCH      (OLED_CJK_FONT_PX + OLED_CJK_ROW_GAP)  /* 13 */
#define OLED_CJK_MAX_ROWS       5    /* 13×4 + 12 = 64 */
#define OLED_CJK_DEMO_TEXT      "LVGL中文字库"
#if LV_FONT_MONTSERRAT_12
#define OLED_FALLBACK_FONT (&lv_font_montserrat_12)
#define OLED_FALLBACK_FONT_NAME "LV_FONT_MONTSERRAT_12"
#else
#define OLED_FALLBACK_FONT LV_FONT_DEFAULT
#define OLED_FALLBACK_FONT_NAME "LV_FONT_DEFAULT"
#endif

#define OLED_I2C_ADDR           0x3C
#define OLED_PIXEL_CLOCK_HZ     (400 * 1000)
#define OLED_PIN_SDA            9
#define OLED_PIN_SCL            10
#define OLED_PIN_RST            (-1)
#define OLED_H_RES              128
#define OLED_V_RES              64
#define OLED_I2C_PORT           I2C_NUM_0

#define OLED_LVGL_TICK_MS       5
#define OLED_LVGL_PALETTE       8
#define OLED_LVGL_TASK_MAX_MS   500
#define OLED_LVGL_TASK_MIN_MS   (1000 / CONFIG_FREERTOS_HZ)

#define OLED_MAX_DYN_LABELS     10
#define OLED_TEXT_QUEUE_LEN     16
#define OLED_TEXT_MAX_LEN       96

typedef struct {
	int x;
	int y;
	char text[OLED_TEXT_MAX_LEN];
} oled_text_req_t;

static uint8_t s_oled_bitmap[OLED_H_RES * OLED_V_RES / 8];
static _lock_t s_lvgl_lock;
static bool s_lvgl_lock_inited;
static QueueHandle_t s_text_queue;
static lv_display_t* s_display;
static lv_obj_t* s_scr;
static esp_lcd_panel_handle_t s_panel;
static lv_obj_t* s_label_mac;
// static lv_obj_t* s_label_ip;
// static lv_obj_t* s_device_name;
// static lv_obj_t* s_label_cjk_demo;
// static lv_obj_t* s_label_cjk_demo_2;

static lv_font_t* s_cjk_font = NULL;
static const lv_font_t* s_cjk_active_font = OLED_FALLBACK_FONT;
static const char* s_cjk_active_font_name = OLED_FALLBACK_FONT_NAME;

static volatile bool s_net_refresh_pending;

static struct {
	lv_obj_t* obj;
	int x;
	int y;
	bool used;
} s_dyn[OLED_MAX_DYN_LABELS];

static i2c_master_bus_handle_t s_i2c_bus;
static bool s_i2c_bus_owned;

static bool s_inited;

int oled_compute_cjk_row_y(int row_idx)
{
	if (row_idx < 0) {
		row_idx = 0;
	}
	if (row_idx >= OLED_CJK_MAX_ROWS) {
		row_idx = OLED_CJK_MAX_ROWS - 1;
	}
	return row_idx * OLED_CJK_ROW_PITCH;
}

/* fusion_12 字库头 line_height 常为 23；按 '0' 字模校准 base_line，保留格内 1px 顶距 */
static void oled_fixup_cjk_font_metrics(lv_font_t* font)
{
	lv_font_glyph_dsc_t g = { 0 };

	if (font == NULL) {
		return;
	}
	font->line_height = OLED_CJK_FONT_PX;
	font->base_line = 0;

	/* glyph_top = label_y + (lh-bl) - box_h - ofs_y；目标：相对行顶下移 TOP_BLANK */
	if (lv_font_get_glyph_dsc(font, &g, (uint32_t)'0', 0) && g.box_h > 0) {
		int bl = OLED_CJK_FONT_PX - (int)g.box_h - (int)g.ofs_y - OLED_CJK_FONT_TOP_BLANK;
		if (bl < 0) {
			bl = 0;
		}
		if (bl > OLED_CJK_FONT_PX) {
			bl = OLED_CJK_FONT_PX;
		}
		font->base_line = (int8_t)bl;
		ESP_LOGI(TAG, "CJK font fix: '0' box_h=%d ofs_y=%d top_blank=%d -> base_line=%d",
			(int)g.box_h, (int)g.ofs_y, OLED_CJK_FONT_TOP_BLANK, (int)font->base_line);
	}
}

static void oled_init_row_layout(void)
{
	ESP_LOGI(TAG, "CJK layout: font_px=%d, row_pitch=%d, max_rows=%d (lh=%d bl=%d)",
		OLED_CJK_FONT_PX, OLED_CJK_ROW_PITCH, OLED_CJK_MAX_ROWS,
		(int)s_cjk_active_font->line_height, (int)s_cjk_active_font->base_line);
}

static void oled_style_cjk_label_row(lv_obj_t* label)
{
	lv_obj_set_style_text_font(label, s_cjk_active_font, 0);
	lv_obj_set_style_text_line_space(label, 0, 0);
	lv_obj_set_style_pad_all(label, 0, 0);
	lv_obj_set_height(label, OLED_CJK_FONT_PX);
}

static bool oled_init_cjk_font(void)
{
#if LV_USE_FS_MEMFS
	s_cjk_font = lv_binfont_create_from_buffer(
		(void*)fusion_12_font_data, (uint32_t)fusion_12_font_size);
	if (s_cjk_font == NULL) {
		ESP_LOGW(TAG, "lv_binfont_create_from_buffer failed");
		return false;
	}
	oled_fixup_cjk_font_metrics(s_cjk_font);
#else
	ESP_LOGW(TAG, "LV_USE_FS_MEMFS=0, cannot create font from embedded buffer");
	return false;
#endif

	s_cjk_active_font = s_cjk_font;
	s_cjk_active_font_name = "fusion_12";
	ESP_LOGI(TAG, "CJK font embedded init success, size=%u", (unsigned)fusion_12_font_size);
	return true;
}

static void oled_log_i2c_probe(i2c_master_bus_handle_t bus)
{
	// SSD1306 常见地址 0x3C/0x3D；同时探测 CW2015(0x62) 便于定位硬件连接问题
	const uint16_t addrs[] = { 0x3C, 0x3D, 0x62 };
	for (int i = 0; i < (int)(sizeof(addrs) / sizeof(addrs[0])); i++) {
		esp_err_t r = i2c_master_probe(bus, addrs[i], 50);
		if (r == ESP_OK) {
			ESP_LOGI(TAG, "I2C probe OK: addr=0x%02X", (unsigned)addrs[i]);
		}
		else {
			ESP_LOGW(TAG, "I2C probe FAIL: addr=0x%02X (%s)", (unsigned)addrs[i], esp_err_to_name(r));
		}
	}
}

/* ---------- LVGL flush (与 IDF i2c_oled 示例一致) ---------- */

static bool oled_notify_flush_ready(esp_lcd_panel_io_handle_t io_panel, esp_lcd_panel_io_event_data_t* edata, void* user_ctx)
{
	lv_display_t* disp = (lv_display_t*)user_ctx;
	/* 不可在此加锁：lv_refr_now/lv_timer_handler 持锁时会等 flush 完成，会死锁 */
	lv_display_flush_ready(disp);
	return false;
}

static void oled_lvgl_lock_init_once(void)
{
	if (!s_lvgl_lock_inited) {
		_lock_init(&s_lvgl_lock);
		s_lvgl_lock_inited = true;
	}
}

static void oled_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map)
{
	esp_lcd_panel_handle_t panel = lv_display_get_user_data(disp);
	px_map += OLED_LVGL_PALETTE;

	uint16_t hor_res = lv_display_get_physical_horizontal_resolution(disp);
	int x1 = area->x1;
	int x2 = area->x2;
	int y1 = area->y1;
	int y2 = area->y2;

	memset(s_oled_bitmap, 0, sizeof(s_oled_bitmap));

	for (int y = y1; y <= y2; y++) {
		for (int x = x1; x <= x2; x++) {
			bool on = (px_map[(hor_res >> 3) * y + (x >> 3)] & (1 << (7 - (x % 8))));
			uint8_t* buf = s_oled_bitmap + hor_res * (y >> 3) + x;
			if (on) {
				(*buf) |= (1 << (y % 8));
			}
			else {
				(*buf) &= ~(1 << (y % 8));
			}
		}
	}
	esp_lcd_panel_draw_bitmap(panel, x1, y1, x2 + 1, y2 + 1, s_oled_bitmap);
}

static void oled_lvgl_tick_cb(void* arg)
{
	lv_tick_inc(OLED_LVGL_TICK_MS);
}

static void oled_apply_text_req(const oled_text_req_t* req)
{
	if (req == NULL || s_scr == NULL || !lv_obj_is_valid(s_scr)) {
		return;
	}

	int x = req->x;
	int y = req->y;
	const char* text = req->text;
	if (x < 0) x = 0;
	if (y < 0) y = 0;
	if (x >= OLED_H_RES) x = OLED_H_RES - 1;
	if (y >= OLED_V_RES) y = OLED_V_RES - 1;

	int slot = -1;
	for (int i = 0; i < OLED_MAX_DYN_LABELS; i++) {
		if (s_dyn[i].used && s_dyn[i].x == x && s_dyn[i].y == y) {
			slot = i;
			break;
		}
	}
	if (slot < 0) {
		for (int i = 0; i < OLED_MAX_DYN_LABELS; i++) {
			if (!s_dyn[i].used) {
				slot = i;
				break;
			}
		}
	}
	if (slot < 0) {
		slot = 0;
	}

	if (s_dyn[slot].obj == NULL || !lv_obj_is_valid(s_dyn[slot].obj)) {
		s_dyn[slot].obj = lv_label_create(s_scr);
		lv_label_set_long_mode(s_dyn[slot].obj, LV_LABEL_LONG_CLIP);
		lv_obj_set_width(s_dyn[slot].obj, OLED_H_RES - x);
		lv_obj_set_style_text_color(s_dyn[slot].obj, lv_color_white(), 0);
		oled_style_cjk_label_row(s_dyn[slot].obj);
	}
	s_dyn[slot].x = x;
	s_dyn[slot].y = y;
	s_dyn[slot].used = true;
	lv_obj_set_pos(s_dyn[slot].obj, x, y);
	lv_label_set_text(s_dyn[slot].obj, text);
	lv_obj_invalidate(s_dyn[slot].obj);
}

static void oled_drain_text_queue(void)
{
	oled_text_req_t req;
	while (s_text_queue != NULL && xQueueReceive(s_text_queue, &req, 0) == pdTRUE) {
		oled_apply_text_req(&req);
	}
}

static void oled_apply_network_labels(void)
{
	if (s_display == NULL || s_scr == NULL || s_label_mac == NULL) return;
	if (!lv_obj_is_valid(s_label_mac)) return;


	uint8_t mac[6] = { 0 };
	char mac_str[24];
	// char ip_str[20];

	if (esp_read_mac(mac, ESP_MAC_WIFI_STA) == ESP_OK) {
		snprintf(mac_str, sizeof(mac_str), " MAC地址:%02X%02X%02X%02X%02X%02X",
			mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
	}
	else {
		snprintf(mac_str, sizeof(mac_str), " MAC地址:??");
	}
	lv_label_set_text(s_label_mac, mac_str);
	lv_obj_invalidate(s_label_mac);

	// snprintf(ip_str, sizeof(ip_str), "IP:%u.%u.%u.%u",
	// 	(unsigned)current_ip[0], (unsigned)current_ip[1],
	// 	(unsigned)current_ip[2], (unsigned)current_ip[3]);
	// lv_label_set_text(s_label_ip, ip_str);


	// char anchor_utf8[sizeof(deviceConfig.anchor_name) * 3 / 2 + 4];
	// (void)gbk_to_utf8((const char*)deviceConfig.anchor_name,
	// 	strnlen((const char*)deviceConfig.anchor_name, sizeof(deviceConfig.anchor_name)),
	// 	anchor_utf8, sizeof(anchor_utf8));

	// char device_name_str[sizeof(anchor_utf8) + 16];
	// snprintf(device_name_str, sizeof(device_name_str), "Name:%s", anchor_utf8);
	// lv_label_set_text(s_device_name, device_name_str);

	// char device_name_str[sizeof(deviceConfig.anchor_name) + 1];
	// snprintf(device_name_str, sizeof(device_name_str), "Name:%s", deviceConfig.anchor_name);
	// lv_label_set_text(s_device_name, device_name_str);

}

static void oled_lvgl_task(void* arg)
{
	(void)arg;
	uint32_t next_ms;
	while (1) {
		_lock_acquire(&s_lvgl_lock);
		oled_drain_text_queue();
		if (s_net_refresh_pending) {
			s_net_refresh_pending = false;
			oled_apply_network_labels();
		}
		next_ms = lv_timer_handler();
		_lock_release(&s_lvgl_lock);

		next_ms = MAX(next_ms, OLED_LVGL_TASK_MIN_MS);
		next_ms = MIN(next_ms, OLED_LVGL_TASK_MAX_MS);
		usleep(1000 * next_ms);
	}
}

static void oled_build_static_ui(void)
{
	s_scr = lv_display_get_screen_active(s_display);
	lv_obj_t* scr = s_scr;
	lv_obj_set_style_bg_color(scr, lv_color_black(), 0);
	lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);

	// s_device_name = lv_label_create(scr);
	// lv_obj_set_pos(s_device_name, 0, oled_compute_cjk_row_y(0));
	// lv_obj_set_width(s_device_name, OLED_H_RES);
	// lv_label_set_long_mode(s_device_name, LV_LABEL_LONG_CLIP);
	// lv_obj_set_style_text_color(s_device_name, lv_color_white(), 0);
	// lv_obj_set_style_text_font(s_device_name, s_cjk_active_font, 0);
	// lv_obj_set_style_text_opa(s_device_name, LV_OPA_COVER, 0);
	// lv_label_set_text(s_device_name, "Name:---");

	s_label_mac = lv_label_create(scr);
	lv_obj_set_pos(s_label_mac, 0, oled_compute_cjk_row_y(0));
	lv_obj_set_width(s_label_mac, OLED_H_RES);
	lv_obj_set_height(s_label_mac, OLED_CJK_FONT_PX + 1);
	lv_label_set_long_mode(s_label_mac, LV_LABEL_LONG_CLIP);
	lv_obj_set_style_text_color(s_label_mac, lv_color_black(), 0);
	lv_obj_set_style_bg_color(s_label_mac, lv_color_white(), 0);
	lv_obj_set_style_bg_opa(s_label_mac, LV_OPA_COVER, 0);
	oled_style_cjk_label_row(s_label_mac);
	lv_obj_set_style_text_opa(s_label_mac, LV_OPA_COVER, 0);
	lv_label_set_text(s_label_mac, "MAC:--");

	// s_label_ip = lv_label_create(scr);
	// lv_obj_set_pos(s_label_ip, 0, oled_compute_cjk_row_y(2));
	// lv_obj_set_width(s_label_ip, OLED_H_RES);
	// lv_label_set_long_mode(s_label_ip, LV_LABEL_LONG_CLIP);
	// lv_obj_set_style_text_color(s_label_ip, lv_color_white(), 0);
	// lv_obj_set_style_text_font(s_label_ip, s_cjk_active_font, 0);
	// lv_obj_set_style_text_opa(s_label_ip, LV_OPA_COVER, 0);
	// lv_label_set_text(s_label_ip, "IP:---.---.---.---");

	// s_label_cjk_demo = lv_label_create(scr);
	// lv_obj_set_pos(s_label_cjk_demo, 0, oled_compute_cjk_row_y(3));
	// lv_obj_set_width(s_label_cjk_demo, OLED_H_RES);
	// lv_label_set_long_mode(s_label_cjk_demo, LV_LABEL_LONG_WRAP);
	// lv_obj_set_style_text_color(s_label_cjk_demo, lv_color_white(), 0);
	// lv_obj_set_style_text_font(s_label_cjk_demo, s_cjk_active_font, 0);
	// lv_obj_set_style_text_opa(s_label_cjk_demo, LV_OPA_COVER, 0);
	// lv_label_set_text(s_label_cjk_demo, OLED_CJK_DEMO_TEXT);

	// s_label_cjk_demo_2 = lv_label_create(scr);
	// lv_obj_set_pos(s_label_cjk_demo_2, 0, oled_compute_cjk_row_y(4));
	// lv_obj_set_width(s_label_cjk_demo_2, OLED_H_RES);
	// lv_label_set_long_mode(s_label_cjk_demo_2, LV_LABEL_LONG_WRAP);
	// lv_obj_set_style_text_color(s_label_cjk_demo_2, lv_color_white(), 0);
	// lv_obj_set_style_text_font(s_label_cjk_demo_2, s_cjk_active_font, 0);
	// lv_obj_set_style_text_opa(s_label_cjk_demo_2, LV_OPA_COVER, 0);
	// lv_label_set_text(s_label_cjk_demo_2, OLED_CJK_DEMO_TEXT);


	memset(s_dyn, 0, sizeof(s_dyn));
}

esp_err_t oled_display_init(void)
{
	if (s_inited) {
		return ESP_OK;
	}

	oled_lvgl_lock_init_once();

	s_i2c_bus = NULL;
	s_i2c_bus_owned = false;

	if (s_i2c_bus == NULL) {
		ESP_LOGW(TAG, "s_i2c_bus is null, creating I2C bus SDA=%d SCL=%d", OLED_PIN_SDA, OLED_PIN_SCL);
		i2c_master_bus_config_t bus_cfg = {
			.clk_source = I2C_CLK_SRC_DEFAULT,
			.glitch_ignore_cnt = 7,
			.i2c_port = OLED_I2C_PORT,
			.sda_io_num = OLED_PIN_SDA,
			.scl_io_num = OLED_PIN_SCL,
			.flags.enable_internal_pullup = true,
		};
		esp_err_t ret = i2c_new_master_bus(&bus_cfg, &s_i2c_bus);
		if (ret != ESP_OK) {
			ESP_LOGE(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(ret));
			return ret;
		}
		s_i2c_bus_owned = true;
	}

	oled_log_i2c_probe(s_i2c_bus);

	esp_err_t err = ESP_FAIL;
	esp_lcd_panel_io_handle_t io_handle = NULL;

	// 自动尝试 0x3C/0x3D, 并在 400k 失败时降到 100k 再试, 避免地址/线材导致的 NACK
	const uint16_t try_addrs[] = { 0x3C, 0x3D };
	const uint32_t try_speeds[] = { OLED_PIXEL_CLOCK_HZ, 50000 };
	for (int ai = 0; ai < (int)(sizeof(try_addrs) / sizeof(try_addrs[0])); ai++) {
		for (int si = 0; si < (int)(sizeof(try_speeds) / sizeof(try_speeds[0])); si++) {
			esp_lcd_panel_io_i2c_config_t io_cfg = {
				.dev_addr = try_addrs[ai],
				.scl_speed_hz = try_speeds[si],
				.control_phase_bytes = 1,
				.lcd_cmd_bits = 8,
				.lcd_param_bits = 8,
				.dc_bit_offset = 6,
			};
			ESP_LOGI(TAG, "Try SSD1306 addr=0x%02X speed=%luHz", (unsigned)try_addrs[ai], (unsigned long)try_speeds[si]);
			err = esp_lcd_new_panel_io_i2c(s_i2c_bus, &io_cfg, &io_handle);
			if (err != ESP_OK) {
				ESP_LOGW(TAG, "esp_lcd_new_panel_io_i2c failed: %s", esp_err_to_name(err));
				io_handle = NULL;
				continue;
			}

			esp_lcd_panel_dev_config_t panel_cfg = {
				.reset_gpio_num = OLED_PIN_RST,
				.bits_per_pixel = 1,
			};
			esp_lcd_panel_ssd1306_config_t ssd_cfg = { .height = OLED_V_RES };
			panel_cfg.vendor_config = &ssd_cfg;

			err = esp_lcd_new_panel_ssd1306(io_handle, &panel_cfg, &s_panel);
			if (err != ESP_OK) {
				ESP_LOGW(TAG, "esp_lcd_new_panel_ssd1306 failed: %s", esp_err_to_name(err));
				s_panel = NULL;
				continue;
			}

			// 不要用 ESP_ERROR_CHECK, 避免硬件未接时直接 abort
			err = esp_lcd_panel_reset(s_panel);
			if (err != ESP_OK) {
				ESP_LOGW(TAG, "esp_lcd_panel_reset failed: %s", esp_err_to_name(err));
				continue;
			}
			err = esp_lcd_panel_init(s_panel);
			if (err != ESP_OK) {
				ESP_LOGW(TAG, "esp_lcd_panel_init failed: %s", esp_err_to_name(err));
				continue;
			}
			err = esp_lcd_panel_disp_on_off(s_panel, true);
			if (err != ESP_OK) {
				ESP_LOGW(TAG, "esp_lcd_panel_disp_on_off failed: %s", esp_err_to_name(err));
				continue;
			}
			esp_lcd_panel_mirror(s_panel, true, true);  /* 180° 翻转 */
			goto panel_ok;
		}
	}

	ESP_LOGE(TAG, "SSD1306 init failed (addr/speed tried). Check wiring/pullups/address.");
	goto fail_bus;

panel_ok:

	lv_init();
	bool cjk_ok = oled_init_cjk_font();
	if (!cjk_ok) {
		s_cjk_active_font = OLED_FALLBACK_FONT;
		s_cjk_active_font_name = OLED_FALLBACK_FONT_NAME;
		ESP_LOGW(TAG, "CJK font load failed, fallback font=%s", s_cjk_active_font_name);
	}
	ESP_LOGI(TAG, "CJK final font=%s", s_cjk_active_font_name);
	oled_init_row_layout();
	s_display = lv_display_create(OLED_H_RES, OLED_V_RES);
	lv_display_set_user_data(s_display, s_panel);
	// 1-bit 单色屏关闭抗锯齿可避免细笔画在灰阶阈值时被稳定“吃掉”
	lv_display_set_antialiasing(s_display, false);

	size_t draw_sz = OLED_H_RES * OLED_V_RES / 8 + OLED_LVGL_PALETTE;
	void* buf = heap_caps_calloc(1, draw_sz, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
	if (!buf) {
		ESP_LOGE(TAG, "LVGL buffer alloc failed");
		err = ESP_ERR_NO_MEM;
		goto fail_bus;
	}

	lv_display_set_color_format(s_display, LV_COLOR_FORMAT_I1);
	lv_display_set_buffers(s_display, buf, NULL, draw_sz, LV_DISPLAY_RENDER_MODE_FULL);
	lv_display_set_flush_cb(s_display, oled_flush_cb);

	const esp_lcd_panel_io_callbacks_t cbs = { .on_color_trans_done = oled_notify_flush_ready };
	err = esp_lcd_panel_io_register_event_callbacks(io_handle, &cbs, s_display);
	if (err != ESP_OK) {
		ESP_LOGE(TAG, "esp_lcd_panel_io_register_event_callbacks failed: %s", esp_err_to_name(err));
		goto fail_bus;
	}

	esp_timer_create_args_t tick_args = {
		.callback = &oled_lvgl_tick_cb,
		.name = "oled_lvgl_tick",
	};
	esp_timer_handle_t tick = NULL;
	err = esp_timer_create(&tick_args, &tick);
	if (err != ESP_OK) {
		ESP_LOGE(TAG, "esp_timer_create failed: %s", esp_err_to_name(err));
		goto fail_bus;
	}
	err = esp_timer_start_periodic(tick, OLED_LVGL_TICK_MS * 1000);
	if (err != ESP_OK) {
		ESP_LOGE(TAG, "esp_timer_start_periodic failed: %s", esp_err_to_name(err));
		goto fail_bus;
	}

	if (s_text_queue == NULL) {
		s_text_queue = xQueueCreate(OLED_TEXT_QUEUE_LEN, sizeof(oled_text_req_t));
		if (s_text_queue == NULL) {
			ESP_LOGE(TAG, "text queue create failed");
			err = ESP_ERR_NO_MEM;
			goto fail_bus;
		}
	}

	_lock_acquire(&s_lvgl_lock);
	oled_build_static_ui();
	oled_apply_network_labels();
	lv_obj_invalidate(s_scr);
	lv_refr_now(s_display);
	_lock_release(&s_lvgl_lock);

	s_inited = true;

	xTaskCreatePinnedToCore(oled_lvgl_task, "task_oled_lvgl", TASK_OLED_LVGL_STACK, NULL,
		TASK_OLED_LVGL_PRIO, NULL, 0);

	ESP_LOGI(TAG, "OLED init OK (128x64 SSD1306), font=%s, cjk_font=%s, antialias=off", OLED_FONT_NAME, s_cjk_active_font_name);
	return ESP_OK;

fail_bus:
	if (s_i2c_bus_owned && s_i2c_bus) {
		i2c_del_master_bus(s_i2c_bus);
		s_i2c_bus = NULL;
	}
	return err;
}

void oled_text_set_at(int x, int y, const char* text)
{
	if (!s_inited || text == NULL || s_text_queue == NULL) {
		return;
	}

	oled_text_req_t req = {
		.x = x,
		.y = y,
	};
	strncpy(req.text, text, sizeof(req.text) - 1);
	req.text[sizeof(req.text) - 1] = '\0';

	if (xQueueSend(s_text_queue, &req, 0) != pdTRUE) {
		ESP_LOGW(TAG, "text queue full, drop: (%d,%d) %s", x, y, text);
	}
}

void oled_refresh_network_info(void)
{
	s_net_refresh_pending = true;
}

void oled_request_network_refresh(void)
{
	oled_refresh_network_info();
}
