/*
 * BRLTTY - A background process providing access to the console screen (when in
 *          text mode) for a blind person using a refreshable braille display.
 *
 * Copyright (C) 1995-2025 by The BRLTTY Developers.
 *
 * BRLTTY comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
 *
 * Web Page: http://brltty.app/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

#include "prologue.h"

#include <string.h>

#include "log.h"
#include "strfmt.h"
#include "bitmask.h"
#include "hid_defs.h"
#include "hid_items.h"
#include "hid_tables.h"
#include "hid_inspect.h"

static int
hidCompareTableEntriesByValue (const void *element1, const void *element2) {
  const HidTableEntryHeader *const *header1 = element1;
  const HidTableEntryHeader *const *header2 = element2;

  HidUnsignedValue value1 = (*header1)->value;
  HidUnsignedValue value2 = (*header2)->value;

  if (value1 < value2) return -1;
  if (value1 > value2) return 1;
  return 0;
}

const void *
hidTableEntry (HidTable *table, HidUnsignedValue value) {
  if (!table->sorted) {
    if (!(table->sorted = malloc(ARRAY_SIZE(table->sorted, table->count)))) {
      logMallocError();
      return NULL;
    }

    {
      const void *entry = table->entries;
      const HidTableEntryHeader **header = table->sorted;

      for (unsigned int index=0; index<table->count; index+=1) {
        *header++ = entry;
        entry += table->size;
      }
    }

    qsort(
      table->sorted, table->count, sizeof(*table->sorted),
      hidCompareTableEntriesByValue
    );
  }

  unsigned int from = 0;
  unsigned int to = table->count;

  while (from < to) {
    unsigned int current = (from + to) / 2;
    const HidTableEntryHeader *header = table->sorted[current];
    if (value == header->value) return header;

    if (value < header->value) {
      to = current;
    } else {
      from = current + 1;
    }
  }

  return NULL;
}

static int
hidCompareReportIdentifiers (const void *element1, const void *element2) {
  const HidReportIdentifier *identifier1 = element1;
  const HidReportIdentifier *identifier2 = element2;

  if (*identifier1 < *identifier2) return -1;
  if (*identifier1 > *identifier2) return 1;
  return 0;
}

HidReports *
hidGetReports (const HidItemsDescriptor *items) {
  HidReportIdentifier identifiers[UINT8_MAX];
  unsigned char count = 0;

  BITMASK(haveIdentifier, UINT8_MAX+1, char);
  BITMASK_ZERO(haveIdentifier);

  const unsigned char *nextByte = items->bytes;
  size_t bytesLeft = items->count;

  while (1) {
    HidItem item;
    if (!hidNextItem(&item, &nextByte, &bytesLeft)) break;

    switch (item.tag) {
      case HID_ITM_ReportID: {
        HidUnsignedValue identifier = item.value.u;

        if (!identifier) continue;
        if (identifier > UINT8_MAX) continue;
        if (BITMASK_TEST(haveIdentifier, identifier)) continue;

        BITMASK_SET(haveIdentifier, identifier);
        identifiers[count++] = identifier;
        break;
      }

      case HID_ITM_Input:
      case HID_ITM_Output:
      case HID_ITM_Feature:
        if (!count) identifiers[count++] = 0;
        break;
    }
  }

  if (count > 1) {
    qsort(
      identifiers, count, sizeof(identifiers[0]),
      hidCompareReportIdentifiers
    );
  }

  HidReports *reports;
  size_t size = sizeof(*reports);
  size += count;

  if ((reports = malloc(size))) {
    memset(reports, 0, sizeof(*reports));

    reports->count = count;
    memcpy(reports->identifiers, identifiers, count);

    return reports;
  } else {
    logMallocError();
  }

  return NULL;
}

STR_BEGIN_FORMATTER(hidFormatUsageFlags, HidUnsignedValue flags)
  typedef struct {
    const char *on;
    const char *off;
    HidUnsignedValue bit;
  } FlagEntry;

  static const FlagEntry flagTable[] = {
    { .bit = HID_USG_FLG_CONSTANT,
      .on = "const",
      .off = "data"
    },

    { .bit = HID_USG_FLG_VARIABLE,
      .on = "var",
      .off = "array"
    },

    { .bit = HID_USG_FLG_RELATIVE,
      .on = "rel",
      .off = "abs"
    },

    { .bit = HID_USG_FLG_WRAP,
      .on = "wrap",
    },

    { .bit = HID_USG_FLG_NON_LINEAR,
      .on = "nonlin",
    },

    { .bit = HID_USG_FLG_NO_PREFERRED,
      .on = "nopref",
    },

    { .bit = HID_USG_FLG_NULL_STATE,
      .on = "null",
    },

    { .bit = HID_USG_FLG_VOLATILE,
      .on = "volatile",
    },

    { .bit = HID_USG_FLG_BUFFERED_BYTE,
      .on = "buffbyte",
    },
  };

  const FlagEntry *flag = flagTable;
  const FlagEntry *end = flag + ARRAY_COUNT(flagTable);

  while (flag < end) {
    const char *name = (flags & flag->bit)? flag->on: flag->off;

    if (name) {
      if (STR_LENGTH > 0) STR_PRINTF(" ");
      STR_PRINTF("%s", name);
    }

    flag += 1;
  }
STR_END_FORMATTER

static int
hidListItem (const char *line, void *data) {
  return logMessage((LOG_CATEGORY(HID_IO) | LOG_DEBUG), "%s", line);
}

int
hidListItems (const HidItemsDescriptor *items, HidItemLister *listItem, void *data) {
  if (!listItem) listItem = hidListItem;
  const char *label = "Items List";

  {
    char line[0X40];
    STR_BEGIN(line, sizeof(line));
    STR_PRINTF("Begin %s: Bytes:%"PRIsize, label, items->count);
    STR_END;
    if (!listItem(line, data)) return 0;
  }

  unsigned int itemCount = 0;
  const unsigned char *nextByte = items->bytes;
  size_t bytesLeft = items->count;

  int decOffsetWidth;
  int hexOffsetWidth;
  {
    unsigned int maximumOffset = bytesLeft;
    char buffer[0X20];

    decOffsetWidth = snprintf(buffer, sizeof(buffer), "%u", maximumOffset);
    hexOffsetWidth = snprintf(buffer, sizeof(buffer), "%x", maximumOffset);
  }

  HidUnsignedValue usagePage = 0;

  while (1) {
    unsigned int offset = nextByte - items->bytes;
    HidItem item;
    int ok = hidNextItem(&item, &nextByte, &bytesLeft);

    char line[0X100];
    STR_BEGIN(line, sizeof(line));

    STR_PRINTF(
      "Item: %*u (0X%.*X):",
      decOffsetWidth, offset, hexOffsetWidth, offset
    );

    if (ok) {
      itemCount += 1;

      switch (item.tag) {
        case HID_ITM_UsagePage:
          usagePage = item.value.u;
          break;
      }

      {
        const HidItemTagEntry *tag = hidItemTagEntry(item.tag);

        if (tag) {
          STR_PRINTF(" %s", tag->header.name);
        } else {
          STR_PRINTF(" unknown item tag: 0X%02X", item.tag);
        }
      }

      if (item.valueSize > 0) {
        HidUnsignedValue hexValue = item.value.u & ((UINT64_C(1) << (item.valueSize * 8)) - 1);
        int hexPrecision = item.valueSize * 2;

        STR_PRINTF(
          " = %" PRId32 " (0X%.*" PRIX32 ")",
          item.value.s, hexPrecision, hexValue
        );
      }

      {
        HidUnsignedValue value = item.value.u;

        char name[0X100];
        STR_BEGIN(name, sizeof(name));

        switch (item.tag) {
          case HID_ITM_UsagePage: {
            const HidUsagePageEntry *upg = hidUsagePageEntry(value);
            if (upg) STR_PRINTF("%s", upg->header.name);
            break;
          }

          case HID_ITM_UsageMinimum:
          case HID_ITM_UsageMaximum:
          case HID_ITM_Usage: {
            HidUnsignedValue usage = item.value.u;

            HidUnsignedValue page;
            const HidUsagePageEntry *upg;

            if (item.valueSize == 4) {
              page = usage >> 0X10;
              usage &= UINT16_MAX;
            } else {
              page = usagePage;
            }

            if ((upg = hidUsagePageEntry(page))) {
              HidTable *utb = upg->usageTable;

              if (utb) {
                const HidUsageEntryHeader *usg = hidTableEntry(utb, usage);

                if (usg) {
                  STR_PRINTF("%s", usg->header.name);

                  {
                    const HidUsageTypeEntry *type = hidUsageTypeEntry(usg->usageType);
                    if (type) STR_PRINTF(" (%s)", type->header.name);
                  }
                }
              }
            }

            if (page != usagePage) {
              if (*name) STR_PRINTF(" ");
              STR_PRINTF("[");

              if (upg) {
                STR_PRINTF("%s", upg->header.name);
              } else {
                STR_PRINTF("0X%02"PRIX32, page);
              }

              STR_PRINTF("]");
            }

            break;
          }

          case HID_ITM_Collection: {
            const HidCollectionTypeEntry *col = hidCollectionTypeEntry(value);
            if (col) STR_PRINTF("%s", col->header.name);
            break;
          }

          case HID_ITM_Input:
          case HID_ITM_Output:
          case HID_ITM_Feature: {
            STR_FORMAT(hidFormatUsageFlags, value);
            break;
          }
        }

        STR_END;
        if (*name) STR_PRINTF(": %s", name);
      }
    } else if (bytesLeft) {
      STR_PRINTF(" incomplete:");
      const unsigned char *end = nextByte + bytesLeft;

      while (nextByte < end) {
        STR_PRINTF(" %02X", *nextByte++);
      }
    } else {
      STR_PRINTF(" end");
    }

    STR_END;
    if (!listItem(line, data)) return 0;
    if (!ok) break;
  }

  {
    char line[0X40];
    STR_BEGIN(line, sizeof(line));
    STR_PRINTF("End %s: Items:%u", label, itemCount);
    STR_END;
    return listItem(line, data);
  }
}
