#include "hidparse.h"

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

static uint8_t size_to_bytes(uint8_t sz)
{
    switch (sz)
    {
    case 0: return 0;
    case 1: return 1;
    case 2: return 2;
    case 3: return 4;
    default: return 0;
    }
}

static int32_t read_int(uint8_t sz, uint8_t* raw)
{
    switch (sz)
    {
    case 1: return *((int8_t*) raw); // ensure we cast to sign-extend
    case 2: return *((int16_t*) raw);
    case 3: return *((int32_t*) raw);
    }

    assert(0);
    return 0;
}

static uint32_t read_uint(uint8_t sz, uint8_t* raw)
{
    switch (sz)
    {
    case 1: return *raw;
    case 2: return *((uint16_t*) raw);
    case 3: return *((uint32_t*) raw);
    }

    assert(0);
    return 0;
}


const uint8_t HID_LONG_ITEM = 0xFE; // see 6.2.2.3

// following definitions come from 6.2.2.4
#define HID_COLLECTION_ITEM 0xA
#define HID_END_COLLECTION_ITEM 0xC
#define HID_INPUT_ITEM 0x8
#define HID_OUTPUT_ITEM 0x9
#define HID_FEATURE_ITEM 0xB

#define HID_USAGE_PAGE_ITEM 0x0
#define HID_LOGICAL_MINIMUM_ITEM 0x1
#define HID_LOGICAL_MAXIMUM_ITEM 0x2
#define HID_PHYSICAL_MINIMUM_ITEM 0x3
#define HID_PHYSICAL_MAXIMUM_ITEM 0x4
#define HID_EXPONENT_ITEM 0x5
#define HID_UNIT_ITEM 0x6
#define HID_REPORT_SIZE_ITEM 0x7
#define HID_REPORT_ID_ITEM 0x8
#define HID_REPORT_COUNT_ITEM 0x9
#define HID_PUSH_ITEM 0xA
#define HID_POP_ITEM 0xB

// following definitions come from 6.2.2.8
#define HID_USAGE_ITEM 0x0
#define HID_USAGE_MINIMUM_ITEM 0x1
#define HID_USAGE_MAXIMUM_ITEM 0x2
#define HID_DESIGNATOR_INDEX_ITEM 0x3
#define HID_DESIGNATOR_MINIMUM_ITEM 0x4
#define HID_DESIGNATOR_MAXIMUM_ITEM 0x5
#define HID_STRING_INDEX_ITEM 0x7
#define HID_STRING_MINIMUM_ITEM 0x8
#define HID_STRING_MAXIMUM_ITEM 0x9
#define HID_DELIMITER_ITEM 0xA

#define MAX_ITEM_STACK 4

hid_item item_stack[MAX_ITEM_STACK];
uint8_t item_stack_size = 0;

uint32_t usage_array[16];
uint8_t usage_count;

void init_local_data();
void clear_local_data();

void set_local_minimum(uint8_t tag, uint32_t data);
void set_local_maximum(uint8_t tag, uint32_t data);

void push_local_item(uint8_t tag, uint32_t data);

uint32_t pop_local_item(uint8_t tag);

hid_item* build_item(uint8_t tag, uint8_t flags, hid_item* global_state)
{
    hid_item* item = (hid_item*) calloc(sizeof(hid_item), 1);
    item->flags = flags;
    item->usage = pop_local_item(HID_USAGE_ITEM);
    item->type = tag;

// copy from global state
    item->report_size = global_state->report_size;
    item->logical_min = global_state->logical_min;
    item->logical_max = global_state->logical_max;
    item->physical_min = global_state->physical_min;
    item->physical_max = global_state->physical_max;
    item->unit = global_state->unit;
    item->unit_exponent = global_state->unit_exponent;
    item->report_id = global_state->report_id;

    return item;
}

hid_item* build_collection(uint8_t collectionType)
{
    hid_item* col = (hid_item*) calloc(sizeof(hid_item), 1);
    assert(col);
    col->type = collectionType;
    col->usage = pop_local_item(HID_USAGE_ITEM);
    return col;
}

