// FGHIDDevice.cxx -- HID input device implementation via HIDAPI
//
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2017 James Turner <james@flightgear.org>

#include "FGHIDDevice.hxx"

#include "FGAxisEvent.hxx"
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <string>

#include <simgear/debug/ErrorReportingCallback.hxx>
#include <simgear/debug/debug_types.h>
#include <simgear/io/lowlevel.hxx>
#include <simgear/misc/sg_path.hxx>
#include <simgear/misc/strutils.hxx>
#include <simgear/structure/exception.hxx>

#include <Main/fg_props.hxx>

#include "FGHIDEventInput.hxx"

using simgear::strutils::encodeHex;

namespace HID {

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;
}

static int hatXValue(int hatValue)
{
    switch (hatValue) {
    case 0:
    case 1:
        return 0;
    case 2:
    case 3:
    case 4:
        return 1;
    case 5:
        return 0;
    case 6:
    case 7:
    case 8:
        return -1;
    default:
        return 0;
    }
}

static int hatYValue(int hatValue)
{
    switch (hatValue) {
    case 0:
        return 0;
    case 1:
    case 2:
        return 1;
    case 3:
        return 0;
    case 4:
    case 5:
    case 6:
        return -1;
    case 7:
        return 0;
    case 8:
        return 1;
    default:
        return 0;
    }
}

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;
}

} // namespace HID

///////////////////////////////////////////////////////////////////////////////
// HIDEventData — used only within this file

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::FGHIDDevice(hid_device_info* devInfo, FGHIDEventInput*) : FGInputDevice({})
{
    class_id = "FGHIDDevice";
    _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));
    }

    SetVendorDeviceId(devInfo->vendor_id << 16 | devInfo->product_id);

    std::string _usage = HID::nameForUsage(devInfo->usage_page, devInfo->usage);
    SG_LOG(SG_INPUT, SG_DEBUG, "HID device " << _hidPath << " " << "0x" << std::hex << devInfo->vendor_id << ":0x" << std::hex << devInfo->product_id << " " << "release " << devInfo->release_number << " " << "usage " << _usage << "(0x" << std::hex << devInfo->usage_page << ":0x" << std::hex << devInfo->usage << ") " << "ifn " << devInfo->interface_number << " " << GetName());
}

#if defined(BUILDING_TESTSUITE)

FGHIDDevice::FGHIDDevice(const std::string& testName, const simgear::UInt8Vector& rawDescriptor)
    : FGInputDevice(testName),
      _rawXMLDescriptor(rawDescriptor)
{
    class_id = "FGHIDDevice";
    // Provide a valid deviceNode so the FGInputDevice destructor does not
    // dereference a null pointer when there is no active HID session.
    deviceNode = new SGPropertyNode;
}

bool FGHIDDevice::parseDescriptorForTesting()
{
    return parseUSBHIDDescriptor();
}

const FGHIDDevice::Item* FGHIDDevice::findItem(const std::string& name) const
{
    return itemWithName(name).second;
}

HID::ReportType FGHIDDevice::reportTypeForItem(const std::string& name) const
{
    const auto r = itemWithName(name);
    if (!r.first)
        return HID::ReportType::Invalid;
    return r.first->type;
}

uint8_t FGHIDDevice::reportIdForItem(const std::string& name) const
{
    const auto r = itemWithName(name);
    if (!r.first)
        return 0;
    return r.first->number;
}

