File size: 28,573 Bytes
18a519f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine.InputSystem.Utilities;

namespace UnityEngine.InputSystem.Layouts
{
    /// <summary>
    /// Specification that can be matched against an <see cref="InputDeviceDescription"/>. This is
    /// used to find which <see cref="InputControlLayout"/> to create for a device when it is discovered.
    /// </summary>
    /// <remarks>
    /// Each matcher is basically a set of key/value pairs where each value may either be
    /// a regular expression or a plain value object. The central method for testing a given matcher
    /// against an <see cref="InputDeviceDescription"/> is <see cref="MatchPercentage"/>.
    ///
    /// Various helper methods such as <see cref="WithInterface"/> or <see cref="WithCapability{TValue}"/>
    /// assist with creating matchers.
    ///
    /// <example>
    /// <code>
    /// // A matcher that matches a PS4 controller by name.
    /// new InputDeviceMatcher()
    ///     .WithInterface("HID")
    ///     .WithManufacturer("Sony.+Entertainment") // Regular expression
    ///     .WithProduct("Wireless Controller"));
    ///
    /// // A matcher that matches the same controller by PID and VID.
    /// new InputDeviceMatcher()
    ///     .WithInterface("HID")
    ///     .WithCapability("vendorId", 0x54C) // Sony Entertainment.
    ///     .WithCapability("productId", 0x9CC)); // Wireless controller.
    /// </code>
    /// </example>
    ///
    /// For each registered <see cref="InputControlLayout"/> in the system that represents
    /// a device, arbitrary many matchers can be added. A matcher can be supplied either
    /// at registration time or at any point after using <see cref="InputSystem.RegisterLayoutMatcher"/>.
    ///
    /// <example>
    /// <code>
    /// // Supply a matcher at registration time.
    /// InputSystem.RegisterLayout&lt;DualShock4GamepadHID&gt;(
    ///     matches: new InputDeviceMatcher()
    ///         .WithInterface("HID")
    ///         .WithCapability("vendorId", 0x54C) // Sony Entertainment.
    ///         .WithCapability("productId", 0x9CC)); // Wireless controller.
    ///
    /// // Supply a matcher for an already registered layout.
    /// // This can be called repeatedly and will add another matcher
    /// // each time.
    /// InputSystem.RegisterLayoutMatcher&lt;DualShock4GamepadHID&gt;(
    ///     matches: new InputDeviceMatcher()
    ///         .WithInterface("HID")
    ///         .WithManufacturer("Sony.+Entertainment")
    ///         .WithProduct("Wireless Controller"));
    /// </code>
    /// </example>
    /// </remarks>
    /// <seealso cref="InputDeviceDescription"/>
    /// <seealso cref="InputDevice.description"/>
    /// <seealso cref="InputSystem.RegisterLayoutMatcher"/>
    public struct InputDeviceMatcher : IEquatable<InputDeviceMatcher>
    {
        private KeyValuePair<InternedString, object>[] m_Patterns;

        /// <summary>
        /// If true, the matcher has been default-initialized and contains no
        /// matching <see cref="patterns"/>.
        /// </summary>
        /// <value>Whether the matcher contains any matching patterns.</value>
        /// <seealso cref="patterns"/>
        public bool empty => m_Patterns == null;

        /// <summary>
        /// The list of patterns to match.
        /// </summary>
        /// <value>List of matching patterns.</value>
        /// <remarks>
        /// Each pattern is comprised of a key and a value. The key determines which part
        /// of an <see cref="InputDeviceDescription"/> to match.
        ///
        /// The value represents the expected value. This can be either a plain string
        /// (matched case-insensitive) or a regular expression.
        /// </remarks>
        /// <seealso cref="WithInterface"/>
        /// <seealso cref="WithCapability{TValue}"/>
        /// <seealso cref="WithProduct"/>
        /// <seealso cref="WithManufacturer"/>
        /// <seealso cref="WithVersion"/>
        /// <seealso cref="WithDeviceClass"/>
        public IEnumerable<KeyValuePair<string, object>> patterns
        {
            get
            {
                if (m_Patterns == null)
                    yield break;

                var count = m_Patterns.Length;
                for (var i = 0; i < count; ++i)
                    yield return new KeyValuePair<string, object>(m_Patterns[i].Key.ToString(), m_Patterns[i].Value);
            }
        }

