// SPDX-License-Identifier: Apache-2.0
#include "tab5_keyboard.h"

#include "devices_common.h"

#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/log.h>
#include <lvgl/lvgl.h>
#include <tactility/module.h>

#include <driver/gpio.h>
#include <esp_timer.h>

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

#include <lvgl.h>

#include <cstdlib>
#include <cstring>

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

static constexpr uint8_t I2C_ADDRESS = 0x6D;

// Software key-repeat timing
static constexpr uint32_t REPEAT_INITIAL_MS = 400;
static constexpr uint32_t REPEAT_RATE_MS = 80;

// I2C event-poll interval - mirrors the old deprecated-HAL's 20ms Timer period. Drives both
// REG_INT_STAT polling (when no IRQ pin) and software key-repeat ticking.
static constexpr uint32_t POLL_INTERVAL_MS = 20;

// Hot-plug attach-state check interval. I2C probes can false-positive on a floating/half-connected
// bus (e.g. mid-unplug), so a state change is only acted on once it's seen on two consecutive
// checks in a row (see check_attach_state()).
static constexpr uint32_t ATTACH_CHECK_INTERVAL_MS = 1000;

// ---------------------------------------------------------------------------
// Register addresses
// ---------------------------------------------------------------------------
static constexpr uint8_t REG_INT_CFG = 0x00;
static constexpr uint8_t REG_INT_STAT = 0x01;
static constexpr uint8_t REG_EVENT_NUM = 0x02;
static constexpr uint8_t REG_BRIGHTNESS = 0x03;
static constexpr uint8_t REG_KEYBOARD_MODE = 0x10;
static constexpr uint8_t REG_RGB_MODE = 0x11;
static constexpr uint8_t REG_KEY_EVENT = 0x20;
static constexpr uint8_t REG_RGB_BASE = 0x60;
static constexpr uint8_t KEY_EVENT_EMPTY = 0xFF;

// ---------------------------------------------------------------------------
// Modifier key positions in the 5x14 matrix
// ---------------------------------------------------------------------------
static constexpr uint8_t MOD_ROW_SYM = 3, MOD_COL_SYM = 0;
static constexpr uint8_t MOD_ROW_AA = 3, MOD_COL_AA = 1;
static constexpr uint8_t MOD_ROW_CTRL = 4, MOD_COL_CTRL = 0;
static constexpr uint8_t MOD_ROW_ALT = 4, MOD_COL_ALT = 1;

// ---------------------------------------------------------------------------
// HID lookup tables
// Row-major: index = row * 14 + col, 5 rows x 14 cols = 70 entries.
// modifier 0x02 = Left Shift (pre-baked by firmware for shifted characters).
// ---------------------------------------------------------------------------
struct HidMapping {
    uint8_t keycode;
    uint8_t modifier;
};

