// FGHIDEventInput.cxx -- handle event driven input devices via HIDAPI
//
// Written by James Turner
//
// Copyright (C) 2017, James Turner <zakalawe@mac.com>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//

#include "config.h"

#include "FGHIDEventInput.hxx"

#include <cstdlib>
#include <cassert>
#include <algorithm>

#include <hidapi/hidapi.h>
#include <hidapi/hidparse.h>

#include <simgear/structure/exception.hxx>
#include <simgear/sg_inlines.h>
#include <simgear/misc/strutils.hxx>
#include <simgear/io/lowlevel.hxx>

const char* hexTable = "0123456789ABCDEF";

namespace HID
{
    enum class UsagePage
    {
        Undefined = 0,
        GenericDesktop,
        Simulation,
        VR,
        Sport,
        Game,
        GenericDevice,
        Keyboard,
        LEDs,
        Button,
        Ordinal,
        Telephony,
        Consumer,
        Digitizer,
        // reserved 0x0E
        // PID 0x0f
        Unicode = 0x10,
        AlphanumericDisplay = 0x14,

        VendorDefinedStart = 0xFF00
    };

    enum GenericDesktopUsage
    {
        // generic desktop section
        GD_Joystick = 0x04,
        GD_GamePad = 0x05,
        GD_Keyboard = 0x06,
        GD_Keypad = 0x07,
        GD_MultiAxisController = 0x08,
        GD_X = 0x30,
        GD_Y,
        GD_Z,
        GD_Rx,
        GD_Ry,
        GD_Rz,
        GD_Slider,
        GD_Dial,
        GD_Wheel,
        GD_Hatswitch,
        GD_DpadUp = 0x90,
        GD_DpadDown,
        GD_DpadRight,
        GD_DpadLeft
    };

    enum LEDUsage
    {
        LED_Undefined = 0,
        LED_Play = 0x36,
        LED_Pause = 0x37,
        LED_GenericIndicator = 0x4B
    };

    enum AlphanumericUsage
    {
        AD_AlphanumericDisplay = 0x01,
        AD_BitmappedDisplay = 0x2,
        AD_DisplayControlReport = 0x24,
        AD_ClearDisplay = 0x25,
        AD_CharacterReport = 0x2B,
        AD_DisplayData = 0x2C,
        AD_DisplayStatus = 0x2D,
        AD_Rows = 0x35,
        AD_Columns = 0x36,
        AD_7SegmentDirectMap = 0x43,
        AD_14SegmentDirectMap = 0x45,
        AD_DisplayBrightness = 0x46,
        AD_DisplayContrast = 0x47
    };

    enum class ReportType
    {
        Invalid = 0,
        In = 0x08,
        Out = 0x09,
        Feature = 0x0B
    };

    std::string nameForUsage(uint32_t usagePage, uint32_t usage)
    {
        const auto enumUsage = static_cast<UsagePage>(usagePage);
        if (enumUsage == UsagePage::Undefined) {
          std::stringstream os;
          os << "undefined-" << usage;
          return os.str();
        }

        if (enumUsage == UsagePage::GenericDesktop) {
            switch (usage) {
                case GD_Joystick:          return "joystick";
                case GD_Wheel:             return "wheel";
                case GD_Dial:              return "dial";
                case GD_Hatswitch:         return "hat";
                case GD_Slider:            return "slider";
                case GD_Rx:                return "x-rotate";
                case GD_Ry:                return "y-rotate";
                case GD_Rz:                return "z-rotate";
                case GD_X:                 return "x-translate";
                case GD_Y:                 return "y-translate";
                case GD_Z:                 return "z-translate";
                default:
                    SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID generic desktop usage:" << usage);
            }
        } else if (enumUsage == UsagePage::Simulation) {
            switch (usage) {
                default:
                    SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID simulation usage:" << usage);
            }
        } else if (enumUsage == UsagePage::Consumer) {
            switch (usage) {
                default:
                    SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID consumer usage:" << usage);
            }
        } else if (enumUsage == UsagePage::AlphanumericDisplay) {
            switch (usage) {
                case AD_AlphanumericDisplay:       return "alphanumeric";
                case AD_CharacterReport:           return "character-report";
                case AD_DisplayData:               return "display-data";
                case AD_DisplayBrightness:         return "display-brightness";
                case AD_7SegmentDirectMap:          return "seven-segment-direct";
                case AD_14SegmentDirectMap:         return "fourteen-segment-direct";

                default:
                    SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID alphanumeric usage:" << usage);
            }
        } else if (enumUsage == UsagePage::LEDs) {
            switch (usage) {
                case LED_GenericIndicator:         return "led-misc";
                case LED_Pause:                    return "led-pause";
                default:
                    SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID LED usage:" << usage);

            }
        } else if (enumUsage == UsagePage::Button) {
            std::stringstream os;
            os << "button-" << usage;
            return os.str();
        } else if (enumUsage >= UsagePage::VendorDefinedStart) {
            return "vendor";
        } else {
            SG_LOG(SG_INPUT, SG_WARN, "Unhandled HID usage page:" << std::hex << usagePage
                   << " with usage " << std::hex << usage);
        }

        return "unknown";
    }

