Editor/ExternGen.cs
#nullable enable

using System;
using System.IO;
using System.Text;
using System.Linq;
using System.Xml.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Sandbox;

static class ExternGen
{
    private static readonly Dictionary<string, StubType> ExternalStubs = new(StringComparer.Ordinal);
    private static readonly Dictionary<string, int> RootArityRuntimeCache = new(StringComparer.Ordinal);
    private static readonly Dictionary<string, HashSet<int>> ExternalBaseAritiesUsed = new(StringComparer.Ordinal);
    private static readonly Dictionary<string, int> RootGenericArity = new(StringComparer.Ordinal);
    private static readonly Dictionary<string, string> MapTypeCacheImplicit = new(StringComparer.Ordinal);
    private static readonly Dictionary<string, string> MapTypeCacheExplicit = new(StringComparer.Ordinal);
    private static readonly Dictionary<Type, string> TypeSchemaCache = new();

    private static readonly bool GenerateExternalStubs = true;

    private static readonly Dictionary<string, string> PrimitiveTypeMap = new(StringComparer.Ordinal)
    {
        ["Dynamic"] = "Dynamic",
        ["Void"] = "Void",
        ["Int"] = "Int",
        ["UInt"] = "UInt",
        ["Float"] = "Float",
        ["Bool"] = "Bool",
        ["String"] = "String",
        ["System.Void"] = "Void",
        ["System.Boolean"] = "Bool",
        ["System.String"] = "String",
        ["System.FormattableString"] = "String",
        ["System.Single"] = "Single",
        ["System.Double"] = "Float",
        ["System.Decimal"] = "cs.system.Decimal",
        ["System.Int16"] = "Int",
        ["System.UInt16"] = "Int",
        ["System.Int32"] = "Int",
        ["System.Byte"] = "Int",
        ["System.SByte"] = "Int",
        ["System.UInt32"] = "UInt",
        ["System.Int64"] = "haxe.Int64",
        ["System.Object"] = "cs.system.Object",
        ["System.Type"] = "cs.system.Type"
    };

    private static bool TryMapPrimitive(string csType, out string hxType)
    {
        if (PrimitiveTypeMap.TryGetValue(csType, out var v)) { hxType = v; return true; }
        hxType = "";
        return false;
    }

    private readonly struct Root
    {
        public readonly string Name;
        public readonly string Lower;
        public Root(string name) { Name = name; Lower = name.ToLowerInvariant(); }
    }

    private sealed class DocStore
    {
        private readonly Dictionary<string, string> _summary = new(StringComparer.Ordinal);

        public static DocStore Load(IEnumerable<Assembly> assemblies)
        {
            var ds = new DocStore();
            foreach (var a in assemblies)
            {
                try
                {
                    var loc = SafeLocation(a);
                    if (string.IsNullOrWhiteSpace(loc)) continue;
                    var xml = Path.ChangeExtension(loc, ".xml");
                    if (!File.Exists(xml)) continue;

                    var xdoc = XDocument.Load(xml);
                    var members = xdoc.Root?.Element("members")?.Elements("member");
                    if (members == null) continue;

                    foreach (var m in members)
                    {
                        var idAttr = m.Attribute("name");
                        if (idAttr == null) continue;
                        var id = idAttr.Value;
                        if (id.Length == 0) continue;
                        var sum = Normalize(m.Element("summary")?.Value);
                        if (sum.Length == 0) continue;
                        ds._summary[id] = sum;
                    }
                }
                catch { }
            }
            return ds;
        }

        public string TypeSummary(Type t) => Get("T:" + XmlDocTypeName(t));

        public string PropSummary(PropertyInfo p)
        {
            var dt = p.DeclaringType;
            return dt == null ? "" : Get("P:" + XmlDocTypeName(dt) + "." + p.Name);
        }

        public string MethodSummary(MethodBase m)
        {
            var dt = m.DeclaringType;
            if (dt == null) return "";

            var baseId = "M:" + XmlDocTypeName(dt) + "." + m.Name;
            if (m is MethodInfo mi && mi.IsGenericMethodDefinition) baseId += "``" + mi.GetGenericArguments().Length;

            var ps = m.GetParameters();
            if (ps.Length == 0)
            {
                var s0 = Get(baseId);
                if (s0.Length != 0) return s0;
                if (m is ConstructorInfo) return Get("M:" + XmlDocTypeName(dt) + ".#ctor");
                return "";
            }

            var sig = "(" + string.Join(",", ps.Select(p => XmlDocTypeName(p.ParameterType))) + ")";
            var s1 = Get(baseId + sig);
            if (s1.Length != 0) return s1;
            if (m is ConstructorInfo) return Get("M:" + XmlDocTypeName(dt) + ".#ctor" + sig);
            return "";
        }

        private string Get(string id) => _summary.TryGetValue(id, out var s) ? s : "";

        private static string SafeLocation(Assembly a) { try { return a.Location; } catch { return ""; } }

        private static string Normalize(string? s)
        {
            s ??= "";
            s = s.Replace("\r", " ").Replace("\n", " ").Trim();
            while (s.Contains("  ", StringComparison.Ordinal)) s = s.Replace("  ", " ", StringComparison.Ordinal);
            return s.Replace("*/", "* /", StringComparison.Ordinal).Trim();
        }

        private static string XmlDocTypeName(Type t)
        {
            if (t.IsByRef)
            {
                var et = t.GetElementType();
                if (et == null) return "System.Object";
                t = et;
            }

            if (t.IsArray)
            {
                var et = t.GetElementType();
                if (et == null) return "System.Object[]";
                return XmlDocTypeName(et) + "[]";
            }

            if (t.IsPointer)
            {
                var et = t.GetElementType();
                if (et == null) return "System.Object*";
                return XmlDocTypeName(et) + "*";
            }

            if (t.IsGenericParameter) return t.Name;

            var name = (t.FullName ?? t.Name ?? "System.Object").Replace('+', '.');

            if (t.IsGenericType)
            {
                var def = t.IsGenericTypeDefinition ? t : t.GetGenericTypeDefinition();
                var defName = (def.FullName ?? def.Name ?? name).Replace('+', '.');
                var tick = defName.IndexOf('`');
                if (tick >= 0) defName = defName.Substring(0, tick);

                var args = t.GetGenericArguments();
                if (args.Length > 0) defName += "{" + string.Join(",", args.Select(XmlDocTypeName)) + "}";
                return defName;
            }

            return name;
        }
    }

    private sealed class ApiType
    {
        public string NativeFullName = "";
        public string Namespace = "";
        public string Name = "";
        public string BaseType = "";
        public bool IsFinal;
        public bool IsEnum;
        public bool IsValueType;
        public string Summary = "";

