#define LOG_MODULE "hid-generic"

#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>

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

#include "geninput/hid-generic-strings.h"
#include "geninput/hid-generic.h"
#include "geninput/hid-meta-in.h"
#include "geninput/hid-meta-out.h"
#include "geninput/hid.h"

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

struct hid_generic {
    struct hid_fd super;

    char *dev_node;

    HANDLE fd;
    PHIDP_PREPARSED_DATA ppd;

    OVERLAPPED in_ovl;
    OVERLAPPED out_ovl;

    struct hid_generic_strings strings;
    struct hid_meta_in meta_in;
    struct hid_meta_out meta_out;

    uint8_t *in_buf;
    size_t in_nbytes;

    uint8_t *out_buf;
    size_t out_nbytes;
    bool out_started;
    bool out_faulty;
};

static bool
hid_generic_get_device_usage(const struct hid *hid, uint32_t *usage);
static bool
hid_generic_get_name(const struct hid *hid, wchar_t *chars, size_t *nchars);
static bool hid_generic_get_controls(
    const struct hid *hid, struct hid_control *controls, size_t *ncontrols);
static bool hid_generic_get_lights(
    const struct hid *hid, struct hid_light *lights, size_t *nlights);
static bool
hid_generic_get_value(struct hid *hid, size_t control_no, int32_t *out_value);
static bool
hid_generic_set_light(struct hid *hid, size_t light_no, uint32_t intensity);
static bool
hid_generic_handle_event(struct hid_fd *super, OVERLAPPED *ovl, size_t nbytes);
static void hid_generic_close(struct hid *hid);

static struct hid_fd_vtbl hid_generic_vtbl = {
    {/* .close               = */ hid_generic_close,
     /* .get_device_usage    = */ hid_generic_get_device_usage,
     /* .get_name            = */ hid_generic_get_name,
     /* .get_controls        = */ hid_generic_get_controls,
     /* .get_lights          = */ hid_generic_get_lights,
     /* .get_value           = */ hid_generic_get_value,
     /* .set_light           = */ hid_generic_set_light},
    /* .handle_event        = */ hid_generic_handle_event};

bool hid_generic_open(
    struct hid_fd **out, const char *dev_node, HANDLE iocp, uintptr_t iocp_ctx)
{
    struct hid_generic *hg;
    uint32_t tlc_usage;

    hg = xmalloc(sizeof(*hg));

    hg->fd = CreateFile(
        dev_node,
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_OVERLAPPED,
        NULL);

    if (hg->fd == INVALID_HANDLE_VALUE) {
        /* Probably a keyboard that's locked by the OS */
        goto open_fail;
    }

    if (!HidD_GetPreparsedData(hg->fd, &hg->ppd)) {
        log_warning("HidD_GetPreparsedData failed on dev node %s", dev_node);

        goto ppd_fail;
    }

    if (!hid_meta_in_init(&hg->meta_in, hg->ppd)) {
        goto meta_in_fail;
    }

    tlc_usage = hid_meta_in_get_tlc_usage(&hg->meta_in);

    if ((tlc_usage >> 24) == 0xFF) {
        log_misc("Skipping vendor-specific HID on dev node %s", dev_node);

        goto vsp_fail;
    }

    if (!hid_meta_out_init(&hg->meta_out, hg->ppd, hg->fd)) {
        goto meta_out_fail;
    }

    hid_generic_strings_init(&hg->strings, hg->fd);

    /* Dump diagnostics */

    log_misc("Initializing generic HID on dev node %s:", dev_node);
    log_misc("... Product: %s", hg->strings.str_prod);
    log_misc("... Manufacturer: %s", hg->strings.str_manf);
    log_misc("... Device usage: %08x", hid_meta_in_get_tlc_usage(&hg->meta_in));
    log_misc(
        "... Number of controls: %u",
        (unsigned int) hid_meta_in_get_ncontrols(&hg->meta_in));
    log_misc(
        "... Number of \"lights\": %u",
        (unsigned int) hid_meta_out_get_nlights(&hg->meta_out));

    /* Start up input double-buffer chain by reading a report.
       We bind to the IO completion port here. */

    CreateIoCompletionPort(hg->fd, iocp, iocp_ctx, 0);

    hg->in_nbytes = hid_meta_in_get_buffer_size(&hg->meta_in);
    hg->in_buf = xmalloc(hg->in_nbytes);