static constexpr HidMapping KEY_MATRIX_HID_BASE[70] = {
    // Row 0: Esc 1 2 3 4 5 6 7 8 9 0 - + Del
    {0x29, 0x00}, {0x1E, 0x00}, {0x1F, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00},
    {0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, {0x2D, 0x00},
    {0x2E, 0x02}, {0x4C, 0x00},
    // Row 1: ` ! @ # $ % ^ & * ( ) [ ] backslash
    {0x35, 0x00}, {0x1E, 0x02}, {0x1F, 0x02}, {0x20, 0x02}, {0x21, 0x02}, {0x22, 0x02},
    {0x23, 0x02}, {0x24, 0x02}, {0x25, 0x02}, {0x26, 0x02}, {0x27, 0x02}, {0x2F, 0x00},
    {0x30, 0x00}, {0x31, 0x00},
    // Row 2: Tab q w e r t y u i o p ; ' Backspace
    {0x2B, 0x00}, {0x14, 0x00}, {0x1A, 0x00}, {0x08, 0x00}, {0x15, 0x00}, {0x17, 0x00},
    {0x1C, 0x00}, {0x18, 0x00}, {0x0C, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x33, 0x00},
    {0x34, 0x00}, {0x2A, 0x00},
    // Row 3: Sym Aa a s d f g h j k l ↑ _ Enter
    {0x00, 0x00}, {0x00, 0x00}, {0x04, 0x00}, {0x16, 0x00}, {0x07, 0x00}, {0x09, 0x00},
    {0x0A, 0x00}, {0x0B, 0x00}, {0x0D, 0x00}, {0x0E, 0x00}, {0x0F, 0x00}, {0x52, 0x00},
    {0x2D, 0x02}, {0x28, 0x00},
    // Row 4: Ctrl Alt z x c v b n m . ← ↓ → Space
    {0x00, 0x00}, {0x00, 0x00}, {0x1D, 0x00}, {0x1B, 0x00}, {0x06, 0x00}, {0x19, 0x00},
    {0x05, 0x00}, {0x11, 0x00}, {0x10, 0x00}, {0x37, 0x00}, {0x50, 0x00}, {0x51, 0x00},
    {0x4F, 0x00}, {0x2C, 0x00},
};

static constexpr HidMapping KEY_MATRIX_HID_SYM[70] = {
    // Row 0: identical to base
    {0x29, 0x00}, {0x1E, 0x00}, {0x1F, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00},
    {0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, {0x2D, 0x00},
    {0x2E, 0x02}, {0x4C, 0x00},
    // Row 1: Sym deltas: ` → ~, ! → ?, * → /, ( → <, ) → >, [ → {, ] → }, backslash → |
    {0x35, 0x02}, {0x38, 0x02}, {0x1F, 0x02}, {0x20, 0x02}, {0x21, 0x02}, {0x22, 0x02},
    {0x23, 0x02}, {0x24, 0x02}, {0x38, 0x00}, {0x36, 0x02}, {0x37, 0x02}, {0x2F, 0x02},
    {0x30, 0x02}, {0x31, 0x02},
    // Row 2: Sym deltas: ; → :, ' → "
    {0x2B, 0x00}, {0x14, 0x00}, {0x1A, 0x00}, {0x08, 0x00}, {0x15, 0x00}, {0x17, 0x00},
    {0x1C, 0x00}, {0x18, 0x00}, {0x0C, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x33, 0x02},
    {0x34, 0x02}, {0x2A, 0x00},
    // Row 3: Sym delta: _ → =
    {0x00, 0x00}, {0x00, 0x00}, {0x04, 0x00}, {0x16, 0x00}, {0x07, 0x00}, {0x09, 0x00},
    {0x0A, 0x00}, {0x0B, 0x00}, {0x0D, 0x00}, {0x0E, 0x00}, {0x0F, 0x00}, {0x52, 0x00},
    {0x2E, 0x00}, {0x28, 0x00},
    // Row 4: Sym delta: . → ,
    {0x00, 0x00}, {0x00, 0x00}, {0x1D, 0x00}, {0x1B, 0x00}, {0x06, 0x00}, {0x19, 0x00},
    {0x05, 0x00}, {0x11, 0x00}, {0x10, 0x00}, {0x36, 0x00}, {0x50, 0x00}, {0x51, 0x00},
    {0x4F, 0x00}, {0x2C, 0x00},
};

// ---------------------------------------------------------------------------
// HID usage code + modifier → LVGL key
// Covers all codes present in the Tab5 matrix tables above. LV_KEY_* are plain uint32_t
// constants - matching KeyboardKeyData::key's driver-defined contract and the same convention
// m5stack-module's cardputer_keyboard.cpp kernel driver already uses.
// ---------------------------------------------------------------------------
static uint32_t tab5_translate_key(uint8_t keycode, uint8_t modifier, bool ctrl) {
    const bool shift = (modifier & 0x22U) != 0U;

    // Navigation → LVGL key constants
    switch (keycode) {
        case 0x29: return LV_KEY_ESC;
        case 0x28: return LV_KEY_ENTER;
        case 0x2A: return LV_KEY_BACKSPACE;
        case 0x4C: return LV_KEY_DEL;
        case 0x2B: return '\t';
        // Arrows: Ctrl+arrow = focus navigation, plain arrow = raw cursor movement
        case 0x52: return ctrl ? (uint32_t)LV_KEY_PREV : (uint32_t)LV_KEY_UP;
        case 0x51: return ctrl ? (uint32_t)LV_KEY_NEXT : (uint32_t)LV_KEY_DOWN;
        case 0x50: return ctrl ? (uint32_t)LV_KEY_PREV : (uint32_t)LV_KEY_LEFT;
        case 0x4F: return ctrl ? (uint32_t)LV_KEY_NEXT : (uint32_t)LV_KEY_RIGHT;
        default: break;
    }

    // Letters a–z / A–Z
    if (keycode >= 0x04U && keycode <= 0x1DU) {
        uint32_t c = static_cast<uint32_t>('a' + (keycode - 0x04U));
        return shift ? (c - 0x20U) : c;
    }

    // Numbers 1–0 and their shifted symbols
    if (keycode >= 0x1EU && keycode <= 0x27U) {
        static constexpr char nums[] = "1234567890";
        static constexpr char snums[] = "!@#$%^&*()";
        return shift ? static_cast<uint32_t>(snums[keycode - 0x1EU])
                     : static_cast<uint32_t>(nums[keycode - 0x1EU]);
    }

    // Space and punctuation - all codes present in the Tab5 matrix
    switch (keycode) {
        case 0x2C: return ' ';
        case 0x2D: return shift ? '_' : '-';
        case 0x2E: return shift ? '+' : '=';
        case 0x2F: return shift ? '{' : '[';
        case 0x30: return shift ? '}' : ']';
        case 0x31: return shift ? '|' : '\\';
        case 0x33: return shift ? ':' : ';';
        case 0x34: return shift ? '"' : '\'';
        case 0x35: return shift ? '~' : '`';
        case 0x36: return shift ? '<' : ',';
        case 0x37: return shift ? '>' : '.';
        case 0x38: return shift ? '?' : '/';
        default: return 0;
    }
}

static uint32_t now_ms() {
    return static_cast<uint32_t>(esp_timer_get_time() / 1000);
}

struct Tab5KeyboardInternal {
    QueueHandle_t queue;

    // Modifier state
    bool sym_active;
    bool aa_sticky;
    bool aa_held;
    bool aa_tapped;
    bool ctrl_held;

    // IRQ-driven event gating
    volatile bool irq_pending;
    bool irq_configured;
    gpio_num_t irq_pin;

    // Poll/attach-check throttling (real-time based, since read_key() is called at whatever rate
    // LVGL's indev timer and its own drain-loop - via continue_reading - happen to run at, unlike
    // the old deprecated-HAL's fixed 20ms Timer)
    uint32_t last_poll_ms;
    uint32_t last_attach_check_ms;
    bool was_attached;
    bool pending_attach_state;
    uint8_t pending_attach_confirm_count;

    // Software key-repeat state (tracked by position to survive modifier changes)
    uint32_t repeat_key;
    uint8_t repeat_row;
    uint8_t repeat_col;
    uint32_t repeat_start_ms;
    uint32_t repeat_last_ms;

    Tab5KeyboardAttachListener attach_listener;
    void* attach_listener_context;
};

// ---------------------------------------------------------------------------
// I2C helpers
// ---------------------------------------------------------------------------
static bool read_reg(Device* device, uint8_t reg, uint8_t* value) {
    auto* parent = device_get_parent(device);
    return i2c_controller_read_register(parent, I2C_ADDRESS, reg, value, 1, pdMS_TO_TICKS(50)) == ERROR_NONE;
}

static bool write_reg(Device* device, uint8_t reg, uint8_t value) {
    auto* parent = device_get_parent(device);
    return i2c_controller_write_register(parent, I2C_ADDRESS, reg, &value, 1, pdMS_TO_TICKS(50)) == ERROR_NONE;
}

static bool is_attached_raw(Device* device) {
    auto* parent = device_get_parent(device);
    return i2c_controller_has_device_at_address(parent, I2C_ADDRESS, pdMS_TO_TICKS(100)) == ERROR_NONE;
}

// ---------------------------------------------------------------------------
// LED helpers - LED0 = Sym indicator (green), LED1 = Aa indicator (red)
// RGB register layout: [B, G, R] per LED, stride 4 (byte 3 reserved)
// ---------------------------------------------------------------------------
static void update_leds(Device* device, const Tab5KeyboardInternal* internal) {
    auto* parent = device_get_parent(device);
    // [LED0: B,G,R, reserved, LED1: B,G,R]
    uint8_t buf[7] = {
        0x00, internal->sym_active ? uint8_t(0xA0) : uint8_t(0x00), 0x00, 0x00,
        0x00, 0x00, internal->aa_sticky ? uint8_t(0xA0) : uint8_t(0x00),
    };
    i2c_controller_write_register(parent, I2C_ADDRESS, REG_RGB_BASE, buf, 7, pdMS_TO_TICKS(50));
}

// ---------------------------------------------------------------------------
// IRQ pin - active-low, falling edge
// ---------------------------------------------------------------------------
static void IRAM_ATTR irq_handler(void* arg) {
    auto* internal = static_cast<Tab5KeyboardInternal*>(arg);
    internal->irq_pending = true;
}

static bool configure_irq_pin(Tab5KeyboardInternal* internal) {
    gpio_config_t io_conf{};
    io_conf.pin_bit_mask = (1ULL << internal->irq_pin);
    io_conf.mode = GPIO_MODE_INPUT;
    io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
    io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
    io_conf.intr_type = GPIO_INTR_NEGEDGE;
    if (gpio_config(&io_conf) != ESP_OK) {
        return false;
    }

    const esp_err_t svc = gpio_install_isr_service(0);
    if (svc != ESP_OK && svc != ESP_ERR_INVALID_STATE) {
        return false;
    }

    if (gpio_isr_handler_add(internal->irq_pin, irq_handler, internal) != ESP_OK) {
        gpio_set_intr_type(internal->irq_pin, GPIO_INTR_DISABLE);
        return false;
    }

    internal->irq_configured = true;
    return true;
}

static void remove_irq_pin(Tab5KeyboardInternal* internal) {
    if (!internal->irq_configured) {
        return;
    }
    gpio_isr_handler_remove(internal->irq_pin);
    internal->irq_configured = false;
    internal->irq_pending = false;
}

// ---------------------------------------------------------------------------
// drain_events - reads all pending events from the device queue
// ---------------------------------------------------------------------------
static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
    uint8_t count = 0;
    if (!read_reg(device, REG_EVENT_NUM, &count) || count == 0) {
        return;
    }

    while (count > 0) {
        uint8_t raw = 0;
        if (!read_reg(device, REG_KEY_EVENT, &raw) || raw == KEY_EVENT_EMPTY) {
            break;
        }

        const bool pressed = (raw & 0x80U) != 0U;
        const uint8_t row = (raw >> 4U) & 0x07U;
        const uint8_t col = raw & 0x0FU;

        // Modifier keys: update state, no key output
        if (row == MOD_ROW_SYM && col == MOD_COL_SYM) {
            internal->sym_active = pressed;
            update_leds(device, internal);
            count--;
            continue;
        }
        if (row == MOD_ROW_AA && col == MOD_COL_AA) {
            if (pressed) {
                internal->aa_held = true;
                internal->aa_tapped = true; // assume tap until a real key is pressed while held
            } else {
                // Only latch sticky if no non-modifier key was pressed during this hold
                if (internal->aa_tapped) {
                    internal->aa_sticky = !internal->aa_sticky;
                }
                internal->aa_held = false;
                internal->aa_tapped = false;
            }
            update_leds(device, internal);
            count--;
            continue;
        }
        if (row == MOD_ROW_CTRL && col == MOD_COL_CTRL) {
            internal->ctrl_held = pressed;
            count--;
            continue;
        }
        if (row == MOD_ROW_ALT && col == MOD_COL_ALT) {
            count--;
            continue;
        }

        if (row < 5U && col < 14U) {
            const bool aa_active = internal->aa_held || internal->aa_sticky;
            const HidMapping& m = internal->sym_active
                ? KEY_MATRIX_HID_SYM[row * 14U + col]
                : KEY_MATRIX_HID_BASE[row * 14U + col];
            if (m.keycode != 0U) {
                const uint8_t modifier = static_cast<uint8_t>(m.modifier | (aa_active ? 0x02U : 0U));
                const uint32_t lv_key = tab5_translate_key(m.keycode, modifier, internal->ctrl_held);
                if (lv_key != 0U) {
                    if (pressed) {
                        // A real key was pressed — this hold is a chord, not a tap
                        internal->aa_tapped = false;
                        // Note: ESC used to stop the foreground app directly (tt::app::stop()) in
                        // the deprecated-HAL version - that's an app-layer concern the driver has
                        // no business reaching into, so ESC is now just queued as a normal key
                        // like everything else (LVGL/app code already handles ESC via focus/group
                        // navigation the same way a dedicated ESC key on any other keyboard would).
                        xQueueSend(internal->queue, &lv_key, 0);
                        // Arm software repeat tracking by row/col to survive modifier changes
                        const uint32_t now = now_ms();
                        internal->repeat_key = lv_key;
                        internal->repeat_row = row;
                        internal->repeat_col = col;
                        internal->repeat_start_ms = now;
                        internal->repeat_last_ms = 0;
                        // Consume sticky Aa after one keypress
                        if (internal->aa_sticky) {
                            internal->aa_sticky = false;
                            internal->aa_held = false;
                            update_leds(device, internal);
                        }
                    } else if (row == internal->repeat_row && col == internal->repeat_col) {
                        // Match release by position, not translated value — survives sticky Aa clear
                        internal->repeat_key = 0;
                    }
                }
            }
        }
        count--;
    }

    // Clear INT status after draining so the line de-asserts
    write_reg(device, REG_INT_STAT, 0x00);
}

// ---------------------------------------------------------------------------
// reinit_device - (re)applies the device register configuration. Used at start() and again on
// hot-plug reattach, since the device's RGB mode and interrupt configuration are volatile and
// reset to power-on defaults when the keyboard is unplugged and reconnected.
// ---------------------------------------------------------------------------
static void reinit_device(Device* device, Tab5KeyboardInternal* internal) {
    write_reg(device, REG_KEYBOARD_MODE, 0x00); // Normal mode
    write_reg(device, REG_EVENT_NUM, 0x00);     // flush event queue
    write_reg(device, REG_INT_STAT, 0x00);      // clear pending INT
    write_reg(device, REG_RGB_MODE, 0x01);      // Custom RGB mode (manual LED control)
    write_reg(device, REG_BRIGHTNESS, 50);      // 50% brightness
    update_leds(device, internal);              // restore current LED state

    if (internal->irq_configured) {
        write_reg(device, REG_INT_CFG, 0x01);   // re-enable Normal-mode interrupt (bit 0)
    }
}

// ---------------------------------------------------------------------------
// check_attach_state - throttled (~1s) hot-plug detection. Reapplies device register
// configuration on reattach, and notifies the registered attach listener (if any) of confirmed
// transitions - see Tab5KeyboardAttachListener's doc comment for the retry contract.
// ---------------------------------------------------------------------------
static void check_attach_state(Device* device, Tab5KeyboardInternal* internal) {
    uint32_t now = now_ms();
    if (now - internal->last_attach_check_ms < ATTACH_CHECK_INTERVAL_MS) {
        return;
    }
    internal->last_attach_check_ms = now;

    const bool attached = is_attached_raw(device);
    if (attached == internal->was_attached) {
        internal->pending_attach_confirm_count = 0;
        return;
    }

    // Require the new state to be confirmed on a second consecutive check before acting - a
    // single probe on a floating/half-connected bus (e.g. mid-unplug) can false-positive.
    if (attached != internal->pending_attach_state || internal->pending_attach_confirm_count == 0) {
        internal->pending_attach_state = attached;
        internal->pending_attach_confirm_count = 1;
        return;
    }
    internal->pending_attach_confirm_count = 0;

    if (attached) {
        reinit_device(device, internal);
    }

    if (internal->attach_listener != nullptr) {
        if (!internal->attach_listener(device, attached, internal->attach_listener_context)) {
            return; // not handled yet (e.g. LVGL lock busy) - retry on the next confirmed check
        }
    }

    internal->was_attached = attached;
}

// ---------------------------------------------------------------------------
// poll_if_due - the closest equivalent to the old deprecated-HAL's 20ms-Timer-driven
// processKeyboard(): drains new key events (IRQ-gated or polled), ticks software key-repeat, and
// checks hot-plug attach state. Called from read_key(), throttled to real elapsed time rather
// than call count, since read_key() can be called back-to-back multiple times per LVGL indev
// timer tick while draining an already-queued burst (continue_reading).
// ---------------------------------------------------------------------------
static void poll_if_due(Device* device, Tab5KeyboardInternal* internal) {
    uint32_t now = now_ms();
    if (!internal->irq_pending && (now - internal->last_poll_ms) < POLL_INTERVAL_MS) {
        return;
    }
    internal->last_poll_ms = now;

    bool should_drain = false;
    if (internal->irq_configured) {
        if (internal->irq_pending) {
            internal->irq_pending = false;
            should_drain = true;
        }
    } else {
        // Polling: check INT_STAT first — bit 0 = Normal mode event pending
        uint8_t status = 0;
        if (read_reg(device, REG_INT_STAT, &status) && (status & 0x01U)) {
            should_drain = true;
        }
    }
    if (should_drain) {
        drain_events(device, internal);
    }

    // Software key-repeat (runs every tick regardless of IRQ)
    if (internal->repeat_key != 0U) {
        if ((now - internal->repeat_start_ms) >= REPEAT_INITIAL_MS) {
            const uint32_t last = internal->repeat_last_ms;
            if (last == 0 || (now - last) >= REPEAT_RATE_MS) {
                internal->repeat_last_ms = now;
                xQueueSend(internal->queue, &internal->repeat_key, 0);
            }
        }
    }

    check_attach_state(device, internal);
}

static gpio_num_t pin_or_nc(const GpioPinSpec& pin) {
    return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
}

// region Driver lifecycle

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

    const auto* config = GET_CONFIG(device);

    auto* internal = static_cast<Tab5KeyboardInternal*>(malloc(sizeof(Tab5KeyboardInternal)));
    if (internal == nullptr) {
        return ERROR_OUT_OF_MEMORY;
    }
    memset(internal, 0, sizeof(Tab5KeyboardInternal));

    internal->queue = xQueueCreate(20, sizeof(uint32_t));
    if (internal->queue == nullptr) {
        free(internal);
        return ERROR_OUT_OF_MEMORY;
    }

    internal->repeat_row = 0xFF;
    internal->repeat_col = 0xFF;

    internal->irq_pin = pin_or_nc(config->pin_interrupt);
    if (internal->irq_pin != GPIO_NUM_NC) {
        configure_irq_pin(internal); // best-effort; falls back to polling if it fails. Must
                                     // happen before reinit_device() so REG_INT_CFG is written
                                     // if IRQ setup succeeded.
    }

    // Best-effort: if the keyboard isn't attached yet (e.g. this device is constructed
    // speculatively at boot so it can be hot-plug-detected later), these I2C writes fail
    // silently and reinit_device() runs again once attach is detected.
    reinit_device(device, internal);
    internal->was_attached = is_attached_raw(device);

    device_set_driver_data(device, internal);
    return ERROR_NONE;
}

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

    remove_irq_pin(internal);
    write_reg(device, REG_INT_CFG, 0x00); // disable all interrupts
    internal->sym_active = false;
    internal->aa_sticky = false;
    internal->aa_held = false;
    update_leds(device, internal); // turn LEDs off

    vQueueDelete(internal->queue);
    free(internal);
    device_set_driver_data(device, nullptr);
    return ERROR_NONE;
}