        public string EnumUnderlying = "System.Int32";
        public List<string> EnumMembers = new();

        public List<string> TypeGenericParams = new();
        public List<ApiCtor> Ctors = new();
        public List<ApiProp> Props = new();
        public List<ApiMethod> Methods = new();
    }

    private sealed class ApiCtor
    {
        public List<ApiParam> Params = new();
        public bool IsProtected;
        public string Summary = "";
    }

    private sealed class ApiProp
    {
        public string Name = "";
        public string Type = "System.Object";
        public MethodInfo? GetMethod;
        public MethodInfo? SetMethod;
        public bool IsProtected;
        public bool IsStatic;
        public bool IsReadOnly;
        public bool IsConst;
        public bool IsField;
        public string Summary = "";
    }

    private sealed class ApiMethod
    {
        public string Name = "";
        public string ReturnType = "System.Void";
        public bool IsStatic;
        public bool IsProtected;
        public List<ApiParam> Params = new();
        public List<string> MethodGenericParams = new();
        public string Summary = "";
    }

    private readonly struct ApiParam
    {
        public readonly string Name;
        public readonly string Type;
        public ApiParam(string name, string type) { Name = name; Type = type; }
    }

    private sealed class StubType
    {
        public string CsNative = "";
        public string HxPackage = "";
        public string HxName = "";
        public int GenericArity;
    }

    private sealed class AttributeInfo
    {
        public string FullName { get; set; } = "";
        public string ShortName { get; set; } = "";
        public string Assembly { get; set; } = "";
        public string Namespace { get; set; } = "";
        public string ValidOn { get; set; } = "";
        public List<ConstructorSig> Ctors { get; set; } = new();
    }

    private sealed class ConstructorSig
    {
        public List<string> Args { get; set; } = new();
    }

    public static string GenerateFromRuntime(string[] rootNamespaces)
    {
        if (rootNamespaces == null || rootNamespaces.Length == 0)
            throw new ArgumentException("rootNamespaces must not be empty", nameof(rootNamespaces));

        string outRoot = Path.Combine(HaxeBox.path, "haxe", "extern");
        Directory.CreateDirectory(outRoot);

        var roots = rootNamespaces
            .Where(x => !string.IsNullOrWhiteSpace(x))
            .Select(x => x.Trim())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Select(x => new Root(x))
            .ToArray();

        if (roots.Length == 0)
            throw new ArgumentException("rootNamespaces must not be empty", nameof(rootNamespaces));

        ExternalStubs.Clear();
        ExternalBaseAritiesUsed.Clear();
        RootGenericArity.Clear();
        RootArityRuntimeCache.Clear();
        MapTypeCacheImplicit.Clear();
        MapTypeCacheExplicit.Clear();
        TypeSchemaCache.Clear();

        var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).ToArray();
        var docs = DocStore.Load(assemblies);
        var primary = roots[0];

        var types = CollectTypes(assemblies, roots, docs);
        CollectExternalBaseAritiesUsed(types, roots);

        foreach (var t in types)
        {
            var arity = t.TypeGenericParams.Count;
            if (arity <= 0) continue;

            var schemaFull = string.IsNullOrWhiteSpace(t.Namespace) ? t.Name : (t.Namespace + "." + t.Name);
            RootGenericArity[schemaFull] = arity;
            RootGenericArity[schemaFull + "`" + arity] = arity;
        }

        var knownGlobal = new HashSet<string>(StringComparer.Ordinal);
        var generatedNative = new HashSet<string>(StringComparer.Ordinal);
        foreach (var t in types)
        {
            if (t.Namespace.Length == 0) knownGlobal.Add(t.Name);
            generatedNative.Add(t.NativeFullName);
        }

        var typeCount = 0;
        var memberCount = 0;