        /// <summary>
        /// Add a pattern to <see cref="patterns"/> to match an <see cref="InputDeviceDescription.interfaceName"/>.
        /// </summary>
        /// <param name="pattern">String to match.</param>
        /// <param name="supportRegex">If true (default), <paramref name="pattern"/> can be
        /// a regular expression.</param>
        /// <returns>The modified device matcher with the added pattern.</returns>
        /// <seealso cref="InputDeviceDescription.interfaceName"/>
        public InputDeviceMatcher WithInterface(string pattern, bool supportRegex = true)
        {
            return With(kInterfaceKey, pattern, supportRegex);
        }

        /// <summary>
        /// Add a pattern to <see cref="patterns"/> to match a <see cref="InputDeviceDescription.deviceClass"/>.
        /// </summary>
        /// <param name="pattern">String to match.</param>
        /// <param name="supportRegex">If true (default), <paramref name="pattern"/> can be
        /// a regular expression.</param>
        /// <returns>The modified device matcher with the added pattern.</returns>
        /// <seealso cref="InputDeviceDescription.deviceClass"/>
        public InputDeviceMatcher WithDeviceClass(string pattern, bool supportRegex = true)
        {
            return With(kDeviceClassKey, pattern, supportRegex);
        }

        /// <summary>
        /// Add a pattern to <see cref="patterns"/> to match a <see cref="InputDeviceDescription.manufacturer"/>.
        /// </summary>
        /// <param name="pattern">String to match.</param>
        /// <param name="supportRegex">If true (default), <paramref name="pattern"/> can be
        /// a regular expression.</param>
        /// <returns>The modified device matcher with the added pattern.</returns>
        /// <seealso cref="InputDeviceDescription.manufacturer"/>
        public InputDeviceMatcher WithManufacturer(string pattern, bool supportRegex = true)
        {
            return With(kManufacturerKey, pattern, supportRegex);
        }

        /// <summary>
        /// Add a pattern to <see cref="patterns"/> to match a <see cref="InputDeviceDescription.product"/>.
        /// </summary>
        /// <param name="pattern">String to match.</param>
        /// <param name="supportRegex">If true (default), <paramref name="pattern"/> can be
        /// a regular expression.</param>
        /// <returns>The modified device matcher with the added pattern.</returns>
        /// <seealso cref="InputDeviceDescription.product"/>
        public InputDeviceMatcher WithProduct(string pattern, bool supportRegex = true)
        {
            return With(kProductKey, pattern, supportRegex);
        }

        /// <summary>
        /// Add a pattern to <see cref="patterns"/> to match a <see cref="InputDeviceDescription.version"/>.
        /// </summary>
        /// <param name="pattern">String to match.</param>
        /// <param name="supportRegex">If true (default), <paramref name="pattern"/> can be
        /// a regular expression.</param>
        /// <returns>The modified device matcher with the added pattern.</returns>
        /// <seealso cref="InputDeviceDescription.version"/>
        public InputDeviceMatcher WithVersion(string pattern, bool supportRegex = true)
        {
            return With(kVersionKey, pattern, supportRegex);
        }

