#define LOG_MODULE "mouse"

// clang-format off
// Don't format because the order is important here
#include <windows.h>
#include <initguid.h>
// clang-format on

#include <malloc.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>

#include "geninput/hid.h"
#include "geninput/mouse.h"

#include "util/defs.h"
#include "util/log.h"
#include "util/mem.h"
#include "util/str.h"

enum mouse_ctl_no {
    MOUSE_CTL_BTN1,
    MOUSE_CTL_BTN2,
    MOUSE_CTL_BTN3,
    MOUSE_CTL_BTN4,
    MOUSE_CTL_BTN5,
    MOUSE_CTL_X,
    MOUSE_CTL_Y,
    MOUSE_CTL_WHEEL,

    MOUSE_CTL_COUNT
};

struct mouse {
    struct hid_ri super;
    wchar_t *name;
    long last_x;
    long last_y;
    long values[MOUSE_CTL_COUNT];
};

static const struct hid_control mouse_controls[MOUSE_CTL_COUNT] = {
    /* Left, right, middle, AUX1, AUX2 buttons */
    {0x00090001, 0x00000000, 0x00000001, 0},
    {0x00090002, 0x00000000, 0x00000001, 0},
    {0x00090003, 0x00000000, 0x00000001, 0},
    {0x00090004, 0x00000000, 0x00000001, 0},
    {0x00090005, 0x00000000, 0x00000001, 0},

    /* X axis */
    {0x00010030, 0xC0000000, 0x3FFFFFFF, HID_FLAG_RELATIVE},

    /* Y axis */
    {0x00010031, 0xC0000000, 0x3FFFFFFF, HID_FLAG_RELATIVE},

    /* Mouse wheel */
    {0x00010038, 0xFFFF8000, 0x00007FFF, HID_FLAG_RELATIVE},
};

static const uint16_t mouse_down_flg[] = {
    RI_MOUSE_BUTTON_1_DOWN,
    RI_MOUSE_BUTTON_2_DOWN,
    RI_MOUSE_BUTTON_3_DOWN,
    RI_MOUSE_BUTTON_4_DOWN,
    RI_MOUSE_BUTTON_5_DOWN,
};

static const uint16_t mouse_up_flg[] = {
    RI_MOUSE_BUTTON_1_UP,
    RI_MOUSE_BUTTON_2_UP,
    RI_MOUSE_BUTTON_3_UP,
    RI_MOUSE_BUTTON_4_UP,
    RI_MOUSE_BUTTON_5_UP,
};

DEFINE_GUID(
    mouse_guid,
    0x378DE44C,
    0x56EF,
    0x11D1,
    0xBC,
    0x8C,
    0x00,
    0xA0,
    0xC9,
    0x14,
    0x05,
    0xDD);

static bool mouse_get_device_usage(const struct hid *hid, uint32_t *usage);
static bool
mouse_get_name(const struct hid *hid, wchar_t *chars, size_t *nchars);
static bool mouse_get_controls(
    const struct hid *hid, struct hid_control *controls, size_t *ncontrols);
static bool mouse_get_lights(
    const struct hid *hid, struct hid_light *lights, size_t *nlights);
static bool
mouse_get_value(struct hid *hid, size_t control_no, int32_t *out_value);
static bool
mouse_set_light(struct hid *hid, size_t light_no, uint32_t intensity);
static void mouse_handle_event(struct hid_ri *hid_ri, const RAWINPUT *ri);
static void mouse_close(struct hid *hid);

static const struct hid_ri_vtbl mouse_vtbl = {
    {/* .close               = */ mouse_close,
     /* .get_device_usage    = */ mouse_get_device_usage,
     /* .get_name            = */ mouse_get_name,
     /* .get_controls        = */ mouse_get_controls,
     /* .get_lights          = */ mouse_get_lights,
     /* .get_value           = */ mouse_get_value,
     /* .set_light           = */ mouse_set_light},
    /* .handle_event        = */ mouse_handle_event};