        foreach (var t in types.OrderBy(x => x.NativeFullName, StringComparer.Ordinal))
        {
            var isGlobal = string.IsNullOrWhiteSpace(t.Namespace);

            string pkg;
            string dir;

            if (isGlobal) { pkg = ""; dir = outRoot; }
            else
            {
                var root = PickRootForType(t, roots);
                pkg = BuildHaxePackage(root, t.Namespace);
                dir = BuildOutDir(outRoot, root, t.Namespace);
            }

            Directory.CreateDirectory(dir);

            var hxName = SanitizeTypeName(t.Name);
            var sb = new StringBuilder(512);

            sb.Append(pkg.Length == 0 ? "package;\n\n" : $"package {pkg};\n\n");

            if (!string.IsNullOrWhiteSpace(t.Summary))
                sb.Append("/** ").Append(EscDoc(t.Summary)).Append(" */\n");

            sb.Append("@:native(\"").Append(Esc(t.NativeFullName.Replace("+", "."))).Append("\")\n");

            if (t.IsEnum)
            {
                sb.Append("extern enum abstract ").Append(hxName).Append("(")
                  .Append(MapType(t.EnumUnderlying, roots, primary, knownGlobal)).Append(") {\n");

                var uniq = new HashSet<string>(StringComparer.Ordinal);
                foreach (var em in t.EnumMembers)
                {
                    var n = SanitizeMemberName(em);
                    if (n.Length == 0 || !uniq.Add(n)) continue;
                    sb.Append("    var ").Append(n).Append(";\n");
                }

                sb.Append("}\n");
                File.WriteAllText(Path.Combine(dir, hxName + ".hx"), sb.ToString(), Encoding.UTF8);
                typeCount++;
                continue;
            } else if (t.IsFinal)
                sb.Append("final ");

            var tgen = t.TypeGenericParams.Count > 0 ? "<" + string.Join(",", t.TypeGenericParams) + ">" : "";
            var baseHx = "";
            if (!string.IsNullOrWhiteSpace(t.BaseType))
                baseHx = MapTypeInternal(t.BaseType, roots, primary, knownGlobal, allowImplicitRootGenerics: false);

            if (!string.IsNullOrWhiteSpace(baseHx))
            {
                var currentHxFull = pkg.Length == 0 ? hxName : (pkg + "." + hxName);
                if (StripTypeArgs(baseHx).Equals(currentHxFull, StringComparison.Ordinal))
                    baseHx = "";
            }

            sb.Append("extern class ").Append(hxName).Append(tgen);
            if (!string.IsNullOrWhiteSpace(baseHx) && !baseHx.Equals("Dynamic", StringComparison.Ordinal))
                sb.Append(" extends ").Append(baseHx);
            sb.Append(" {\n");

            if (t.Ctors.Count > 0)
            {
                var uniq = new Dictionary<string, ApiCtor>(StringComparer.Ordinal);
                foreach (var c in t.Ctors)
                {
                    var key = "new(" + string.Join(",", c.Params.Select(p => NormKey(MapType(p.Type, roots, primary, knownGlobal)))) + ")";
                    if (!uniq.TryGetValue(key, out var ex) || (ex.IsProtected && !c.IsProtected))
                        uniq[key] = c;
                }

                var keys = uniq.Keys.OrderBy(x => x, StringComparer.Ordinal).ToList();
                var over = keys.Count > 1;

                foreach (var k in keys)
                {
                    var c = uniq[k];
                    if (!string.IsNullOrWhiteSpace(c.Summary))
                        sb.Append("    /** ").Append(EscDoc(c.Summary)).Append(" */\n");

                    if (c.IsProtected)
                        sb.Append("    @:protected ");
                    else
                        sb.Append("    ");

                    sb.Append(over ? "overload " : "")
                      .Append("function new(").Append(FormatParams(c.Params, roots, primary, knownGlobal)).Append("):Void;\n");

                    memberCount++;
                }
            }

            foreach (var p in t.Props)
            {
                var n = SanitizeMemberName(p.Name);
                if (n.Length == 0) continue;

                static string MapAccessor(MethodInfo? m)
                {
                    if (m == null) return "never";
                    if (m.IsPublic) return "default";
                    if (m.IsFamily || m.IsFamilyOrAssembly) return "null";
                    return "never";
                }

                static string MapFieldAcc(ApiProp p)
                {
                    var r = p.IsProtected ? "null" : "default";
                    var w = (p.IsReadOnly || p.IsConst) ? "never" : (p.IsProtected ? "null" : "default");
                    return r + "," + w;
                }

                static string MapPropertyAcc(ApiProp p)
                {
                    var gm = p.GetMethod;
                    var sm = p.SetMethod;
                    if (gm == null && sm == null) return "default";
                    var r = MapAccessor(gm);
                    var w = MapAccessor(sm);
                    return r + "," + w;
                }

                bool canWrite = p.IsField ? (!p.IsReadOnly && !p.IsConst) : (p.SetMethod != null);
                bool canRead = p.IsField ? true : (p.GetMethod != null);
                var hxType = MapType(p.Type, roots, primary, knownGlobal);
                if (p.IsStatic && UsesClassGenericTypeParam(p.Type, t.TypeGenericParams))
                    hxType = "Any";

                if (!string.IsNullOrWhiteSpace(p.Summary))
                    sb.Append("    /** ").Append(EscDoc(p.Summary)).Append(" */\n");

                if (p.IsProtected)
                    sb.Append("    @:protected\n");

                if (p.IsConst)
                {
                    sb.Append("    ");
                    if (p.IsStatic) sb.Append("static ");
                    sb.Append("final ").Append(n).Append(":").Append(hxType).Append(";\n");
                    memberCount++;
                    continue;
                }

                if (t.IsValueType && canWrite && !p.IsStatic)
                {
                    var readAcc = canRead ? (p.IsField ? (p.IsProtected ? "null" : "default") : MapAccessor(p.GetMethod)) : "never";
                    var acc2 = readAcc + ",set";

                    sb.Append("    ");
                    if (p.IsStatic) sb.Append("static ");
                    sb.Append("var ").Append(n).Append("(").Append(acc2).Append("):").Append(hxType).Append(";\n");

                    sb.Append("\n");
                    sb.Append("    private inline function set_").Append(n).Append("(value:").Append(hxType).Append("):").Append(hxType).Append(" {\n");
                    sb.Append("        var __value:").Append(hxType).Append(" = cast value;\n");
                    sb.Append("        return untyped __cs__(\"{0}.").Append(Esc(p.Name)).Append(" = {1}\", this, __value);\n");
                    sb.Append("    }\n\n");

                    memberCount++;
                    continue;
                }

                var acc = p.IsField ? MapFieldAcc(p) : MapPropertyAcc(p);

                sb.Append("    ");
                if (p.IsStatic) sb.Append("static ");

                if (p.IsReadOnly || (!p.IsField && p.SetMethod == null && p.GetMethod != null))
                    sb.Append("var ").Append(n).Append("(").Append(p.IsField ? (p.IsProtected ? "null,never" : "default,never") : acc).Append("):").Append(hxType).Append(";\n");
                else
                    sb.Append("var ").Append(n).Append("(").Append(acc).Append("):").Append(hxType).Append(";\n");

                memberCount++;
            }

            var byName = new Dictionary<string, List<ApiMethod>>(StringComparer.Ordinal);
            foreach (var m in t.Methods)
            {
                if (!byName.TryGetValue(m.Name, out var arr)) { arr = new List<ApiMethod>(); byName[m.Name] = arr; }
                arr.Add(m);
            }

            foreach (var name in byName.Keys.OrderBy(x => x, StringComparer.Ordinal))
            {
                var group = byName[name];
                var uniq = new Dictionary<string, ApiMethod>(StringComparer.Ordinal);

                foreach (var m in group)
                {
                    var key = SigKey(m, t.TypeGenericParams, roots, primary, knownGlobal);
                    if (!uniq.ContainsKey(key)) uniq[key] = m;
                }

                var list = uniq.Values.ToList();
                if (list.Count == 0) continue;

                list.Sort((a, b) =>
                {
                    var d = a.Params.Count.CompareTo(b.Params.Count);
                    if (d != 0) return d;
                    return string.Compare(
                        SigKey(a, t.TypeGenericParams, roots, primary, knownGlobal),
                        SigKey(b, t.TypeGenericParams, roots, primary, knownGlobal),
                        StringComparison.Ordinal
                    );
                });

                var over = list.Count > 1;
                var doc = "";
                foreach (var item in list)
                {
                    if (string.IsNullOrWhiteSpace(item.Summary)) continue;
                    doc = item.Summary;
                    break;
                }
                if (!string.IsNullOrWhiteSpace(doc))
                    sb.Append("    /** ").Append(EscDoc(doc)).Append(" */\n");

                foreach (var m in list)
                {
                    sb.Append("    ");
                    if (m.IsProtected) sb.Append("@:protected ");
                    if (m.IsStatic) sb.Append("static ");
                    if (over) sb.Append("overload ");

                    sb.Append("function ").Append(SanitizeMemberName(m.Name));
                    var effMethodGenerics = GetEffectiveMethodGenericParams(m, t.TypeGenericParams);
                    if (effMethodGenerics.Count > 0) sb.Append("<").Append(string.Join(",", effMethodGenerics)).Append(">");

                    sb.Append("(").Append(FormatParams(m.Params, roots, primary, knownGlobal)).Append("):")
                      .Append(MapType(m.ReturnType, roots, primary, knownGlobal)).Append(";\n");

                    memberCount++;
                }
            }

            sb.Append("}\n");
            File.WriteAllText(Path.Combine(dir, hxName + ".hx"), sb.ToString(), Encoding.UTF8);
            typeCount++;
        }