        /// <summary>
        /// Add a pattern to <see cref="patterns"/> to match an individual capability in <see cref="InputDeviceDescription.capabilities"/>.
        /// </summary>
        /// <param name="path">Path to the JSON property using '/' as a separator,
        /// e.g. <c>"elements/count"</c>.</param>
        /// <param name="value">Value to match. This can be a string, a regular expression,
        /// a boolean, an integer, or a float. Floating-point numbers are matched with respect
        /// for <c>Mathf.Epsilon</c>. Values are converted between types automatically as
        /// needed (meaning that a bool can be compared to a string, for example).</param>
        /// <typeparam name="TValue">Type of value to match.</typeparam>
        /// <returns>The modified device matcher with the added pattern.</returns>
        /// <remarks>
        /// Capabilities are stored as JSON strings in <see cref="InputDeviceDescription.capabilities"/>.
        /// A matcher has the ability to match specific properties from the JSON object
        /// contained in the capabilities string.
        ///
        /// <example>
        /// <code>
        /// // The description for a HID will usually have a HIDDeviceDescriptor in
        /// // JSON format found on its InputDeviceDescription.capabilities. So, a
        /// // real-world device description could look the equivalent of this:
        /// var description = new InputDeviceDescription
        /// {
        ///     interfaceName = "HID",
        ///     capabilities = new HID.HIDDeviceDescriptor
        ///     {
        ///         vendorId = 0x54C,
        ///         productId = 0x9CC
        ///     }.ToJson()
        /// };
        ///
        /// // We can create a device matcher that looks for those to properties
        /// // directly in the JSON object.
        /// new InputDeviceMatcher()
        ///     .WithCapability("vendorId", 0x54C)
        ///     .WithCapability("productId", 0x9CC);
        /// </code>
        /// </example>
        ///
        /// Properties in nested objects can be referenced by separating properties
        /// with <c>/</c> and properties in arrays can be indexed with <c>[..]</c>.
        /// </remarks>
        /// <seealso cref="InputDeviceDescription.capabilities"/>
        public InputDeviceMatcher WithCapability<TValue>(string path, TValue value)
        {
            return With(new InternedString(path), value);
        }

        private InputDeviceMatcher With(InternedString key, object value, bool supportRegex = true)
        {
            // If it's a string, check whether it's a regex.
            if (supportRegex && value is string str)
            {
                var mayBeRegex = !str.All(ch => char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch)) &&
                    !double.TryParse(str, out var _);              // Avoid '.' in floats forcing the value to be a regex.
                if (mayBeRegex)
                    value = new Regex(str, RegexOptions.IgnoreCase);
            }

            // Add to list.
            var result = this;
            ArrayHelpers.Append(ref result.m_Patterns, new KeyValuePair<InternedString, object>(key, value));
            return result;
        }

