#include "touch.h"

#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_rom_sys.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ili9341.h"

#define SCREEN_W 320
#define SCREEN_H 240
#define PREV_W   240
#define PREV_H   320

#define TOUCH_PIN_CLK GPIO_NUM_25
#define TOUCH_PIN_MOSI GPIO_NUM_32
#define TOUCH_PIN_CS GPIO_NUM_33
#define TOUCH_PIN_MISO GPIO_NUM_39
#define TOUCH_PIN_IRQ GPIO_NUM_36
#define TOUCH_SAMPLES 7

static const char *TAG = "touch";

static touch_calib_t s_cal = {
    .x_min = 281,
    .x_max = 1781,
    .y_min = 162,
    .y_max = 1725,
    .invert_x = false,
    .invert_y = true,
};

static inline void touch_delay_us(int us)
{
    esp_rom_delay_us(us);
}

static inline void touch_clk_pulse(void)
{
    gpio_set_level(TOUCH_PIN_CLK, 1);
    touch_delay_us(1);
    gpio_set_level(TOUCH_PIN_CLK, 0);
    touch_delay_us(1);
}

static uint16_t xpt2046_transfer_12b(uint8_t cmd)
{
    uint16_t value = 0;

    gpio_set_level(TOUCH_PIN_CS, 0);
    touch_delay_us(2);

    for (int i = 7; i >= 0; --i) {
        gpio_set_level(TOUCH_PIN_MOSI, (cmd >> i) & 0x1);
        touch_clk_pulse();
    }

    for (int i = 0; i < 16; ++i) {
        gpio_set_level(TOUCH_PIN_CLK, 1);
        touch_delay_us(1);
        value = (uint16_t)((value << 1) | gpio_get_level(TOUCH_PIN_MISO));
        gpio_set_level(TOUCH_PIN_CLK, 0);
        touch_delay_us(1);
    }

    gpio_set_level(TOUCH_PIN_CS, 1);
    touch_delay_us(2);

    return (uint16_t)(value >> 4);
}

static int map_clamped(int v, int in_min, int in_max, int out_min, int out_max)
{
    if (v < in_min) {
        v = in_min;
    }
    if (v > in_max) {
        v = in_max;
    }

    int den = (in_max - in_min);
    if (den == 0) {
        return out_min;
    }

    int num = (v - in_min) * (out_max - out_min);
    return out_min + (num / den);
}

static void draw_crosshair(int x, int y, uint16_t color)
{
    const int arm = 10;

    for (int dx = -arm; dx <= arm; ++dx) {
        int px = x + dx;
        if (px >= 0 && px < SCREEN_W && y >= 0 && y < SCREEN_H) {
            (void)ili9341_draw_pixel((uint16_t)px, (uint16_t)y, color);
        }
    }

    for (int dy = -arm; dy <= arm; ++dy) {
        int py = y + dy;
        if (x >= 0 && x < SCREEN_W && py >= 0 && py < SCREEN_H) {
            (void)ili9341_draw_pixel((uint16_t)x, (uint16_t)py, color);
        }
    }
}

static bool wait_for_stable_touch(uint16_t *out_x, uint16_t *out_y)
{
    uint32_t sx = 0;
    uint32_t sy = 0;
    int count = 0;
    int idle_loops = 0;

    while (count < 20) {
        uint16_t rx = 0;
        uint16_t ry = 0;
        uint16_t rz = 0;

        if (touch_read_raw(&rx, &ry, &rz)) {
            sx += rx;
            sy += ry;
            ++count;
            idle_loops = 0;
        } else {
            ++idle_loops;
            if (count > 0 && idle_loops > 25) {
                sx = 0;
                sy = 0;
                count = 0;
            }
        }

        vTaskDelay(pdMS_TO_TICKS(8));
    }

    *out_x = (uint16_t)(sx / count);
    *out_y = (uint16_t)(sy / count);
    return true;
}