// endregion

// region KeyboardApi

static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) {
    auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));

    poll_if_due(device, internal);

    uint32_t lv_key = 0;
    if (xQueueReceive(internal->queue, &lv_key, 0) == pdTRUE) {
        data->key = lv_key;
        data->pressed = true;
        data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0;
    } else {
        data->key = 0;
        data->pressed = false;
        data->continue_reading = false;
    }

    return ERROR_NONE;
}

// endregion

static const KeyboardApi tab5_keyboard_api = {
    .read_key = tab5_keyboard_read_key,
};

// Defined in module.cpp - this driver is registered directly by m5stack-tab5's own module,
// not a separate Drivers/ module.
extern Module m5stack_tab5_module;

Driver tab5_keyboard_driver = {
    .name = "tab5-keyboard",
    .compatible = (const char*[]) { "m5stack,tab5-keyboard", nullptr },
    .start_device = start,
    .stop_device = stop,
    .api = &tab5_keyboard_api,
    .device_type = &KEYBOARD_TYPE,
    .owner = &m5stack_tab5_module,
    .internal = nullptr
};

// region Attach listener

void tab5_keyboard_add_attach_listener(Device* device, Tab5KeyboardAttachListener callback, void* context) {
    auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
    if (internal->attach_listener != nullptr) {
        LOG_W(TAG, "Replacing existing attach listener without it being removed first");
    }
    internal->attach_listener = callback;
    internal->attach_listener_context = context;
}