        var stubCount = 0;

        if (GenerateExternalStubs)
        {
            foreach (var st in ExternalStubs.Values.OrderBy(x => x.CsNative, StringComparer.Ordinal))
            {
                if (generatedNative.Contains(st.CsNative)) continue;

                var pkgPath = st.HxPackage.Replace('.', Path.DirectorySeparatorChar);
                var isSandboxPkg =
                    st.HxPackage.Equals(primary.Lower, StringComparison.Ordinal) ||
                    st.HxPackage.StartsWith(primary.Lower + ".", StringComparison.Ordinal);

                var baseDir = isSandboxPkg ? Path.Combine(outRoot, primary.Lower) : outRoot;
                var dir = Path.Combine(baseDir, pkgPath);

                Directory.CreateDirectory(dir);

                var file = Path.Combine(dir, st.HxName + ".hx");
                if (File.Exists(file)) continue;

                var sb = new StringBuilder(256);
                sb.Append("package ").Append(st.HxPackage).Append(";\n\n");
                sb.Append("@:native(\"").Append(Esc(st.CsNative)).Append("\")\n");

                if (st.GenericArity > 0)
                {
                    var gs = Enumerable.Range(1, st.GenericArity).Select(i => "T" + i);
                    sb.Append("extern class ").Append(st.HxName).Append("<").Append(string.Join(",", gs)).Append("> {\n}\n");
                }
                else
                {
                    sb.Append("extern class ").Append(st.HxName).Append(" {\n}\n");
                }

                File.WriteAllText(file, sb.ToString(), Encoding.UTF8);
                stubCount++;
            }
        }