    bool shouldPrefixWithAbs(uint32_t usagePage, uint32_t usage)
    {
        const auto enumUsage = static_cast<UsagePage>(usagePage);
        if (enumUsage == UsagePage::GenericDesktop) {
            switch (usage) {
            case GD_Wheel:
            case GD_Dial:
            case GD_Hatswitch:
            case GD_Slider:
            case GD_Rx:
            case GD_Ry:
            case GD_Rz:
            case GD_X:
            case GD_Y:
            case GD_Z:
                return true;
            default:
                break;
            }
        }

        return false;
    }

    ReportType reportTypeFromString(const std::string& s)
    {
        if (s == "input") return ReportType::In;
        if (s == "output") return ReportType::Out;
        if (s == "feature") return ReportType::Feature;
        return ReportType::Invalid;
    }
} // of namespace

class FGHIDEventInput::FGHIDEventInputPrivate
{
public:
    FGHIDEventInput* p = nullptr;

    void evaluateDevice(hid_device_info* deviceInfo);
};

// anonymous namespace to define our device subclass
namespace
{

class FGHIDDevice : public FGInputDevice {
public:
    FGHIDDevice(hid_device_info* devInfo,
        FGHIDEventInput* subsys);

    virtual ~FGHIDDevice();

    bool Open() override;
    void Close() override;
    void Configure(SGPropertyNode_ptr node) override;

    void update(double dt) override;
    const char *TranslateEventName(FGEventData &eventData) override;
    void Send( const char * eventName, double value ) override;
    void SendFeatureReport(unsigned int reportId, const std::string& data) override;

    class Item
    {
    public:
        Item(const std::string& n, uint32_t offset, uint8_t size) :
            name(n),
            bitOffset(offset),
            bitSize(size)
        {}

        std::string name;
        uint32_t bitOffset = 0; // form the start of the report
        uint8_t bitSize = 1;
        bool isRelative = false;
        bool doSignExtend = false;
        int lastValue = 0;
        // int defaultValue = 0;
        // range, units, etc not needed for now
        // hopefully this doesn't need to be a list
        FGInputEvent_ptr event;
    };
private:
    class Report
    {
    public:
        Report(HID::ReportType ty, uint8_t n = 0) : type(ty), number(n) {}

        HID::ReportType type;
        uint8_t number = 0;
        std::vector<Item*> items;

        uint32_t currentBitSize() const
        {
            uint32_t size = 0;
            for (auto i : items) {
                size += i->bitSize;
            }
            return size;
        }
    };

    bool parseUSBHIDDescriptor();
    void parseCollection(hid_item* collection);
    void parseItem(hid_item* item);

    Report* getReport(HID::ReportType ty, uint8_t number, bool doCreate = false);

    void sendReport(Report* report) const;

    uint8_t countWithName(const std::string& name) const;
    std::pair<Report*, Item*> itemWithName(const std::string& name) const;

    void processInputReport(Report* report, unsigned char* data, size_t length,
                            double dt, int keyModifiers);

    int maybeSignExtend(Item* item, int inValue);

    void defineReport(SGPropertyNode_ptr reportNode);

    std::vector<Report*> _reports;
    std::string _hidPath;
    hid_device* _device = nullptr;
    bool _haveNumberedReports = false;
    bool _debugRaw = false;