        /// <summary>
        /// Return the level of matching to the given <paramref name="deviceDescription"/>.
        /// </summary>
        /// <param name="deviceDescription">A device description.</param>
        /// <returns>A score usually in the range between 0 and 1.</returns>
        /// <remarks>
        /// The algorithm computes a score of how well the matcher matches the given description.
        /// Essentially, a matcher that matches every single property that is present (as in
        /// not <c>null</c> and not an empty string) in <paramref name="deviceDescription"/> receives
        /// a score of 1, a matcher that matches none a score of 0. Matches that match only a subset
        /// receive a score in-between.
        ///
        /// An exception to this are capabilities. Every single match of a capability is counted
        /// as one property match and added to the score. This means that matchers that match
        /// on multiple capabilities may actually achieve a score &gt;1.
        ///
        /// <example>
        /// <code>
        /// var description = new InputDeviceDescription
        /// {
        ///     interfaceName = "HID",
        ///     product = "MadeUpDevice",
        ///     capabilities = new HID.HIDDeviceDescriptor
        ///     {
        ///         vendorId = 0xABC,
        ///         productId = 0xDEF
        ///     }.ToJson()
        /// };
        ///
        /// // This matcher will achieve a score of 0.666 (2/3) as it
        /// // matches two out of three available properties.
        /// new InputDeviceMatcher()
        ///     .WithInterface("HID")
        ///     .WithProduct("MadeUpDevice");
        ///
        /// // This matcher will achieve a score of 1 despite not matching
        /// // 'product'. The reason is that it matches two keys in
        /// // 'capabilities'.
        /// new InputDeviceMatcher()
        ///     .WithInterface("HID")
        ///     .WithCapability("vendorId", 0xABC)
        ///     .WithCapability("productId", 0xDEF);
        /// </code>
        /// </example>
        /// </remarks>
        public float MatchPercentage(InputDeviceDescription deviceDescription)
        {
            if (empty)
                return 0;

            // Go through all patterns. Score is 0 if any of the patterns
            // doesn't match.
            var numPatterns = m_Patterns.Length;
            for (var i = 0; i < numPatterns; ++i)
            {
                var key = m_Patterns[i].Key;
                var pattern = m_Patterns[i].Value;

                if (key == kInterfaceKey)
                {
                    if (string.IsNullOrEmpty(deviceDescription.interfaceName)
                        || !MatchSingleProperty(pattern, deviceDescription.interfaceName))
                        return 0;
                }
                else if (key == kDeviceClassKey)
                {
                    if (string.IsNullOrEmpty(deviceDescription.deviceClass)
                        || !MatchSingleProperty(pattern, deviceDescription.deviceClass))
                        return 0;
                }
                else if (key == kManufacturerKey)
                {
                    if (string.IsNullOrEmpty(deviceDescription.manufacturer)
                        || !MatchSingleProperty(pattern, deviceDescription.manufacturer))
                        return 0;
                }
                else if (key == kProductKey)
                {
                    if (string.IsNullOrEmpty(deviceDescription.product)
                        || !MatchSingleProperty(pattern, deviceDescription.product))
                        return 0;
                }
                else if (key == kVersionKey)
                {
                    if (string.IsNullOrEmpty(deviceDescription.version)
                        || !MatchSingleProperty(pattern, deviceDescription.version))
                        return 0;
                }
                else
                {
                    // Capabilities match. Take the key as a path into the JSON
                    // object and match the value found at the given path.

                    if (string.IsNullOrEmpty(deviceDescription.capabilities))
                        return 0;

                    var graph = new JsonParser(deviceDescription.capabilities);
                    if (!graph.NavigateToProperty(key.ToString()) ||
                        !graph.CurrentPropertyHasValueEqualTo(new JsonParser.JsonValue { type = JsonParser.JsonValueType.Any, anyValue = pattern}))
                        return 0;
                }
            }

            // All patterns matched. Our score is determined by the number of properties
            // we matched against.
            var propertyCountInDescription = GetNumPropertiesIn(deviceDescription);
            var scorePerProperty = 1.0f / propertyCountInDescription;

            return numPatterns * scorePerProperty;
        }

        private static bool MatchSingleProperty(object pattern, string value)
        {
            // String match.
            if (pattern is string str)
                return string.Compare(str, value, StringComparison.OrdinalIgnoreCase) == 0;

            // Regex match.
            if (pattern is Regex regex)
                return regex.IsMatch(value);

            return false;
        }

        private static int GetNumPropertiesIn(InputDeviceDescription description)
        {
            var count = 0;
            if (!string.IsNullOrEmpty(description.interfaceName))
                count += 1;
            if (!string.IsNullOrEmpty(description.deviceClass))
                count += 1;
            if (!string.IsNullOrEmpty(description.manufacturer))
                count += 1;
            if (!string.IsNullOrEmpty(description.product))
                count += 1;
            if (!string.IsNullOrEmpty(description.version))
                count += 1;
            if (!string.IsNullOrEmpty(description.capabilities))
                count += 1;
            return count;
        }