void append_item_with_head(hid_item* head, hid_item* i)
{
    assert(head);
    for (; head->next; head = head->next) {}
    head->next = i;
}

void append_item(hid_item* col, hid_item* i)
{
    assert(col);
    assert(i);
    assert(i->next == NULL);
    assert(i->parent == NULL);

    i->parent = col;
    hid_item* last = col->collection;
    if (!last)
    {
        col->collection = i;
        return;
    }

    append_item_with_head(last, i);
}

int hid_parse_reportdesc(uint8_t* rdesc_buf, uint32_t rdesc_size, hid_item** item)
{
    hid_item current_state;
    uint8_t report_id   = 0;
    uint8_t report_count = 1;
    uint8_t *p = rdesc_buf;
    uint8_t* pEnd = p + rdesc_size;

    hid_item* root_collection = NULL;
    hid_item* current_collection = NULL;

    // zero the entire array
    memset(item_stack, 0, sizeof(hid_item) * MAX_ITEM_STACK);
    item_stack_size = 0;

    memset(&current_state, 0, sizeof(hid_item));
    usage_count = 0;

    init_local_data();

    while (p < pEnd)
    {
         /* See 6.2.2.2 Short Items */
         uint8_t pfx     = *p++;
         if (pfx == HID_LONG_ITEM)
         {
             printf("encountered long item\n");
         }

         uint8_t size    = pfx & 0x3; /* bits 0-1 */
         uint8_t bytes   = size_to_bytes(size);
         uint8_t type    = (pfx >> 2) & 0x3; /* bits 3-2 */
         uint8_t tag     = pfx >> 4;

        // fprintf(stderr, "short item: size=%d, bytes=%d, type = %d, tag=%d\n",
         //       size, bytes, type, tag);

         /* If it's a main item */
         if (type == 0)
         {
             switch (tag)
             {
             case HID_COLLECTION_ITEM:
             {
                 //fprintf(stderr, "opening collection\n");
                 uint8_t flags = read_uint(size, p);
                 hid_item* col = build_collection(flags);
                 if (!current_collection) {
                     if (root_collection) {
                         append_item_with_head(root_collection, col);
                     } else {
                         root_collection = col;
                     }
                    current_collection = col;
                 } else {
                     append_item(current_collection, col);
                     current_collection = col;
                 }
                 break;
            }

             case HID_END_COLLECTION_ITEM:
                 //fprintf(stderr, "closing collection\n");
                 assert(current_collection);
                 current_collection = current_collection->parent;
                 break;

             case HID_INPUT_ITEM:
             case HID_OUTPUT_ITEM:
             case HID_FEATURE_ITEM:
             {
                 int i;
                 uint8_t flags = read_uint(size, p);

                 for (i=0; i<report_count; ++i) {
                     hid_item* item = build_item(tag, flags, &current_state);
                     append_item(current_collection, item);
                 }
                 break;
             }

             default:
                 fprintf(stderr, "bad item %d\n", tag);
             }
             /* reset all local data */
             clear_local_data();
         }
         /* Else, if it's a global item */
         else if (type == 1)
         {
             switch (tag)
             {
             case HID_USAGE_PAGE_ITEM:
                 current_state.usage = read_uint(size, p) << 16;
                 break;

             case HID_LOGICAL_MINIMUM_ITEM:
                 current_state.logical_min = read_int(size, p);
                 break;

             case HID_LOGICAL_MAXIMUM_ITEM:
                 current_state.logical_max = read_int(size, p);
                 break;

             case HID_UNIT_ITEM:
                 current_state.unit = read_uint(size, p);
                 break;

             case HID_EXPONENT_ITEM:
                 current_state.unit_exponent = read_uint(size, p);
                 break;

            case HID_REPORT_ID_ITEM:
                report_id = read_uint(size, p);
                //fprintf(stderr, "report id is:%d\n", report_id);
                current_state.report_id = report_id;
                break;

            case HID_REPORT_SIZE_ITEM:
                current_state.report_size = read_uint(size, p);
                break;

            case HID_REPORT_COUNT_ITEM:
                report_count = read_uint(size, p);
                break;

            case HID_POP_ITEM:
                assert(item_stack_size > 0);
                current_state = item_stack[--item_stack_size];
                break;

            case HID_PUSH_ITEM:
                item_stack[item_stack_size++] = current_state;
                break;
             }
         } else if (type == 2) {
             /* it's a local item */
             uint32_t value = read_uint(size, p);
             switch (tag)
             {
             /* section 6.2.2.8, remark about Usage size; current_state only
                ever holds a usage page
            */
            case HID_USAGE_ITEM:
                if (size < sizeof(uint32_t))
                    value |= current_state.usage;
                push_local_item(tag, value);
                break;

            case HID_USAGE_MINIMUM_ITEM:
                set_local_minimum(HID_USAGE_ITEM, value | current_state.usage);
                break;

            case HID_USAGE_MAXIMUM_ITEM:
                set_local_maximum(HID_USAGE_ITEM, value | current_state.usage);
                break;

             case HID_DESIGNATOR_MINIMUM_ITEM:
                 set_local_minimum(HID_DESIGNATOR_INDEX_ITEM, value);
                 break;

             case HID_DESIGNATOR_MAXIMUM_ITEM:
                 set_local_maximum(HID_DESIGNATOR_INDEX_ITEM, value);
                  break;

              case HID_STRING_MINIMUM_ITEM:
                  set_local_minimum(HID_STRING_INDEX_ITEM, value);
                  break;

              case HID_STRING_MAXIMUM_ITEM:
                  set_local_maximum(HID_STRING_INDEX_ITEM, value);
                   break;

             default:
                 push_local_item(tag, value);
             }
         } else {
             fprintf(stderr, "bad type value: %d\n", type);
         }

         p += bytes;
    }

    clear_local_data(); // free transient parsing data
    *item = root_collection;
    return 0;
}