void tab5_keyboard_remove_attach_listener(Device* device, Tab5KeyboardAttachListener callback) {
    auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
    if (internal->attach_listener != callback) {
        return;
    }
    internal->attach_listener = nullptr;
    internal->attach_listener_context = nullptr;
}

// endregion

// region Dynamic construction

// Reacts to the Tab5 keyboard accessory's hot-plug attach state (see Tab5KeyboardAttachListener's
// doc comment above for the retry contract). This is UI-layer behavior the driver itself can't do
// (it has no LVGL dependency): switch to landscape while the keyboard is attached, restoring
// whatever rotation was active before once it's removed - but only if the user hasn't manually
// changed it since attaching, in which case their choice is respected. Ported as-is from the
// deprecated HAL's Tab5Keyboard::applyAutoRotation().
static bool on_keyboard_attach_changed(Device* /*device*/, bool attached, void* /*context*/) {
    static lv_display_rotation_t saved_rotation = LV_DISPLAY_ROTATION_0;
    static bool rotation_override_active = false;

    auto* display = lv_display_get_default();
    if (display == nullptr) {
        return false; // LVGL not ready yet - retry on the next confirmed check
    }

    if (!lvgl_try_lock(pdMS_TO_TICKS(1000))) {
        return false; // retry next check
    }

    if (attached) {
        if (lv_display_get_rotation(display) != LV_DISPLAY_ROTATION_90) {
            saved_rotation = lv_display_get_rotation(display);
            rotation_override_active = true;
            lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
        }
    } else {
        // Only restore if rotation is still what we set it to - if the user manually changed it
        // since attaching, respect their choice instead.
        if (rotation_override_active && lv_display_get_rotation(display) == LV_DISPLAY_ROTATION_90) {
            lv_display_set_rotation(display, saved_rotation);
        }
        rotation_override_active = false;
    }

    lvgl_unlock();
    return true;
}