#endif

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()
{
    SG_LOG(SG_INPUT, SG_INFO, "HID open " << GetUniqueName());
    _device = hid_open_path(_hidPath.c_str());
    if (_device == nullptr) {
        const auto path = SGPath(_hidPath);
        simgear::reportFailure(simgear::LoadFailure::IOError,
                               simgear::ErrorCode::InputDeviceConfig,
                               "Failed to open HID device " + _hidPath + " '" + GetUniqueName() + "'. On Linux you may need to adjust permissions of the device using UDev rules.",
                               path);
        return false;
    }

    if (_rawXMLDescriptor.empty()) {
        _rawXMLDescriptor.resize(2048);
        int descriptorSize = hid_get_report_descriptor(_device, _rawXMLDescriptor.data(), _rawXMLDescriptor.size());
        if (descriptorSize <= 0) {
            const auto path = SGPath(_hidPath);
            simgear::reportFailure(simgear::LoadFailure::IOError,
                                   simgear::ErrorCode::InputDeviceConfig,
                                   "Failed to read HID descriptor from " + _hidPath + " '" + GetUniqueName() + "'.", path);
            return false;
        }

        _rawXMLDescriptor.resize(descriptorSize);
    }

    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);
            const auto path = SGPath(_hidPath);
            simgear::reportFailure(simgear::LoadFailure::Misconfigured,
                                   simgear::ErrorCode::InputDeviceConfig,
                                   "HID device:" + GetUniqueName() + " has no element for event:" + v.first,
                                   path);
            continue;
        }

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

        reportItem.second->event = event;
        FGAxisEvent* axisEvent = dynamic_cast<FGAxisEvent*>(event.get());
        if (axisEvent) {
            axisEvent->SetDefaultRange(reportItem.second->logicalMin, reportItem.second->logicalMax);
            if (debugEvents) {
                SG_LOG(SG_INPUT, SG_INFO, "\tset default axis range to: " << reportItem.second->logicalMin << ":" << reportItem.second->logicalMax);
            }
        }
    }

    return true;
}

bool FGHIDDevice::parseUSBHIDDescriptor()
{
    if (_debugRaw) {
        SG_LOG(SG_INPUT, SG_INFO, "\nHID: descriptor for:" << GetUniqueName() << "\n\t" << encodeHex(_rawXMLDescriptor, ':'));
    }

    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 name
        name = name + "-" + std::to_string(existingCount);
    }

    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);
    itemObject->logicalMin = item->logical_min;
    itemObject->logicalMax = item->logical_max;

    report->items.push_back(itemObject);
    // synthesize axes for hats, to match Linux Event Input behaviour
    // also makes porting legacy joystick configs easier
    const auto u = HID::usage(item->usage);
    if ((u.first == HID::UsagePage::GenericDesktop) && (u.second == HID::GD_Hatswitch)) {
        const bool zeroIsCenter = (item->logical_min == 1) && (item->logical_max == 8);
        // FIXME : multiple hats will not work here, because they won't get renamed
        // when we suffix the item
        Item* hatXAxis = new Item(name + "-x", bitOffset, item->report_size);
        hatXAxis->logicalMin = -1;
        hatXAxis->logicalMax = 1;
        hatXAxis->isHatX = true;
        hatXAxis->zeroIsCenter = zeroIsCenter;
        report->items.push_back(hatXAxis);

        Item* hatYAxis = new Item(name + "-y", bitOffset, item->report_size);
        hatYAxis->logicalMin = -1;
        hatYAxis->logicalMax = 1;
        hatYAxis->isHatY = true;
        hatYAxis->zeroIsCenter = zeroIsCenter;
        report->items.push_back(hatYAxis);
    }
}

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) {
        SG_LOG(SG_INPUT, SG_INFO, "sending bytes: " << encodeHex(reportBytes, ':'));
    }

    // 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;
}

