using System;
using UnityEngine.InputSystem.Utilities;

////TODO: add a 'devicePath' property that platforms can use to relay their internal device locators
////      (but do *not* take it into account when comparing descriptions for disconnected devices)

namespace UnityEngine.InputSystem.Layouts
{
    /// <summary>
    /// Metadata for an input device.
    /// </summary>
    /// <remarks>
    /// Device descriptions are mainly used to determine which <see cref="InputControlLayout"/>
    /// to create an actual <see cref="InputDevice"/> instance from. Each description is comprised
    /// of a set of properties that each are individually optional. However, for a description
    /// to be usable, at least some need to be set. Generally, the minimum viable description
    /// for a device is one with <see cref="deviceClass"/> filled out.
    ///
    /// <example>
    /// <code>
    /// // Device description equivalent to a generic gamepad with no
    /// // further information about the device.
    /// new InputDeviceDescription
    /// {
    ///     deviceClass = "Gamepad"
    /// };
    /// </code>
    /// </example>
    ///
    /// Device descriptions will usually be supplied by the Unity runtime but can also be manually
    /// fed into the system using <see cref="InputSystem.AddDevice(InputDeviceDescription)"/>. The
    /// system will remember each device description it has seen regardless of whether it was
    /// able to successfully create a device from the description. To query the list of descriptions
    /// that for whatever reason did not result in a device being created, call <see
    /// cref="InputSystem.GetUnsupportedDevices()"/>.
    ///
    /// Whenever layout registrations in the system are changed (e.g. by calling <see
    /// cref="InputSystem.RegisterLayout{T}"/> or whenever <see cref="InputSettings.supportedDevices"/>
    /// is changed, the system will go through the list of unsupported devices itself and figure out
    /// if there are device descriptions that now it can turn into devices. The same also applies
    /// in reverse; if, for example, a layout is removed that is currently used a device, the
    /// device will be removed and its description (if any) will be placed on the list of
    /// unsupported devices.
    /// </remarks>
    /// <seealso cref="InputDevice.description"/>
    /// <seealso cref="InputDeviceMatcher"/>
    [Serializable]
    public struct InputDeviceDescription : IEquatable<InputDeviceDescription>
    {
        /// <summary>
        /// How we talk to the device; usually name of the underlying backend that feeds
        /// state for the device (e.g. "HID" or "XInput").
        /// </summary>
        /// <value>Name of interface through which the device is reported.</value>
        /// <see cref="InputDeviceMatcher.WithInterface"/>
        public string interfaceName
        {
            get => m_InterfaceName;
            set => m_InterfaceName = value;
        }

        /// <summary>
        /// What the interface thinks the device classifies as.
        /// </summary>
        /// <value>Broad classification of device.</value>
        /// <remarks>
        /// If there is no layout specifically matching a device description,
        /// the device class is used as as fallback. If, for example, this field
        /// is set to "Gamepad", the "Gamepad" layout is used as a fallback.
        /// </remarks>
        /// <seealso cref="InputDeviceMatcher.WithDeviceClass"/>
        public string deviceClass
        {
            get => m_DeviceClass;
            set => m_DeviceClass = value;
        }

        /// <summary>
        /// Name of the vendor that produced the device.
        /// </summary>
        /// <value>Name of manufacturer.</value>
        /// <seealso cref="InputDeviceMatcher.WithManufacturer"/>
        public string manufacturer
        {
            get => m_Manufacturer;
            set => m_Manufacturer = value;
        }

        /// <summary>
        /// Name of the product assigned by the vendor to the device.
        /// </summary>
        /// <value>Name of product.</value>
        /// <seealso cref="InputDeviceMatcher.WithProduct"/>
        public string product
        {
            get => m_Product;
            set => m_Product = value;
        }

        /// <summary>
        /// If available, serial number for the device.
        /// </summary>
        /// <value>Serial number of device.</value>
        public string serial
        {
            get => m_Serial;
            set => m_Serial = value;
        }

        /// <summary>
        /// Version string of the device and/or driver.
        /// </summary>
        /// <value>Version of device and/or driver.</value>
        /// <seealso cref="InputDeviceMatcher.WithVersion"/>
        public string version
        {
            get => m_Version;
            set => m_Version = value;
        }

        /// <summary>
        /// An optional JSON string listing device-specific capabilities.
        /// </summary>
        /// <value>Interface-specific listing of device capabilities.</value>
        /// <remarks>
        /// The primary use of this field is to allow custom layout factories
        /// to create layouts on the fly from in-depth device descriptions delivered
        /// by external APIs.
        ///
        /// In the case of HID, for example, this field contains a JSON representation
        /// of the HID descriptor (see <see cref="HID.HID.HIDDeviceDescriptor"/>) as
        /// reported by the device driver. This descriptor contains information about
        /// all I/O elements on the device which can be used to determine the control
        /// setup and data format used by the device.
        /// </remarks>
        /// <seealso cref="InputDeviceMatcher.WithCapability{T}"/>
        public string capabilities
        {
            get => m_Capabilities;
            set => m_Capabilities = value;
        }

