#include <windows.h>

#include <stdlib.h>

#include "geninput/dev-list.h"
#include "geninput/hid-generic.h"
#include "geninput/hid-mgr.h"
#include "geninput/hid.h"
#include "geninput/io-thread.h"
#include "geninput/pacdrive.h"

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

enum io_thread_cmd {
    IO_THREAD_CMD_STOP,
    IO_THREAD_CMD_ADD_DEVICE,
};

struct io_thread_msg {
    enum io_thread_cmd cmd;
    const char *dev_node;
    HANDLE barrier;
};

static HANDLE io_thread_cp;
static int io_thread_id;

static void io_thread_proc_add_device(const char *dev_node);
static void io_thread_proc_init(void);
static void io_thread_proc_main_loop(void);
static void io_thread_proc_fini(void);
static int io_thread_proc(void *param);

static void io_thread_proc_add_device(const char *dev_node)
{
    struct hid_stub *stub;
    struct hid_fd *hid_fd;
    uintptr_t ctx;

    hid_mgr_lock();

    stub = hid_mgr_get_named_stub(dev_node);

    if (!hid_stub_is_attached(stub)) {
        ctx = (uintptr_t) stub;

        if (pac_open(&hid_fd, dev_node, io_thread_cp, ctx) ||
            hid_generic_open(&hid_fd, dev_node, io_thread_cp, ctx)) {
            hid_stub_attach(stub, (struct hid *) hid_fd);
        }
    }

    hid_mgr_unlock();
}

static void io_thread_proc_init(void)
{
    const char *dev_node;
    struct dev_list devs;

    io_thread_cp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

    dev_list_init(&devs, &hid_guid);

    while (dev_list_next(&devs)) {
        dev_node = dev_list_get_dev_node(&devs);
        io_thread_proc_add_device(dev_node);
    }

    dev_list_fini(&devs);

    SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);

    log_info(
        "USB I/O thread ready, thread id = %d",
        (unsigned int) GetCurrentThreadId());
}

static void io_thread_proc_main_loop(void)
{
    uintptr_t ctx;
    OVERLAPPED *p_ovl;
    DWORD nbytes;
    struct hid_stub *stub;
    struct io_thread_msg *msg;
    BOOL ok;

    for (;;) {
        ok = GetQueuedCompletionStatus(
            io_thread_cp, &nbytes, (ULONG_PTR *) &ctx, &p_ovl, 0);

        if (p_ovl != NULL) {
            /* An I/O completed (depending on the value of `ok' either
               successfuly or not), either way dispatch the completion event */

            stub = (struct hid_stub *) ctx;

            hid_mgr_lock();

            if (!ok) {
                log_warning(
                    "Async IO returned failure: %08x",
                    (unsigned int) GetLastError());
                hid_stub_detach(stub);
            } else {
                ok = hid_stub_handle_completion(stub, p_ovl, nbytes);
            }

            hid_mgr_unlock();

        } else if (ok) {
            /* No OVERLAPPED*, so this is a command message queued from a
               different thread. */

            msg = (struct io_thread_msg *) ctx;

            if (msg->cmd == IO_THREAD_CMD_STOP) {
                break;
            } else if (msg->cmd == IO_THREAD_CMD_ADD_DEVICE) {
                io_thread_proc_add_device(msg->dev_node);
                SetEvent(msg->barrier);
            }

        } else if (GetLastError() == WAIT_TIMEOUT) {
            /* Failure, but a benign one: timeout due to no event.
               I don't exactly know why, but we need to back off here for a bit
               or we end up soaking the CPU (in kernel mode...). It's probably
               a bemanitools bug instead of an NT deficiency but I'll have to
               track down the root cause later. */

            Sleep(1);
        } else {
            /* Otherwise idk what's going on */

            log_warning(
                "Spurious wakeup in I/O mux: %08x",
                (unsigned int) GetLastError());
        }
    }
}

static void io_thread_proc_fini(void)
{
    SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL);
    CloseHandle(io_thread_cp);

    log_misc("USB I/O thread shutting down");
}

static int io_thread_proc(void *param)
{
    HANDLE barrier;

    barrier = param;

    io_thread_proc_init();
    SetEvent(barrier);

    io_thread_proc_main_loop();

    io_thread_proc_fini();

    return 0;
}

void io_thread_init(void)
{
    HANDLE barrier;

    barrier = CreateEvent(NULL, TRUE, FALSE, NULL);

    io_thread_id = thread_create(io_thread_proc, barrier, 16384, 0);

    WaitForSingleObject(barrier, INFINITE);
    CloseHandle(barrier);
}

void io_thread_add_device(const char *dev_node)
{
    struct io_thread_msg msg;

    msg.cmd = IO_THREAD_CMD_ADD_DEVICE;
    msg.dev_node = dev_node;
    msg.barrier = CreateEvent(NULL, TRUE, FALSE, NULL);

    PostQueuedCompletionStatus(io_thread_cp, 0, (uintptr_t) &msg, NULL);

    WaitForSingleObject(msg.barrier, INFINITE);
    CloseHandle(msg.barrier);
}

void io_thread_fini(void)
{
    struct io_thread_msg msg;

    msg.cmd = IO_THREAD_CMD_STOP;

    PostQueuedCompletionStatus(io_thread_cp, 0, (uintptr_t) &msg, NULL);

    thread_join(io_thread_id, NULL);
    thread_destroy(io_thread_id);
}
