using System;
using System.Collections.Generic;

////TODO: array support
////TODO: delimiter support
////TODO: designator support

#pragma warning disable CS0649
namespace UnityEngine.InputSystem.HID
{
    /// <summary>
    /// Turns binary HID descriptors into <see cref="HID.HIDDeviceDescriptor"/> instances.
    /// </summary>
    /// <remarks>
    /// For information about the format, see the <a href="http://www.usb.org/developers/hidpage/HID1_11.pdf">
    /// Device Class Definition for Human Interface Devices</a> section 6.2.2.
    /// </remarks>
    internal static class HIDParser
    {
        /// <summary>
        /// Parse a HID report descriptor as defined by section 6.2.2 of the
        /// <a href="http://www.usb.org/developers/hidpage/HID1_11.pdf">HID
        /// specification</a> and add the elements and collections from the
        /// descriptor to the given <paramref name="deviceDescriptor"/>.
        /// </summary>
        /// <param name="buffer">Buffer containing raw HID report descriptor.</param>
        /// <param name="deviceDescriptor">HID device descriptor to complete with the information
        /// from the report descriptor. Elements and collections will get added to this descriptor.</param>
        /// <returns>True if the report descriptor was successfully parsed.</returns>
        /// <remarks>
        /// Will also set <see cref="HID.HIDDeviceDescriptor.inputReportSize"/>,
        /// <see cref="HID.HIDDeviceDescriptor.outputReportSize"/>, and
        /// <see cref="HID.HIDDeviceDescriptor.featureReportSize"/>.
        /// </remarks>
        public static unsafe bool ParseReportDescriptor(byte[] buffer, ref HID.HIDDeviceDescriptor deviceDescriptor)
        {
            if (buffer == null)
                throw new ArgumentNullException(nameof(buffer));

            fixed(byte* bufferPtr = buffer)
            {
                return ParseReportDescriptor(bufferPtr, buffer.Length, ref deviceDescriptor);
            }
        }

