1 module godot.tools.generator.util;
2 
3 import godot.tools.generator.classes;
4 
5 import std.range;
6 import std.algorithm.searching;
7 import std.algorithm.iteration;
8 import std.path;
9 import std.conv : text;
10 import std.string;
11 
12 import asdf;
13 
14 //import asdf.source.asdf.asdf;
15 
16 struct TypeStruct {
17     @serdeKeys("name", "type") string name;
18     @serdeOptional string meta;
19 
20     SerdeException deserializeFromAsdf(Asdf data) {
21         // here we try read 3 options, 'name' variant is for native_structs
22         name = data["type"].get!string(null);
23         meta = data["meta"].get!string(null);
24         if (name is null) {
25             string val;
26             if (auto exc = deserializeScopedString(data, val))
27                 return exc;
28             name = val;
29         }
30 
31         return null;
32     }
33 }
34 
35 //@serdeProxy!string
36 @serdeProxy!TypeStruct
37 class Type {
38     static Type[string] typesByGodotName;
39     static Type[string] typesByDName;
40 
41     static Type[] enums;
42 
43     GodotClass objectClass;
44     GodotClass original; // original GodotClass associated with this Type
45     string dType;
46     string godotType;
47     bool isNativeStruct;
48 
49     @property string dRef() const {
50         return isRef ? ("Ref!" ~ dType) : dType;
51     }
52 
53     Type enumParent;
54 
55     //alias dType this;
56 
57     string asModuleName() const {
58         if (isPrimitive || isCoreType)
59             return null;
60         if (isNativeStruct)
61             return "structs"; // module godot.structs
62         return godotType.chompPrefix("_").toLower;
63     }
64 
65     /// Backing opaque type to use instead of raw GDExtensionTypePtr
66     string asOpaqueType() const {
67         switch (godotType) {
68         case "TypedArray":
69         case "Array":
70             return "godot_array";
71         case "Variant":
72             return "godot_variant";
73         case "String":
74         case "StringName":
75             return "godot_string";
76         case "NodePath":
77             return "godot_node_path";
78         case "Dictionary":
79             return "godot_dictionary";
80         case "Callable":
81             return "godot_callable";
82         case "Signal":
83             return "godot_signal";
84         case "RID":
85             return "godot_rid";
86         case "Object":
87         case "Nil": // we don't need Nil at all but for now let just make it work
88             return "godot_object";
89         case "PackedByteArray":
90         case "PackedInt32Array":
91         case "PackedInt64Array":
92         case "PackedFloat32Array":
93         case "PackedFloat64Array":
94         case "PackedStringArray":
95         case "PackedVector2Array":
96         case "PackedVector3Array":
97         case "PackedColorArray":
98             //case "Nil":
99             return "GDExtensionTypePtr";
100         default:
101             break;
102         }
103 
104         if (!isRef || canBeCopied)
105             return this.dType;
106 
107         return "godot_object";
108     }
109 
110     bool isEnum() const {
111         return godotType.startsWith("enum::");
112     }
113 
114     bool isBitfield() const {
115         return godotType.startsWith("bitfield::");
116     }
117 
118     bool isTypedArray() const {
119         return godotType.startsWith("typedarray::");
120     }
121 
122     bool isMetaType() const {
123         return isEnum || isBitfield || isTypedArray;
124     }
125 
126     bool isPointerType() const {
127         return dType.indexOf("*") != -1;
128     }
129 
130     // Any type that is internally backed by godot string
131     bool isGodotStringType() const {
132         import std.algorithm : among;
133         return godotType.among("StringName", "String", "NodePath") > 0;
134     }
135 
136     bool isPrimitive() const {
137         if (isEnum || isBitfield)
138             return true;
139         return only("int", "bool", "real", "float", "void", "double", "real_t",
140             "uint8_t", "int8_t", "uint16_t", "int16_t", "uint32_t", "int32_t", "uint64_t", "int64_t", // well...
141             "uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64" // hope they will merge it or smth
142 
143             
144 
145         ).canFind(stripConstPointer.godotType);
146     }
147 
148     // types that have simple value semantics and doesn't require special wrappers
149     bool canBeCopied() const {
150         return only("Vector2", "Vector2i", "Vector3", "Vector3i", "Vector4", "Vector4i",
151             "Transform2D", "Transform3D", "Projection", "Rect2", "Rect2i",
152             "Color", "Plane", "AABB", "Quaternion", "Basis", "RID", "Nil"
153         ).canFind(stripConstPointer.godotType);
154     }
155 
156     bool isCoreType() const {
157         if (auto arraytype = arrayType())
158             return arraytype.isCoreType();
159         // basically all types from extension_api.json from builtin_classes
160         auto coreTypes = only("AABB",
161             "Array",
162             "Basis",
163             "Callable",
164             "Color",
165             "Dictionary",
166             "GodotError",
167             "NodePath",
168             "StringName",
169             "Plane",
170             "PackedByteArray",
171             "PackedInt32Array",
172             "PackedInt64Array",
173             "PackedFloat32Array",
174             "PackedFloat64Array",
175             "PackedStringArray",
176             "PackedVector2Array",
177             "PackedVector3Array",
178             "PackedColorArray",
179             "Quaternion",
180             "Rect2",
181             "Rect2i",
182             "RID",
183             "Signal",
184             "String",
185             "Transform3D",
186             "Transform2D",
187             "TypedArray",
188             "Projection",
189             "Variant",
190             "Vector2",
191             "Vector2i",
192             "Vector3",
193             "Vector3i",
194             "Vector4",
195             "Vector4i",
196             "Nil", // why godot, why?
197             "ObjectID");
198         return coreTypes.canFind(godotType);
199     }
200 
201     /// Get variant type name for method calls
202     string asNativeVariantType() const {
203         import godot.util.string;
204 
205         // useless but ok
206         if (!(isCoreType || isPrimitive))
207             return "GDEXTENSION_VARIANT_TYPE_OBJECT";
208 
209         return "GDEXTENSION_VARIANT_TYPE_" ~ camelToSnake(godotType).toUpper;
210     }
211 
212     /// returns TypedArray type
213     Type arrayType() const {
214         if (!isTypedArray)
215             return null;
216         return Type.get(godotType["typedarray::".length .. $]);
217     }
218 
219     bool isRef() const {
220         return objectClass && objectClass.is_reference;
221     }
222 
223     /// type should be taken as template arg by methods to allow implicit conversion in ptrcall
224     bool acceptImplicit() const {
225         auto accept = only("Variant");
226         return accept.canFind(godotType);
227     }
228 
229     /// prefix for function parameter of this type
230     string dCallParamPrefix() const {
231         if (isRef)
232             return "";
233         else if (objectClass)
234             return "";
235         else if (godotType.indexOf("const") != -1)
236             return "";
237         else
238             return "in ";
239     }
240     /// how to pass parameters of this type into ptrcall void** arg
241     string ptrCallArgPrefix() const {
242         if (isPrimitive || isCoreType)
243             return "&";
244         return "";
245         //return "cast(godot_object)"; // for both base classes and D classes (through alias this)
246     }
247 
248     /// returns value name from string literal, default values in api.json uses integers instead of qualified enum values
249     string getEnumValueStr(string value) const {
250         import std.conv : to;
251         import godot.util.string;
252 
253         if (!isEnum())
254             return null;
255 
256         import godot.tools.generator.enums;
257 
258         // global enums doesn't have parent
259         auto parent = Type.get(godot.tools.generator.enums.enumParent(godotType));
260         if (!parent)
261             parent = Type.get("CoreConstants");
262 
263         // just the enum name, without parent class name
264         string innerName = splitEnumName(godotType)[1];
265 
266         // FIXME why it's always parent.original?
267         auto searchInParent = parent.godotType == "CoreConstants" ? parent.original
268             : parent.original;
269 
270         // HACK: core types not available here
271         // if (searchInParent is null && parent.isCoreType) {
272         // FIXME higher if throws access violation when searching later
273         if (searchInParent is null) {
274             return "cast(" ~ dType ~ ")" ~ value;
275         }
276         auto found = searchInParent.enums.find!(s => s.name == innerName);
277         if (!found.empty) {
278             foreach (pair; found.front.values)
279                 if (pair.value == to!int(value))
280                     return dType ~ "." ~ snakeToCamel(pair.name);
281         }
282 
283         return "cast(" ~ dType ~ ")" ~ value;
284     }
285 
286     /// strip constness, also strips indirections (despite the name)
287     Type stripConstPointer() const {
288         char[] unqualified = cast(char[]) godotType.replace("const ", "").dup;
289         while (unqualified[$ - 1] == '*') {
290             unqualified[$ - 1] = '\0';
291             unqualified = unqualified[0 .. $ - 1];
292         }
293         unqualified = unqualified.stripRight; // strip whitespace leftovers
294         return Type.get(cast(string) unqualified);
295     }
296 
297     // same as stripConstPointer() but only strips constness, useful for return types and template params
298     Type stripConst() const {
299         char[] unqualified = cast(char[]) godotType.replace("const ", "").dup;
300         unqualified = unqualified.stripRight; // strip whitespace leftovers
301         return Type.get(cast(string) unqualified);
302     }
303 
304     // removes any prefixed meta names such as "enum::" in "enum::MyClass.MyEnum"
305     Type stripMeta() const {
306         // NOTE: this method is marked const, but we want it to be convenient.
307         // this can lead to potential UB when user modifies the returned value,
308         // and there is no other way around because Type.get(godotName) will return
309         // same object as this.
310         if (isEnum) {
311             static import enumutils = godot.tools.generator.enums;
312             // returns cached parent first, otherwise extract from name string
313             if (enumParent)
314                 return cast() enumParent;
315             return Type.get(enumutils.enumParent(godotType));
316         }
317         if (isBitfield) {
318             return Type.get(godotType["bitfield::".length .. $]);
319         }
320         if (isTypedArray) {
321             return Type.get(godotType["typedarray::".length .. $]);
322         }
323         return cast() this;
324     }
325 
326     // Companion method for stripMeta, usually meta types is a nested types inside another class
327     // so we have to take their enclosing type
328     Type getParentType() const {
329         import std.string : lastIndexOf;
330         auto pos = godotType.lastIndexOf('.');
331         if (pos > 0) {
332             const parentName = godotType[0..pos];
333             if (auto parent = parentName in Type.typesByGodotName)
334                 return *parent;
335         }
336         return null;
337     }
338 
339     this(string godotName) {
340         godotType = godotName;
341         dType = godotName.escapeGodotType;
342     }
343 
344     this(TypeStruct t) {
345         // here t.name usually specifies old type like int, with meta describing actual length like int64
346         if (t.meta)
347             this(t.meta);
348         else
349             this(t.name);
350     }
351 
352     static Type get(string godotName) {
353         if (!godotName.length)
354             return null; // no type (used in base_class)
355         if (Type* ptr = godotName in typesByGodotName)
356             return *ptr;
357         Type ret = new Type(godotName);
358 
359         static import godot.tools.generator.enums;
360 
361         if (ret.isEnum) {
362             ret.enumParent = get(godot.tools.generator.enums.enumParent(godotName));
363             enums ~= ret;
364         }
365 
366         typesByGodotName[godotName] = ret;
367         typesByDName[ret.dType] = ret;
368 
369         return ret;
370     }
371 
372     /*
373 	SerdeException deserializeFromAsdf(Asdf data)
374     {
375         string val;
376         if (auto exc = deserializeScopedString(data, val))
377             return exc;
378 
379         godotType = val;
380 		dType = godotType.escapeGodotType;
381 
382         return null;
383     }
384 */
385     /*
386 	static Type deserialize(ref Asdf asdf)
387 	{
388 		string gn = asdf.get!string(null);
389 		Type ret = get(gn);
390 		return ret;
391 	}
392 	*/
393 }
394 
395 /// the default value to use for an argument if none is provided
396 string defaultTypeString(in Type type) {
397     import std.string;
398     import std.conv : text;
399 
400     bool isPointer = type.dType.indexOf("*") != -1;
401 
402     switch (type.dType) {
403     case "String":
404         // FIXME: might cause some issues with auto-conversion?
405         return `gs!""`;
406     case "Dictionary":
407         return type.dType ~ ".make()";
408     case "Array":
409         return type.dType ~ ".make()";
410     default: { // all default-blittable types
411             if (isPointer) {
412                 return "(" ~ type.dType ~ ").init";
413             } else {
414                 return type.dType ~ ".init"; // D's default initializer
415             }
416             ///return "null";
417         }
418     }
419 }
420 
421 /++
422 PoolVector2Array
423 PoolColorArray
424 Array
425 Vector2
426 float
427 Color
428 bool
429 Object
430 PoolVector3Array
431 Vector3
432 Transform2D
433 RID
434 int
435 Transform
436 Rect2
437 String
438 Variant
439 PoolStringArray
440 +/
441 string escapeDefaultType(in Type type, string arg) {
442     import std.string;
443     import std.conv : text;
444 
445     if (!arg || arg.length == 0)
446         return defaultTypeString(type);
447 
448     if (type.isTypedArray) {
449         // examples of typedarray:
450         //   Array[RDPipelineSpecializationConstant]([])
451         //   Array[RID]([])
452         //   Array[Array]([])
453         //   []
454         // TODO: other cases such as that where it actually has values in it
455         if (arg.startsWith("Array[") && arg.endsWith("([])")) {
456             return "[]";
457         }
458     }
459     // parse the defaults in api.json
460     switch (type.dType) {
461     case "Color": // "1,1,1,1"
462         if (arg.startsWith("Color("))
463             return arg;
464         return "Color(" ~ arg ~ ")";
465     case "bool": // True, False
466         return arg.toLower;
467     case "Array": // "[]", "Null" - just use the empty one
468     case "Dictionary":
469     case "PackedByteArray":
470     case "PackedInt32Array":
471     case "PackedInt64Array":
472     case "PackedFloat32Array":
473     case "PackedFloat64Array":
474     case "PackedVector2Array":
475     case "PackedVector3Array":
476     case "PackedStringArray":
477     case "PackedColorArray":
478         return defaultTypeString(type);
479     case "Transform3D": // "1, 0, 0, 0, 1, 0, 0, 0, 1 - 0, 0, 0" TODO: parse this
480         if (arg.startsWith("Transform3D("))
481             return arg;
482         return "Transform3D(" ~ arg ~ ")";
483     case "Transform2D":
484         if (arg.startsWith("Transform2D("))
485             return arg;
486         return "Transform2D(" ~ arg ~ ")";
487     case "Projection":
488         if (arg.startsWith("Projection("))
489             return arg;
490         return "Projection(" ~ arg ~ ")";
491     case "RID": // always empty?
492         return defaultTypeString(type); // D's default initializer
493     case "Vector2": // "(0, 0)"
494     case "Vector2i": // "(0, 0)"
495     case "Vector3":
496     case "Vector3i":
497     case "Vector4":
498     case "Vector4i":
499     case "Rect2": // "(0, 0, 0, 0)"
500     case "AABB":
501         if (arg.startsWith(type.godotType)) // prevent junk like 'Vector2Vector2(0, 0)'
502             arg = arg[type.godotType.length .. $];
503         return type.dType ~ arg;
504     case "Variant":
505         if (arg == "Null")
506             return "Variant.nil";
507         else
508             return arg;
509     case "String":
510     case "StringName":
511     case "NodePath": 
512         // TODO: use this instead 
513         version(none) {
514             return type.dType ~ "(" ~ stripStringDefaultValueType() ~ ")"; 
515         } 
516         if (arg[0] == '&')
517             arg = arg[1 .. $];
518         // node path has default value that includes type, 
519         // must strip it from here as we deal with string helpers later
520         // example value: NodePath("")
521         if (arg.startsWith("NodePath("))
522             arg = arg["NodePath(".length..$-1];
523 
524         // HACK: hack in string, trying auto-convert?
525         // if (arg.canFind('"'))
526         //     return "gn!" ~ arg;
527         // return "gn!\"" ~ arg ~ "\"";
528         if (arg.canFind('"'))
529             return type.dType ~ "(" ~ arg ~ ")";
530         return type.dType ~ "(\"" ~ arg ~ "\")";
531     default: // all Object types
532     {
533             if (arg == "Null" || arg == "null")
534                 return defaultTypeString(type);
535             if (arg == "[Object:null]")
536                 return defaultTypeString(type);
537             if (type.isEnum)
538                 return type.getEnumValueStr(arg);
539             return arg;
540         }
541     }
542 }
543 
544 // Same as above except it returns only the string itself without the prepending type
545 string stripStringDefaultValueType(in Type type, string arg) {
546     // only mess up with strings
547     if (!type.isGodotStringType)
548         return arg;
549 
550     if (arg[0] == '&')
551         arg = arg[1 .. $];
552     // node path has default value that includes type, 
553     // must strip it from here as we deal with string helpers later
554     // example value: NodePath("")
555     if (arg.startsWith("NodePath("))
556         arg = arg["NodePath(".length..$-1];
557 
558     // HACK: hack in string, trying auto-convert?
559     // if (arg.canFind('"'))
560     //     return "gn!" ~ arg;
561     // return "gn!\"" ~ arg ~ "\"";
562     if (arg[0]=='"')
563         return arg;
564     else
565         return "";
566 }
567 
568 string escapeGodotType(string t) {
569     import godot.tools.generator.enums : asEnumName;
570 
571     t = t.chompPrefix("_");
572 
573     if (t == "Object")
574         return "GodotObject";
575     if (t == "Error")
576         return "GodotError";
577     if (t == "Callable")
578         return "GodotCallable";
579     if (t == "Signal")
580         return "GodotSignal";
581     if (t == "float")
582         return "double";
583     if (t == "int")
584         return "long";
585     if (t == "Nil")
586         return "GDExtensionTypePtr";
587     if (t.startsWith("enum::"))
588         return t.asEnumName;
589     if (t.startsWith("bitfield::"))
590         return t["bitfield::".length .. $];
591     if (t.startsWith("typedarray::"))
592         return t.asTypedArray;
593     return t;
594 }
595 
596 string escapeDType(string s, string godotType = "") {
597     import std.meta;
598     import std.uni, std.utf;
599 
600     /// TODO: there must be a better way of doing this...
601     /// maybe one of the D parser libraries has a list of keywords and basic types
602 
603     if (s.toUTF32[0].isNumber)
604         s = "_" ~ s; // can't start with a number
605 
606     alias keywords = AliasSeq!(
607         "class",
608         "interface",
609         "struct",
610         "enum",
611         "bool",
612         "ubyte",
613         "byte",
614         "ushort",
615         "short",
616         "uint",
617         "int",
618         "ulong",
619         "long",
620         "cent", // really?
621         "ucent",
622         "float",
623         "double",
624         "real",
625         "char",
626         "wchar",
627         "dchar",
628         "function",
629         "delegate",
630         "override",
631         "default",
632         "case",
633         "switch",
634         "export",
635         "import",
636         "template",
637         "new",
638         "delete",
639         "return",
640         "with",
641         "align",
642         "in",
643         "out",
644         "ref",
645         "scope",
646         "auto",
647         "init",
648         "version",
649         "body",
650         "debug",
651     );
652     switch (s) {
653     case "Object":
654         return "GodotObject";
655     case "Error":
656         return "GodotError";
657     case "Signal":
658         return "GodotSignal";
659     case "Callable":
660         return "GodotCallable";
661     foreach (kw; keywords) case kw:
662         return "_" ~ s;
663     default:
664         return s;
665     }
666 }
667 
668 string asTypedArray(string type) {
669     return "TypedArray!(" ~ type["typedarray::".length .. $] ~ ")";
670 }
671 
672 string tab(string s, int tabs) {
673     import std.conv : to;
674     return repeat('\t', tabs).to!string ~ s;
675 }