        /// <summary>
        /// Produce a matcher that matches the given device description verbatim.
        /// </summary>
        /// <param name="deviceDescription">A device description.</param>
        /// <returns>A matcher that matches <paramref name="deviceDescription"/> exactly.</returns>
        /// <remarks>
        /// This method can be used to produce a matcher for an existing device description,
        /// e.g. when writing a layout <see cref="InputControlLayout.Builder"/> that produces
        /// layouts for devices on the fly.
        /// </remarks>
        public static InputDeviceMatcher FromDeviceDescription(InputDeviceDescription deviceDescription)
        {
            var matcher = new InputDeviceMatcher();
            if (!string.IsNullOrEmpty(deviceDescription.interfaceName))
                matcher = matcher.WithInterface(deviceDescription.interfaceName, false);
            if (!string.IsNullOrEmpty(deviceDescription.deviceClass))
                matcher = matcher.WithDeviceClass(deviceDescription.deviceClass, false);
            if (!string.IsNullOrEmpty(deviceDescription.manufacturer))
                matcher = matcher.WithManufacturer(deviceDescription.manufacturer, false);
            if (!string.IsNullOrEmpty(deviceDescription.product))
                matcher = matcher.WithProduct(deviceDescription.product, false);
            if (!string.IsNullOrEmpty(deviceDescription.version))
                matcher = matcher.WithVersion(deviceDescription.version, false);
            // We don't include capabilities in this conversion.
            return matcher;
        }

        /// <summary>
        /// Return a string representation useful for debugging. Lists the
        /// <see cref="patterns"/> contained in the matcher.
        /// </summary>
        /// <returns>A string representation of the matcher.</returns>
        public override string ToString()
        {
            if (empty)
                return "<empty>";

            var result = string.Empty;
            foreach (var pattern in m_Patterns)
            {
                if (result.Length > 0)
                    result += $",{pattern.Key}={pattern.Value}";
                else
                    result += $"{pattern.Key}={pattern.Value}";
            }

            return result;
        }

        /// <summary>
        /// Test whether this matcher is equivalent to the <paramref name="other"/> matcher.
        /// </summary>
        /// <param name="other">Another device matcher.</param>
        /// <returns>True if the two matchers are equivalent.</returns>
        /// <remarks>
        /// Two matchers are equivalent if they contain the same number of patterns and the
        /// same pattern occurs in each of the matchers. Order of the patterns does not
        /// matter.
        /// </remarks>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "False positive.")]
        public bool Equals(InputDeviceMatcher other)
        {
            if (m_Patterns == other.m_Patterns)
                return true;

            if (m_Patterns == null || other.m_Patterns == null)
                return false;

            if (m_Patterns.Length != other.m_Patterns.Length)
                return false;

            // Pattern count matches. Compare pattern by pattern. Order of patterns doesn't matter.
            for (var i = 0; i < m_Patterns.Length; ++i)
            {
                var thisPattern = m_Patterns[i];
                var foundPattern = false;
                for (var n = 0; n < m_Patterns.Length; ++n)
                {
                    var otherPattern = other.m_Patterns[n];
                    if (thisPattern.Key != otherPattern.Key)
                        continue;
                    if (!thisPattern.Value.Equals(otherPattern.Value))
                        return false;
                    foundPattern = true;
                    break;
                }

                if (!foundPattern)
                    return false;
            }

            return true;
        }