        public unsafe static bool ParseReportDescriptor(byte* bufferPtr, int bufferLength, ref HID.HIDDeviceDescriptor deviceDescriptor)
        {
            // Item state.
            var localItemState = new HIDItemStateLocal();
            var globalItemState = new HIDItemStateGlobal();

            // Lists where we accumulate the data from the HID items.
            var reports = new List<HIDReportData>();
            var elements = new List<HID.HIDElementDescriptor>();
            var collections = new List<HID.HIDCollectionDescriptor>();
            var currentCollection = -1;

            // Parse the linear list of items.
            var endPtr = bufferPtr + bufferLength;
            var currentPtr = bufferPtr;
            while (currentPtr < endPtr)
            {
                var firstByte = *currentPtr;

                ////TODO
                if (firstByte == 0xFE)
                    throw new NotImplementedException("long item support");

                // Read item header.
                var itemSize = (byte)(firstByte & 0x3);
                var itemTypeAndTag = (byte)(firstByte & 0xFC);
                ++currentPtr;

                // Process item.
                switch (itemTypeAndTag)
                {
                    // ------------ Global Items --------------
                    // These set item state permanently until it is reset.

                    // Usage Page
                    case (int)HIDItemTypeAndTag.UsagePage:
                        globalItemState.usagePage = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // Report Count
                    case (int)HIDItemTypeAndTag.ReportCount:
                        globalItemState.reportCount = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // Report Size
                    case (int)HIDItemTypeAndTag.ReportSize:
                        globalItemState.reportSize = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // Report ID
                    case (int)HIDItemTypeAndTag.ReportID:
                        globalItemState.reportId = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // Logical Minimum
                    case (int)HIDItemTypeAndTag.LogicalMinimum:
                        globalItemState.logicalMinimum = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // Logical Maximum
                    case (int)HIDItemTypeAndTag.LogicalMaximum:
                        globalItemState.logicalMaximum = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // Physical Minimum
                    case (int)HIDItemTypeAndTag.PhysicalMinimum:
                        globalItemState.physicalMinimum = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // Physical Maximum
                    case (int)HIDItemTypeAndTag.PhysicalMaximum:
                        globalItemState.physicalMaximum = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // Unit Exponent
                    case (int)HIDItemTypeAndTag.UnitExponent:
                        globalItemState.unitExponent = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // Unit
                    case (int)HIDItemTypeAndTag.Unit:
                        globalItemState.unit = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // ------------ Local Items --------------
                    // These set the state for the very next elements to be generated.

                    // Usage
                    case (int)HIDItemTypeAndTag.Usage:
                        localItemState.SetUsage(ReadData(itemSize, currentPtr, endPtr));
                        break;

                    // Usage Minimum
                    case (int)HIDItemTypeAndTag.UsageMinimum:
                        localItemState.usageMinimum = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // Usage Maximum
                    case (int)HIDItemTypeAndTag.UsageMaximum:
                        localItemState.usageMaximum = ReadData(itemSize, currentPtr, endPtr);
                        break;

                    // ------------ Main Items --------------
                    // These emit things into the descriptor based on the local and global item state.

                    // Collection
                    case (int)HIDItemTypeAndTag.Collection:

                        // Start new collection.
                        var parentCollection = currentCollection;
                        currentCollection = collections.Count;
                        collections.Add(new HID.HIDCollectionDescriptor
                        {
                            type = (HID.HIDCollectionType)ReadData(itemSize, currentPtr, endPtr),
                            parent = parentCollection,
                            usagePage = globalItemState.GetUsagePage(0, ref localItemState),
                            usage = localItemState.GetUsage(0),
                            firstChild = elements.Count
                        });

                        HIDItemStateLocal.Reset(ref localItemState);
                        break;

                    // EndCollection
                    case (int)HIDItemTypeAndTag.EndCollection:
                        if (currentCollection == -1)
                            return false;

                        // Close collection.
                        var collection = collections[currentCollection];
                        collection.childCount = elements.Count - collection.firstChild;
                        collections[currentCollection] = collection;

                        // Switch back to parent collection (if any).
                        currentCollection = collection.parent;

                        HIDItemStateLocal.Reset(ref localItemState);
                        break;

                    // Input/Output/Feature
                    case (int)HIDItemTypeAndTag.Input:
                    case (int)HIDItemTypeAndTag.Output:
                    case (int)HIDItemTypeAndTag.Feature:

                        // Determine report type.
                        var reportType = itemTypeAndTag == (int)HIDItemTypeAndTag.Input
                            ? HID.HIDReportType.Input
                            : itemTypeAndTag == (int)HIDItemTypeAndTag.Output
                            ? HID.HIDReportType.Output
                            : HID.HIDReportType.Feature;

                        // Find report.
                        var reportIndex = HIDReportData.FindOrAddReport(globalItemState.reportId, reportType, reports);
                        var report = reports[reportIndex];

                        // If we have a report ID, then reports start with an 8 byte report ID.
                        // Shift our offsets accordingly.
                        if (report.currentBitOffset == 0 && globalItemState.reportId.HasValue)
                            report.currentBitOffset = 8;

                        // Add elements to report.
                        var reportCount = globalItemState.reportCount.GetValueOrDefault(1);
                        var flags = ReadData(itemSize, currentPtr, endPtr);
                        for (var i = 0; i < reportCount; ++i)
                        {
                            var element = new HID.HIDElementDescriptor
                            {
                                usage = localItemState.GetUsage(i) & 0xFFFF, // Mask off usage page, if set.
                                usagePage = globalItemState.GetUsagePage(i, ref localItemState),
                                reportType = reportType,
                                reportSizeInBits = globalItemState.reportSize.GetValueOrDefault(8),
                                reportOffsetInBits = report.currentBitOffset,
                                reportId = globalItemState.reportId.GetValueOrDefault(1),
                                flags = (HID.HIDElementFlags)flags,
                                logicalMin = globalItemState.logicalMinimum.GetValueOrDefault(0),
                                logicalMax = globalItemState.logicalMaximum.GetValueOrDefault(0),
                                physicalMin = globalItemState.GetPhysicalMin(),
                                physicalMax = globalItemState.GetPhysicalMax(),
                                unitExponent = globalItemState.unitExponent.GetValueOrDefault(0),
                                unit = globalItemState.unit.GetValueOrDefault(0),
                            };
                            report.currentBitOffset += element.reportSizeInBits;
                            elements.Add(element);
                        }
                        reports[reportIndex] = report;

                        HIDItemStateLocal.Reset(ref localItemState);
                        break;
                }

                if (itemSize == 3)
                    currentPtr += 4;
                else
                    currentPtr += itemSize;
            }

            deviceDescriptor.elements = elements.ToArray();
            deviceDescriptor.collections = collections.ToArray();

            // Set usage and usage page on device descriptor to what's
            // on the toplevel application collection.
            foreach (var collection in collections)
            {
                if (collection.parent == -1 && collection.type == HID.HIDCollectionType.Application)
                {
                    deviceDescriptor.usage = collection.usage;
                    deviceDescriptor.usagePage = collection.usagePage;
                    break;
                }
            }

            return true;
        }

        private unsafe static int ReadData(int itemSize, byte* currentPtr, byte* endPtr)
        {
            if (itemSize == 0)
                return 0;

            // Read byte.
            if (itemSize == 1)
            {
                if (currentPtr >= endPtr)
                    return 0;
                return *currentPtr;
            }

            // Read short.
            if (itemSize == 2)
            {
                if (currentPtr + 2 >= endPtr)
                    return 0;
                var data1 = *currentPtr;
                var data2 = *(currentPtr + 1);
                return (data2 << 8) | data1;
            }

            // Read int.
            if (itemSize == 3) // Item size 3 means 4 bytes!
            {
                if (currentPtr + 4 >= endPtr)
                    return 0;

                var data1 = *currentPtr;
                var data2 = *(currentPtr + 1);
                var data3 = *(currentPtr + 2);
                var data4 = *(currentPtr + 3);

                return (data4 << 24) | (data3 << 24) | (data2 << 8) | data1;
            }

            Debug.Assert(false, "Should not reach here");
            return 0;
        }