void mouse_create(struct hid_ri **hid_ri, const char *dev_node)
{
    struct mouse *m;
    char *tmp;

    m = xcalloc(sizeof(*m));
    m->super.vptr = &mouse_vtbl;
    m->name = hid_ri_init_name(&mouse_guid, dev_node);

    *hid_ri = &m->super;

    if (!wstr_narrow(m->name, &tmp)) {
        tmp = str_dup("???");
    }

    log_misc("Opened mouse on dev node %s", dev_node);
    log_misc("... Product: %s", tmp);

    free(tmp);
}

static bool mouse_get_device_usage(const struct hid *hid, uint32_t *usage)
{
    *usage = MOUSE_DEVICE_USAGE;

    return true;
}

static bool
mouse_get_name(const struct hid *hid, wchar_t *chars, size_t *nchars)
{
    struct mouse *m = containerof(hid, struct mouse, super);

    return hid_ri_get_name(chars, nchars, m->name);
}

static bool mouse_get_controls(
    const struct hid *hid, struct hid_control *controls, size_t *ncontrols)
{
    return hid_ri_get_controls(
        controls, ncontrols, mouse_controls, MOUSE_CTL_COUNT);
}

static bool mouse_get_lights(
    const struct hid *hid, struct hid_light *lights, size_t *nlights)
{
    log_assert(nlights != NULL);

    if (lights == NULL) {
        *nlights = 0;
    }

    return true;
}

static bool
mouse_get_value(struct hid *hid, size_t control_no, int32_t *out_value)
{
    struct mouse *m = containerof(hid, struct mouse, super);
    long tmp;

    log_assert(control_no < MOUSE_CTL_COUNT);

    switch (control_no) {
        case MOUSE_CTL_BTN1:
        case MOUSE_CTL_BTN2:
        case MOUSE_CTL_BTN3:
        case MOUSE_CTL_BTN4:
        case MOUSE_CTL_BTN5:
            *out_value = m->values[control_no];

            return true;

        case MOUSE_CTL_X:
        case MOUSE_CTL_Y:
        case MOUSE_CTL_WHEEL:
            do {
                tmp = m->values[control_no];
            } while (InterlockedCompareExchange(
                         &m->values[control_no], 0, tmp) != tmp);

            *out_value = tmp;

            return true;

        default:
            return false;
    }
}

static bool
mouse_set_light(struct hid *hid, size_t light_no, uint32_t intensity)
{
    return false;
}

static void mouse_handle_event(struct hid_ri *hid_ri, const RAWINPUT *ri)
{
    struct mouse *m = containerof(hid_ri, struct mouse, super);
    long tmp;
    long dwheel;
    long dx;
    long dy;
    int i;

    if (ri->data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE) {
        dx = ri->data.mouse.lLastX - m->last_x;
        dy = ri->data.mouse.lLastY - m->last_y;

        m->last_x = ri->data.mouse.lLastX;
        m->last_y = ri->data.mouse.lLastY;
    } else {
        dx = ri->data.mouse.lLastX;
        dy = ri->data.mouse.lLastY;

        m->last_x += dx;
        m->last_y += dy;
    }

    do {
        tmp = m->values[MOUSE_CTL_X];
    } while (InterlockedCompareExchange(
                 &m->values[MOUSE_CTL_X], tmp + dx, tmp) != tmp);

    do {
        tmp = m->values[MOUSE_CTL_Y];
    } while (InterlockedCompareExchange(
                 &m->values[MOUSE_CTL_Y], tmp + dy, tmp) != tmp);

    if (ri->data.mouse.usButtonFlags & RI_MOUSE_WHEEL) {
        dwheel = ri->data.mouse.usButtonData;

        do {
            tmp = m->values[MOUSE_CTL_WHEEL];
        } while (InterlockedCompareExchange(
                     &m->values[MOUSE_CTL_WHEEL], tmp + dwheel, tmp) != tmp);
    }

    for (i = 0; i < 5; i++) {
        if (ri->data.mouse.usButtonFlags & mouse_down_flg[i]) {
            m->values[i] = 1;
        } else if (ri->data.mouse.usButtonFlags & mouse_up_flg[i]) {
            m->values[i] = 0;
        }
    }
}

static void mouse_close(struct hid *hid)
{
    struct mouse *m = containerof(hid, struct mouse, super);

    free(m->name);
    free(m);
}