    /// set if we parsed the device description our XML
    /// instead of from the USB data. Useful on Windows where the data
    /// is inaccessible, or devices with broken descriptors
    bool _haveLocalDescriptor = false;

    /// allow specifying the descriptor as hex bytes in XML
    std::vector<uint8_t>_rawXMLDescriptor;

    // all sets which will be send on the next update() call.
    std::set<Report*> _dirtyReports;
};

class HIDEventData : public FGEventData
{
public:
    // item, value, dt, keyModifiers
    HIDEventData(FGHIDDevice::Item* it, int value, double dt, int keyMods) :
        FGEventData(value, dt, keyMods),
        item(it)
    {
        assert(item);
    }

    FGHIDDevice::Item* item = nullptr;
};

FGHIDDevice::FGHIDDevice(hid_device_info *devInfo, FGHIDEventInput *)
{
    _hidPath = devInfo->path;

    std::wstring manufacturerName, productName;
    productName = devInfo->product_string ? std::wstring(devInfo->product_string)
                                          : L"unknown HID device";

    if (devInfo->manufacturer_string) {
        manufacturerName = std::wstring(devInfo->manufacturer_string);
        SetName(simgear::strutils::convertWStringToUtf8(manufacturerName) + " " +
            simgear::strutils::convertWStringToUtf8(productName));
    } else {
        SetName(simgear::strutils::convertWStringToUtf8(productName));
    }

    const auto serial = devInfo->serial_number;
    std::string path(devInfo->path);
    // most devices return an empty serial number, unfortunately
    if ((serial != nullptr) && std::wcslen(serial) > 0) {
        SetSerialNumber(simgear::strutils::convertWStringToUtf8(serial));
    }

    SG_LOG(SG_INPUT, SG_DEBUG, "HID device:" << GetName() << " at path " << _hidPath);
}

FGHIDDevice::~FGHIDDevice()
{
    if (_device) {
        hid_close(_device);
    }
}

void FGHIDDevice::Configure(SGPropertyNode_ptr node)
{
    // base class first
    FGInputDevice::Configure(node);

    if (node->hasChild("hid-descriptor")) {
        _haveLocalDescriptor = true;
        if (debugEvents) {
            SG_LOG(SG_INPUT, SG_INFO, GetUniqueName() << " will configure using local HID descriptor");
        }

        for (auto report : node->getChild("hid-descriptor")->getChildren("report")) {
            defineReport(report);
        }
    }

    if (node->hasChild("hid-raw-descriptor")) {
        _rawXMLDescriptor = simgear::strutils::decodeHex(node->getStringValue("hid-raw-descriptor"));
        if (debugEvents) {
            SG_LOG(SG_INPUT, SG_INFO, GetUniqueName() << " will configure using XML-defined raw HID descriptor");
        }
    }

    if (node->getBoolValue("hid-debug-raw")) {
        _debugRaw = true;
    }
}

bool FGHIDDevice::Open()
{
    _device = hid_open_path(_hidPath.c_str());
    if (_device == nullptr) {
        SG_LOG(SG_INPUT, SG_WARN, GetUniqueName() << ": HID: Failed to open:" << _hidPath);
        SG_LOG(SG_INPUT, SG_WARN, "\tnote on Linux you may need to adjust permissions of the device using UDev rules.");
        return false;
    }

#if !defined(SG_WINDOWS)
    if (_rawXMLDescriptor.empty()) {
        _rawXMLDescriptor.resize(2048);
        int descriptorSize = hid_get_descriptor(_device, _rawXMLDescriptor.data(), _rawXMLDescriptor.size());
        if (descriptorSize <= 0) {
           SG_LOG(SG_INPUT, SG_WARN, "HID: " << GetUniqueName() << " failed to read HID descriptor");
           return false;
        }

        _rawXMLDescriptor.resize(descriptorSize);
    }
#endif

    if (!_haveLocalDescriptor) {
        bool ok = parseUSBHIDDescriptor();
        if (!ok)
            return false;
    }

    for (auto& v : handledEvents) {
        auto reportItem = itemWithName(v.first);
        if (!reportItem.second) {
            SG_LOG(SG_INPUT, SG_WARN, "HID device:" << GetUniqueName() << " has no element for event:" << v.first);
            continue;
        }

        FGInputEvent_ptr event = v.second;
        if (debugEvents) {
            SG_LOG(SG_INPUT, SG_INFO, "\tfound item for event:" << v.first);
        }

        reportItem.second->event = event;
    }

    return true;
}

bool FGHIDDevice::parseUSBHIDDescriptor()
{
#if defined(SG_WINDOWS)
    if (_rawXMLDescriptor.empty()) {
        SG_LOG(SG_INPUT, SG_ALERT, GetUniqueName() << ": on Windows, there is no way to extract the UDB-HID report descriptor. "
               << "\nPlease supply the report descriptor in the device XML configuration.");
        SG_LOG(SG_INPUT, SG_ALERT, "See this page:<> for information on extracting the report descriptor on Windows");
        return false;
    }
#endif

    if (_debugRaw) {
        SG_LOG(SG_INPUT, SG_INFO, "\nHID: descriptor for:" << GetUniqueName());
        {
            std::ostringstream byteString;

            for (auto i=0; i<_rawXMLDescriptor.size(); ++i) {
                byteString << hexTable[_rawXMLDescriptor[i] >> 4];
                byteString << hexTable[_rawXMLDescriptor[i] & 0x0f];
                byteString << " ";
            }
            SG_LOG(SG_INPUT, SG_INFO, "\tbytes: " << byteString.str());
        }
    }

    hid_item* rootItem = nullptr;
    hid_parse_reportdesc(_rawXMLDescriptor.data(), _rawXMLDescriptor.size(), &rootItem);
    if (debugEvents) {
        SG_LOG(SG_INPUT, SG_INFO, "\nHID: scan for:" << GetUniqueName());
    }

    parseCollection(rootItem);

    hid_free_reportdesc(rootItem);
    return true;
}

void FGHIDDevice::parseCollection(hid_item* c)
{
    for (hid_item* child = c->collection; child != nullptr; child = child->next) {
        if (child->collection) {
            parseCollection(child);
        } else {
            // leaf item
            parseItem(child);
        }
    }
}

auto FGHIDDevice::getReport(HID::ReportType ty, uint8_t number, bool doCreate) -> Report*
{
    if (number > 0) {
        _haveNumberedReports = true;
    }

    for (auto report : _reports) {
        if ((report->type == ty) && (report->number == number)) {
            return report;
        }
    }

    if (doCreate) {
        auto r = new Report{ty, number};
        _reports.push_back(r);
        return r;
    } else {
        return nullptr;
    }
}

auto FGHIDDevice::itemWithName(const std::string& name) const -> std::pair<Report*, Item*>
{
    for (auto report : _reports) {
        for (auto item : report->items) {
            if (item->name == name) {
                return std::make_pair(report, item);
            }
        }
    }

    return std::make_pair(static_cast<Report*>(nullptr), static_cast<Item*>(nullptr));
}

uint8_t FGHIDDevice::countWithName(const std::string& name) const
{
    uint8_t result = 0;
    size_t nameLength = name.length();

    for (auto report : _reports) {
        for (auto item : report->items) {
            if (strncmp(name.c_str(), item->name.c_str(), nameLength) == 0) {
                result++;
            }
        }
    }

    return result;
}

void FGHIDDevice::parseItem(hid_item* item)
{
    std::string name = HID::nameForUsage(item->usage >> 16, item->usage & 0xffff);
    if (hid_parse_is_relative(item)) {
        name = "rel-" + name; // prefix relative names
    } else if (HID::shouldPrefixWithAbs(item->usage >> 16, item->usage & 0xffff)) {
        name = "abs-" + name;
    }

    const auto ty = static_cast<HID::ReportType>(item->type);
    auto existingItem = itemWithName(name);
    if (existingItem.second) {
        // type fixup
        const HID::ReportType existingItemType = existingItem.first->type;
        if (existingItemType != ty) {
            // might be an item named identically in input/output and feature reports
            //  -> prefix the feature one with 'feature'
            if (ty == HID::ReportType::Feature) {
                name = "feature-" + name;
            } else if (existingItemType == HID::ReportType::Feature) {
                // rename this existing item since it's a feature
                existingItem.second->name = "feature-" + name;
            }
        }
    }

    // do the count now, after we did any renaming, since we might have
    // N > 1 for the new name
    int existingCount = countWithName(name);
    if (existingCount > 0) {
        if (existingCount == 1) {
            // rename existing item 0 to have the "-0" suffix
            auto existingItem = itemWithName(name);
            existingItem.second->name += "-0";
        }

        // define the new nae
        std::stringstream os;
        os << name << "-" << existingCount;
        name = os.str();
    }

    auto report = getReport(ty, item->report_id, true /* create */);
    uint32_t bitOffset = report->currentBitSize();

    if (debugEvents) {
        SG_LOG(SG_INPUT, SG_INFO, GetUniqueName() << ": add:" << name << ", bits: " << bitOffset << ":" << (int) item->report_size
            << ", report=" << (int) item->report_id);
    }

    Item* itemObject = new Item{name, bitOffset, item->report_size};
    itemObject->isRelative = hid_parse_is_relative(item);
    itemObject->doSignExtend = (item->logical_min < 0) || (item->logical_max < 0);
    report->items.push_back(itemObject);
}

void FGHIDDevice::Close()
{
    if (_device) {
        hid_close(_device);
        _device = nullptr;
    }
}

void FGHIDDevice::update(double dt)
{
    if (!_device) {
        return;
    }

    uint8_t reportBuf[65];
    int readCount = 0;
    while (true) {
        readCount = hid_read_timeout(_device, reportBuf, sizeof(reportBuf), 0);

        if (readCount <= 0) {
            break;
        }

        int modifiers = fgGetKeyModifiers();
        const uint8_t reportNumber = _haveNumberedReports ? reportBuf[0] : 0;
        auto inputReport = getReport(HID::ReportType::In, reportNumber, false);
        if (!inputReport) {
            SG_LOG(SG_INPUT, SG_WARN, GetName() << ": FGHIDDevice: Unknown input report number:" <<
                static_cast<int>(reportNumber));
        } else {
            uint8_t* reportBytes = _haveNumberedReports ? reportBuf + 1 : reportBuf;
            size_t reportSize = _haveNumberedReports ? readCount -  1 : readCount;
            processInputReport(inputReport, reportBytes, reportSize, dt, modifiers);
        }
    }

    FGInputDevice::update(dt);

    for (auto rep : _dirtyReports) {
        sendReport(rep);
    }

    _dirtyReports.clear();
}

void FGHIDDevice::sendReport(Report* report) const
{
    if (!_device) {
        return;
    }

    uint8_t reportBytes[65];
    size_t reportLength = 0;
    memset(reportBytes, 0, sizeof(reportBytes));
    reportBytes[0] = report->number;

// fill in valid data
    for (auto item : report->items) {
        reportLength += item->bitSize;
        if (item->lastValue == 0) {
            continue;
        }

        writeBits(reportBytes + 1, item->bitOffset, item->bitSize, item->lastValue);
    }

    reportLength /= 8;

    if (_debugRaw) {
        std::ostringstream byteString;
        for (size_t i=0; i<reportLength; ++i) {
            byteString << hexTable[reportBytes[i] >> 4];
            byteString << hexTable[reportBytes[i] & 0x0f];
            byteString << " ";
        }
        SG_LOG(SG_INPUT, SG_INFO, "sending bytes: " << byteString.str());
    }


// send the data, based on the report type
    if (report->type == HID::ReportType::Feature) {
        hid_send_feature_report(_device, reportBytes, reportLength + 1);
    } else {
        assert(report->type == HID::ReportType::Out);
        hid_write(_device, reportBytes, reportLength + 1);
    }
}

int FGHIDDevice::maybeSignExtend(Item* item, int inValue)
{
    return item->doSignExtend ? signExtend(inValue, item->bitSize) : inValue;
}

void FGHIDDevice::processInputReport(Report* report, unsigned char* data,
                                     size_t length,
                                     double dt, int keyModifiers)
{
    if (_debugRaw) {
        SG_LOG(SG_INPUT, SG_INFO, GetName() << " FGHIDDeivce received input report:" << (int) report->number << ", len=" << length);
        {
            std::ostringstream byteString;
            for (size_t i=0; i<length; ++i) {
                byteString << hexTable[data[i] >> 4];
                byteString << hexTable[data[i] & 0x0f];
                byteString << " ";
            }
            SG_LOG(SG_INPUT, SG_INFO, "\tbytes: " << byteString.str());
        }
    }

    for (auto item : report->items) {
        int value = extractBits(data, length, item->bitOffset, item->bitSize);

        value = maybeSignExtend(item, value);

        // suppress events for values that aren't changing
        if (item->isRelative) {
            // supress spurious 0-valued relative events
            if (value == 0) {
                continue;
            }
        } else {
            // supress no-change events for absolute items
            if (value == item->lastValue) {
                continue;
            }
        }

        item->lastValue = value;
        if (!item->event)
            continue;

        if (_debugRaw) {
            SG_LOG(SG_INPUT, SG_INFO, "\titem:" << item->name << " = " << value);
        }

        HIDEventData event{item, value, dt, keyModifiers};
        HandleEvent(event);
    }
}

void FGHIDDevice::SendFeatureReport(unsigned int reportId, const std::string& data)
{
    if (!_device) {
        return;
    }

    if (_debugRaw) {
        SG_LOG(SG_INPUT, SG_INFO, GetName() << ": FGHIDDevice: Sending feature report:" << (int) reportId << ", len=" << data.size());
        {
            std::ostringstream byteString;

            for (unsigned int i=0; i<data.size(); ++i) {
                byteString << hexTable[data[i] >> 4];
                byteString << hexTable[data[i] & 0x0f];
                byteString << " ";
            }
            SG_LOG(SG_INPUT, SG_INFO, "\tbytes: " << byteString.str());
        }
    }

    uint8_t buf[65];
    size_t len = std::min(data.length() + 1, sizeof(buf));
    buf[0] = reportId;
    memcpy(buf + 1, data.data(), len - 1);
    size_t r = hid_send_feature_report(_device, buf, len);
    if (r < 0) {
        SG_LOG(SG_INPUT, SG_WARN, GetName() << ": FGHIDDevice: Sending feature report failed, error-string is:\n"
               << simgear::strutils::error_string(errno));
    }
}

const char *FGHIDDevice::TranslateEventName(FGEventData &eventData)
{
    HIDEventData& hidEvent = static_cast<HIDEventData&>(eventData);
    return hidEvent.item->name.c_str();
}

void FGHIDDevice::Send(const char *eventName, double value)
{
    auto item = itemWithName(eventName);
    if (item.second == nullptr) {
        SG_LOG(SG_INPUT, SG_WARN, GetName() << ": FGHIDDevice:unknown item name:" << eventName);
        return;
    }

    int intValue = static_cast<int>(value);
    if (item.second->lastValue == intValue) {
        return; // not actually changing
    }

    // update the stored value prior to sending
    item.second->lastValue = intValue;
    _dirtyReports.insert(item.first);
}

void FGHIDDevice::defineReport(SGPropertyNode_ptr reportNode)
{
    const int nChildren = reportNode->nChildren();
    uint32_t bitCount = 0;
    const auto rty = HID::reportTypeFromString(reportNode->getStringValue("type"));
    if (rty == HID::ReportType::Invalid) {
        SG_LOG(SG_INPUT, SG_WARN, GetName() << ": FGHIDDevice: invalid report type:" <<
               reportNode->getStringValue("type"));
        return;
    }

    const auto id = reportNode->getIntValue("id");
    if (id > 0) {
        _haveNumberedReports = true;
    }

    auto report = new Report(rty, id);
    _reports.push_back(report);

    for (int c=0; c < nChildren; ++c) {
        const auto nd = reportNode->getChild(c);
        const int size = nd->getIntValue("size", 1); // default to a single bit
        if (!strcmp(nd->getName(), "unused-bits")) {
            bitCount += size;
            continue;
        }

        if (!strcmp(nd->getName(), "type") || !strcmp(nd->getName(), "id")) {
            continue; // already handled above
        }

        // allow repeating items
        uint8_t count = nd->getIntValue("count", 1);
        std::string name = nd->getNameString();
        const auto lastHypen = name.rfind("-");
        std::string baseName = name.substr(0, lastHypen + 1);
        int baseIndex = std::stoi(name.substr(lastHypen + 1));

        const bool isRelative = (name.find("rel-") == 0);
        const bool isSigned = nd->getBoolValue("is-signed", false);

        for (uint8_t i=0; i < count; ++i) {
            std::ostringstream oss;
            oss << baseName << (baseIndex + i);
            Item* itemObject = new Item{oss.str(), bitCount, static_cast<uint8_t>(size)};
            itemObject->isRelative = isRelative;
            itemObject->doSignExtend = isSigned;
            report->items.push_back(itemObject);
            bitCount += size;
        }
    }
}


} // of anonymous namespace