        private struct HIDReportData
        {
            public int reportId;
            public HID.HIDReportType reportType;
            public int currentBitOffset;

            public static int FindOrAddReport(int? reportId, HID.HIDReportType reportType, List<HIDReportData> reports)
            {
                var id = 1;
                if (reportId.HasValue)
                    id = reportId.Value;

                for (var i = 0; i < reports.Count; ++i)
                {
                    if (reports[i].reportId == id && reports[i].reportType == reportType)
                        return i;
                }

                reports.Add(new HIDReportData
                {
                    reportId = id,
                    reportType = reportType
                });

                return reports.Count - 1;
            }
        }

        // All types and tags with size bits (low order two bits) masked out (i.e. being 0).
        private enum HIDItemTypeAndTag
        {
            Input = 0x80,
            Output = 0x90,
            Feature = 0xB0,
            Collection = 0xA0,
            EndCollection = 0xC0,
            UsagePage = 0x04,
            LogicalMinimum = 0x14,
            LogicalMaximum = 0x24,
            PhysicalMinimum = 0x34,
            PhysicalMaximum = 0x44,
            UnitExponent = 0x54,
            Unit = 0x64,
            ReportSize = 0x74,
            ReportID = 0x84,
            ReportCount = 0x94,
            Push = 0xA4,
            Pop = 0xB4,
            Usage = 0x08,
            UsageMinimum = 0x18,
            UsageMaximum = 0x28,
            DesignatorIndex = 0x38,
            DesignatorMinimum = 0x48,
            DesignatorMaximum = 0x58,
            StringIndex = 0x78,
            StringMinimum = 0x88,
            StringMaximum = 0x98,
            Delimiter = 0xA8,
        }

        // State that needs to be defined for each main item separately.
        // See section 6.2.2.8
        private struct HIDItemStateLocal
        {
            public int? usage;
            public int? usageMinimum;
            public int? usageMaximum;
            public int? designatorIndex;
            public int? designatorMinimum;
            public int? designatorMaximum;
            public int? stringIndex;
            public int? stringMinimum;
            public int? stringMaximum;

            public List<int> usageList;

            // Wipe state but preserve usageList allocation.
            public static void Reset(ref HIDItemStateLocal state)
            {
                var usageList = state.usageList;
                state = new HIDItemStateLocal();
                if (usageList != null)
                {
                    usageList.Clear();
                    state.usageList = usageList;
                }
            }

            // Usage can be set repeatedly to provide an enumeration of usages.
            public void SetUsage(int value)
            {
                if (usage.HasValue)
                {
                    if (usageList == null)
                        usageList = new List<int>();
                    usageList.Add(usage.Value);
                }
                usage = value;
            }

            // Get usage for Nth element in [0-reportCount] list.
            public int GetUsage(int index)
            {
                // If we have minimum and maximum usage, interpolate between that.
                if (usageMinimum.HasValue && usageMaximum.HasValue)
                {
                    var min = usageMinimum.Value;
                    var max = usageMaximum.Value;

                    var range = max - min;
                    if (range < 0)
                        return 0;
                    if (index >= range)
                        return max;
                    return min + index;
                }

                // If we have a list of usages, index into that.
                if (usageList != null && usageList.Count > 0)
                {
                    var usageCount = usageList.Count;
                    if (index >= usageCount)
                        return usage.Value;

                    return usageList[index];
                }

                if (usage.HasValue)
                    return usage.Value;

                ////TODO: min/max

                return 0;
            }
        }

        // State that is carried over from main item to main item.
        // See section 6.2.2.7
        private struct HIDItemStateGlobal
        {
            public int? usagePage;
            public int? logicalMinimum;
            public int? logicalMaximum;
            public int? physicalMinimum;
            public int? physicalMaximum;
            public int? unitExponent;
            public int? unit;
            public int? reportSize;
            public int? reportCount;
            public int? reportId;

            public HID.UsagePage GetUsagePage(int index, ref HIDItemStateLocal localItemState)
            {
                if (!usagePage.HasValue)
                {
                    var usage = localItemState.GetUsage(index);
                    return (HID.UsagePage)(usage >> 16);
                }

                return (HID.UsagePage)usagePage.Value;
            }

            public int GetPhysicalMin()
            {
                if (physicalMinimum == null || physicalMaximum == null ||
                    (physicalMinimum.Value == 0 && physicalMaximum.Value == 0))
                    return logicalMinimum.GetValueOrDefault(0);
                return physicalMinimum.Value;
            }

            public int GetPhysicalMax()
            {
                if (physicalMinimum == null || physicalMaximum == null ||
                    (physicalMinimum.Value == 0 && physicalMaximum.Value == 0))
                    return logicalMaximum.GetValueOrDefault(0);
                return physicalMaximum.Value;
            }
        }
    }
}