    memset(&hg->in_ovl, 0, sizeof(hg->in_ovl));

    if (!ReadFile(hg->fd, hg->in_buf, hg->in_nbytes, NULL, &hg->in_ovl)) {
        if (GetLastError() != ERROR_IO_PENDING) {
            log_warning(
                "Initial ReadFile failed: %08x: %s",
                (unsigned int) GetLastError(),
                dev_node);

            goto read_fail;
        }
    }

    /* Allocate OUT report buffers, but don't start sending the reports until
       the user tries to set a light. Some HIDs (e.g. Razer devices) freak out
       if you send them HID reports that they aren't expecting.

       These OUT reports are probably used for configuration settings or,
       worse yet, firmware updates. Configuration changes are what feature
       reports are for. As for firmware updates over HID... no. Just, no. */

    hg->out_nbytes = hid_meta_out_get_buffer_size(&hg->meta_out);
    hg->out_buf = xmalloc(hg->out_nbytes);
    hg->out_started = false;

    memset(&hg->out_ovl, 0, sizeof(hg->out_ovl));

    /* (finish up and return success) */

    hg->dev_node = str_dup(dev_node);
    hg->super.vptr = &hid_generic_vtbl;

    *out = &hg->super;

    return true;

read_fail:
    free(hg->in_buf);

    hid_generic_strings_fini(&hg->strings);
    hid_meta_out_fini(&hg->meta_out);

meta_out_fail:
vsp_fail:
    hid_meta_in_fini(&hg->meta_in);

meta_in_fail:
    HidD_FreePreparsedData(hg->ppd);

ppd_fail:
    CloseHandle(hg->fd);

open_fail:
    free(hg);

    return false;
}

static bool hid_generic_get_device_usage(const struct hid *hid, uint32_t *usage)
{
    const struct hid_generic *hg = containerof(hid, struct hid_generic, super);

    *usage = hid_meta_in_get_tlc_usage(&hg->meta_in);

    return true;
}

static bool
hid_generic_get_name(const struct hid *hid, wchar_t *chars, size_t *nchars)
{
    const struct hid_generic *hg = containerof(hid, struct hid_generic, super);
    size_t len;

    log_assert(nchars != NULL);

    len = wcslen(hg->strings.wstr_prod) + 1;

    if (chars == NULL) {
        *nchars = len;

        return true;
    } else if (*nchars >= len) {
        memcpy(chars, hg->strings.wstr_prod, len * sizeof(wchar_t));

        return true;
    } else {
        return false;
    }
}

static bool hid_generic_get_controls(
    const struct hid *hid, struct hid_control *controls, size_t *ncontrols)
{
    const struct hid_generic *hg = containerof(hid, struct hid_generic, super);

    const struct hid_control *meta_controls;
    size_t meta_ncontrols;

    if (ncontrols == NULL) {
        return false;
    }

    meta_controls = hid_meta_in_get_controls(&hg->meta_in);
    meta_ncontrols = hid_meta_in_get_ncontrols(&hg->meta_in);

    if (controls == NULL) {
        *ncontrols = meta_ncontrols;

        return true;
    } else if (*ncontrols >= meta_ncontrols) {
        *ncontrols = meta_ncontrols;
        memcpy(controls, meta_controls, sizeof(*controls) * meta_ncontrols);

        return true;
    } else {
        return false;
    }
}

static bool hid_generic_get_lights(
    const struct hid *hid, struct hid_light *lights, size_t *nlights)
{
    const struct hid_generic *hg =
        containerof(hid, const struct hid_generic, super);

    const struct hid_light *meta_lights;
    size_t meta_nlights;

    log_assert(nlights != NULL);

    meta_lights = hid_meta_out_get_lights(&hg->meta_out);
    meta_nlights = hid_meta_out_get_nlights(&hg->meta_out);

    if (lights == NULL) {
        *nlights = meta_nlights;

        return true;
    } else if (*nlights >= meta_nlights) {
        *nlights = meta_nlights;
        memcpy(lights, meta_lights, sizeof(*lights) * meta_nlights);

        return true;
    } else {
        return false;
    }
}

static bool
hid_generic_get_value(struct hid *hid, size_t control_no, int32_t *out_value)
{
    struct hid_generic *hg = containerof(hid, struct hid_generic, super);

    return hid_meta_in_get_value(&hg->meta_in, control_no, out_value);
}

