// SPDX-License-Identifier: Apache-2.0
#include <drivers/cst328.h>

#include <cst328_module.h>

#include <tactility/check.h>
#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/gpio.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/pointer.h>
#include <tactility/error.h>
#include <tactility/log.h>

#include <cstdlib>
#include <utility>

#define TAG "CST328"
#define GET_CONFIG(device) (static_cast<const Cst328Config*>((device)->config))

static const TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50);

// The CST328/CST226SE protocol addresses registers via a 2-byte [class, sub-register] prefix
// (not a plain 1-byte register index), sent as a raw write, optionally followed by a read of the
// response - ported from LilyGO's SensorLib TouchClassCST226.cpp (touch/TouchClassCST226.cpp),
// the vendor driver actually used for the T-Deck Pro's CST328.
static constexpr uint8_t CST328_MAX_POINTS = 5;
static constexpr uint8_t STATUS_BUFFER_SIZE = 28;

struct Cst328Internal {
    int16_t res_x;
    int16_t res_y;
    bool swap_xy;
    bool mirror_x;
    bool mirror_y;
    uint8_t point_count;
    int16_t x[CST328_MAX_POINTS];
    int16_t y[CST328_MAX_POINTS];
    uint8_t strength[CST328_MAX_POINTS];
};

static bool write2(Device* i2c, uint8_t address, uint8_t b0, uint8_t b1) {
    uint8_t cmd[2] = { b0, b1 };
    return i2c_controller_write(i2c, address, cmd, sizeof(cmd), I2C_TIMEOUT) == ERROR_NONE;
}

static bool query(Device* i2c, uint8_t address, uint8_t b0, uint8_t b1, uint8_t* out, size_t out_len) {
    uint8_t cmd[2] = { b0, b1 };
    return i2c_controller_write_read(i2c, address, cmd, sizeof(cmd), out, out_len, I2C_TIMEOUT) == ERROR_NONE;
}

// region Driver lifecycle

// Some boards wire a dedicated reset GPIO (e.g. the T-Deck Pro's GPIO45); others rely on the
// chip's own software reset command instead.
static void perform_reset(Device* i2c, uint8_t address, const Cst328Config* config) {
    if (config->pin_reset.gpio_controller != nullptr) {
        auto* rst = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_FLAG_DIRECTION_OUTPUT | GPIO_FLAG_ACTIVE_LOW, GPIO_OWNER_GPIO);
        if (rst != nullptr) {
            gpio_descriptor_set_level(rst, true);
            delay_millis(100);
            gpio_descriptor_set_level(rst, false);
            gpio_descriptor_release(rst);
            delay_millis(100);
            return;
        }
    }

    write2(i2c, address, 0xD1, 0x0E);
    delay_millis(20);
}