        return $"Externs generated: Types={typeCount} | Members={memberCount} | Stubs={stubCount}";
    }

    private static Root PickRootForType(ApiType t, Root[] roots)
    {
        if (!string.IsNullOrWhiteSpace(t.Namespace))
        {
            var best = roots[0];
            var bestLen = -1;

            foreach (var r in roots)
            {
                if (t.Namespace == r.Name)
                {
                    if (r.Name.Length > bestLen) { best = r; bestLen = r.Name.Length; }
                    continue;
                }

                if (t.Namespace.StartsWith(r.Name + ".", StringComparison.Ordinal))
                {
                    if (r.Name.Length > bestLen) { best = r; bestLen = r.Name.Length; }
                }
            }

            if (bestLen >= 0) return best;
        }

        return roots[0];
    }

    private static List<ApiType> CollectTypes(IEnumerable<Assembly> assemblies, Root[] roots, DocStore docs)
    {
        var list = new List<ApiType>();
        var seen = new HashSet<string>(StringComparer.Ordinal);

        foreach (var a in assemblies)
        {
            var asmName = a.GetName().Name ?? "";
            var asmIsRoot = false;
            for (var i = 0; i < roots.Length; i++)
            {
                if (!asmName.StartsWith(roots[i].Name, StringComparison.OrdinalIgnoreCase)) continue;
                asmIsRoot = true;
                break;
            }

            Type[] ts;

            try { ts = a.GetTypes(); }
            catch (ReflectionTypeLoadException ex)
            {
                ts = ex.Types.Where(static t => t != null).Select(static t => t!).ToArray();
            }
            catch { continue; }

            foreach (var t in ts)
            {
                if (!(t.IsPublic || t.IsNestedPublic)) continue;
                if (t.IsPointer) continue;
                if (IsCompilerGeneratedType(t)) continue;

                var ns = t.Namespace ?? "";
                var nsIsRoot = IsInRootNamespace(ns, roots);
                if (!(nsIsRoot || (asmIsRoot && ns.Length == 0))) continue;

                var api = BuildApiType(t, docs);
                if (api == null) continue;
                if (seen.Add(api.NativeFullName))
                    list.Add(api);
            }
        }

        return list;
    }

    private static bool IsInRootNamespace(string ns, Root[] roots)
    {
        if (string.IsNullOrWhiteSpace(ns)) return false;
        foreach (var r in roots)
            if (ns == r.Name || ns.StartsWith(r.Name + ".", StringComparison.Ordinal))
                return true;
        return false;
    }

    private static ApiType? BuildApiType(Type t, DocStore docs)
    {
        var name = t.Name;
        var full = t.FullName ?? name;

        if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(full)) return null;
        if (BadTypeName(full) || BadTypeName(name)) return null;

        var clrFull = t.FullName ?? t.Name ?? "System.Object";
        var schemaFull = FormatTypeLikeSchema(t);

        var lastDot = schemaFull.LastIndexOf(".", StringComparison.Ordinal);
        var schemaNs = lastDot >= 0 ? schemaFull.Substring(0, lastDot) : "";
        var schemaName = lastDot >= 0 ? schemaFull.Substring(lastDot + 1) : schemaFull;

        var api = new ApiType
        {
            NativeFullName = clrFull,
            Namespace = schemaNs,
            Name = StripTick(schemaName),
            BaseType = ResolveBaseType(t),
            Summary = docs.TypeSummary(t),
            IsEnum = t.IsEnum,
            IsValueType = t.IsValueType && !t.IsEnum,
            IsFinal = t.IsSealed && !t.IsEnum
        };

        api.TypeGenericParams = GetTypeGenericParams(t);
        var inheritedMembers = CollectInheritedMemberNames(t);

        if (api.IsEnum)
        {
            api.EnumUnderlying = FormatTypeLikeSchema(Enum.GetUnderlyingType(t));
            var enumMembers = new List<string>();
            foreach (var f in t.GetFields(BindingFlags.Public | BindingFlags.Static))
            {
                var member = f.Name;
                if (string.IsNullOrWhiteSpace(member) || BadMemberName(member)) continue;
                enumMembers.Add(member);
            }
            api.EnumMembers = enumMembers;
            return api;
        }

        foreach (var c in t.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
        {
            if (!(c.IsPublic || c.IsFamily || c.IsFamilyOrAssembly))
                continue;

            var ps = c.GetParameters();
            var pars = new List<ApiParam>(ps.Length);

            for (int i = 0; i < ps.Length; i++)
            {
                var p = ps[i];
                var pn = p.Name;
                if (string.IsNullOrWhiteSpace(pn)) pn = "arg" + i;
                pars.Add(new ApiParam(pn, FormatTypeLikeSchema(p.ParameterType)));
            }

            api.Ctors.Add(new ApiCtor {
                Params = pars,
                IsProtected = !c.IsPublic,
                Summary = docs.MethodSummary(c)
            });
        }

        var propFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
        if (!t.IsInterface) propFlags |= BindingFlags.DeclaredOnly;

        foreach (var p in t.GetProperties(propFlags))
        {
            if (inheritedMembers.Contains(p.Name))
                continue;

            var get = p.GetGetMethod(true);
            var set = p.GetSetMethod(true);

            var visible =
                (get != null && (get.IsPublic || get.IsFamily || get.IsFamilyOrAssembly)) ||
                (set != null && (set.IsPublic || set.IsFamily || set.IsFamilyOrAssembly));

            if (!visible) continue;

            var isProtected =
                (get != null && (get.IsFamily || get.IsFamilyOrAssembly) && !get.IsPublic) ||
                (set != null && (set.IsFamily || set.IsFamilyOrAssembly) && !set.IsPublic);

            api.Props.Add(new ApiProp
            {
                Name = p.Name,
                Type = FormatTypeLikeSchema(p.PropertyType),
                GetMethod = get,
                SetMethod = set,
                IsProtected = isProtected,
                IsStatic = (get?.IsStatic ?? set?.IsStatic) ?? false,
                IsReadOnly = set == null && get != null,
                IsConst = false,
                IsField = false,
                Summary = docs.PropSummary(p)
            });
        }

        var fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
        if (!t.IsInterface) fieldFlags |= BindingFlags.DeclaredOnly;

        foreach (var f in t.GetFields(fieldFlags))
        {
            var visible = f.IsPublic || f.IsFamily || f.IsFamilyOrAssembly;
            if (!visible) continue;

            var fn = f.Name ?? "";
            if (inheritedMembers.Contains(fn)) continue;
            if (BadMemberName(fn)) continue;
            if (fn.StartsWith("<", StringComparison.Ordinal)) continue;

            api.Props.Add(new ApiProp
            {
                Name = fn,
                Type = FormatTypeLikeSchema(f.FieldType),
                GetMethod = null,
                SetMethod = null,
                IsProtected = (f.IsFamily || f.IsFamilyOrAssembly) && !f.IsPublic,
                IsStatic = f.IsStatic,
                IsReadOnly = f.IsInitOnly,
                IsConst = f.IsLiteral,
                IsField = true,
                Summary = ""
            });
        }

        foreach (var m in t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
        {
            if (m.IsSpecialName) continue;
            if (m.DeclaringType != t) continue;

            var isPublic = m.IsPublic;
            var isProtected = (m.IsFamily || m.IsFamilyOrAssembly) && !m.IsPublic;
            if (!isPublic && !isProtected) continue;

            var mn = m.Name;
            if (string.IsNullOrWhiteSpace(mn) || BadMemberName(mn)) continue;

            var ps = m.GetParameters();
            var pars = new List<ApiParam>(ps.Length);

            for (int i = 0; i < ps.Length; i++)
            {
                var p = ps[i];
                var pn = p.Name;
                if (string.IsNullOrWhiteSpace(pn)) pn = "arg" + i;
                pars.Add(new ApiParam(pn, FormatTypeLikeSchema(p.ParameterType)));
            }

            var mg = m.IsGenericMethodDefinition
                ? m.GetGenericArguments().Select(x => x.Name).Where(x => !string.IsNullOrWhiteSpace(x)).ToList()
                : new List<string>();

            api.Methods.Add(new ApiMethod
            {
                Name = mn,
                ReturnType = FormatTypeLikeSchema(m.ReturnType),
                IsStatic = m.IsStatic,
                IsProtected = isProtected,
                Params = pars,
                MethodGenericParams = mg,
                Summary = docs.MethodSummary(m)
            });
        }

        return api;
    }

    private static HashSet<string> CollectInheritedMemberNames(Type t)
    {
        var result = new HashSet<string>(StringComparer.Ordinal);
        if (t.IsInterface) return result;

        var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
        var bt = t.BaseType;

        while (bt != null && bt != typeof(object))
        {
            foreach (var p in bt.GetProperties(flags))
            {
                var get = p.GetGetMethod(true);
                var set = p.GetSetMethod(true);
                var visible =
                    (get != null && (get.IsPublic || get.IsFamily || get.IsFamilyOrAssembly)) ||
                    (set != null && (set.IsPublic || set.IsFamily || set.IsFamilyOrAssembly));
                if (!visible) continue;
                if (BadMemberName(p.Name)) continue;
                result.Add(p.Name);
            }

            foreach (var f in bt.GetFields(flags))
            {
                var visible = f.IsPublic || f.IsFamily || f.IsFamilyOrAssembly;
                if (!visible) continue;
                var fn = f.Name ?? "";
                if (BadMemberName(fn)) continue;
                if (fn.StartsWith("<", StringComparison.Ordinal)) continue;
                result.Add(fn);
            }

            bt = bt.BaseType;
        }

        return result;
    }

    private static string ResolveBaseType(Type t)
    {
        try
        {
            if (t.IsEnum) return "";
            if (t.IsValueType) return "";
            if (t.IsInterface) return "";

            var bt = t.BaseType;
            if (bt == null) return "";

            var full = NormalizeFullName(bt.FullName ?? bt.Name ?? "");
            if (string.IsNullOrWhiteSpace(full)) return "";
            if (full.Equals("System.Object", StringComparison.Ordinal)) return "";
            return FormatTypeLikeSchema(bt);
        }
        catch
        {
            return "";
        }
    }

    private static List<string> GetTypeGenericParams(Type t)
    {
        try
        {
            if (!t.IsGenericType && !t.IsGenericTypeDefinition) return new List<string>();
            return t.GetGenericArguments().Select(x => x.Name).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
        }
        catch { return new List<string>(); }
    }

    private static string BuildHaxePackage(Root root, string ns)
    {
        if (string.IsNullOrWhiteSpace(ns)) return root.Lower;
        ns = ns.Trim();
        if (ns == root.Name) return root.Lower;
        if (ns.StartsWith(root.Name + ".", StringComparison.Ordinal))
            return root.Lower + "." + ns.Substring(root.Name.Length + 1).ToLowerInvariant();
        return root.Lower + "." + ns.ToLowerInvariant();
    }

    private static string BuildOutDir(string outRoot, Root root, string ns)
    {
        var baseDir = Path.Combine(outRoot, root.Lower);
        if (string.IsNullOrWhiteSpace(ns) || ns == root.Name) return baseDir;

        if (ns.StartsWith(root.Name + ".", StringComparison.Ordinal))
        {
            var rest = ns.Substring(root.Name.Length + 1).ToLowerInvariant();
            return Path.Combine(baseDir, rest.Replace('.', Path.DirectorySeparatorChar));
        }

        return Path.Combine(baseDir, ns.ToLowerInvariant().Replace('.', Path.DirectorySeparatorChar));
    }

    private static string SigKey(ApiMethod m, List<string> classGenericParams, Root[] roots, Root primary, HashSet<string> knownGlobal)
    {
        var pars = string.Join(",", m.Params.Select(p => NormKey(MapType(p.Type, roots, primary, knownGlobal))));
        var gp = string.Join(",", GetEffectiveMethodGenericParams(m, classGenericParams));

        return (m.IsStatic ? "S" : "I")
            + (m.IsProtected ? "P" : "U")
            + "|" + m.Name
            + "|<" + gp + ">|" + MapType(m.ReturnType, roots, primary, knownGlobal)
            + "|(" + pars + ")";
    }

    private static List<string> GetEffectiveMethodGenericParams(ApiMethod m, List<string> classGenericParams)
    {
        if (!m.IsStatic || classGenericParams.Count == 0)
            return m.MethodGenericParams;

        var result = new List<string>(m.MethodGenericParams);
        foreach (var gp in classGenericParams)
        {
            if (ContainsName(result, gp)) continue;
            if (TypeUsesGenericParam(m.ReturnType, gp))
            {
                result.Add(gp);
                continue;
            }

            foreach (var p in m.Params)
            {
                if (!TypeUsesGenericParam(p.Type, gp)) continue;
                result.Add(gp);
                break;
            }
        }

        return result;
    }

    private static bool UsesClassGenericTypeParam(string typeSchema, List<string> classGenericParams)
    {
        foreach (var gp in classGenericParams)
            if (TypeUsesGenericParam(typeSchema, gp))
                return true;
        return false;
    }

    private static bool ContainsName(List<string> list, string value)
    {
        for (var i = 0; i < list.Count; i++)
            if (string.Equals(list[i], value, StringComparison.Ordinal))
                return true;
        return false;
    }

    private static bool TypeUsesGenericParam(string typeSchema, string genericParam)
    {
        if (string.IsNullOrWhiteSpace(typeSchema) || string.IsNullOrWhiteSpace(genericParam))
            return false;

        var s = typeSchema;
        var idx = 0;
        while (true)
        {
            idx = s.IndexOf(genericParam, idx, StringComparison.Ordinal);
            if (idx < 0) return false;

            var leftOk = idx == 0 || !IsIdentChar(s[idx - 1]);
            var end = idx + genericParam.Length;
            var rightOk = end >= s.Length || !IsIdentChar(s[end]);
            if (leftOk && rightOk) return true;

            idx = end;
        }
    }

    private static bool IsIdentChar(char c) =>
        (c >= 'A' && c <= 'Z') ||
        (c >= 'a' && c <= 'z') ||
        (c >= '0' && c <= '9') ||
        c == '_';

    private static string NormKey(string t)
    {
        t = (t ?? "").Trim();
        if (t.StartsWith("Null<", StringComparison.Ordinal) && t.EndsWith(">", StringComparison.Ordinal))
            return t.Substring(5, t.Length - 6).Trim();
        return t;
    }

    private static string FormatParams(List<ApiParam> pars, Root[] roots, Root primary, HashSet<string> knownGlobal)
    {
        if (pars.Count == 0) return "";

        var used = new HashSet<string>(StringComparer.Ordinal);
        var outList = new List<string>(pars.Count);

        for (int i = 0; i < pars.Count; i++)
        {
            var raw = pars[i].Name;
            var n = SanitizeMemberName(string.IsNullOrWhiteSpace(raw) ? ("arg" + i) : raw.Trim());
            var baseName = n;
            var k = 2;

            while (!used.Add(n)) { n = baseName + k; k++; }
            outList.Add(n + ":" + MapType(pars[i].Type, roots, primary, knownGlobal));
        }

        return string.Join(", ", outList);
    }

    private static string MapType(string csType, Root[] roots, Root primary, HashSet<string> knownGlobal) =>
        MapTypeInternal(csType, roots, primary, knownGlobal, allowImplicitRootGenerics: true);

    private static string MapTypeInternal(string csType, Root[] roots, Root primary, HashSet<string> knownGlobal, bool allowImplicitRootGenerics)
    {
        csType = (csType ?? "").Trim();
        if (csType.Length == 0) return "Dynamic";

        var cache = allowImplicitRootGenerics ? MapTypeCacheImplicit : MapTypeCacheExplicit;
        if (cache.TryGetValue(csType, out var cached))
            return cached;

        if (allowImplicitRootGenerics && csType.IndexOf("<", StringComparison.Ordinal) < 0)
        {
            var baseKey = StripTickSuffix(csType);
            if (IsRootQualified(baseKey, roots))
            {
                var arity = GetRootGenericArityRuntime(baseKey);
                if (arity > 0)
                {
                    var mappedBase = MapTypeInternal(baseKey, roots, primary, knownGlobal, allowImplicitRootGenerics: false);
                    if (mappedBase != "Dynamic")
                        return cache[csType] = mappedBase + "<" + RepeatToken("Dynamic", arity) + ">";
                }
            }
        }

        if (csType.EndsWith("[]", StringComparison.Ordinal))
            return cache[csType] = "Array<" + MapTypeInternal(csType.Substring(0, csType.Length - 2), roots, primary, knownGlobal, true) + ">";

        if (csType.StartsWith("System.Nullable<", StringComparison.Ordinal) && csType.EndsWith(">", StringComparison.Ordinal))
        {
            var inner = ExtractGenericInner(csType);
            return cache[csType] = "Null<" + MapTypeInternal(inner, roots, primary, knownGlobal, true) + ">";
        }

        if (TryMapPrimitive(csType, out var prim)) return cache[csType] = prim;
        if (IsGenericParamName(csType)) return cache[csType] = csType;

        var lt = csType.IndexOf("<", StringComparison.Ordinal);
        if (lt >= 0 && csType.EndsWith(">", StringComparison.Ordinal))
        {
            var baseName = csType.Substring(0, lt).Trim();
            var args = SplitGenericArgs(ExtractGenericInner(csType));

            if (GenerateExternalStubs && IsExternalQualified(baseName, roots))
            {
                var dot2 = baseName.LastIndexOf(".", StringComparison.Ordinal);
                if (dot2 <= 0) return cache[csType] = "Dynamic";

                var ns2 = baseName.Substring(0, dot2);
                var nm2 = baseName.Substring(dot2 + 1);

                var hxPkg2 = ns2.ToLowerInvariant();
                var baseHxName = SanitizeTypeName(nm2);

                var baseKey = StripTickSuffix(NormalizeFullName(baseName));
                var needSuffix =
                    ExternalBaseAritiesUsed.TryGetValue(baseKey, out var set) &&
                    (set.Count > 1 || set.Contains(0));

                var hxName2 = needSuffix ? (baseHxName + args.Count) : baseHxName;
                RegisterExternalStub(baseName, hxPkg2, hxName2, args.Count);

                var mappedArgs = string.Join(",", args.Select(a => MapTypeInternal(a, roots, primary, knownGlobal, true)));
                return cache[csType] = hxPkg2 + "." + hxName2 + "<" + mappedArgs + ">";
            }

            var mappedBase = MapTypeInternal(baseName, roots, primary, knownGlobal, allowImplicitRootGenerics: false);
            if (mappedBase == "Dynamic") return cache[csType] = "Dynamic";

            var mappedArgs2 = string.Join(",", args.Select(a => MapTypeInternal(a, roots, primary, knownGlobal, true)));
            return cache[csType] = mappedBase + "<" + mappedArgs2 + ">";
        }

        var dot = csType.LastIndexOf(".", StringComparison.Ordinal);
        if (dot < 0)
        {
            if (knownGlobal.Contains(csType)) return cache[csType] = SanitizeTypeName(csType);
            return cache[csType] = "Dynamic";
        }

        var ns = csType.Substring(0, dot);
        var name = csType.Substring(dot + 1);

        foreach (var r in roots)
        {
            if (ns == r.Name)
                return cache[csType] = ApplyImplicitRootGenericArgs(r.Lower + "." + SanitizeTypeName(name), csType, allowImplicitRootGenerics);

            if (ns.StartsWith(r.Name + ".", StringComparison.Ordinal))
            {
                var rest = ns.Substring(r.Name.Length + 1).ToLowerInvariant();
                return cache[csType] = ApplyImplicitRootGenericArgs(r.Lower + "." + rest + "." + SanitizeTypeName(name), csType, allowImplicitRootGenerics);
            }
        }

        if (!GenerateExternalStubs) return cache[csType] = "Dynamic";

        var hxPkg = ns.ToLowerInvariant();
        var hxName = SanitizeTypeName(name);

        RegisterExternalStub(csType, hxPkg, hxName, 0);
        return cache[csType] = hxPkg + "." + hxName;
    }

    private static string ApplyImplicitRootGenericArgs(string mapped, string csType, bool allow)
    {
        if (!allow) return mapped;
        if (RootGenericArity.TryGetValue(csType, out var arity) && arity > 0)
            return mapped + "<" + RepeatToken("Dynamic", arity) + ">";
        return mapped;
    }

    private static string RepeatToken(string token, int count)
    {
        if (count <= 0) return "";
        if (count == 1) return token;

        var sb = new StringBuilder(token.Length * count + count - 1);
        sb.Append(token);
        for (var i = 1; i < count; i++)
            sb.Append(',').Append(token);
        return sb.ToString();
    }

    private static void RegisterExternalStub(string csBaseFull, string hxPkg, string hxName, int arity)
    {
        csBaseFull = NormalizeFullName(csBaseFull);
        var csNative = (arity > 0) ? (csBaseFull + "`" + arity) : csBaseFull;

        if (ExternalStubs.TryGetValue(csNative, out var ex))
        {
            ex.HxPackage = hxPkg;
            ex.HxName = hxName;
            ex.GenericArity = Math.Max(ex.GenericArity, arity);
            return;
        }

        ExternalStubs[csNative] = new StubType { CsNative = csNative, HxPackage = hxPkg, HxName = hxName, GenericArity = arity };
    }

    private static string ExtractGenericInner(string s)
    {
        var lt = s.IndexOf("<", StringComparison.Ordinal);
        var gt = s.LastIndexOf(">", StringComparison.Ordinal);
        if (lt < 0 || gt <= lt) return "";
        return s.Substring(lt + 1, gt - lt - 1);
    }

    private static List<string> SplitGenericArgs(string inner)
    {
        inner ??= "";
        var outList = new List<string>();
        var buf = new StringBuilder();
        var depth = 0;

        for (int i = 0; i < inner.Length; i++)
        {
            var ch = inner[i];

            if (ch == '<') { depth++; buf.Append(ch); continue; }
            if (ch == '>') { depth--; buf.Append(ch); continue; }

            if (ch == ',' && depth == 0)
            {
                var s = buf.ToString().Trim();
                if (s.Length != 0) outList.Add(s);
                buf.Clear();
                continue;
            }

            buf.Append(ch);
        }

        var last = buf.ToString().Trim();
        if (last.Length != 0) outList.Add(last);
        return outList;
    }

    private static string FormatTypeLikeSchema(Type t)
    {
        var key = t;
        if (TypeSchemaCache.TryGetValue(key, out var cached))
            return cached;

        string result;

        if (t.IsByRef)
        {
            var et = t.GetElementType();
            result = et == null ? "System.Object" : FormatTypeLikeSchema(et);
            TypeSchemaCache[key] = result;
            return result;
        }

        if (t.IsArray)
        {
            var et = t.GetElementType();
            result = et == null ? "System.Object[]" : (FormatTypeLikeSchema(et) + "[]");
            TypeSchemaCache[key] = result;
            return result;
        }

        if (t.IsGenericParameter)
        {
            result = t.Name;
            TypeSchemaCache[key] = result;
            return result;
        }

        if (t.IsGenericType)
        {
            var def = t.IsGenericTypeDefinition ? t : t.GetGenericTypeDefinition();
            var defName = NormalizeFullName(def.FullName ?? def.Name ?? "System.Object");
            defName = StripTick(defName);

            var args = t.GetGenericArguments();
            if (args.Length == 0)
            {
                TypeSchemaCache[key] = defName;
                return defName;
            }

            result = defName + "<" + string.Join(",", args.Select(FormatTypeLikeSchema)) + ">";
            TypeSchemaCache[key] = result;
            return result;
        }

        result = NormalizeFullName(t.FullName ?? t.Name ?? "System.Object");
        TypeSchemaCache[key] = result;
        return result;
    }

    private static bool IsCompilerGeneratedType(Type t)
    {
        if (t.IsDefined(typeof(CompilerGeneratedAttribute), inherit: false)) return true;
        var name = t.Name ?? "";
        if (name.StartsWith("<", StringComparison.Ordinal)) return true;
        if (name.Contains("DisplayClass", StringComparison.Ordinal) || name.Contains("AnonStorey", StringComparison.Ordinal)) return true;
        return false;
    }

    private static bool BadTypeName(string name)
    {
        if (string.IsNullOrWhiteSpace(name)) return true;
        if (name.Contains("$", StringComparison.Ordinal)) return true;
        if (name.Contains(".<", StringComparison.Ordinal) || name.Contains("+<", StringComparison.Ordinal)) return true;
        if (name.Contains("\\u003C", StringComparison.Ordinal) || name.Contains("\\u003E", StringComparison.Ordinal)) return true;
        return false;
    }

    private static bool BadMemberName(string name)
    {
        if (string.IsNullOrWhiteSpace(name)) return true;
        if (name[0] == '<') return true;
        if (name.Contains("<", StringComparison.Ordinal) || name.Contains(">", StringComparison.Ordinal)) return true;
        return false;
    }

    private static string NormalizeFullName(string s) => (s ?? "").Trim().Replace('+', '.');

    private static string StripTypeArgs(string s)
    {
        s = (s ?? "").Trim();
        var lt = s.IndexOf("<", StringComparison.Ordinal);
        return lt >= 0 ? s.Substring(0, lt) : s;
    }

    private static string StripTick(string s)
    {
        var tick = s.IndexOf('`');
        return tick >= 0 ? s.Substring(0, tick) : s;
    }

    private static string StripTickSuffix(string s)
    {
        s = (s ?? "").Trim();
        var tick = s.IndexOf('`');
        return tick >= 0 ? s.Substring(0, tick) : s;
    }

    private static string SanitizeTypeName(string name)
    {
        name = StripTick((name ?? "").Trim());
        var lt = name.IndexOf("<", StringComparison.Ordinal);
        if (lt >= 0) name = name.Substring(0, lt);

        var sb = new StringBuilder();
        for (int i = 0; i < name.Length; i++)
        {
            var c = name[i];
            var isAZ = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
            var is09 = (c >= '0' && c <= '9');
            if (isAZ || is09 || c == '_') sb.Append(c);
        }

        var s = sb.ToString();
        if (s.Length == 0) s = "Type";
        if (char.IsDigit(s[0])) s = "_" + s;
        return s;
    }

    private static string SanitizeMemberName(string name)
    {
        name = (name ?? "").Trim();
        if (name.Length == 0) return "";
        return name switch
        {
            "new" => "_new",
            "switch" => "_switch",
            "function" => "_function",
            "default" => "_default",
            "var" => "_var",
            _ => name
        };
    }

    private static bool IsGenericParamName(string s)
    {
        s = (s ?? "").Trim();
        if (s.Length == 0) return false;
        if (s == "T") return true;

        if (s.Length == 1)
        {
            var c = s[0];
            return c >= 'A' && c <= 'Z';
        }

        if (s[0] == 'T')
        {
            for (int i = 1; i < s.Length; i++)
            {
                var c = s[i];
                var ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
                if (!ok) return false;
            }
            return true;
        }

        return false;
    }

    private static string Esc(string s) =>
        (s ?? "").Replace("\\", "\\\\", StringComparison.Ordinal).Replace("\"", "\\\"", StringComparison.Ordinal);

    private static string EscDoc(string s)
    {
        s ??= "";
        s = s.Replace("\r", " ").Replace("\n", " ").Trim();
        while (s.Contains("  ", StringComparison.Ordinal)) s = s.Replace("  ", " ", StringComparison.Ordinal);
        return s.Replace("*/", "* /", StringComparison.Ordinal).Trim();
    }

    private static bool IsExternalQualified(string full, Root[] roots)
    {
        full = (full ?? "").Trim();
        if (full.Length == 0) return false;
        if (!full.Contains(".", StringComparison.Ordinal)) return false;

        foreach (var r in roots)
            if (full == r.Name || full.StartsWith(r.Name + ".", StringComparison.Ordinal))
                return false;

        return true;
    }

    private static bool IsRootQualified(string full, Root[] roots)
    {
        full = (full ?? "").Trim();
        if (full.Length == 0) return false;

        foreach (var r in roots)
            if (full == r.Name || full.StartsWith(r.Name + ".", StringComparison.Ordinal))
                return true;

        return false;
    }

    private static int GetRootGenericArityRuntime(string schemaFullNoTick)
    {
        schemaFullNoTick = StripTickSuffix(schemaFullNoTick);
        if (RootArityRuntimeCache.TryGetValue(schemaFullNoTick, out var cached)) return cached;

        var assemblies = AppDomain.CurrentDomain.GetAssemblies();

        for (int n = 1; n <= 16; n++)
        {
            var clrName = schemaFullNoTick + "`" + n;

            for (int i = 0; i < assemblies.Length; i++)
            {
                try
                {
                    var a = assemblies[i];
                    if (a.IsDynamic) continue;

                    var t = a.GetType(clrName, throwOnError: false, ignoreCase: false);
                    if (t != null && t.IsGenericTypeDefinition && t.GetGenericArguments().Length == n)
                    {
                        RootArityRuntimeCache[schemaFullNoTick] = n;
                        return n;
                    }
                }
                catch { }
            }
        }

        RootArityRuntimeCache[schemaFullNoTick] = 0;
        return 0;
    }

    private static void CollectExternalBaseAritiesUsed(List<ApiType> types, Root[] roots)
    {
        void Add(string baseFull, int arity)
        {
            baseFull = StripTickSuffix(NormalizeFullName(baseFull));
            if (!ExternalBaseAritiesUsed.TryGetValue(baseFull, out var set))
            {
                set = new HashSet<int>();
                ExternalBaseAritiesUsed[baseFull] = set;
            }
            set.Add(arity);
        }

        void Scan(string s)
        {
            s = (s ?? "").Trim();
            if (s.Length == 0) return;

            if (s.EndsWith("[]", StringComparison.Ordinal)) { Scan(s.Substring(0, s.Length - 2)); return; }

            if (s.StartsWith("System.Nullable<", StringComparison.Ordinal) && s.EndsWith(">", StringComparison.Ordinal))
            {
                Scan(ExtractGenericInner(s));
                return;
            }

            var lt = s.IndexOf("<", StringComparison.Ordinal);
            if (lt >= 0 && s.EndsWith(">", StringComparison.Ordinal))
            {
                var baseName = s.Substring(0, lt).Trim();
                var args = SplitGenericArgs(ExtractGenericInner(s));

                if (IsExternalQualified(baseName, roots)) Add(baseName, args.Count);
                foreach (var a in args) Scan(a);
                return;
            }

            if (IsExternalQualified(s, roots)) Add(s, 0);
        }

        foreach (var t in types)
        {
            Scan(t.EnumUnderlying);
            Scan(t.BaseType);

            foreach (var c in t.Ctors)
                foreach (var p in c.Params)
                    Scan(p.Type);

            foreach (var p in t.Props)
                Scan(p.Type);

            foreach (var m in t.Methods)
            {
                Scan(m.ReturnType);
                foreach (var p in m.Params) Scan(p.Type);
            }
        }
    }
}