static bool
hid_generic_set_light(struct hid *hid, size_t light_no, uint32_t intensity)
{
    struct hid_generic *hg = containerof(hid, struct hid_generic, super);

    size_t nlights;

    nlights = hid_meta_out_get_nlights(&hg->meta_out);

    if (light_no >= nlights) {
        return false;
    }

    /* Lazily start the lighting double-buffer chain (see hid_generic_init) */

    if (!hg->out_started && !hg->out_faulty) {
        log_misc("Starting light output to %s", hg->dev_node);
        hid_meta_out_get_next_report(&hg->meta_out, hg->out_buf);

        if (!WriteFile(
                hg->fd, hg->out_buf, hg->out_nbytes, NULL, &hg->out_ovl)) {
            if (GetLastError() != ERROR_IO_PENDING) {
                log_warning(
                    "Initial WriteFile failed: %08x: %s",
                    (unsigned int) GetLastError(),
                    hg->dev_node);
                hg->out_faulty = true;

                return false;
            }
        }

        hg->out_started = true;
    }

    return hid_meta_out_set_light(&hg->meta_out, light_no, intensity);
}

static bool
hid_generic_handle_event(struct hid_fd *hid_fd, OVERLAPPED *ovl, size_t nbytes)
{
    struct hid_generic *hg = containerof(hid_fd, struct hid_generic, super);

    if (ovl == &hg->out_ovl) {
        /* OUT transmit ok, prepare and transmit another */

        hid_meta_out_get_next_report(&hg->meta_out, hg->out_buf);
        memset(&hg->out_ovl, 0, sizeof(hg->out_ovl));

        if (!WriteFile(
                hg->fd, hg->out_buf, hg->out_nbytes, NULL, &hg->out_ovl) &&
            GetLastError() != ERROR_IO_PENDING) {
            log_warning(
                "Write error %08x from \"%s\" device on dev node %s",
                (unsigned int) GetLastError(),
                hg->strings.str_prod,
                hg->dev_node);

            return false;
        }

    } else if (ovl == &hg->in_ovl) {
        /* Received an IN report, dispatch it */

        /* The dispatcher will swap buffers with us, i.e. it will take
           ownership of whatever hg->current_in points to, and replace it
           with a pointer to a fresh buffer we can use for subsequent I/O */

        if (!hid_meta_in_dispatch(&hg->meta_in, &hg->in_buf, nbytes)) {
            log_warning(
                "Failed to process input report from \"%s\" device"
                " on dev node %s",
                hg->strings.str_prod,
                hg->dev_node);

            return false;
        }

        if (hg->in_buf == NULL) {
            /* BEEP BOOP FIRMWARE PROGRAMMED BY SHITLORD DETECTED */
            /* (ok, a less obnoxious explanation: The initial report
               solicitation failed, so the report handler did not have a
               valid buffer until we gave it one. This means that we have
               to allocate one for it to work with manually). */

            hg->in_buf = xmalloc(hid_meta_in_get_buffer_size(&hg->meta_in));
        }

        /* Event dispatched successfully. Kick off the next async read. */

        memset(&hg->in_ovl, 0, sizeof(hg->in_ovl));

        if (!ReadFile(hg->fd, hg->in_buf, hg->in_nbytes, NULL, &hg->in_ovl) &&
            GetLastError() != ERROR_IO_PENDING) {
            log_warning(
                "Read error %08x from \"%s\" device on dev node %s",
                (unsigned int) GetLastError(),
                hg->strings.str_prod,
                hg->dev_node);

            return false;
        }
    }

    return true;
}

static void hid_generic_close(struct hid *hid)
{
    struct hid_generic *hg = containerof(hid, struct hid_generic, super);

    /* Must close the FD first. I/O might be in flight and NT could potentially
       complete an I/O while we're freeing our buffers and completely trash our
       process heap by doing so. */

    CloseHandle(hg->fd);

    free(hg->dev_node);
    free(hg->out_buf);
    free(hg->in_buf);
    hid_meta_out_fini(&hg->meta_out);
    hid_meta_in_fini(&hg->meta_in);
    hid_generic_strings_fini(&hg->strings);

    HidD_FreePreparsedData(hg->ppd);

    free(hid);
}