        /// <summary>
        /// Whether any of the properties in the description are set.
        /// </summary>
        /// <value>True if any of <see cref="interfaceName"/>, <see cref="deviceClass"/>,
        /// <see cref="manufacturer"/>, <see cref="product"/>, <see cref="serial"/>,
        /// <see cref="version"/>, or <see cref="capabilities"/> is not <c>null</c> and
        /// not empty.</value>
        public bool empty =>
            string.IsNullOrEmpty(m_InterfaceName) &&
            string.IsNullOrEmpty(m_DeviceClass) &&
            string.IsNullOrEmpty(m_Manufacturer) &&
            string.IsNullOrEmpty(m_Product) &&
            string.IsNullOrEmpty(m_Serial) &&
            string.IsNullOrEmpty(m_Version) &&
            string.IsNullOrEmpty(m_Capabilities);

        /// <summary>
        /// Return a string representation of the description useful for
        /// debugging.
        /// </summary>
        /// <returns>A script representation of the description.</returns>
        public override string ToString()
        {
            var haveProduct = !string.IsNullOrEmpty(product);
            var haveManufacturer = !string.IsNullOrEmpty(manufacturer);
            var haveInterface = !string.IsNullOrEmpty(interfaceName);

            if (haveProduct && haveManufacturer)
            {
                if (haveInterface)
                    return $"{manufacturer} {product} ({interfaceName})";

                return $"{manufacturer} {product}";
            }

            if (haveProduct)
            {
                if (haveInterface)
                    return $"{product} ({interfaceName})";

                return product;
            }

            if (!string.IsNullOrEmpty(deviceClass))
            {
                if (haveInterface)
                    return $"{deviceClass} ({interfaceName})";

                return deviceClass;
            }

            // For some HIDs on Windows, we don't get a product and manufacturer string even though
            // the HID is guaranteed to have a product and vendor ID. Resort to printing capabilities
            // which for HIDs at least include the product and vendor ID.
            if (!string.IsNullOrEmpty(capabilities))
            {
                const int kMaxCapabilitiesLength = 40;

                var caps = capabilities;
                if (capabilities.Length > kMaxCapabilitiesLength)
                    caps = caps.Substring(0, kMaxCapabilitiesLength) + "...";

                if (haveInterface)
                    return $"{caps} ({interfaceName})";

                return caps;
            }

            if (haveInterface)
                return interfaceName;

            return "<Empty Device Description>";
        }

        /// <summary>
        /// Compare the description to the given <paramref name="other"/> description.
        /// </summary>
        /// <param name="other">Another device description.</param>
        /// <returns>True if the two descriptions are equivalent.</returns>
        /// <remarks>
        /// Two descriptions are equivalent if all their properties are equal
        /// (ignore case).
        /// </remarks>
        public bool Equals(InputDeviceDescription other)
        {
            return m_InterfaceName.InvariantEqualsIgnoreCase(other.m_InterfaceName) &&
                m_DeviceClass.InvariantEqualsIgnoreCase(other.m_DeviceClass) &&
                m_Manufacturer.InvariantEqualsIgnoreCase(other.m_Manufacturer) &&
                m_Product.InvariantEqualsIgnoreCase(other.m_Product) &&
                m_Serial.InvariantEqualsIgnoreCase(other.m_Serial) &&
                m_Version.InvariantEqualsIgnoreCase(other.m_Version) &&
                ////REVIEW: this would ideally compare JSON contents not just the raw string
                m_Capabilities.InvariantEqualsIgnoreCase(other.m_Capabilities);
        }

