// SPDX-License-Identifier: Apache-2.0
#include <lvgl/devices/pointer.h>
#include <lvgl/devices/device_context.h>

#include <tactility/device.h>
#include <tactility/drivers/pointer.h>

#include <cstring>
#include <new>

constexpr auto* TAG = "lvgl_pointer";

// Bus reads are expected to complete quickly; bound the wait so a stalled controller can't block the LVGL indev poll.
static const TickType_t LVGL_POINTER_READ_TIMEOUT = pdMS_TO_TICKS(10);

// Tracks the first indev created by lvgl_pointer_add() still alive, for lvgl_pointer_get_default().
// Only ever set/cleared by lvgl_pointer_add()/lvgl_pointer_remove(), so it can never point at an
// indev created by other code (e.g. the deprecated HAL's own LVGL pointer registration).
static lv_indev_t* default_pointer_indev = NULL;

// Mirrors Tactility/Source/settings/TouchCalibrationSettings.cpp's isValid().
static const int32_t LVGL_POINTER_CALIBRATION_MIN_RANGE = 20;

// Caps nearest-neighbor slot tracking (lvgl_pointer_pool_assign) to a fraction of screen width,
// so a lifted finger's slot doesn't jump to grab an unrelated new touch elsewhere on screen.
// Scaled by resolution, not a flat pixel value, so it stays proportionate on any display size.
// Set generously \wide: a too-tight cap misreads a fast scroll's own motion as release+re-press,
// firing whatever is under the finger mid-drag.
//
// \wide False positives (treating one continued drag as two separate touches) are far more
//     disruptive than false negatives (merging an unrelated same-spot lift+relanding), which
//     favors erring toward a larger cap.
static const int32_t LVGL_POINTER_MAX_TRACK_DIST_FRACTION = 3; // 1/3 of screen width

// One physical touch device backs LVGL_POINTER_MAX_SLOTS independent lv_indev_t instances (a
// "pool"), so LVGL can track that many simultaneous fingers - LVGL v9 has no multi-point indev
// concept (confirmed against lv_indev.h/lv_indev.c: lv_indev_data_t carries exactly one
// lv_point_t/state pair), so N simultaneous independently-clickable widgets requires N indevs.
// Each slot's read callback shares one raw multi-touch read per round rather than each slot
// hitting the bus independently - see lvgl_pointer_read_cb().
struct LvglPointerPool {
    struct Device* device;
    bool calibration_enabled;
    struct LvglPointerCalibration calibration;

    uint8_t slot_count;
    lv_indev_t* slot_indev[LVGL_POINTER_MAX_SLOTS];

    // Per-slot "which finger" tracking. A slot with active=false reports RELEASED and is up for
    // grabs by any unmatched raw point next round.
    bool slot_active[LVGL_POINTER_MAX_SLOTS];
    lv_point_t slot_point[LVGL_POINTER_MAX_SLOTS];

    // Shared raw-read cache for this round. Refreshed by whichever slot's read callback runs
    // first each round (see round_pos); the rest just consume it - one bus transaction per
    // slot_count read callbacks, not one per slot.
    uint8_t round_pos;
    uint16_t raw_x[LVGL_POINTER_MAX_SLOTS];
    uint16_t raw_y[LVGL_POINTER_MAX_SLOTS];
    uint8_t raw_count;
};

// Safely narrows an arbitrary lv_indev_t's driver data down to this module's LvglPointerPool,
// or NULL if indev is NULL, wasn't created by lvgl_pointer_add() (LvglDeviceContext is shared by
// every LVGL indev/display this module creates - pointer, trackball, keyboard - so driver_data
// alone doesn't prove it's a pointer pool), or is mid-teardown (context nulled by
// lvgl_pointer_remove()/lvgl_pointer_add()'s cleanup path before the wrapper itself is freed).
static struct LvglPointerPool* lvgl_pointer_pool_from_indev(lv_indev_t* indev) {
    if (indev == NULL) {
        return NULL;
    }
    auto* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
    if (wrapper == NULL || wrapper->device == NULL || device_get_type(wrapper->device) != &POINTER_TYPE) {
        return NULL;
    }
    return (struct LvglPointerPool*)wrapper->context;
}