// Enters command mode, reads the firmware checkcode and panel resolution, verifies the firmware
// is genuinely present, then exits command mode. Ported from TouchClassCST226::initImpl().
static bool identify_and_configure(Device* i2c, uint8_t address, Cst328Internal* internal) {
    if (!write2(i2c, address, 0xD1, 0x01)) {
        return false;
    }
    delay_millis(10);

    uint8_t buffer[8];
    if (!query(i2c, address, 0xD1, 0xFC, buffer, 4)) {
        return false;
    }
    uint32_t checkcode = (static_cast<uint32_t>(buffer[3]) << 24) | (static_cast<uint32_t>(buffer[2]) << 16) |
        (static_cast<uint32_t>(buffer[1]) << 8) | buffer[0];

    if (!query(i2c, address, 0xD1, 0xF8, buffer, 4)) {
        return false;
    }
    internal->res_x = static_cast<int16_t>((buffer[1] << 8) | buffer[0]);
    internal->res_y = static_cast<int16_t>((buffer[3] << 8) | buffer[2]);

    if (!query(i2c, address, 0xD2, 0x08, buffer, 8)) {
        return false;
    }
    uint32_t fw_version = (static_cast<uint32_t>(buffer[3]) << 24) | (static_cast<uint32_t>(buffer[2]) << 16) |
        (static_cast<uint32_t>(buffer[1]) << 8) | buffer[0];

    if (fw_version == 0xA5A5A5A5) {
        LOG_E(TAG, "Chip has no firmware");
        return false;
    }
    if ((checkcode & 0xFFFF0000) != 0xCACA0000) {
        LOG_E(TAG, "Firmware info read error");
        return false;
    }

    LOG_I(TAG, "CST328 identified: %dx%d, firmware 0x%08lX", internal->res_x, internal->res_y, (unsigned long)fw_version);
    return write2(i2c, address, 0xD1, 0x09); // exit command mode
}

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<Cst328Internal*>(calloc(1, sizeof(Cst328Internal)));
    if (internal == nullptr) {
        return ERROR_OUT_OF_MEMORY;
    }
    internal->swap_xy = config->swap_xy;
    internal->mirror_x = config->mirror_x;
    internal->mirror_y = config->mirror_y;

    perform_reset(parent, config->address, config);

    if (!identify_and_configure(parent, config->address, internal)) {
        // Non-fatal: matches the tolerant bring-up pattern used by other touch drivers in this
        // codebase - a transient bus hiccup on the first transaction after boot shouldn't take
        // the whole kernel down. The device just won't report touches until it responds.
        LOG_W(TAG, "CST328 identity not confirmed (continuing)");
    }

    device_set_driver_data(device, internal);
    return ERROR_NONE;
}

static error_t stop(Device* device) {
    auto* internal = static_cast<Cst328Internal*>(device_get_driver_data(device));
    free(internal);
    device_set_driver_data(device, nullptr);
    return ERROR_NONE;
}

// endregion

// region PointerApi

static error_t cst328_enter_sleep(Device* device) {
    auto* parent = device_get_parent(device);
    const auto* config = GET_CONFIG(device);
    return write2(parent, config->address, 0xD1, 0x05) ? ERROR_NONE : ERROR_RESOURCE;
}

static error_t cst328_exit_sleep(Device* device) {
    auto* parent = device_get_parent(device);
    const auto* config = GET_CONFIG(device);
    perform_reset(parent, config->address, config);
    return ERROR_NONE;
}

static error_t cst328_read_data(Device* device, TickType_t /*timeout*/) {
    auto* internal = static_cast<Cst328Internal*>(device_get_driver_data(device));
    const auto* config = GET_CONFIG(device);
    auto* parent = device_get_parent(device);

    uint8_t buffer[STATUS_BUFFER_SIZE];
    uint8_t reg = 0x00;
    if (i2c_controller_write_read(parent, config->address, &reg, 1, buffer, sizeof(buffer), I2C_TIMEOUT) != ERROR_NONE) {
        return ERROR_RESOURCE;
    }

    // Home-button gesture frame (T-Deck Pro doesn't have one wired, but the vendor protocol
    // reports it): no touch point data.
    bool is_home_gesture = buffer[0] == 0x83 && buffer[1] == 0x17 && buffer[5] == 0x80;
    if (is_home_gesture || buffer[6] != 0xAB || buffer[0] == 0xAB || buffer[5] == 0x80) {
        internal->point_count = 0;
        return ERROR_NONE;
    }

    uint8_t point = buffer[5] & 0x7F;
    if (point > CST328_MAX_POINTS || point == 0) {
        write2(parent, config->address, 0x00, 0xAB); // acknowledge/clear
        internal->point_count = 0;
        return ERROR_NONE;
    }

    // A lone point whose status nibble isn't 0x06 is a spurious single-touch frame.
    uint8_t point1_status = buffer[0] & 0x0F;
    if (point == 1 && point1_status != 0x06) {
        point = 0;
    }

    uint8_t index = 0;
    for (uint8_t i = 0; i < point; i++) {
        int16_t raw_x = static_cast<int16_t>((buffer[index + 1] << 4) | ((buffer[index + 3] >> 4) & 0x0F));
        int16_t raw_y = static_cast<int16_t>((buffer[index + 2] << 4) | (buffer[index + 3] & 0x0F));
        uint8_t pressure = buffer[index + 4];

        if (internal->swap_xy) {
            std::swap(raw_x, raw_y);
        }
        if (internal->mirror_x && internal->res_x > 0) {
            raw_x = static_cast<int16_t>(internal->res_x - 1 - raw_x);
        }
        if (internal->mirror_y && internal->res_y > 0) {
            raw_y = static_cast<int16_t>(internal->res_y - 1 - raw_y);
        }

        internal->x[i] = raw_x;
        internal->y[i] = raw_y;
        internal->strength[i] = pressure;

        // First point's slot is 7 bytes wide (id/status + x-hi + y-hi + xy-lo + pressure + 2
        // reserved); subsequent points are 5 bytes wide.
        index = static_cast<uint8_t>((i == 0) ? (index + 7) : (index + 5));
    }
    internal->point_count = point;
    return ERROR_NONE;
}

