/*
 * xteink X3 UC8253 e-ink panel
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "qemu/osdep.h"
#include "qemu/module.h"
#include "qemu/error-report.h"
#include "qemu/cutils.h"
#include "hw/irq.h"
#include "hw/display/xteink_x3_eink.h"

#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>

static QemuConsole *xteink_wasm_console;
static uint32_t xteink_wasm_generation;

EMSCRIPTEN_KEEPALIVE uintptr_t xteink_wasm_framebuffer(void)
{
    DisplaySurface *surface = xteink_wasm_console
                                  ? qemu_console_surface(xteink_wasm_console)
                                  : NULL;
    return surface ? (uintptr_t)surface_data(surface) : 0;
}

EMSCRIPTEN_KEEPALIVE int xteink_wasm_width(void)
{
    DisplaySurface *surface = xteink_wasm_console
                                  ? qemu_console_surface(xteink_wasm_console)
                                  : NULL;
    return surface ? surface_width(surface) : 0;
}

EMSCRIPTEN_KEEPALIVE int xteink_wasm_height(void)
{
    DisplaySurface *surface = xteink_wasm_console
                                  ? qemu_console_surface(xteink_wasm_console)
                                  : NULL;
    return surface ? surface_height(surface) : 0;
}

EMSCRIPTEN_KEEPALIVE int xteink_wasm_stride(void)
{
    DisplaySurface *surface = xteink_wasm_console
                                  ? qemu_console_surface(xteink_wasm_console)
                                  : NULL;
    return surface ? surface_stride(surface) : 0;
}

EMSCRIPTEN_KEEPALIVE uint32_t xteink_wasm_frame_generation(void)
{
    return qatomic_read(&xteink_wasm_generation);
}

static void xteink_wasm_frame_updated(QemuConsole *console)
{
    xteink_wasm_console = console;
    qatomic_inc(&xteink_wasm_generation);
}
#else
static void xteink_wasm_frame_updated(QemuConsole *console)
{
}
#endif

#define CMD_POWER_OFF 0x02
#define CMD_POWER_ON 0x04
#define CMD_DTM1 0x10
#define CMD_REFRESH 0x12
#define CMD_DTM2 0x13
#define CMD_LUT_FIRST 0x20
#define CMD_LUT_LAST 0x24

#define BUSY_TIME_NS (5 * SCALE_MS)

#define XTEINK_X3_LUT_PREFIX_SIZE 13

typedef struct XteinkX3KnownLut {
    bool grayscale;
    uint8_t prefix[XTEINK_X3_LUT_COUNT][XTEINK_X3_LUT_PREFIX_SIZE];
} XteinkX3KnownLut;

/* These known banks have zero-filled bytes 13..41. Comparing both the listed
 * prefixes and those tails identifies the complete 210-byte bank exactly. */