static bool lvgl_pointer_calibration_is_valid(const struct LvglPointerCalibration* calibration) {
    return calibration->x_max > calibration->x_min &&
        calibration->y_max > calibration->y_min &&
        (calibration->x_max - calibration->x_min) >= LVGL_POINTER_CALIBRATION_MIN_RANGE &&
        (calibration->y_max - calibration->y_min) >= LVGL_POINTER_CALIBRATION_MIN_RANGE;
}

// Linear per-axis rescale of [x_min,x_max]/[y_min,y_max] onto [0,target_x_max]/[0,target_y_max],
// clamped. Mirrors TouchCalibrationSettings.cpp's applyCalibration(). Kept as a standalone
// function (not inlined into the read callback) so the math is isolated and easy to reason about.
static void lvgl_pointer_calibration_apply(
    const struct LvglPointerCalibration* calibration,
    int32_t target_x_max,
    int32_t target_y_max,
    uint16_t* x,
    uint16_t* y
) {
    int64_t mapped_x = ((int64_t)*x - calibration->x_min) * target_x_max /
        ((int64_t)calibration->x_max - calibration->x_min);
    int64_t mapped_y = ((int64_t)*y - calibration->y_min) * target_y_max /
        ((int64_t)calibration->y_max - calibration->y_min);

    if (mapped_x < 0) mapped_x = 0;
    if (mapped_x > target_x_max) mapped_x = target_x_max;
    if (mapped_y < 0) mapped_y = 0;
    if (mapped_y > target_y_max) mapped_y = target_y_max;

    *x = (uint16_t)mapped_x;
    *y = (uint16_t)mapped_y;
}