void hid_free_reportdesc(hid_item* root)
{
    free(root);
}

int hid_parse_is_relative(hid_item* item)
{
    return item->flags & 0x04; // bit 2 according to HID 6.2.2.5
}


typedef struct {
    uint8_t allocated, count;
    uint32_t* d;
    uint32_t minimum, maximum, step;
} local_data_array;

local_data_array local_data_store[0xf];

void init_local_data()
{
    int i;
    for (i=0; i<0xf; ++i) {
        memset(&local_data_store[i], 0, sizeof(local_data_array));
    }
}

void clear_local_data()
{
    int i;
    for (i=0; i<0xf; ++i) {
        if (local_data_store[i].d != NULL) {
            free(local_data_store[i].d);
        }

        memset(&local_data_store[i], 0, sizeof(local_data_array));
    }
}

void reallocate_local_item(uint8_t tag)
{
    local_data_array* arr = &local_data_store[tag >> 4];
    arr->allocated = (arr->allocated == 0) ? 4 : arr->allocated << 1;
    arr->d = realloc(arr->d, sizeof(uint32_t) * arr->allocated);
}

void push_local_item(uint8_t tag, uint32_t data)
{
    local_data_array* arr = &local_data_store[tag >> 4];
    if (arr->allocated == arr->count)
        reallocate_local_item(tag);

    arr->d[arr->count++] = data;
}

uint32_t pop_local_item(uint8_t tag)
{
    local_data_array* arr = &local_data_store[tag >> 4];
    if (arr->minimum)
    {
        uint32_t result = arr->minimum + arr->step++;
        if (arr->maximum && (result > arr->maximum)) {
            fprintf(stderr, "hidparse.c:pop_local_item: range exceeded for tag %d", tag);
        }
        
        return result;
    }

    if (arr->count == 0)
        return 0;

    uint32_t result = arr->d[arr->count - 1];
    if (arr->count > 1)
        --arr->count; /* pop off the back */
    return result;
}

void set_local_minimum(uint8_t tag, uint32_t data)
{
    local_data_array* arr = &local_data_store[tag >> 4];
    arr->minimum = data;
}

void set_local_maximum(uint8_t tag, uint32_t data)
{
    local_data_array* arr = &local_data_store[tag >> 4];
    arr->maximum = data;
}