#include "moza/moza.h"

#include <stdio.h>
#include <string.h>

#include <windows.h>

static const char* moza_event_type_to_string(MOZA_EVENT_TYPE type)
{
    switch (type)
    {
    case MOZA_ET_SERVICE_CONNECTION_CLOSED: return "MOZA_ET_SERVICE_CONNECTION_CLOSED";
    case MOZA_ET_DEVICE_CONNECTED: return "MOZA_ET_DEVICE_CONNECTED";
    case MOZA_ET_DEVICE_DISCONNECTED: return "MOZA_ET_DEVICE_DISCONNECTED";
    case MOZA_ET_DEVICE_PARAMETER_VALUE_CHANGED: return "MOZA_ET_DEVICE_PARAMETER_VALUE_CHANGED";
    case MOZA_ET_DEVICE_PARAMETER_ERROR_OCCURRED: return "MOZA_ET_DEVICE_PARAMETER_ERROR_OCCURRED";
    case MOZA_ET_DEVICE_COMMAND_REPLIED: return "MOZA_ET_DEVICE_COMMAND_REPLIED";
    case MOZA_ET_DEVICE_COMMAND_ERROR_OCCURRED: return "MOZA_ET_DEVICE_COMMAND_ERROR_OCCURRED";
    case MOZA_ET_HID_AXIS_DATA_CHANGED: return "MOZA_ET_HID_AXIS_DATA_CHANGED";
    case MOZA_ET_HID_BUTTON_PRESSED: return "MOZA_ET_HID_BUTTON_PRESSED";
    case MOZA_ET_HID_BUTTON_RELEASED: return "MOZA_ET_HID_BUTTON_RELEASED";
    default: return "<unknown>";
    }
}

static const char* moza_log_level_to_string(MOZA_LOG_LEVEL level)
{
    switch (level)
    {
    case MOZA_LL_DEBUG: return "DEBUG";
    case MOZA_LL_INFO: return "INFO";
    case MOZA_LL_WARNING: return "WARNING";
    case MOZA_LL_CRITICAL: return "CRITICAL";
    case MOZA_LL_FATAL: return "FATAL";
    default: return "<unknown>";
    }
}

static const char* moza_errno_to_string(MOZA_ERRNO err)
{
    switch (err)
    {
    case MOZA_ERR_NO_ERROR: return "MOZA_ERR_NO_ERROR";
    case MOZA_ERR_UNKNOWN: return "MOZA_ERR_UNKNOWN";
    case MOZA_ERR_SERVICE_NOT_FOUND: return "MOZA_ERR_SERVICE_NOT_FOUND";
    case MOZA_ERR_SERVICE_VERSION_MISMATCH: return "MOZA_ERR_SERVICE_VERSION_MISMATCH";
    case MOZA_ERR_INVALID_ARGUMENT: return "MOZA_ERR_INVALID_ARGUMENT";
    case MOZA_ERR_NOT_INITIALIZED: return "MOZA_ERR_NOT_INITIALIZED";
    case MOZA_ERR_ALREADY_INITIALIZED: return "MOZA_ERR_ALREADY_INITIALIZED";
    case MOZA_ERR_BUFFER_TOO_SMALL: return "MOZA_ERR_BUFFER_TOO_SMALL";
    case MOZA_ERR_DEVICE_NOT_FOUND: return "MOZA_ERR_DEVICE_NOT_FOUND";
    case MOZA_ERR_DEVICE_NOT_OPEN: return "MOZA_ERR_DEVICE_NOT_OPEN";
    case MOZA_ERR_INVALID_HANDLE: return "MOZA_ERR_INVALID_HANDLE";
    case MOZA_ERR_BUSY: return "MOZA_ERR_BUSY";
    case MOZA_ERR_TIMEOUT: return "MOZA_ERR_TIMEOUT";
    case MOZA_ERR_NOT_SUPPORTED: return "MOZA_ERR_NOT_SUPPORTED";
    case MOZA_ERR_PARAMETER_READ_COMMAND_ERROR: return "MOZA_ERR_PARAMETER_READ_COMMAND_ERROR";
    case MOZA_ERR_PARAMETER_WRITE_COMMAND_ERROR: return "MOZA_ERR_PARAMETER_WRITE_COMMAND_ERROR";
    case MOZA_ERR_INDEX_OUT_OF_BOUNDS: return "MOZA_ERR_INDEX_OUT_OF_BOUNDS";
    case MOZA_ERR_PENDING: return "MOZA_ERR_PENDING";
    default: return "<unknown>";
    }
}