// Reads all currently-touched points from the device into pool->raw_x/raw_y/raw_count, applying
// calibration in the graphics driver's own native (LV_DISPLAY_ROTATION_0) coordinate space -
// native_x_max/native_y_max are just the panel's fixed pixel dimensions, not a rotation. This
// function has no notion of LVGL rotation at all: calibration corrects the raw sensor's fixed
// physical mapping, which never changes with on-screen orientation, so it doesn't belong anywhere
// near rotation math.
static void lvgl_pointer_pool_refresh(struct LvglPointerPool* pool, int32_t native_x_max, int32_t native_y_max) {
    pool->raw_count = 0;

    if (pointer_read_data(pool->device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) {
        return;
    }

    uint8_t point_count = 0;
    if (!pointer_get_touched_points(pool->device, pool->raw_x, pool->raw_y, NULL, &point_count, LVGL_POINTER_MAX_SLOTS) || point_count == 0) {
        return;
    }
    if (point_count > LVGL_POINTER_MAX_SLOTS) point_count = LVGL_POINTER_MAX_SLOTS;

    if (pool->calibration_enabled && native_x_max > 0 && native_y_max > 0) {
        for (uint8_t i = 0; i < point_count; i++) {
            lvgl_pointer_calibration_apply(&pool->calibration, native_x_max, native_y_max, &pool->raw_x[i], &pool->raw_y[i]);
        }
    }

    pool->raw_count = point_count;
}

// Matches this round's raw points onto pool slots by nearest-neighbor to each slot's last known
// position, so a slot "follows" the same physical finger across rounds instead of jumping when
// the touch controller reports points in a different order (no touch-ID/tracking field exists
// anywhere in this stack - see esp_lcd_touch_get_coordinates()/PointerApi.get_touched_points()).
// Unmatched raw points (new touches) claim the nearest inactive slot. Slots with no matching
// point this round go inactive (RELEASED).
static void lvgl_pointer_pool_assign(struct LvglPointerPool* pool, int32_t native_x_max) {
    // Touch drivers clamp raw coordinates to the panel's configured native resolution regardless
    // of calibration, so native_x_max is a valid scale reference for the distance cap even when calibration is disabled. 
    // Falls back to a conservative fixed pixel value if the display/resolution isn't available for some reason.
    const int32_t max_track_dist = native_x_max > 0 ? (native_x_max / LVGL_POINTER_MAX_TRACK_DIST_FRACTION) : 150;
    const int32_t max_track_dist_sq = max_track_dist * max_track_dist;

    bool raw_claimed[LVGL_POINTER_MAX_SLOTS] = {};
    // A slot released this round must report RELEASED for at least one round before it can host
    // a new touch - otherwise pass 2 immediately reassigns it, and LVGL sees a jump instead of a
    // release-then-press.
    bool slot_released_now[LVGL_POINTER_MAX_SLOTS] = {};

    // First pass: let already-active slots keep following their nearest raw point, so a held
    // finger doesn't get reshuffled onto a different slot just because another finger moved.
    for (uint8_t s = 0; s < pool->slot_count; s++) {
        if (!pool->slot_active[s]) continue;
        int32_t best_dist = -1;
        int8_t best_raw = -1;
        for (uint8_t r = 0; r < pool->raw_count; r++) {
            if (raw_claimed[r]) continue;
            int32_t dx = (int32_t)pool->raw_x[r] - pool->slot_point[s].x;
            int32_t dy = (int32_t)pool->raw_y[r] - pool->slot_point[s].y;
            int32_t dist = dx * dx + dy * dy;
            if (best_raw < 0 || dist < best_dist) {
                best_dist = dist;
                best_raw = (int8_t)r;
            }
        }
        if (best_raw >= 0 && best_dist <= max_track_dist_sq) {
            raw_claimed[best_raw] = true;
            pool->slot_point[s].x = (lv_coord_t)pool->raw_x[best_raw];
            pool->slot_point[s].y = (lv_coord_t)pool->raw_y[best_raw];
        } else {
            pool->slot_active[s] = false;
            slot_released_now[s] = true;
        }
    }

    // Second pass: any unclaimed raw point is a new touch - hand it to the first inactive slot
    // that wasn't just released this round.
    for (uint8_t r = 0; r < pool->raw_count; r++) {
        if (raw_claimed[r]) continue;
        for (uint8_t s = 0; s < pool->slot_count; s++) {
            if (pool->slot_active[s] || slot_released_now[s]) continue;
            pool->slot_active[s] = true;
            pool->slot_point[s].x = (lv_coord_t)pool->raw_x[r];
            pool->slot_point[s].y = (lv_coord_t)pool->raw_y[r];
            raw_claimed[r] = true;
            break;
        }
    }
}

// The actual LVGL indev read callback, shared by every slot in the pool. Only the first slot to
// be read each round (round_pos wraps 0..slot_count-1) triggers the real bus read + reassignment;
// the rest just report whatever lvgl_pointer_pool_assign() decided for their slot. Which slot
// happens to run first varies (LVGL calls each indev's timer independently), but since all slots
// share one timer period they complete one full round every slot_count calls regardless of order.
static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
    struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
    if (pool == NULL) {
        data->state = LV_INDEV_STATE_RELEASED;
        return;
    }

    uint8_t slot = 0;
    for (; slot < pool->slot_count; slot++) {
        if (pool->slot_indev[slot] == indev) break;
    }

    if (pool->round_pos == 0) {
        lv_display_t* display = lv_indev_get_display(indev);
        // lv_display_get_original_*_resolution() is the native (LV_DISPLAY_ROTATION_0) size,
        // unaffected by the display's current rotation - no rotation lookup needed to get it.
        int32_t native_x_max = display != NULL ? lv_display_get_original_horizontal_resolution(display) - 1 : 0;
        int32_t native_y_max = display != NULL ? lv_display_get_original_vertical_resolution(display) - 1 : 0;
        lvgl_pointer_pool_refresh(pool, native_x_max, native_y_max);
        lvgl_pointer_pool_assign(pool, native_x_max);
    }
    pool->round_pos = (uint8_t)((pool->round_pos + 1) % pool->slot_count);

    if (slot < pool->slot_count && pool->slot_active[slot]) {
        data->point = pool->slot_point[slot];
        data->state = LV_INDEV_STATE_PRESSED;
    } else {
        data->state = LV_INDEV_STATE_RELEASED;
    }
}