int extractBits(uint8_t* bytes, size_t lengthInBytes, size_t bitOffset, size_t bitSize)
{
    const size_t wholeBytesToSkip = bitOffset >> 3;
    const size_t offsetInByte = bitOffset & 0x7;

    // work out how many whole bytes to copy
    const size_t bytesToCopy = std::min(sizeof(uint32_t), (offsetInByte + bitSize + 7) / 8);
    uint32_t v = 0;
    // this goes from byte alignment to word alignment safely
    memcpy((void*) &v, bytes + wholeBytesToSkip, bytesToCopy);

    // shift down so lowest bit is aligned
    v = v >> offsetInByte;

    // mask off any extraneous top bits
    const uint32_t mask = ~(0xffffffff << bitSize);
    v &= mask;

    return v;
}

int signExtend(int inValue, size_t bitSize)
{
    const int m = 1U << (bitSize - 1);
    return (inValue ^ m) - m;
}

void writeBits(uint8_t* bytes, size_t bitOffset, size_t bitSize, int value)
{
    size_t wholeBytesToSkip = bitOffset >> 3;
    uint8_t* dataByte = bytes + wholeBytesToSkip;
    size_t offsetInByte = bitOffset & 0x7;
    size_t bitsInByte = std::min(bitSize, 8 - offsetInByte);
    uint8_t mask = 0xff >> (8 - bitsInByte);

    *dataByte |= ((value & mask) << offsetInByte);

    if (bitsInByte < bitSize) {
        // if we have more bits to write, recurse
        writeBits(bytes, bitOffset + bitsInByte, bitSize - bitsInByte, value >> bitsInByte);
    }
}