static const XteinkX3KnownLut xteink_x3_known_luts[] = {
    { false, {
        { 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0x20, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0xaa, 0x06, 0x01, 0x06, 0x06, 0x01, 0xa0, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x04, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
    } },
    { false, {
        { 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0xaa, 0x06, 0x01, 0x06, 0x06, 0x01, 0xa0, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0xaa, 0x06, 0x01, 0x06, 0x06, 0x01, 0xa0, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
    } },
    { false, {
        { 0x00, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 },
        { 0x20, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 },
        { 0xaa, 0x04, 0x02, 0x04, 0x04, 0x01, 0x80, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 },
        { 0x55, 0x04, 0x02, 0x04, 0x04, 0x01, 0x40, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 },
        { 0x10, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 },
    } },
    { false, {
        { 0x00, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 },
        { 0x4a, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 },
        { 0x0a, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 },
        { 0x04, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x40, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 },
        { 0x84, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x40, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 },
    } },
    { true, {
        { 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
        { 0x20, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
        { 0x80, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
        { 0x54, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
        { 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    } },
    { false, {
        { 0x00, 0x06, 0x03, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0x20, 0x06, 0x03, 0x06, 0x06, 0x01, 0x20, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0xaa, 0x06, 0x03, 0x06, 0x06, 0x01, 0xa8, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0x55, 0x06, 0x03, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
        { 0x10, 0x06, 0x03, 0x06, 0x06, 0x01, 0x14, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
    } },
    { false, {
        { 0x01, 0x07, 0x01, 0x06, 0x06, 0x01, 0x01, 0x01, 0x06, 0x01, 0x00, 0x00, 0x01 },
        { 0x01, 0x07, 0x81, 0x06, 0x06, 0x01, 0x01, 0x01, 0x06, 0x01, 0x00, 0x00, 0x01 },
        { 0x01, 0x87, 0x81, 0x86, 0x86, 0x01, 0x01, 0x01, 0x86, 0x01, 0x00, 0x00, 0x01 },
        { 0x01, 0x47, 0x41, 0x46, 0x46, 0x01, 0x01, 0x01, 0x46, 0x01, 0x00, 0x00, 0x01 },
        { 0x01, 0x07, 0x01, 0x06, 0x06, 0x01, 0x01, 0x01, 0x06, 0x01, 0x00, 0x00, 0x01 },
    } },
};

static bool xteink_x3_eink_lut_matches(const XteinkX3EinkState *s,
                                        const XteinkX3KnownLut *known)
{
    for (unsigned i = 0; i < XTEINK_X3_LUT_COUNT; i++) {
        if (memcmp(s->lut_bank[i], known->prefix[i],
                   XTEINK_X3_LUT_PREFIX_SIZE)) {
            return false;
        }
        for (unsigned j = XTEINK_X3_LUT_PREFIX_SIZE;
             j < XTEINK_X3_LUT_SIZE; j++) {
            if (s->lut_bank[i][j]) {
                return false;
            }
        }
    }
    return true;
}

static bool xteink_x3_eink_classify_lut(XteinkX3EinkState *s)
{
    for (unsigned i = 0; i < XTEINK_X3_LUT_COUNT; i++) {
        if (s->lut_pos[i] != XTEINK_X3_LUT_SIZE) {
            warn_report("xteink-x3-eink: incomplete LUT bank");
            return false;
        }
    }

    for (unsigned i = 0; i < ARRAY_SIZE(xteink_x3_known_luts); i++) {
        if (xteink_x3_eink_lut_matches(s, &xteink_x3_known_luts[i])) {
            s->grayscale_lut = xteink_x3_known_luts[i].grayscale;
            s->lut_known = true;
            s->unknown_lut_reported = false;
            return true;
        }
    }

    if (!s->unknown_lut_reported) {
        warn_report("xteink-x3-eink: unknown LUT bank; refresh skipped");
        qemu_hexdump(stderr, "xteink-x3-eink: LUT ", s->lut_bank,
                     sizeof(s->lut_bank));
        s->unknown_lut_reported = true;
    }
    s->lut_known = false;
    return false;
}

/* 4-level grayscale drive shades for levels 1..3; level 0 = no drive (keep). */
static const uint32_t xteink_x3_gray[4] = {
    0x00ffffff, 0x00aaaaaa, 0x00555555, 0x00000000,
};

static void xteink_x3_eink_render(XteinkX3EinkState *s)
{
    DisplaySurface *surface = qemu_console_surface(s->console);
    uint32_t *pixels = (uint32_t *)surface_data(surface);

    /* Persistence - E-ink retains undriven pixels. The reader's grayscale pass
     * is a sparse antialiasing overlay (planes near-empty) on top of the crisp
     * BW page from the prior full refresh; level 0 = no drive, so keep the
     * existing pixel and only repaint driven (level>0) ones. BW refreshes drive
     * every pixel, so they repaint in full. The surface is never cleared. */
    for (unsigned y = 0; y < XTEINK_X3_HEIGHT; y++) {
        unsigned row_off = (XTEINK_X3_HEIGHT - 1 - y) * XTEINK_X3_WIDTH_BYTES;
        const uint8_t *msb = &s->new_plane[row_off];
        const uint8_t *lsb = &s->old_plane[row_off];
        for (unsigned x = 0; x < XTEINK_X3_WIDTH; x++) {
            uint8_t bit = 0x80 >> (x % 8);
            unsigned screen_x = XTEINK_X3_HEIGHT - 1 - y;
            unsigned screen_y = x;
            if (s->grayscale_lut) {
                unsigned level = ((msb[x / 8] & bit) ? 2 : 0) |
                                 ((lsb[x / 8] & bit) ? 1 : 0);
                if (level == 0) {
                    continue;
                }
                pixels[screen_y * XTEINK_X3_HEIGHT + screen_x] =
                    xteink_x3_gray[level];
            } else {
                pixels[screen_y * XTEINK_X3_HEIGHT + screen_x] =
                    (msb[x / 8] & bit) ? 0x00ffffff : 0;
            }
        }
    }

    xteink_wasm_frame_updated(s->console);
    dpy_gfx_update(s->console, 0, 0, XTEINK_X3_HEIGHT, XTEINK_X3_WIDTH);
}

static void xteink_x3_eink_busy_done(void *opaque)
{
    XteinkX3EinkState *s = XTEINK_X3_EINK(opaque);
    qemu_set_irq(s->busy, 1);
}

static void xteink_x3_eink_pulse_busy(XteinkX3EinkState *s)
{
    qemu_set_irq(s->busy, 0);
    timer_mod(s->busy_timer,
              qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + BUSY_TIME_NS);
}

static void xteink_x3_eink_reset(DeviceState *dev)
{
    XteinkX3EinkState *s = XTEINK_X3_EINK(dev);

    s->command = 0;
    s->plane_pos = 0;
    memset(s->old_plane, 0xff, sizeof(s->old_plane));
    memset(s->new_plane, 0xff, sizeof(s->new_plane));
    memset(s->lut_pos, 0, sizeof(s->lut_pos));
    s->lut_dirty = false;
    s->lut_known = true;
    s->grayscale_lut = false;
    s->unknown_lut_reported = false;
    qemu_set_irq(s->busy, 1);
    xteink_x3_eink_render(s);
}

static void xteink_x3_eink_set_dc(void *opaque, int n, int level)
{
    XteinkX3EinkState *s = XTEINK_X3_EINK(opaque);
    s->dc = level;
}

static void xteink_x3_eink_set_reset(void *opaque, int n, int level)
{
    if (!level) {
        xteink_x3_eink_reset(DEVICE(opaque));
    }
}

static uint32_t xteink_x3_eink_transfer(SSIPeripheral *peripheral,
                                        uint32_t value)
{
    XteinkX3EinkState *s = XTEINK_X3_EINK(peripheral);
    uint8_t byte = value;

    if (!s->dc) {
        s->command = byte;
        if (byte == CMD_DTM1 || byte == CMD_DTM2) {
            s->plane_pos = 0;
        } else if (byte >= CMD_LUT_FIRST && byte <= CMD_LUT_LAST) {
            s->lut_pos[byte - CMD_LUT_FIRST] = 0;
            s->lut_dirty = true;
        } else if (byte == CMD_REFRESH) {
            if (s->lut_dirty) {
                s->lut_known = xteink_x3_eink_classify_lut(s);
                s->lut_dirty = false;
            }
            if (s->lut_known) {
                xteink_x3_eink_render(s);
            }
            xteink_x3_eink_pulse_busy(s);
        } else if (byte == CMD_POWER_ON || byte == CMD_POWER_OFF) {
            xteink_x3_eink_pulse_busy(s);
        }
    } else if (s->command >= CMD_LUT_FIRST && s->command <= CMD_LUT_LAST) {
        unsigned lut = s->command - CMD_LUT_FIRST;
        if (s->lut_pos[lut] < XTEINK_X3_LUT_SIZE) {
            s->lut_bank[lut][s->lut_pos[lut]++] = byte;
        }
    } else if (s->plane_pos < XTEINK_X3_PLANE_SIZE) {
        if (s->command == CMD_DTM1) {
            s->old_plane[s->plane_pos++] = byte;
        } else if (s->command == CMD_DTM2) {
            s->new_plane[s->plane_pos++] = byte;
        }
    }

    return 0xff;
}

static void xteink_x3_eink_invalidate(void *opaque)
{
    xteink_x3_eink_render(XTEINK_X3_EINK(opaque));
}

static void xteink_x3_eink_update(void *opaque)
{
}

static const GraphicHwOps xteink_x3_eink_graphics_ops = {
    .invalidate = xteink_x3_eink_invalidate,
    .gfx_update = xteink_x3_eink_update,
};

static void xteink_x3_eink_init(Object *obj)
{
    XteinkX3EinkState *s = XTEINK_X3_EINK(obj);

    s->console = graphic_console_init(DEVICE(s), 0,
                                      &xteink_x3_eink_graphics_ops, s);
    dpy_gfx_replace_surface(s->console,
        qemu_create_displaysurface(XTEINK_X3_HEIGHT, XTEINK_X3_WIDTH));
    s->busy_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,
                                 xteink_x3_eink_busy_done, s);
    qdev_init_gpio_in_named(DEVICE(s), xteink_x3_eink_set_dc,
                            XTEINK_X3_EINK_DC, 1);
    qdev_init_gpio_in_named(DEVICE(s), xteink_x3_eink_set_reset,
                            XTEINK_X3_EINK_RESET, 1);
    qdev_init_gpio_out_named(DEVICE(s), &s->busy,
                             XTEINK_X3_EINK_BUSY, 1);
}

static void xteink_x3_eink_realize(SSIPeripheral *peripheral, Error **errp)
{
}

static void xteink_x3_eink_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    SSIPeripheralClass *ssi = SSI_PERIPHERAL_CLASS(klass);

    ssi->realize = xteink_x3_eink_realize;
    ssi->transfer = xteink_x3_eink_transfer;
    ssi->cs_polarity = SSI_CS_LOW;
    device_class_set_legacy_reset(dc, xteink_x3_eink_reset);
    dc->user_creatable = false;
}

static const TypeInfo xteink_x3_eink_info = {
    .name = TYPE_XTEINK_X3_EINK,
    .parent = TYPE_SSI_PERIPHERAL,
    .instance_size = sizeof(XteinkX3EinkState),
    .instance_init = xteink_x3_eink_init,
    .class_init = xteink_x3_eink_class_init,
};

typedef struct XteinkX4EinkState {
    SSIPeripheral parent_obj;
    QemuConsole *console;
    qemu_irq busy;
} XteinkX4EinkState;

#define XTEINK_X4_EINK(obj) \
    OBJECT_CHECK(XteinkX4EinkState, (obj), TYPE_XTEINK_X4_EINK)

static void xteink_x4_eink_render(XteinkX4EinkState *s)
{
    DisplaySurface *surface = qemu_console_surface(s->console);
    memset(surface_data(surface), 0xff,
           surface_stride(surface) * surface_height(surface));
    xteink_wasm_frame_updated(s->console);
    dpy_gfx_update(s->console, 0, 0, 480, 800);
}

static void xteink_x4_eink_reset(DeviceState *dev)
{
    XteinkX4EinkState *s = XTEINK_X4_EINK(dev);
    qemu_set_irq(s->busy, 0);
    xteink_x4_eink_render(s);
}

static void xteink_x4_eink_set_input(void *opaque, int n, int level)
{
}

static uint32_t xteink_x4_eink_transfer(SSIPeripheral *peripheral,
                                        uint32_t value)
{
    return 0xff;
}

static void xteink_x4_eink_invalidate(void *opaque)
{
    xteink_x4_eink_render(XTEINK_X4_EINK(opaque));
}

static const GraphicHwOps xteink_x4_eink_graphics_ops = {
    .invalidate = xteink_x4_eink_invalidate,
};

static void xteink_x4_eink_init(Object *obj)
{
    XteinkX4EinkState *s = XTEINK_X4_EINK(obj);

    s->console = graphic_console_init(DEVICE(s), 0,
                                      &xteink_x4_eink_graphics_ops, s);
    dpy_gfx_replace_surface(s->console, qemu_create_displaysurface(480, 800));
    qdev_init_gpio_in_named(DEVICE(s), xteink_x4_eink_set_input,
                            XTEINK_X3_EINK_DC, 1);
    qdev_init_gpio_in_named(DEVICE(s), xteink_x4_eink_set_input,
                            XTEINK_X3_EINK_RESET, 1);
    qdev_init_gpio_out_named(DEVICE(s), &s->busy,
                             XTEINK_X3_EINK_BUSY, 1);
}

static void xteink_x4_eink_realize(SSIPeripheral *peripheral, Error **errp)
{
}

static void xteink_x4_eink_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    SSIPeripheralClass *ssi = SSI_PERIPHERAL_CLASS(klass);

    ssi->realize = xteink_x4_eink_realize;
    ssi->transfer = xteink_x4_eink_transfer;
    ssi->cs_polarity = SSI_CS_LOW;
    device_class_set_legacy_reset(dc, xteink_x4_eink_reset);
    dc->user_creatable = false;
}

static const TypeInfo xteink_x4_eink_info = {
    .name = TYPE_XTEINK_X4_EINK,
    .parent = TYPE_SSI_PERIPHERAL,
    .instance_size = sizeof(XteinkX4EinkState),
    .instance_init = xteink_x4_eink_init,
    .class_init = xteink_x4_eink_class_init,
};

static void xteink_eink_register_types(void)
{
    type_register_static(&xteink_x3_eink_info);
    type_register_static(&xteink_x4_eink_info);
}

type_init(xteink_eink_register_types)