static void print_event(const MOZA_EVENT_DATA* event, void* userData)
{
    (void)userData;

    if (!event)
    {
        return;
    }

    printf("[event] type=%s timestamp=%lld",
           moza_event_type_to_string(event->type),
           (long long)event->timestamp);

    switch (event->type)
    {
    case MOZA_ET_DEVICE_CONNECTED:
        printf(" deviceId=%u", (unsigned)event->data.deviceConnected.deviceId);
        break;
    case MOZA_ET_DEVICE_DISCONNECTED:
        printf(" deviceId=%u", (unsigned)event->data.deviceDisconnected.deviceId);
        break;
    case MOZA_ET_DEVICE_PARAMETER_VALUE_CHANGED:
        printf(" deviceId=%u parameterId=%d parameterName=%s",
               (unsigned)event->data.parameterValueChanged.deviceId,
               event->data.parameterValueChanged.parameterId,
               event->data.parameterValueChanged.parameterName ? event->data.parameterValueChanged.parameterName : "<null>");
        break;
    case MOZA_ET_DEVICE_PARAMETER_ERROR_OCCURRED:
        printf(" deviceId=%u parameterId=%d parameterName=%s error=%s",
               (unsigned)event->data.parameterErrorOccurred.deviceId,
               event->data.parameterErrorOccurred.parameterId,
               event->data.parameterErrorOccurred.parameterName ? event->data.parameterErrorOccurred.parameterName : "<null>",
               moza_errno_to_string(event->data.parameterErrorOccurred.error));
        break;
    case MOZA_ET_DEVICE_COMMAND_REPLIED:
        printf(" deviceId=%u commandId=%d commandName=%s",
               (unsigned)event->data.commandReplied.deviceId,
               event->data.commandReplied.commandId,
               event->data.commandReplied.commandName ? event->data.commandReplied.commandName : "<null>");
        break;
    case MOZA_ET_DEVICE_COMMAND_ERROR_OCCURRED:
        printf(" deviceId=%u commandId=%d commandName=%s error=%s",
               (unsigned)event->data.commandErrorOccurred.deviceId,
               event->data.commandErrorOccurred.commandId,
               event->data.commandErrorOccurred.commandName ? event->data.commandErrorOccurred.commandName : "<null>",
               moza_errno_to_string(event->data.commandErrorOccurred.error));
        break;
    case MOZA_ET_HID_AXIS_DATA_CHANGED:
        printf(" deviceId=%u hidIndex=%u axis=%d raw=%d normalized=%f",
               (unsigned)event->data.hidAxisDataChanged.deviceId,
               (unsigned)event->data.hidAxisDataChanged.hidIndex,
               (int)event->data.hidAxisDataChanged.axis,
               event->data.hidAxisDataChanged.value.rawValue,
               event->data.hidAxisDataChanged.value.normalizedValue);
        break;
    case MOZA_ET_HID_BUTTON_PRESSED:
    case MOZA_ET_HID_BUTTON_RELEASED:
        printf(" deviceId=%u hidIndex=%u button=%d",
               (unsigned)event->data.hidButtonPressed.deviceId,
               (unsigned)event->data.hidButtonPressed.hidIndex,
               event->data.hidButtonPressed.buttonNumber);
        break;
    default:
        break;
    }

    printf("\n");
}

static void print_log(const MOZA_LOG_CONTEXT* context,
                      void*                   userData)
{
    // (void)userData;
    //
    // printf("[log] level=%s", moza_log_level_to_string(context ? context->level : MOZA_LL_INFO));
    //
    // if (context && context->category && context->categorySize > 0)
    // {
    //     printf(" category=%.*s", (int)context->categorySize, context->category);
    // }
    //
    // if (context)
    // {
    //     printf(" context=available");
    // }
    //
    // printf(" message=%.*s\n",
    //        context ? (int)context->messageSize : 0,
    //        (context && context->message) ? context->message : "");
}

static void print_device_info(const MOZA_DEVICE_INFO* info)
{
    if (!info)
    {
        return;
    }

    printf("deviceId=%u\n", (unsigned)info->id);
    printf("name=%s\n", info->name);
    printf("portName=%s\n", info->portName);
    printf("vendorId=0x%04X\n", info->vendorId);
    printf("productId=0x%04X\n", info->productId);
    printf("category=%d\n", (int)info->category);
    printf("type=%d\n", info->type);
    printf("state=%d\n", (int)info->state);
    printf("hidDeviceCount=%u\n", (unsigned)info->hidDeviceCount);
}