int FGHIDDevice::adjustHatValue(Item* item, int inValue)
{
    if (item->zeroIsCenter) {
        return inValue;
    }
    // really corresponds to -1, but we don't sign-extend
    else if (inValue == 15) {
        return 0;
    } else {
        return inValue + 1;
    }
}

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);
        SG_LOG(SG_INPUT, SG_INFO, "\tbytes: " << encodeHex(data, length, ':'));
    }

    for (auto item : report->items) {
        const int rawValue = extractBits(data, length, item->bitOffset, item->bitSize);
        int value = maybeSignExtend(item, rawValue);
        // do this *before* the lastValue check, so we don't generate events
        // on axes that aren't changing
        if (item->isHatX) {
            value = HID::hatXValue(adjustHatValue(item, value));
        } else if (item->isHatY) {
            value = HID::hatYValue(adjustHatValue(item, value));
        }

        // suppress events for values that aren't changing
        if (item->isRelative) {
            // suppress spurious 0-valued relative events
            if (value == 0) {
                continue;
            }
        } else {
            // real-world consumer hardware can have optimistic ADCs, but cheap
            // potentiometers, leading to bit readings that vary continuously, making a lot of
            // spurious events. To mitigate this, allow the configuration to define a noise
            // threshold in bits. Note the literal value is still used, we don't quantize the
            // value we send in HIDEventData, so the full resolution is still available to event handlers
            auto axis = dynamic_cast<FGAxisEvent*>(item->event.get());
            const uint32_t noiseThresholdBits = axis ? axis->getNoiseThresholdBits() : 0;
            if (noiseThresholdBits > 0) {
                const int noiseThreshold = 1 << noiseThresholdBits;
                if (std::abs(value - item->lastValue) < noiseThreshold) {
                    continue;
                }

            } else {
                if (value == item->lastValue) {
                    continue;
                }
            }
        }

        const auto oldValue = item->lastValue;
        item->lastValue = value;
        // we don't need to forward un-handled events, *but* we do want to record them in `last-event` node,
        // in debug mode
        if (!item->event && !debugEvents) {
            continue;
        }

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

        if (!item->event && debugEvents) {
            SG_LOG(SG_INPUT, SG_INFO, "\tunhandled item:" << item->name << " value:" << value);
        }

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

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

    if (_debugRaw) {
        SG_LOG(SG_INPUT, SG_INFO, GetName() << ": FGHIDDevice: Sending feature report:" << (int)reportId << ", len=" << data.size());
        SG_LOG(SG_INPUT, SG_INFO, "\tbytes: " << encodeHex(data, ':'));
    }

    uint8_t buf[65];
    size_t len = std::min(data.size() + 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)
{
    // even though this called 'Send' it's really 'mark value for sending when we update', to
    // avoid generating multiple output reports in a single update frame.
    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
    }

    lastEventName->setStringValue(eventName);
    lastEventValue->setDoubleValue(value);

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

void FGHIDDevice::SendOutputReport(unsigned int reportId, const simgear::UInt8Vector& data)
{
    if (!_device) {
        return;
    }

    auto r = getReport(HID::ReportType::Out, reportId);
    if (!r) {
        SG_LOG(SG_INPUT, SG_DEV_ALERT, "HID device does not define an output report with ID:" << reportId);
        return;
    }

    if (_debugRaw) {
        SG_LOG(SG_INPUT, SG_INFO, GetName() << ": FGHIDDevice: output feature:" << (int)reportId << ", len=" << data.size());
        SG_LOG(SG_INPUT, SG_INFO, "\tbytes: " << encodeHex(data, ':'));
    }

    uint8_t buf[65];
    size_t len = std::min(data.size() + 1, sizeof(buf));
    buf[0] = reportId;
    memcpy(buf + 1, data.data(), len - 1);

    auto result = hid_write(_device, buf, len);
    if (result < 0) {
        SG_LOG(SG_INPUT, SG_DEV_ALERT, GetName() << ": FGHIDDevice: Sending outpit report failed, error-string is:\n"
                                                 << simgear::strutils::error_string(errno));
    }
}

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 n = reportNode->getChild(c);
        const int size = n->getIntValue("size", 1); // default to a single bit
        if (n->getNameString() == "unused-bits") {
            bitCount += size;
            continue;
        }

        if (n->getNameString() == "type" || n->getNameString() == "id") {
            continue; // already handled above
        }

        // allow repeating items
        uint8_t count = n->getIntValue("count", 1);
        std::string name = n->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 = n->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;
        }
    }
}
