#include "moza/moza.hpp"

#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

namespace {

const char* toString(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>";
    }
}

const char* toString(moza::LogLevel 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>";
    }
}

const char* toString(moza::EventType 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>";
    }
}

void printEvent(const moza::EventData* event, void*)
{
    if (!event)
    {
        return;
    }

    std::cout << "[event] type=" << toString(event->type)
        << " timestamp=" << event->timestamp;

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

    std::cout << '\n';
}

void printLog(const moza::LogContext* context, void*)
{
    // std::cout << "[log] level=" << toString(context ? context->level : MOZA_LL_INFO);
    // if (context && context->category && context->categorySize > 0)
    // {
    //     std::cout << " category=" << std::string_view(context->category, context->categorySize);
    // }
    // if (context)
    // {
    //     std::cout << " context=available";
    // }
    // std::cout << " message="
    //           << (context && context->message ? std::string_view(context->message, context->messageSize) : std::string_view{})
    //           << '\n';
}

void printDeviceInfo(const moza::DeviceInfo& info)
{
    std::cout << "deviceId=" << info.id << '\n'
        << "name=" << info.name << '\n'
        << "portName=" << info.portName << '\n'
        << "vendorId=0x" << std::hex << std::setw(4) << std::setfill('0') << info.vendorId << '\n'
        << "productId=0x" << std::setw(4) << info.productId << '\n'
        << std::dec << std::setfill(' ')
        << "category=" << static_cast<int>(info.category) << '\n'
        << "type=" << info.type << '\n'
        << "state=" << static_cast<int>(info.state) << '\n'
        << "hidDeviceCount=" << static_cast<int>(info.hidDeviceCount) << '\n';
}

void printParameterList(const moza::Device& device)
{
    auto [err, infos] = device.getParameterList();
    if (err != MOZA_ERR_NO_ERROR)
    {
        std::cout << "getParameterList failed: " << toString(err) << '\n';
        return;
    }

    std::cout << "Parameters (showing " << infos.size() << "):\n";
    for (const auto& info : infos)
    {
        std::cout << "  id=" << info.id
            << " name=" << info.name
            << " readable=" << static_cast<int>(info.readable)
            << " writable=" << static_cast<int>(info.writable)
            << '\n';
    }
}

void printCommandList(const moza::Device& device)
{
    auto [err, infos] = device.getCommandList();
    if (err != MOZA_ERR_NO_ERROR)
    {
        std::cout << "getCommandList failed: " << toString(err) << '\n';
        return;
    }

    std::cout << "Commands (showing " << infos.size() << "):\n";
    for (const auto& info : infos)
    {
        std::cout << "  id=" << info.id << " name=" << info.name << '\n';
    }
}

} // namespace

int main()
{
    const moza::Errno logErr = moza::setLogCallback(printLog);
    if (logErr != MOZA_ERR_NO_ERROR)
    {
        std::cout << "setLogCallback failed: " << toString(logErr) << '\n';
        return 1;
    }

    const moza::Errno initErr = moza::initialize();
    if (initErr != MOZA_ERR_NO_ERROR)
    {
        std::cout << "initialize failed: " << toString(initErr) << '\n';
        return 1;
    }

    const moza::Errno callbackErr = moza::setEventCallback(printEvent, nullptr);
    if (callbackErr != MOZA_ERR_NO_ERROR)
    {
        std::cout << "setEventCallback failed: " << toString(callbackErr) << '\n';
        moza::shutdown();
        return 1;
    }

    std::cout << "Waiting for MOZA service to start and enumerate devices...\n";
    std::this_thread::sleep_for(std::chrono::seconds(5));

    auto [listErr, devices] = moza::getDeviceList();
    if (listErr != MOZA_ERR_NO_ERROR)
    {
        std::cout << "getDeviceList failed: " << toString(listErr) << '\n';
        moza::shutdown();
        return 1;
    }

    if (devices.empty())
    {
        std::cout << "No MOZA devices found.\n";
        moza::shutdown();
        return 0;
    }

    std::cout << "Discovered " << devices.size() << " device(s).\n\n";
    printDeviceInfo(devices.front());
    std::cout << '\n';

    auto [openErr, device] = moza::Device::open(devices.front().id);
    if (openErr != MOZA_ERR_NO_ERROR)
    {
        std::cout << "Device::open failed: " << toString(openErr) << '\n';
        moza::shutdown();
        return 1;
    }

    printParameterList(device);
    std::cout << '\n';
    printCommandList(device);

    const moza::Errno closeErr = device.close();
    if (closeErr != MOZA_ERR_NO_ERROR)
    {
        std::cout << "device.close failed: " << toString(closeErr) << '\n';
    }

    moza::shutdown();
    return 0;
}