static Tab5KeyboardConfig tab5_keyboard_config {};
static Device tab5_keyboard_device {};

// The keyboard accessory is a kernel driver device (m5stack,tab5-keyboard, defined directly in
// this project). Unlike the display/touch, it isn't gated on the display-variant detection at
// all (it lives on i2c2, a separate bus) - lvgl-module binds its indev unconditionally at boot
// regardless of physical attach state, and the driver's own read_key() polling handles hot-plug
// internally.
void tab5_create_keyboard(Device* i2c2) {
    tab5_keyboard_device = Device {
        .address = 0,
        .name = "keyboard0",
        .config = nullptr,
        .parent = nullptr,
        .internal = nullptr,
    };

    GpioPinSpec pin_interrupt = GPIO_PIN_SPEC_NONE;
    Device* gpio0 = nullptr;
    if (device_get_by_name("gpio0", &gpio0) == ERROR_NONE) {
        pin_interrupt = GpioPinSpec { gpio0, 50, GPIO_FLAG_NONE };
        device_put(gpio0);
    } else {
        LOG_W(TAG, "display_detect: gpio0 not found, keyboard will fall back to INT_STAT polling");
    }

    tab5_keyboard_config = Tab5KeyboardConfig {
        .address = 0x6D,
        .pin_interrupt = pin_interrupt,
    };
    tab5_keyboard_device.config = &tab5_keyboard_config;

    // Parented to i2c2 itself (not root, unlike the display): the keyboard driver's start() uses
    // device_get_parent() as its I2C bus controller.
    if (construct_add_start(&tab5_keyboard_device, i2c2, "m5stack,tab5-keyboard")) {
        tab5_keyboard_add_attach_listener(&tab5_keyboard_device, on_keyboard_attach_changed, nullptr);
    }
}

// endregion