        /// <summary>
        /// Compare the description to the given object.
        /// </summary>
        /// <param name="obj">An object.</param>
        /// <returns>True if <paramref name="obj"/> is an InputDeviceDescription
        /// equivalent to this one.</returns>
        /// <seealso cref="Equals(InputDeviceDescription)"/>
        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj))
                return false;
            return obj is InputDeviceDescription description && Equals(description);
        }

        /// <summary>
        /// Compute a hash code for the device description.
        /// </summary>
        /// <returns>A hash code.</returns>
        public override int GetHashCode()
        {
            unchecked
            {
                var hashCode = m_InterfaceName != null ? m_InterfaceName.GetHashCode() : 0;
                hashCode = (hashCode * 397) ^ (m_DeviceClass != null ? m_DeviceClass.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ (m_Manufacturer != null ? m_Manufacturer.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ (m_Product != null ? m_Product.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ (m_Serial != null ? m_Serial.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ (m_Version != null ? m_Version.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ (m_Capabilities != null ? m_Capabilities.GetHashCode() : 0);
                return hashCode;
            }
        }

        /// <summary>
        /// Compare the two device descriptions.
        /// </summary>
        /// <param name="left">First device description.</param>
        /// <param name="right">Second device description.</param>
        /// <returns>True if the two descriptions are equivalent.</returns>
        /// <seealso cref="Equals(InputDeviceDescription)"/>
        public static bool operator==(InputDeviceDescription left, InputDeviceDescription right)
        {
            return left.Equals(right);
        }

        /// <summary>
        /// Compare the two device descriptions for inequality.
        /// </summary>
        /// <param name="left">First device description.</param>
        /// <param name="right">Second device description.</param>
        /// <returns>True if the two descriptions are not equivalent.</returns>
        /// <seealso cref="Equals(InputDeviceDescription)"/>
        public static bool operator!=(InputDeviceDescription left, InputDeviceDescription right)
        {
            return !left.Equals(right);
        }

        /// <summary>
        /// Return a JSON representation of the device description.
        /// </summary>
        /// <returns>A JSON representation of the description.</returns>
        /// <remarks>
        /// <example>
        /// The result can be converted back into an InputDeviceDescription
        /// using <see cref="FromJson"/>.
        ///
        /// <code>
        /// var description = new InputDeviceDescription
        /// {
        ///     interfaceName = "HID",
        ///     product = "SomeDevice",
        ///     capabilities = @"
        ///         {
        ///             ""vendorId"" : 0xABA,
        ///             ""productId"" : 0xEFE
        ///         }
        ///     "
        /// };
        ///
        /// Debug.Log(description.ToJson());
        /// // Prints
        /// // {
        /// //     "interface" : "HID",
        /// //     "product" : "SomeDevice",
        /// //     "capabilities" : "{ \"vendorId\" : 0xABA, \"productId\" : 0xEFF }"
        /// // }
        /// </code>
        /// </example>
        /// </remarks>
        /// <seealso cref="FromJson"/>
        public string ToJson()
        {
            var data = new DeviceDescriptionJson
            {
                @interface = interfaceName,
                type = deviceClass,
                product = product,
                manufacturer = manufacturer,
                serial = serial,
                version = version,
                capabilities = capabilities
            };
            return JsonUtility.ToJson(data, true);
        }

        /// <summary>
        /// Read an InputDeviceDescription from its JSON representation.
        /// </summary>
        /// <param name="json">String in JSON format.</param>
        /// <exception cref="ArgumentNullException"><paramref name="json"/> is <c>null</c>.</exception>
        /// <returns>The converted </returns>
        /// <exception cref="ArgumentException">There as a parse error in <paramref name="json"/>.
        /// </exception>
        /// <remarks>
        /// <example>
        /// <code>
        /// InputDeviceDescription.FromJson(@"
        ///     {
        ///         ""interface"" : ""HID"",
        ///         ""product"" : ""SomeDevice""
        ///     }
        /// ");
        /// </code>
        /// </example>
        /// </remarks>
        /// <seealso cref="ToJson"/>
        public static InputDeviceDescription FromJson(string json)
        {
            if (json == null)
                throw new ArgumentNullException(nameof(json));

            var data = JsonUtility.FromJson<DeviceDescriptionJson>(json);

            return new InputDeviceDescription
            {
                interfaceName = data.@interface,
                deviceClass = data.type,
                product = data.product,
                manufacturer = data.manufacturer,
                serial = data.serial,
                version = data.version,
                capabilities = data.capabilities
            };
        }

        internal static bool ComparePropertyToDeviceDescriptor(string propertyName, string propertyValue, string deviceDescriptor)
        {
            // We use JsonParser instead of JsonUtility.Parse in order to not allocate GC memory here.

            var json = new JsonParser(deviceDescriptor);
            if (!json.NavigateToProperty(propertyName))
            {
                if (string.IsNullOrEmpty(propertyValue))
                    return true;
                return false;
            }

            return json.CurrentPropertyHasValueEqualTo(propertyValue);
        }

        [SerializeField] private string m_InterfaceName;
        [SerializeField] private string m_DeviceClass;
        [SerializeField] private string m_Manufacturer;
        [SerializeField] private string m_Product;
        [SerializeField] private string m_Serial;
        [SerializeField] private string m_Version;
        [SerializeField] private string m_Capabilities;

        private struct DeviceDescriptionJson
        {
            public string @interface;
            public string type;
            public string product;
            public string serial;
            public string version;
            public string manufacturer;
            public string capabilities;
        }
    }
}