esp_err_t touch_init(void)
{
    gpio_config_t out_cfg = {
        .pin_bit_mask = (1ULL << TOUCH_PIN_CLK) | (1ULL << TOUCH_PIN_MOSI) | (1ULL << TOUCH_PIN_CS),
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    ESP_ERROR_CHECK(gpio_config(&out_cfg));

    gpio_config_t in_cfg = {
        .pin_bit_mask = (1ULL << TOUCH_PIN_MISO) | (1ULL << TOUCH_PIN_IRQ),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    ESP_ERROR_CHECK(gpio_config(&in_cfg));

    gpio_set_level(TOUCH_PIN_CS, 1);
    gpio_set_level(TOUCH_PIN_CLK, 0);
    gpio_set_level(TOUCH_PIN_MOSI, 0);

    ESP_LOGI(TAG,
             "touch ready x=[%d,%d] y=[%d,%d] invert=(%d,%d)",
             s_cal.x_min,
             s_cal.x_max,
             s_cal.y_min,
             s_cal.y_max,
             s_cal.invert_x,
             s_cal.invert_y);

    return ESP_OK;
}

bool touch_read_raw(uint16_t *x, uint16_t *y, uint16_t *z)
{
    if (x == NULL || y == NULL || z == NULL) {
        return false;
    }

    if (gpio_get_level(TOUCH_PIN_IRQ) != 0) {
        return false;
    }

    uint32_t sx = 0;
    uint32_t sy = 0;
    uint32_t sz = 0;
    int valid = 0;

    for (int i = 0; i < TOUCH_SAMPLES; ++i) {
        uint16_t z1 = xpt2046_transfer_12b(0xB0);
        uint16_t z2 = xpt2046_transfer_12b(0xC0);
        uint16_t pressure = (z1 > 0 && z2 > z1) ? (uint16_t)(z1 + (4095 - z2)) : 0;
        if (pressure < 80) {
            continue;
        }

        sx += xpt2046_transfer_12b(0xD0);
        sy += xpt2046_transfer_12b(0x90);
        sz += pressure;
        ++valid;
    }

    if (valid == 0) {
        return false;
    }

    *x = (uint16_t)(sx / valid);
    *y = (uint16_t)(sy / valid);
    *z = (uint16_t)(sz / valid);
    return true;
}

bool touch_read_screen(int *x, int *y, uint16_t *z)
{
    if (x == NULL || y == NULL || z == NULL) {
        return false;
    }

    uint16_t raw_x = 0;
    uint16_t raw_y = 0;
    uint16_t raw_z = 0;

    if (!touch_read_raw(&raw_x, &raw_y, &raw_z)) {
        return false;
    }

    int old_x = map_clamped((int)raw_x,
                            s_cal.x_min,
                            s_cal.x_max,
                            s_cal.invert_x ? (PREV_W - 1) : 0,
                            s_cal.invert_x ? 0 : (PREV_W - 1));

    int old_y = map_clamped((int)raw_y,
                            s_cal.y_min,
                            s_cal.y_max,
                            s_cal.invert_y ? (PREV_H - 1) : 0,
                            s_cal.invert_y ? 0 : (PREV_H - 1));

    /* Keep 320x240 landscape, rotated 180° from prior orientation. */
    *x = (PREV_H - 1) - old_y;
    *y = old_x;

    *z = raw_z;
    return true;
}

esp_err_t touch_calibrate(void)
{
    const int margin = 18;
    const int tx[4] = {margin, SCREEN_W - margin, SCREEN_W - margin, margin};
    const int ty[4] = {margin, margin, SCREEN_H - margin, SCREEN_H - margin};
    const char *name[4] = {"TOP-LEFT", "TOP-RIGHT", "BOTTOM-RIGHT", "BOTTOM-LEFT"};

    uint16_t raw_x[4] = {0};
    uint16_t raw_y[4] = {0};

    ili9341_fill_screen(ILI9341_COLOR_BLACK);
    ili9341_draw_text(6, 6, "TOUCH CAL", ILI9341_COLOR_WHITE, ILI9341_COLOR_BLACK);

    ESP_LOGI(TAG, "Calibration start");

    for (int i = 0; i < 4; ++i) {
        ili9341_fill_screen(ILI9341_COLOR_BLACK);
        draw_crosshair(tx[i], ty[i], ILI9341_COLOR_GREEN);
        ESP_LOGI(TAG, "Tap and hold %s", name[i]);

        wait_for_stable_touch(&raw_x[i], &raw_y[i]);

        draw_crosshair(tx[i], ty[i], ILI9341_COLOR_BLUE);
        vTaskDelay(pdMS_TO_TICKS(350));
    }

    int x_left = ((int)raw_x[0] + (int)raw_x[3]) / 2;
    int x_right = ((int)raw_x[1] + (int)raw_x[2]) / 2;
    int y_top = ((int)raw_y[0] + (int)raw_y[1]) / 2;
    int y_bottom = ((int)raw_y[2] + (int)raw_y[3]) / 2;

    s_cal.x_min = (x_left < x_right) ? x_left : x_right;
    s_cal.x_max = (x_left > x_right) ? x_left : x_right;
    s_cal.y_min = (y_top < y_bottom) ? y_top : y_bottom;
    s_cal.y_max = (y_top > y_bottom) ? y_top : y_bottom;
    s_cal.invert_x = !(x_right > x_left);
    s_cal.invert_y = !(y_bottom > y_top);

    ESP_LOGI(TAG,
             "Calibration done: x=[%d,%d] y=[%d,%d] invert=(%d,%d)",
             s_cal.x_min,
             s_cal.x_max,
             s_cal.y_min,
             s_cal.y_max,
             s_cal.invert_x,
             s_cal.invert_y);

    return ESP_OK;
}

const touch_calib_t *touch_get_calibration(void)
{
    return &s_cal;
}

esp_err_t touch_set_calibration(const touch_calib_t *calib)
{
    if (calib == NULL) {
        return ESP_ERR_INVALID_ARG;
    }

    s_cal = *calib;
    return ESP_OK;
}
