1 /+
2               Copyright Elias Batek 2017 - 2018.
3      Distributed under the Boost Software License, Version 1.0.
4         (See accompanying file LICENSE_1_0.txt or copy at
5               https://www.boost.org/LICENSE_1_0.txt)
6  +/
7 module midigamepad.lib.translation.mappingsfile;
8 
9 import std.ascii : isAlpha;
10 import std.conv : ConvException, to;
11 import std.exception;
12 import std.file : readText;
13 import std.json;
14 import std.string;
15 import std.stdio : write, writeln;
16 import std.utf : UTFException;
17 
18 import midigamepad.lib.keyboard;
19 import midigamepad.lib.util;
20 
21 public import midigamepad.lib.translation.mapping;
22 
23 /++
24     Parses the specified mappings file
25  +/
26 MappingsCollection parseFile(string filePath)
27 {
28     string json = void;
29 
30     try
31     {
32         json = readText(filePath);
33     }
34     catch (UTFException ex)
35     {
36         throw new MappingsParserException("Not a text file", ex);
37     }
38 
39     return parse(json);
40 }
41 
42 /++
43     Parses the specified mappings json
44  +/
45 MappingsCollection parse(string json)
46 {
47     JSONValue root = void;
48 
49     try
50     {
51         root = parseJSON(json, JSONOptions.escapeNonAsciiChars);
52     }
53     catch (JSONException ex)
54     {
55         throw new MappingsParserException("Bad JSON", ex);
56     }
57 
58     // Make sure there is a 'map' field
59     enforce(("map" in root.object), new MappingsParserException("Missing 'map' field", root));
60     immutable JSONValue map = root.object["map"];
61 
62     MappingsCollection output = MappingsCollection();
63 
64     // onNote mappings
65     if ("onNote" in map.object)
66     {
67         immutable JSONValue onNote = map.object["onNote"];
68         output.noteOnOff = parseNoteOnOffMappings(onNote);
69     }
70 
71     return output;
72 }
73 
74 /++
75     Parses the specified NoteOnOffMappings
76  +/
77 NoteOnOffMapping[] parseNoteOnOffMappings(JSONValue noteOnOffRoot)
78 {
79     auto ar = noteOnOffRoot.array;
80     NoteOnOffMapping[] output = new NoteOnOffMapping[ar.length];
81 
82     foreach (size_t i, JSONValue m; ar)
83     {
84         uint vKey = void;
85 
86         // Ensure that the required elements exist
87         enforce(("vkey" in m), new MappingsParserException("Missing 'vkey' field", m));
88         enforce(("note" in m), new MappingsParserException("Missing 'note' field", m));
89 
90         immutable string vKeyStr = m["vkey"].str;
91         immutable string noteStr = m["note"].str;
92 
93         // Try to parse the vKey
94         if (!vKeyStr.tryParseVKEY(vKey))
95             throw new MappingsParserException("Cannot parse invalid vKey: `" ~ vKeyStr ~ "`", m);
96 
97         immutable ushort scancode = vKey.toScancode.to!ushort;
98         immutable bool isExtended = vKey.hasExtendedScancode;
99 
100         try
101         {
102             immutable byte note = noteStr.hexOrDecToNumber!byte;
103             output[i] = NoteOnOffMapping(scancode, isExtended, note);
104         }
105         catch (ConvException ex)
106         {
107             throw new MappingsParserException(
108                     "Cannot parse invalid MIDI note number: `" ~ noteStr ~ "`", m, ex);
109         }
110     }
111 
112     return output;
113 }
114 
115 /++
116     Tries to parse the passed vKey string and converts it into a scan
117 
118     Params:
119         input   = input vKey string
120         vKey    = contains the vKey
121 
122     Returns:
123         true = success
124  +/
125 bool tryParseVKEY(string input, out uint vKey) nothrow pure @safe
126 {
127     // Check whether vKey is a number (hex or dec)
128     if (input.startsWith("0x") || input.isNumeric)
129     {
130         try
131         {
132             vKey = input.hexOrDecToNumber!uint;
133             return true;
134         }
135         catch (Exception)
136         {
137             // Conversion failure
138             return false;
139         }
140     }
141 
142     // Check whether vKey is a single char
143     else if ((input.length == 1) && input[0].isAlpha)
144     {
145         vKey = uint(input[0].toUpper);
146         return true;
147     }
148 
149     // Not parseable
150     return false;
151 }
152 
153 @safe unittest
154 {
155     uint output;
156 
157     assert("0x12".tryParseVKEY(output));
158     assert(output == 0x12);
159 
160     assert("0x1B".tryParseVKEY(output));
161     assert(output == 0x1B);
162 
163     assert("31".tryParseVKEY(output));
164     assert(output == 31);
165 
166     assert("A".tryParseVKEY(output));
167     assert(output == 'A');
168 
169     assert("b".tryParseVKEY(output));
170     assert(output == 'B');
171 
172     assert(!"".tryParseVKEY(output));
173     assert(!"yy".tryParseVKEY(output));
174     assert(!"DD".tryParseVKEY(output));
175     assert(!"→".tryParseVKEY(output));
176     assert(!"♠".tryParseVKEY(output));
177     assert(!".".tryParseVKEY(output));
178 }
179 
180 /++
181     This is exception is thrown when the parser fails to parse a mapping.
182 
183     Check the .faulyMapping property to see which one caused the problem.
184  +/
185 class MappingsParserException : Exception
186 {
187 @nogc nothrow pure @safe:
188 
189     private
190     {
191         JSONValue _faultyMapping;
192     }
193 
194     /++
195         The faulty mapping that could not be parsed
196      +/
197     public @property JSONValue faultyMapping() const
198     {
199         return this._faultyMapping;
200     }
201 
202     /++
203         ctor
204      +/
205     public this(string msg, Throwable next = null)
206     {
207         super(msg, next);
208     }
209 
210     /++ ditto +/
211     public this(string msg, JSONValue faultyMapping, Throwable next = null)
212     {
213         this(msg, next);
214         this._faultyMapping = faultyMapping;
215     }
216 }