        /// <summary>
        /// Compare this matcher to another.
        /// </summary>
        /// <param name="obj">A matcher object or <c>null</c>.</param>
        /// <returns>True if the matcher is equivalent.</returns>
        /// <seealso cref="Equals(InputDeviceMatcher)"/>
        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj))
                return false;
            return obj is InputDeviceMatcher matcher && Equals(matcher);
        }

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

        /// <summary>
        /// Compare two matchers for non-equivalence.
        /// </summary>
        /// <param name="left">First device matcher.</param>
        /// <param name="right">Second device matcher.</param>
        /// <returns>True if the two matchers are not equivalent.</returns>
        /// <seealso cref="Equals(InputDeviceMatcher)"/>
        public static bool operator!=(InputDeviceMatcher left, InputDeviceMatcher right)
        {
            return !(left == right);
        }

        /// <summary>
        /// Compute a hash code for the device matcher.
        /// </summary>
        /// <returns>A hash code for the matcher.</returns>
        public override int GetHashCode()
        {
            return m_Patterns != null ? m_Patterns.GetHashCode() : 0;
        }

        private static readonly InternedString kInterfaceKey = new InternedString("interface");
        private static readonly InternedString kDeviceClassKey = new InternedString("deviceClass");
        private static readonly InternedString kManufacturerKey = new InternedString("manufacturer");
        private static readonly InternedString kProductKey = new InternedString("product");
        private static readonly InternedString kVersionKey = new InternedString("version");

        [Serializable]
        internal struct MatcherJson
        {
            public string @interface;
            public string[] interfaces;
            public string deviceClass;
            public string[] deviceClasses;
            public string manufacturer;
            public string[] manufacturers;
            public string product;
            public string[] products;
            public string version;
            public string[] versions;
            public Capability[] capabilities;

            public struct Capability
            {
                public string path;
                public string value;
            }

            public static MatcherJson FromMatcher(InputDeviceMatcher matcher)
            {
                if (matcher.empty)
                    return new MatcherJson();

                var json = new MatcherJson();
                foreach (var pattern in matcher.m_Patterns)
                {
                    var key = pattern.Key;
                    var value = pattern.Value.ToString();

                    if (key == kInterfaceKey)
                    {
                        if (json.@interface == null)
                            json.@interface = value;
                        else
                            ArrayHelpers.Append(ref json.interfaces, value);
                    }
                    else if (key == kDeviceClassKey)
                    {
                        if (json.deviceClass == null)
                            json.deviceClass = value;
                        else
                            ArrayHelpers.Append(ref json.deviceClasses, value);
                    }
                    else if (key == kManufacturerKey)
                    {
                        if (json.manufacturer == null)
                            json.manufacturer = value;
                        else
                            ArrayHelpers.Append(ref json.manufacturers, value);
                    }
                    else if (key == kProductKey)
                    {
                        if (json.product == null)
                            json.product = value;
                        else
                            ArrayHelpers.Append(ref json.products, value);
                    }
                    else if (key == kVersionKey)
                    {
                        if (json.version == null)
                            json.version = value;
                        else
                            ArrayHelpers.Append(ref json.versions, value);
                    }
                    else
                    {
                        ArrayHelpers.Append(ref json.capabilities, new Capability {path = key, value = value});
                    }
                }

                return json;
            }

            public InputDeviceMatcher ToMatcher()
            {
                var matcher = new InputDeviceMatcher();

                ////TODO: get rid of the piecemeal array allocation and do it in one step

                // Interfaces.
                if (!string.IsNullOrEmpty(@interface))
                    matcher = matcher.WithInterface(@interface);
                if (interfaces != null)
                    foreach (var value in interfaces)
                        matcher = matcher.WithInterface(value);

                // Device classes.
                if (!string.IsNullOrEmpty(deviceClass))
                    matcher = matcher.WithDeviceClass(deviceClass);
                if (deviceClasses != null)
                    foreach (var value in deviceClasses)
                        matcher = matcher.WithDeviceClass(value);

                // Manufacturer.
                if (!string.IsNullOrEmpty(manufacturer))
                    matcher = matcher.WithManufacturer(manufacturer);
                if (manufacturers != null)
                    foreach (var value in manufacturers)
                        matcher = matcher.WithManufacturer(value);

                // Product.
                if (!string.IsNullOrEmpty(product))
                    matcher = matcher.WithProduct(product);
                if (products != null)
                    foreach (var value in products)
                        matcher = matcher.WithProduct(value);

                // Version.
                if (!string.IsNullOrEmpty(version))
                    matcher = matcher.WithVersion(version);
                if (versions != null)
                    foreach (var value in versions)
                        matcher = matcher.WithVersion(value);

                // Capabilities.
                if (capabilities != null)
                    foreach (var value in capabilities)
                        ////FIXME: we're turning all values into strings here
                        matcher = matcher.WithCapability(value.path, value.value);

                return matcher;
            }
        }
    }
}