error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, uint8_t max_touch_points, lv_indev_t** out_indevs) {
    if (device == NULL || out_indevs == NULL) {
        return ERROR_INVALID_ARGUMENT;
    }
    if (device_get_type(device) != &POINTER_TYPE) {
        return ERROR_INVALID_ARGUMENT;
    }
    if (max_touch_points == 0) max_touch_points = 1;
    if (max_touch_points > LVGL_POINTER_MAX_SLOTS) max_touch_points = LVGL_POINTER_MAX_SLOTS;

    auto* pool = new(std::nothrow) LvglPointerPool();
    if (pool == NULL) {
        return ERROR_OUT_OF_MEMORY;
    }
    pool->device = device;
    pool->slot_count = max_touch_points;

    // Every slot gets its own LvglDeviceContext wrapper, but all of them point at this same pool.
    // On the way out - success or failure - every wrapper's context pointer is nulled before any
    // of them are deleted, then the pool itself is freed exactly once: LvglDeviceContext's
    // destructor does `::operator delete(context)`, so leaving more than one wrapper owning the
    // same pool pointer would double-free it (see lvgl_pointer_remove(), same pattern).
    uint8_t created = 0;
    for (; created < max_touch_points; created++) {
        auto* wrapper = new(std::nothrow) LvglDeviceContext(pool);
        if (wrapper == NULL) {
            break;
        }
        wrapper->device = device;

        lv_indev_t* indev = lv_indev_create();
        if (indev == NULL) {
            delete wrapper;
            break;
        }

        lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
        lv_indev_set_read_cb(indev, lvgl_pointer_read_cb);
        lv_indev_set_driver_data(indev, wrapper);
        if (display != NULL) {
            lv_indev_set_display(indev, display);
        }

        pool->slot_indev[created] = indev;
        out_indevs[created] = indev;
    }

    if (created < max_touch_points) {
        for (uint8_t j = 0; j < created; j++) {
            lv_indev_t* slot_indev = pool->slot_indev[j];
            auto* slot_wrapper = (LvglDeviceContext*)lv_indev_get_driver_data(slot_indev);
            slot_wrapper->context = NULL;
            lv_indev_delete(slot_indev);
            delete slot_wrapper;
        }
        delete pool;
        return ERROR_OUT_OF_MEMORY;
    }

    if (default_pointer_indev == NULL) {
        default_pointer_indev = pool->slot_indev[0];
    }

    return ERROR_NONE;
}

lv_indev_t* lvgl_pointer_get_default(void) {
    return default_pointer_indev;
}

// Any slot indev's wrapper->context points at the same shared pool, so calibration set through
// any one of them applies to the whole physical device/all its slots (one panel, one calibration).
error_t lvgl_pointer_set_calibration(lv_indev_t* indev, const struct LvglPointerCalibration* calibration) {
    struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
    if (pool == NULL) {
        return ERROR_INVALID_ARGUMENT;
    }

    if (calibration == NULL) {
        pool->calibration_enabled = false;
        return ERROR_NONE;
    }
    if (!lvgl_pointer_calibration_is_valid(calibration)) {
        return ERROR_INVALID_ARGUMENT;
    }

    pool->calibration = *calibration;
    pool->calibration_enabled = true;
    return ERROR_NONE;
}

bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibration* out_calibration) {
    if (out_calibration == NULL) {
        return false;
    }
    struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
    if (pool == NULL || !pool->calibration_enabled) {
        return false;
    }
    *out_calibration = pool->calibration;
    return true;
}

int8_t lvgl_pointer_get_slot_index(lv_indev_t* indev) {
    struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
    if (pool == NULL) {
        return -1;
    }
    for (uint8_t i = 0; i < pool->slot_count; i++) {
        if (pool->slot_indev[i] == indev) return (int8_t)i;
    }
    return -1;
}

// Removes every slot indev belonging to the same pool as `indev` (a partial pool removal isn't a
// case that comes up: apps ask for "the pointer device" and get every slot back from
// lvgl_pointer_add(), so they hold either all of a pool's indevs or none).
void lvgl_pointer_remove(lv_indev_t* indev) {
    if (indev == NULL) {
        return;
    }

    struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
    struct LvglPointerPool* pool = (struct LvglPointerPool*)wrapper->context;
    uint8_t slot_count = pool->slot_count;
    lv_indev_t* slot_indevs[LVGL_POINTER_MAX_SLOTS];
    memcpy(slot_indevs, pool->slot_indev, sizeof(lv_indev_t*) * slot_count);

    // Every LvglDeviceContext wrapper points at the same pool; null `context` out on all of them
    // before deleting any (LvglDeviceContext's destructor frees `context`, and pool must stay
    // valid for every wrapper's own delete to run safely) - then free the pool exactly once
    // ourselves at the end.
    for (uint8_t i = 0; i < slot_count; i++) {
        lv_indev_t* slot_indev = slot_indevs[i];
        if (slot_indev == NULL) continue;
        if (default_pointer_indev == slot_indev) {
            default_pointer_indev = NULL;
        }
        struct LvglDeviceContext* slot_wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(slot_indev);
        slot_wrapper->context = NULL;
        lv_indev_delete(slot_indev);
        delete slot_wrapper;
    }
    delete pool;
}