static void print_parameter_list(MOZA_DEVICE_HANDLE handle)
{
    MOZA_SIZE  count = 0;
    MOZA_ERRNO err   = Moza_DeviceParameterGetList(handle, NULL, &count);
    if (err != MOZA_ERR_NO_ERROR)
    {
        printf("Moza_DeviceParameterGetList(size) failed: %s\n", moza_errno_to_string(err));
        return;
    }

    if (count == 0)
    {
        printf("No parameters reported by the device.\n");
        return;
    }

    MOZA_PARAMETER_INFO infos[64];
    if (count > (MOZA_SIZE)(sizeof(infos) / sizeof(infos[0])))
    {
        count = (MOZA_SIZE)(sizeof(infos) / sizeof(infos[0]));
    }

    err = Moza_DeviceParameterGetList(handle, infos, &count);
    if (err != MOZA_ERR_NO_ERROR)
    {
        printf("Moza_DeviceParameterGetList(data) failed: %s\n", moza_errno_to_string(err));
        return;
    }

    printf("Parameters (showing %u):\n", (unsigned)count);
    for (MOZA_SIZE i = 0; i < count; ++i)
    {
        printf("  [%u] id=%d name=%s readable=%u writable=%u\n",
               (unsigned)i,
               infos[i].id,
               infos[i].name,
               (unsigned)infos[i].readable,
               (unsigned)infos[i].writable);
    }
}

static void print_command_list(MOZA_DEVICE_HANDLE handle)
{
    MOZA_SIZE  count = 0;
    MOZA_ERRNO err   = Moza_DeviceCommandGetList(handle, NULL, &count);
    if (err != MOZA_ERR_NO_ERROR)
    {
        printf("Moza_DeviceCommandGetList(size) failed: %s\n", moza_errno_to_string(err));
        return;
    }

    if (count == 0)
    {
        printf("No commands reported by the device.\n");
        return;
    }

    MOZA_COMMAND_INFO infos[64];
    if (count > (MOZA_SIZE)(sizeof(infos) / sizeof(infos[0])))
    {
        count = (MOZA_SIZE)(sizeof(infos) / sizeof(infos[0]));
    }

    err = Moza_DeviceCommandGetList(handle, infos, &count);
    if (err != MOZA_ERR_NO_ERROR)
    {
        printf("Moza_DeviceCommandGetList(data) failed: %s\n", moza_errno_to_string(err));
        return;
    }

    printf("Commands (showing %u):\n", (unsigned)count);
    for (MOZA_SIZE i = 0; i < count; ++i)
    {
        printf("  [%u] id=%d name=%s\n", (unsigned)i, infos[i].id, infos[i].name);
    }
}

int main(void)
{
    MOZA_ERRNO err = Moza_SetLogCallback(print_log, NULL);
    if (err != MOZA_ERR_NO_ERROR)
    {
        printf("Moza_SetLogCallback failed: %s\n", moza_errno_to_string(err));
        return 1;
    }

    err = Moza_Initialize();
    if (err != MOZA_ERR_NO_ERROR)
    {
        printf("Moza_Initialize failed: %s\n", moza_errno_to_string(err));
        return 1;
    }

    err = Moza_SetEventCallback(print_event, NULL);
    if (err != MOZA_ERR_NO_ERROR)
    {
        printf("Moza_SetEventCallback failed: %s\n", moza_errno_to_string(err));
        Moza_Shutdown();
        return 1;
    }

    printf("Waiting for MOZA service to start and enumerate devices...\n");
    Sleep(5000);

    MOZA_SIZE deviceCount = 0;
    err                   = Moza_DeviceGetList(NULL, &deviceCount);
    if (err != MOZA_ERR_NO_ERROR)
    {
        printf("Moza_DeviceGetList(size) failed: %s\n", moza_errno_to_string(err));
        Moza_Shutdown();
        return 1;
    }

    if (deviceCount == 0)
    {
        printf("No MOZA devices found.\n");
        Moza_Shutdown();
        return 0;
    }

    MOZA_DEVICE_INFO devices[16];
    if (deviceCount > (MOZA_SIZE)(sizeof(devices) / sizeof(devices[0])))
    {
        deviceCount = (MOZA_SIZE)(sizeof(devices) / sizeof(devices[0]));
    }

    err = Moza_DeviceGetList(devices, &deviceCount);
    if (err != MOZA_ERR_NO_ERROR)
    {
        printf("Moza_DeviceGetList(data) failed: %s\n", moza_errno_to_string(err));
        Moza_Shutdown();
        return 1;
    }

    printf("Discovered %u device(s).\n\n", (unsigned)deviceCount);
    print_device_info(&devices[0]);
    printf("\n");

    MOZA_DEVICE_HANDLE handle = MOZA_INVALID_DEVICE_HANDLE;
    err                       = Moza_DeviceOpen(devices[0].id, &handle);
    if (err != MOZA_ERR_NO_ERROR)
    {
        printf("Moza_DeviceOpen failed: %s\n", moza_errno_to_string(err));
        Moza_Shutdown();
        return 1;
    }

    print_parameter_list(handle);
    printf("\n");
    print_command_list(handle);

    if (handle != MOZA_INVALID_DEVICE_HANDLE)
    {
        MOZA_ERRNO closeErr = Moza_DeviceClose(handle);
        if (closeErr != MOZA_ERR_NO_ERROR)
        {
            printf("Moza_DeviceClose failed: %s\n", moza_errno_to_string(closeErr));
        }
    }

    Moza_Shutdown();
    return 0;
}