FGHIDEventInput::FGHIDEventInput() :
    FGEventInput(),
    d(new FGHIDEventInputPrivate)
{
    d->p = this; // store back pointer to outer object on pimpl
}

FGHIDEventInput::~FGHIDEventInput()
{
}

void FGHIDEventInput::init()
{
    FGEventInput::init();
    // have to wait until postinit since loading config files
    // requires Nasal to be running
}

void FGHIDEventInput::reinit()
{
    SG_LOG(SG_INPUT, SG_INFO, "Re-Initializing HID input bindings");
    FGHIDEventInput::shutdown();
    FGHIDEventInput::init();
    FGHIDEventInput::postinit();
}

void FGHIDEventInput::postinit()
{
    SG_LOG(SG_INPUT, SG_INFO, "HID event input starting up");

    hid_init();

    hid_device_info* devices = hid_enumerate(0 /* vendor ID */, 0 /* product ID */);

    for (hid_device_info* curDev = devices; curDev != nullptr; curDev = curDev->next) {
        d->evaluateDevice(curDev);
    }

    hid_free_enumeration(devices);
}

void FGHIDEventInput::shutdown()
{
    SG_LOG(SG_INPUT, SG_INFO, "HID event input shutting down");
    FGEventInput::shutdown();

    hid_exit();
}

//
// read all elements in each input device
//
void FGHIDEventInput::update(double dt)
{
    FGEventInput::update(dt);
}


// Register the subsystem.
SGSubsystemMgr::Registrant<FGHIDEventInput> registrantFGHIDEventInput;

///////////////////////////////////////////////////////////////////////////////////////////////

void FGHIDEventInput::FGHIDEventInputPrivate::evaluateDevice(hid_device_info* deviceInfo)
{
    // allocate an input device, and add to the base class to see if we have
    // a config
    p->AddDevice(new FGHIDDevice(deviceInfo, p));
}

///////////////////////////////////////////////////////////////////////////////////////////////
