using Ghost.NativeWrapperGen.Config;
using Ghost.NativeWrapperGen.Model;
using Ghost.NativeWrapperGen.Transform;
using System.Text.RegularExpressions;
namespace Ghost.NativeWrapperGen.Emit;
///
/// Emits partial struct files containing low-level method wrappers that route
/// DllImport functions from the Api class into the native struct types they belong to.
/// No wrapper classes, no properties, no owned/marshalled types — just methods.
///
public sealed class WrapperGeneratorEmitter
{
public IEnumerable Emit(NativeLibrary library, WrapperConfig config)
{
var naming = new NamingConventions(config);
var resolver = new BindingTypeResolver(library);
var skipFunctions = new HashSet(config.SkipFunctions, StringComparer.Ordinal);
// Collect all DllImport functions, grouped by the target struct they'll be emitted on.
// Each struct tracks a list of methods AND a set of base types (from INHERITANCE apply steps).
var methodsByStruct = new Dictionary>(StringComparer.Ordinal);
var baseTypesByStruct = new Dictionary>(StringComparer.Ordinal);
foreach (var func in library.Functions)
{
if (!func.IsDllImport)
{
continue;
}
if (skipFunctions.Contains(func.Name))
{
continue;
}
RouteFunction(func, config, resolver, methodsByStruct, baseTypesByStruct);
}
// Emit one file per struct that has at least one routed method.
foreach (var (structName, methods) in methodsByStruct.OrderBy(static kv => kv.Key, StringComparer.Ordinal))
{
baseTypesByStruct.TryGetValue(structName, out var baseTypes);
yield return EmitStructFile(structName, methods, baseTypes, config, naming);
}
}
// ─── Routing ─────────────────────────────────────────────────────────────
///
/// Attempts to match the function against each action in order. On match, processes
/// every apply step in the action's Apply list — INSTANCE_METHOD/STATIC_METHOD steps
/// produce a RoutedMethod; INHERITANCE steps record base types on the target struct.
/// Only the first matching action is used (first-match-wins).
///
private static void RouteFunction(
NativeFunction func,
WrapperConfig config,
BindingTypeResolver resolver,
Dictionary> methodsByStruct,
Dictionary> baseTypesByStruct)
{
foreach (var action in config.Actions)
{
if (!string.Equals(action.Filter, "EXTERN_API", StringComparison.Ordinal))
{
continue;
}
// Determine the target struct name before evaluating NAME_CONDITION
// (because NAME_CONDITION may reference $TSelf / $TBare).
var targetStruct = ResolveTargetType(func, action.TargetType, resolver);
if (targetStruct is null)
{
continue;
}
if (!EvaluateConditions(func, action.Conditions, resolver, targetStruct, config))
{
continue;
}
// Process each apply step.
foreach (var apply in action.Apply)
{
var applyType = apply.Type;
if (string.Equals(applyType, "INSTANCE_METHOD", StringComparison.Ordinal) ||
string.Equals(applyType, "STATIC_METHOD", StringComparison.Ordinal))
{
var isInstance = string.Equals(applyType, "INSTANCE_METHOD", StringComparison.Ordinal);
var routed = new RoutedMethod
{
Function = func,
TargetStructName = targetStruct,
IsInstance = isInstance,
Apply = apply,
};
if (!methodsByStruct.TryGetValue(targetStruct, out var list))
{
list = [];
methodsByStruct[targetStruct] = list;
}
list.Add(routed);
}
else if (string.Equals(applyType, "INHERITANCE", StringComparison.Ordinal))
{
var baseTypes = apply.Opts?.baseType as object?[];
if (baseTypes is { Length: > 0 })
{
if (!baseTypesByStruct.TryGetValue(targetStruct, out var set))
{
set = new HashSet(StringComparer.Ordinal);
baseTypesByStruct[targetStruct] = set;
}
foreach (var bt in baseTypes)
{
if (bt is string s) set.Add(s);
}
}
}
}
// First-match-wins: stop after the first matching action.
return;
}
}
private static bool EvaluateConditions(
NativeFunction func,
List conditions,
BindingTypeResolver resolver,
string targetStructName,
WrapperConfig config)
{
foreach (var condition in conditions)
{
var inverseCondition = false;
var conditionSpan = condition.AsSpan();
if (conditionSpan[0] == '!')
{
inverseCondition = true;
}
var remainingCondition = conditionSpan[(inverseCondition ? 1 : 0)..].Trim();
// Handle parameterised conditions before the switch.
if (remainingCondition.StartsWith("NAME_CONDITION(", StringComparison.Ordinal) &&
remainingCondition.EndsWith(')'))
{
var pattern = remainingCondition["NAME_CONDITION(".Length..^1];
// $TSelf → full struct name (e.g. "NvttSurface")
// $TBare → struct name with the library prefix stripped (e.g. "Surface")
var bareName = StripPrefix(targetStructName, config.NativeTypePrefix);
var resolvedPattern = pattern.ToString()
.Replace("$TBare", bareName, StringComparison.Ordinal)
.Replace("$TSelf", targetStructName, StringComparison.Ordinal);
var match = Regex.IsMatch(func.Name, resolvedPattern, RegexOptions.IgnoreCase);
if (inverseCondition)
{
match = !match;
}
if (!match)
{
return false;
}
continue;
}
switch (remainingCondition)
{
case "SELF_PTR":
if (func.Parameters.Count == 0 ||
resolver.TryGetBindingStructName(func.Parameters[0].TypeName) is null)
{
return inverseCondition;
}
break;
case "FIRST_PARAM_OTHER_TYPE":
if (func.Parameters.Count > 0 &&
resolver.TryGetBindingStructName(func.Parameters[0].TypeName) is not null)
{
return inverseCondition;
}
break;
case "RETURN_BINDING_TYPE":
if (resolver.TryGetBindingStructName(func.ReturnType) is null)
{
return inverseCondition;
}
break;
case "VOID_RETURN":
if (!string.Equals(func.ReturnType, "void", StringComparison.Ordinal))
{
return inverseCondition;
}
break;
default:
// Unknown condition — treat as not-matched.
return false;
}
}
return true;
}
private static string? ResolveTargetType(
NativeFunction func,
string targetTypeRule,
BindingTypeResolver resolver)
{
return targetTypeRule switch
{
"FIRST_PARAM_TYPE" when func.Parameters.Count > 0 =>
resolver.TryGetBindingStructName(func.Parameters[0].TypeName),
"RETURN_TYPE" =>
resolver.TryGetBindingStructName(func.ReturnType),
_ => targetTypeRule,
};
}
// ─── Code emission ────────────────────────────────────────────────────────
private static GeneratedFile EmitStructFile(
string structName,
List methods,
HashSet? baseTypes,
WrapperConfig config,
NamingConventions naming)
{
var writer = new CodeWriter();
writer.WriteLine("// ");
writer.WriteLine("// This file is generated by Ghost.NativeWrapperGen. Do not edit manually.");
writer.WriteLine("// ");
writer.WriteLine();
writer.WriteLine($"namespace {config.OutputNamespace};");
writer.WriteLine();
// Build struct header with optional base-type list.
var baseTypeList = baseTypes is { Count: > 0 }
? " : " + string.Join(", ", baseTypes.Order(StringComparer.Ordinal))
: "";
writer.WriteLine($"public unsafe partial struct {structName}{baseTypeList}");
writer.WriteLine("{");
using (writer.IndentScope())
{
var first = true;
foreach (var method in methods)
{
if (!first)
{
writer.WriteLine();
}
first = false;
EmitMethod(writer, method, config, naming);
}
}
writer.WriteLine("}");
return new GeneratedFile
{
FileName = $"{structName}.nativegen.cs",
Content = writer.ToString(),
};
}
private static void EmitMethod(
CodeWriter writer,
RoutedMethod routed,
WrapperConfig config,
NamingConventions naming)
{
var func = routed.Function;
var nameOpts = routed.Apply.Opts?.name;
var methodName = naming.GetName(func.Name, nameOpts, routed.TargetStructName);
// Build the parameter plan: for each native parameter, determine the public type
// and how to pass it to the Api call (applying remaps).
var plan = BuildParameterPlan(func, config, routed);
// Comment showing the source function.
writer.WriteLine("/// ");
writer.WriteLine($"/// From: p.TypeName))})\" />");
writer.WriteLine("/// ");
writer.WriteLine("[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]");
// Signature
var staticModifier = routed.IsInstance ? "" : "static ";
var publicParams = plan.PublicParams;
var paramList = string.Join(", ", publicParams.Select(static p => $"{p.PublicType} {p.PublicName}"));
writer.WriteLine($"public {staticModifier}{func.ReturnType} {methodName}({paramList})");
writer.WriteLine("{");
using (writer.IndentScope())
{
EmitMethodBody(writer, func, routed, plan, config);
}
writer.WriteLine("}");
}
private static void EmitMethodBody(
CodeWriter writer,
NativeFunction func,
RoutedMethod routed,
ParameterPlan plan,
WrapperConfig config)
{
// Collect the remapped params that need wrapCall blocks, in order.
var wrappedParams = plan.PublicParams
.Where(static p => p.WrapCall is not null)
.ToList();
// Build the Api call arguments.
var args = new List();
if (routed.IsInstance)
{
// Self pointer — first native param is skipped from the public signature.
// Use the passAs expression from apply opts if present, substituting $TSelf.
var passAs = (string?)routed.Apply.Opts?.passAs;
if (passAs is not null)
{
args.Add(passAs.Replace("$TSelf", routed.TargetStructName, StringComparison.Ordinal));
}
else
{
// Fallback: construct the self pointer expression directly.
args.Add($"({routed.TargetStructName}*)Unsafe.AsPointer(ref this)");
}
}
foreach (var p in plan.AllNativeParams)
{
if (p.IsConsumedByDerivesFrom)
{
args.Add(p.DerivedExpr!);
}
else if (p.IsSelfParam)
{
// Already handled above.
}
else if (p.PassAs is not null)
{
args.Add(p.PassAs.Replace("$arg", p.PublicName, StringComparison.Ordinal));
}
else
{
args.Add(p.PublicName);
}
}
var hasReturn = !string.Equals(func.ReturnType, "void", StringComparison.Ordinal);
var returnKeyword = hasReturn ? "return " : "";
// Wrap the call in nested fixed() blocks (one per remapped parameter with a wrapCall).
EmitNestedWrapCall(writer, wrappedParams, 0, (w) =>
{
if (args.Count <= 1)
{
var callExpr = $"Api.{func.Name}({string.Join(", ", args)})";
w.WriteLine($"{returnKeyword}{callExpr};");
}
else
{
w.WriteLine($"{returnKeyword}Api.{func.Name}(");
using (w.IndentScope())
{
for (var i = 0; i < args.Count; i++)
{
var trailing = i < args.Count - 1 ? "," : ");";
w.WriteLine($"{args[i]}{trailing}");
}
}
}
});
}
private static void EmitNestedWrapCall(
CodeWriter writer,
List wrappedParams,
int index,
Action emitInner)
{
if (index >= wrappedParams.Count)
{
emitInner(writer);
return;
}
var param = wrappedParams[index];
var wrapCall = param.WrapCall!;
var callPlaceholder = "$CALL";
var splitIndex = wrapCall.IndexOf(callPlaceholder, StringComparison.Ordinal);
if (splitIndex < 0)
{
writer.WriteLine(wrapCall.Replace("$arg", param.PublicName, StringComparison.Ordinal));
EmitNestedWrapCall(writer, wrappedParams, index + 1, emitInner);
return;
}
var before = wrapCall[..splitIndex].Replace("$arg", param.PublicName, StringComparison.Ordinal).Trim();
var after = wrapCall[(splitIndex + callPlaceholder.Length)..].Replace("$arg", param.PublicName, StringComparison.Ordinal).Trim();
if (before.EndsWith('{'))
{
writer.WriteLine(before[..^1].TrimEnd());
writer.WriteLine("{");
using (writer.IndentScope())
{
EmitNestedWrapCall(writer, wrappedParams, index + 1, emitInner);
}
if (after.StartsWith('}'))
{
writer.WriteLine("}");
var remaining = after[1..].Trim();
if (remaining.Length > 0)
{
writer.WriteLine(remaining);
}
}
}
else
{
writer.WriteLine(before);
using (writer.IndentScope())
{
EmitNestedWrapCall(writer, wrappedParams, index + 1, emitInner);
}
if (after.Length > 0)
{
writer.WriteLine(after);
}
}
}
// ─── Parameter planning ───────────────────────────────────────────────────
private static ParameterPlan BuildParameterPlan(
NativeFunction func,
WrapperConfig config,
RoutedMethod routed)
{
var allNativeParams = new List();
var publicParams = new List();
var consumedByDerivesFrom = new HashSet();
var derivedExprs = new Dictionary();
var remapHasSibling = new HashSet();
for (var i = 0; i < func.Parameters.Count; i++)
{
var param = func.Parameters[i];
var remap = FindRemap(param, config);
if (remap?.DerivesFrom is null)
{
continue;
}
var expectedSiblingName = remap.DerivesFrom.ParamPrefix + param.Name + remap.DerivesFrom.ParamSuffix;
for (var j = i + 1; j < func.Parameters.Count; j++)
{
if (string.Equals(func.Parameters[j].Name, expectedSiblingName, StringComparison.Ordinal))
{
consumedByDerivesFrom.Add(j);
derivedExprs[j] = remap.DerivesFrom.Expr.Replace("$arg", param.Name, StringComparison.Ordinal);
remapHasSibling.Add(i);
break;
}
}
}
var selfParamIndex = routed.IsInstance ? 0 : -1;
for (var i = 0; i < func.Parameters.Count; i++)
{
var param = func.Parameters[i];
var isSelf = i == selfParamIndex;
var isConsumed = consumedByDerivesFrom.Contains(i);
if (isSelf)
{
allNativeParams.Add(new NativeParamInfo
{
NativeName = param.Name,
PublicName = param.Name,
IsSelfParam = true,
});
continue;
}
if (isConsumed)
{
allNativeParams.Add(new NativeParamInfo
{
NativeName = param.Name,
PublicName = param.Name,
IsConsumedByDerivesFrom = true,
DerivedExpr = derivedExprs[i],
});
continue;
}
var matchedRemap = FindRemap(param, config);
if (matchedRemap?.DerivesFrom is not null && !remapHasSibling.Contains(i))
{
matchedRemap = null;
}
if (matchedRemap?.Adapter?.ConvertBack is not null)
{
var convertBack = matchedRemap.Adapter.ConvertBack;
var publicParam = new PublicParam
{
PublicType = matchedRemap.Dst.Replace("$TYPE", param.RawTypeName),
PublicName = param.Name,
WrapCall = convertBack.WrapCall.Replace("$TYPE", param.RawTypeName),
PassAs = convertBack.PassAs.Replace("$TYPE", param.RawTypeName),
};
publicParams.Add(publicParam);
allNativeParams.Add(new NativeParamInfo
{
NativeName = param.Name,
PublicName = param.Name,
PassAs = convertBack.PassAs.Replace("$TYPE", param.RawTypeName),
});
}
else
{
publicParams.Add(new PublicParam
{
PublicType = param.TypeName,
PublicName = param.Name,
});
allNativeParams.Add(new NativeParamInfo
{
NativeName = param.Name,
PublicName = param.Name,
});
}
}
return new ParameterPlan
{
PublicParams = publicParams,
AllNativeParams = allNativeParams,
};
}
private static string StripPrefix(string name, string? prefix)
{
if (string.IsNullOrEmpty(prefix))
{
return name;
}
if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return name[prefix.Length..];
}
return name;
}
private static RemapConfig? FindRemap(NativeParameter param, WrapperConfig config)
{
foreach (var remap in config.Remaps)
{
if (!remap.Scope.Contains("parameter", StringComparer.Ordinal))
{
continue;
}
var src = remap.Src.Replace("$TYPE", param.RawTypeName);
if (!string.Equals(src, param.TypeName, StringComparison.Ordinal))
{
continue;
}
if (remap.Filter is { Count: > 0 } filters)
{
var matches = filters.Any(f =>
Regex.IsMatch(param.Name, f, RegexOptions.IgnoreCase));
if (!matches)
{
continue;
}
}
return remap;
}
return null;
}
// ─── Inner types ──────────────────────────────────────────────────────────
private sealed class RoutedMethod
{
public required NativeFunction Function { get; init; }
public required string TargetStructName { get; init; }
public required bool IsInstance { get; init; }
/// The specific INSTANCE_METHOD/STATIC_METHOD apply step that produced this method.
public required ActionApplyConfig Apply { get; init; }
}
private sealed class ParameterPlan
{
public required List PublicParams { get; init; }
public required List AllNativeParams { get; init; }
}
private sealed class PublicParam
{
public required string PublicType { get; init; }
public required string PublicName { get; init; }
public string? WrapCall { get; init; }
public string? PassAs { get; init; }
}
private sealed class NativeParamInfo
{
public required string NativeName { get; init; }
public required string PublicName { get; init; }
public bool IsSelfParam { get; init; }
public bool IsConsumedByDerivesFrom { get; init; }
public string? DerivedExpr { get; init; }
public string? PassAs { get; init; }
}
}