static bool cst328_get_touched_points(Device* device, uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* point_count, uint8_t max_point_count) {
    auto* internal = static_cast<Cst328Internal*>(device_get_driver_data(device));
    uint8_t count = internal->point_count < max_point_count ? internal->point_count : max_point_count;
    for (uint8_t i = 0; i < count; i++) {
        x[i] = static_cast<uint16_t>(internal->x[i]);
        y[i] = static_cast<uint16_t>(internal->y[i]);
        if (strength != nullptr) {
            strength[i] = internal->strength[i];
        }
    }
    *point_count = count;
    return count > 0;
}

static error_t cst328_set_swap_xy(Device* device, bool swap) {
    static_cast<Cst328Internal*>(device_get_driver_data(device))->swap_xy = swap;
    return ERROR_NONE;
}

static error_t cst328_get_swap_xy(Device* device, bool* swap) {
    *swap = static_cast<Cst328Internal*>(device_get_driver_data(device))->swap_xy;
    return ERROR_NONE;
}

static error_t cst328_set_mirror_x(Device* device, bool mirror) {
    static_cast<Cst328Internal*>(device_get_driver_data(device))->mirror_x = mirror;
    return ERROR_NONE;
}

static error_t cst328_get_mirror_x(Device* device, bool* mirror) {
    *mirror = static_cast<Cst328Internal*>(device_get_driver_data(device))->mirror_x;
    return ERROR_NONE;
}

static error_t cst328_set_mirror_y(Device* device, bool mirror) {
    static_cast<Cst328Internal*>(device_get_driver_data(device))->mirror_y = mirror;
    return ERROR_NONE;
}

static error_t cst328_get_mirror_y(Device* device, bool* mirror) {
    *mirror = static_cast<Cst328Internal*>(device_get_driver_data(device))->mirror_y;
    return ERROR_NONE;
}

// endregion

static const PointerApi cst328_pointer_api = {
    .enter_sleep = cst328_enter_sleep,
    .exit_sleep = cst328_exit_sleep,
    .read_data = cst328_read_data,
    .get_touched_points = cst328_get_touched_points,
    .set_swap_xy = cst328_set_swap_xy,
    .get_swap_xy = cst328_get_swap_xy,
    .set_mirror_x = cst328_set_mirror_x,
    .get_mirror_x = cst328_get_mirror_x,
    .set_mirror_y = cst328_set_mirror_y,
    .get_mirror_y = cst328_get_mirror_y,
};

Driver cst328_driver = {
    .name = "cst328",
    .compatible = (const char*[]) { "hynitron,cst328", nullptr },
    .start_device = start,
    .stop_device = stop,
    .api = &cst328_pointer_api,
    .device_type = &POINTER_TYPE,
    .owner = &cst328_module,
    .internal = nullptr
};
