using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Abi.RoslynGenerator;

internal static class AbiInteropSourceEmitter
{
    internal const string ToolVersion = "2.0.0";

    public static IdlModel ParseIdl(string text)
    {
        JsonDocument document;
        try
        {
            document = JsonDocument.Parse(text);
        }
        catch (JsonException ex)
        {
            throw new GeneratorException($"IDL JSON is invalid: {ex.Message}");
        }

        using (document)
        {
            var root = document.RootElement;
            if (root.ValueKind != JsonValueKind.Object)
            {
                throw new GeneratorException("IDL root must be an object.");
            }

            var abiVersion = ParseAbiVersion(root);
            var enumNames = ParseHeaderTypeNames(root, "enums");
            var structNames = ParseHeaderTypeNames(root, "structs");
            var functions = ParseFunctions(root);
            var parameterOverrides = ParseFunctionParameterOverrides(root, functions);
            var symbolPrefix = ParseSymbolPrefix(root);

            return new IdlModel(functions, enumNames, structNames, abiVersion, parameterOverrides, symbolPrefix);
        }
    }

    public static string RenderCode(IdlModel model, GeneratorOptions options)
    {
        var methods = model.Functions.Select(function => BuildMethod(function, model, options)).ToArray();

        var classDeclaration = SyntaxFactory.ClassDeclaration(options.ClassName)
            .AddModifiers(ResolveAccessModifierTokens(options.AccessModifier))
            .AddModifiers(
                SyntaxFactory.Token(SyntaxKind.StaticKeyword),
                SyntaxFactory.Token(SyntaxKind.PartialKeyword)
            )
            .AddMembers(methods);

        var namespaceDeclaration = SyntaxFactory.FileScopedNamespaceDeclaration(
            SyntaxFactory.ParseName(options.NamespaceName)
        ).AddMembers(classDeclaration);

        var compilationUnit = SyntaxFactory.CompilationUnit()
            .AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Runtime.InteropServices")))
            .AddMembers(namespaceDeclaration)
            .NormalizeWhitespace(eol: "\n", indentation: "    ");

        return
            "// <auto-generated />\n" +
            $"// Generated by abi_roslyn_codegen source generator {ToolVersion}\n" +
            compilationUnit.ToFullString() +
            "\n";
    }

    private static string ParseSymbolPrefix(JsonElement root)
    {
        if (!root.TryGetProperty("codegen", out var codegenObj) || codegenObj.ValueKind != JsonValueKind.Object)
        {
            return string.Empty;
        }

        if (!codegenObj.TryGetProperty("symbol_prefix", out var prefixElement) || prefixElement.ValueKind != JsonValueKind.String)
        {
            return string.Empty;
        }

        return prefixElement.GetString() ?? string.Empty;
    }

    private static string ParseAbiVersion(JsonElement root)
    {
        if (!root.TryGetProperty("abi_version", out var abiVersionObj) || abiVersionObj.ValueKind != JsonValueKind.Object)
        {
            return "n/a";
        }

        var major = GetIntOrDefault(abiVersionObj, "major");
        var minor = GetIntOrDefault(abiVersionObj, "minor");
        var patch = GetIntOrDefault(abiVersionObj, "patch");
        return $"{major}.{minor}.{patch}";
    }

    private static int GetIntOrDefault(JsonElement obj, string key)
    {
        if (!obj.TryGetProperty(key, out var value))
        {
            return 0;
        }

        if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number))
        {
            return number;
        }

        return 0;
    }

    private static HashSet<string> ParseHeaderTypeNames(JsonElement root, string section)
    {
        if (!root.TryGetProperty("header_types", out var headerTypes) || headerTypes.ValueKind != JsonValueKind.Object)
        {
            return new HashSet<string>(StringComparer.Ordinal);
        }

        if (!headerTypes.TryGetProperty(section, out var sectionObj) || sectionObj.ValueKind != JsonValueKind.Object)
        {
            return new HashSet<string>(StringComparer.Ordinal);
        }

        var names = new HashSet<string>(StringComparer.Ordinal);
        foreach (var property in sectionObj.EnumerateObject())
        {
            names.Add(property.Name);
        }

        return names;
    }

    private static List<FunctionSpec> ParseFunctions(JsonElement root)
    {
        if (!root.TryGetProperty("functions", out var functionsObj) || functionsObj.ValueKind != JsonValueKind.Array)
        {
            throw new GeneratorException("IDL is missing required array 'functions'.");
        }

        var functions = new List<FunctionSpec>();

        foreach (var item in functionsObj.EnumerateArray())
        {
            if (item.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var name = GetRequiredString(item, "name");
            var cReturnType = GetOptionalString(item, "c_return_type", "void");
            var documentation = GetOptionalString(item, "documentation", string.Empty);
            var deprecated = GetOptionalBool(item, "deprecated", false);

            var parameters = new List<ParameterSpec>();
            if (item.TryGetProperty("parameters", out var paramsObj) && paramsObj.ValueKind == JsonValueKind.Array)
            {
                var paramIndex = 0;
                foreach (var param in paramsObj.EnumerateArray())
                {
                    if (param.ValueKind != JsonValueKind.Object)
                    {
                        paramIndex++;
                        continue;
                    }

                    var paramName = GetOptionalString(param, "name", $"arg{paramIndex}");
                    var cType = GetOptionalString(param, "c_type", "void");
                    var variadic = GetOptionalBool(param, "variadic", false);
                    parameters.Add(new ParameterSpec(paramName, cType, variadic));
                    paramIndex++;
                }
            }

            functions.Add(new FunctionSpec(name, cReturnType, parameters, documentation, deprecated));
        }

        functions.Sort((left, right) => string.CompareOrdinal(left.Name, right.Name));
        return functions;
    }

    private static Dictionary<string, FunctionParameterOverrideSpec> ParseFunctionParameterOverrides(
        JsonElement root,
        IReadOnlyList<FunctionSpec> functions)
    {
        var result = new Dictionary<string, FunctionParameterOverrideSpec>(StringComparer.Ordinal);

        if (!TryGetInteropFunctionsObject(root, out var interopFunctions))
        {
            return result;
        }

        var knownFunctions = functions.ToDictionary(
            static item => item.Name,
            StringComparer.Ordinal);

        foreach (var functionProperty in interopFunctions.EnumerateObject())
        {
            if (!knownFunctions.TryGetValue(functionProperty.Name, out var functionSpec))
            {
                throw new GeneratorException(
                    $"bindings.interop.functions.{functionProperty.Name} references unknown ABI function.");
            }

            if (functionProperty.Value.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            if (!functionProperty.Value.TryGetProperty("parameters", out var parametersObj) ||
                parametersObj.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var knownParameterNames = new HashSet<string>(
                functionSpec.Parameters.Select(static parameter => parameter.Name),
                StringComparer.Ordinal);

            foreach (var parameterProperty in parametersObj.EnumerateObject())
            {
                var parameterContext =
                    $"bindings.interop.functions.{functionProperty.Name}.parameters.{parameterProperty.Name}";
                if (!knownParameterNames.Contains(parameterProperty.Name))
                {
                    throw new GeneratorException($"{parameterContext} references unknown ABI parameter.");
                }

                if (parameterProperty.Value.ValueKind != JsonValueKind.Object)
                {
                    continue;
                }

                var managedType = GetOptionalString(parameterProperty.Value, "managed_type", string.Empty);
                var modifierRaw = GetOptionalString(parameterProperty.Value, "modifier", string.Empty);
                var modifier = NormalizeParameterModifier(modifierRaw, parameterContext);

                var marshalAsI1 = ReadOptionalNullableBool(parameterProperty.Value, "marshal_as_i1");
                var marshalAs = GetOptionalString(parameterProperty.Value, "marshal_as", string.Empty);
                if (!string.IsNullOrWhiteSpace(marshalAs))
                {
                    if (!string.Equals(marshalAs.Trim(), "i1", StringComparison.OrdinalIgnoreCase))
                    {
                        throw new GeneratorException(
                            $"{parameterContext}.marshal_as must be 'i1' when specified.");
                    }

                    marshalAsI1 = true;
                }

                if (string.IsNullOrWhiteSpace(managedType) &&
                    string.IsNullOrWhiteSpace(modifier) &&
                    !marshalAsI1.HasValue)
                {
                    continue;
                }

                var key = BuildFunctionParameterOverrideKey(functionProperty.Name, parameterProperty.Name);
                result[key] = new FunctionParameterOverrideSpec(managedType, modifier, marshalAsI1);
            }
        }

        return result;
    }

    private static bool TryGetInteropFunctionsObject(JsonElement root, out JsonElement interopFunctions)
    {
        interopFunctions = default;

        if (!root.TryGetProperty("bindings", out var bindingsObj) || bindingsObj.ValueKind != JsonValueKind.Object)
        {
            return false;
        }

        if (!bindingsObj.TryGetProperty("interop", out var interopObj) || interopObj.ValueKind != JsonValueKind.Object)
        {
            return false;
        }

        if (!interopObj.TryGetProperty("functions", out interopFunctions) || interopFunctions.ValueKind != JsonValueKind.Object)
        {
            return false;
        }

        return true;
    }

    private static string GetRequiredString(JsonElement obj, string key)
    {
        if (obj.TryGetProperty(key, out var value) && value.ValueKind == JsonValueKind.String)
        {
            var text = value.GetString();
            if (!string.IsNullOrWhiteSpace(text))
            {
                return text!;
            }
        }

        throw new GeneratorException($"IDL function is missing required string '{key}'.");
    }

    private static string GetOptionalString(JsonElement obj, string key, string fallback)
    {
        if (obj.TryGetProperty(key, out var value) && value.ValueKind == JsonValueKind.String)
        {
            var text = value.GetString();
            if (!string.IsNullOrWhiteSpace(text))
            {
                return text!;
            }
        }

        return fallback;
    }

    private static bool GetOptionalBool(JsonElement obj, string key, bool fallback)
    {
        if (obj.TryGetProperty(key, out var value) && (value.ValueKind == JsonValueKind.True || value.ValueKind == JsonValueKind.False))
        {
            return value.GetBoolean();
        }

        return fallback;
    }

    private static bool? ReadOptionalNullableBool(JsonElement obj, string key)
    {
        if (!obj.TryGetProperty(key, out var value))
        {
            return null;
        }

        if (value.ValueKind == JsonValueKind.True)
        {
            return true;
        }

        if (value.ValueKind == JsonValueKind.False)
        {
            return false;
        }

        return null;
    }

    private static string NormalizeParameterModifier(string value, string context)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        var normalized = value.Trim().ToLowerInvariant();
        if (normalized == "none" || normalized == "null" || normalized == "default")
        {
            return string.Empty;
        }

        if (normalized == "ref" || normalized == "out" || normalized == "in")
        {
            return normalized;
        }

        throw new GeneratorException(
            $"{context}.modifier must be one of: ref, out, in, none.");
    }

    private static string BuildFunctionParameterOverrideKey(string functionName, string parameterName)
    {
        return functionName + "::" + parameterName;
    }

    private static MethodDeclarationSyntax BuildMethod(
        FunctionSpec function,
        IdlModel model,
        GeneratorOptions options
    )
    {
        var returnType = MapManagedType(function.CReturnType, model);
        var parameters = new List<ParameterSyntax>(function.Parameters.Count);

        for (var idx = 0; idx < function.Parameters.Count; idx++)
        {
            var parameter = function.Parameters[idx];
            var spec = MapManagedParameter(function, parameter, model);
            var managedName = SanitizeIdentifier(parameter.Name, $"arg{idx}");
            var parameterSyntax = SyntaxFactory.Parameter(SyntaxFactory.Identifier(managedName))
                .WithType(SyntaxFactory.ParseTypeName(spec.TypeName));

            if (!string.IsNullOrWhiteSpace(spec.Modifier))
            {
                var modifierToken = spec.Modifier switch
                {
                    "ref" => SyntaxFactory.Token(SyntaxKind.RefKeyword),
                    "out" => SyntaxFactory.Token(SyntaxKind.OutKeyword),
                    "in" => SyntaxFactory.Token(SyntaxKind.InKeyword),
                    _ => throw new GeneratorException($"Unsupported parameter modifier '{spec.Modifier}'"),
                };
                parameterSyntax = parameterSyntax.WithModifiers(
                    SyntaxFactory.TokenList(modifierToken)
                );
            }

            if (spec.MarshalAsI1)
            {
                var marshalAsAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("MarshalAs"))
                    .WithArgumentList(
                        SyntaxFactory.AttributeArgumentList(
                            SyntaxFactory.SingletonSeparatedList(
                                SyntaxFactory.AttributeArgument(
                                    SyntaxFactory.ParseExpression("UnmanagedType.I1")
                                )
                            )
                        )
                    );

                parameterSyntax = parameterSyntax.WithAttributeLists(
                    SyntaxFactory.SingletonList(
                        SyntaxFactory.AttributeList(
                            SyntaxFactory.SingletonSeparatedList(marshalAsAttribute)
                        )
                    )
                );
            }

            parameters.Add(parameterSyntax);
        }

        var libraryExpression = ParseExpression(options.LibraryExpression, "AbiLibraryExpression");
        var callingConventionExpression = ParseExpression(
            $"CallingConvention.{options.CallingConvention}",
            "AbiCallingConvention"
        );

        var dllImportAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DllImport"))
            .WithArgumentList(
                SyntaxFactory.AttributeArgumentList(
                    SyntaxFactory.SeparatedList(
                        new[]
                        {
                            SyntaxFactory.AttributeArgument(libraryExpression),
                            SyntaxFactory.AttributeArgument(callingConventionExpression)
                                .WithNameEquals(
                                    SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName("CallingConvention"))
                                ),
                            SyntaxFactory.AttributeArgument(
                                SyntaxFactory.LiteralExpression(
                                    SyntaxKind.StringLiteralExpression,
                                    SyntaxFactory.Literal(function.Name)
                                )
                            ).WithNameEquals(
                                SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName("EntryPoint"))
                            ),
                        }
                    )
                )
            );

        var attributes = new List<AttributeSyntax> { dllImportAttribute };
        if (function.Deprecated)
        {
            var obsoleteMessage = string.IsNullOrWhiteSpace(function.Documentation)
                ? $"{function.Name} is deprecated in ABI metadata."
                : $"{function.Name} is marked deprecated. {function.Documentation}";
            var obsoleteAttribute = SyntaxFactory.Attribute(SyntaxFactory.ParseName("global::System.Obsolete"))
                .WithArgumentList(
                    SyntaxFactory.AttributeArgumentList(
                        SyntaxFactory.SingletonSeparatedList(
                            SyntaxFactory.AttributeArgument(
                                SyntaxFactory.LiteralExpression(
                                    SyntaxKind.StringLiteralExpression,
                                    SyntaxFactory.Literal(obsoleteMessage)
                                )
                            )
                        )
                    )
                );
            attributes.Add(obsoleteAttribute);
        }

        var attributeList = SyntaxFactory.AttributeList(
            SyntaxFactory.SeparatedList(attributes)
        );

        return SyntaxFactory.MethodDeclaration(
                SyntaxFactory.ParseTypeName(returnType),
                SyntaxFactory.Identifier(function.Name)
            )
            .WithAttributeLists(SyntaxFactory.SingletonList(attributeList))
            .WithModifiers(
                SyntaxFactory.TokenList(
                    ResolveAccessModifierTokens(options.AccessModifier)
                        .Concat(
                            new[]
                            {
                                SyntaxFactory.Token(SyntaxKind.StaticKeyword),
                                SyntaxFactory.Token(SyntaxKind.ExternKeyword),
                            }
                        )
                )
            )
            .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(parameters)))
            .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken));
    }

    private static ExpressionSyntax ParseExpression(string expressionText, string optionName)
    {
        var expression = SyntaxFactory.ParseExpression(expressionText);
        if (expression.ContainsDiagnostics)
        {
            throw new GeneratorException(
                $"{optionName} produced invalid expression: {expressionText}"
            );
        }

        return expression;
    }

    private static SyntaxToken[] ResolveAccessModifierTokens(string value)
    {
        var normalized = value.Trim().ToLowerInvariant();
        switch (normalized)
        {
            case "public":
                return new[] { SyntaxFactory.Token(SyntaxKind.PublicKeyword) };
            case "internal":
                return new[] { SyntaxFactory.Token(SyntaxKind.InternalKeyword) };
            case "private":
                return new[] { SyntaxFactory.Token(SyntaxKind.PrivateKeyword) };
            case "protected":
                return new[] { SyntaxFactory.Token(SyntaxKind.ProtectedKeyword) };
            case "protectedinternal":
            case "protected_internal":
            case "protected-internal":
                return new[]
                {
                    SyntaxFactory.Token(SyntaxKind.ProtectedKeyword),
                    SyntaxFactory.Token(SyntaxKind.InternalKeyword),
                };
            case "privateprotected":
            case "private_protected":
            case "private-protected":
                return new[]
                {
                    SyntaxFactory.Token(SyntaxKind.PrivateKeyword),
                    SyntaxFactory.Token(SyntaxKind.ProtectedKeyword),
                };
            default:
                throw new GeneratorException($"Unsupported access modifier: '{value}'");
        }
    }

    private static string SanitizeIdentifier(string value, string fallback)
    {
        var candidate = string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();

        if (!Regex.IsMatch(candidate, "^[A-Za-z_][A-Za-z0-9_]*$"))
        {
            candidate = Regex.Replace(candidate, "[^A-Za-z0-9_]", "_");
            if (!Regex.IsMatch(candidate, "^[A-Za-z_].*$"))
            {
                candidate = "_" + candidate;
            }
        }

        if (AbiTypeConstants.CSharpKeywords.Contains(candidate))
        {
            return "@" + candidate;
        }

        return candidate;
    }

    private static ParameterRenderSpec MapManagedParameter(
        FunctionSpec function,
        ParameterSpec parameter,
        IdlModel model)
    {
        var baseline = MapManagedParameterCore(parameter, model);
        return ApplyParameterOverride(function, parameter, model, baseline);
    }

    private static ParameterRenderSpec MapManagedParameterCore(ParameterSpec parameter, IdlModel model)
    {
        if (parameter.Variadic)
        {
            return new ParameterRenderSpec("IntPtr", modifier: null, marshalAsI1: false);
        }

        var info = ParseCTypeInfo(parameter.CType);
        if (info.PointerDepth == 0)
        {
            var scalarType = MapManagedBaseType(info.BaseType, model);

            return new ParameterRenderSpec(
                scalarType,
                modifier: null,
                marshalAsI1: string.Equals(scalarType, "bool", StringComparison.Ordinal)
            );
        }

        if (info.PointerDepth > 1)
        {
            return new ParameterRenderSpec("IntPtr", modifier: null, marshalAsI1: false);
        }

        if (model.StructNames.Contains(info.BaseType))
        {
            var structType = MapManagedBaseType(info.BaseType, model);
            return new ParameterRenderSpec(structType, modifier: "ref", marshalAsI1: false);
        }

        return new ParameterRenderSpec("IntPtr", modifier: null, marshalAsI1: false);
    }

    private static ParameterRenderSpec ApplyParameterOverride(
        FunctionSpec function,
        ParameterSpec parameter,
        IdlModel model,
        ParameterRenderSpec baseline)
    {
        var key = BuildFunctionParameterOverrideKey(function.Name, parameter.Name);
        if (!model.FunctionParameterOverrides.TryGetValue(key, out var overrideSpec))
        {
            return baseline;
        }

        var hasManagedTypeOverride = !string.IsNullOrWhiteSpace(overrideSpec.ManagedType);
        var typeName = string.IsNullOrWhiteSpace(overrideSpec.ManagedType)
            ? baseline.TypeName
            : overrideSpec.ManagedType!;
        var modifier = string.IsNullOrWhiteSpace(overrideSpec.Modifier)
            ? (hasManagedTypeOverride ? null : baseline.Modifier)
            : overrideSpec.Modifier;

        var marshalAsI1 = overrideSpec.MarshalAsI1
            ?? (string.IsNullOrWhiteSpace(overrideSpec.ManagedType)
                ? baseline.MarshalAsI1
                : string.Equals(typeName, "bool", StringComparison.Ordinal));

        return new ParameterRenderSpec(typeName, modifier, marshalAsI1);
    }

    private static CTypeInfo ParseCTypeInfo(string cType)
    {
        var stripped = StripCTypeQualifiers(cType);
        var pointerDepth = stripped.Count(ch => ch == '*');
        var baseType = stripped.Replace("*", string.Empty).Trim();
        return new CTypeInfo(baseType, pointerDepth);
    }

    private static string MapManagedType(string cType, IdlModel model)
    {
        if (cType == "...")
        {
            return "IntPtr";
        }

        var info = ParseCTypeInfo(cType);
        if (info.PointerDepth > 0)
        {
            return "IntPtr";
        }

        return MapManagedBaseType(info.BaseType, model);
    }

    private static string MapManagedBaseType(string cTypeBase, IdlModel model)
    {
        var stripped = StripCTypeQualifiers(cTypeBase);

        if (AbiTypeConstants.InteropPrimitiveTypeMap.TryGetValue(stripped, out var primitive))
        {
            return primitive;
        }

        if (model.EnumNames.Contains(stripped) || model.StructNames.Contains(stripped))
        {
            return ToManagedTypeName(stripped, stripTypedefSuffix: true);
        }

        if (stripped.EndsWith("_t", StringComparison.Ordinal))
        {
            return ToManagedTypeName(stripped, stripTypedefSuffix: true);
        }

        if (stripped.EndsWith("_cb", StringComparison.Ordinal))
        {
            return ToManagedTypeName(stripped, stripTypedefSuffix: false);
        }

        if (Regex.IsMatch(stripped, "^[A-Za-z_][A-Za-z0-9_]*$"))
        {
            return ToManagedTypeName(stripped, stripTypedefSuffix: false);
        }

        return "IntPtr";
    }

    private static string NormalizeCType(string value)
    {
        var text = Regex.Replace(value, "\\s+", " ").Trim();
        text = Regex.Replace(text, "\\s*\\*\\s*", "*");
        return text;
    }

    private static string StripCTypeQualifiers(string value)
    {
        var text = NormalizeCType(value);
        text = Regex.Replace(text, "\\b(const|volatile|restrict)\\b", " ");
        text = Regex.Replace(text, "\\b(struct|enum)\\s+", " ");
        text = Regex.Replace(text, "\\s+", " ").Trim();
        text = Regex.Replace(text, "\\s*\\*\\s*", "*");
        return text;
    }

    private static string ToManagedTypeName(string cIdentifier, bool stripTypedefSuffix)
    {
        var value = cIdentifier;
        if (stripTypedefSuffix && value.EndsWith("_t", StringComparison.Ordinal))
        {
            value = value.Substring(0, value.Length - 2);
        }

        var builder = new StringBuilder();
        var parts = value.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
        foreach (var rawPart in parts)
        {
            var part = rawPart.Trim();
            if (part.Length == 0)
            {
                continue;
            }

            builder.Append(char.ToUpperInvariant(part[0]));
            if (part.Length > 1)
            {
                builder.Append(part.Substring(1));
            }
        }

        var joined = builder.ToString();
        return string.IsNullOrWhiteSpace(joined) ? "IntPtr" : joined;
    }
}

internal sealed class GeneratorOptions
{
    private static readonly StringComparison PathComparison =
        System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)
            ? StringComparison.OrdinalIgnoreCase
            : StringComparison.Ordinal;

    private const string DefaultIdlPath = "abi/generated/abi.idl.json";
    private const string DefaultNamespace = "Abi.Interop";
    private const string DefaultClassName = "NativeMethods";
    private const string DefaultConstantsClassName = "AbiConstants";
    private const string DefaultAccessModifier = "internal";
    private const string DefaultCallingConvention = "Cdecl";
    private const string DefaultLibraryExpression = "LibName";
    private const string DefaultManagedMetadataPath = "abi/bindings/abi.managed.json";
    private const string DefaultManagedApiMetadataPath = "abi/bindings/abi.managed_api.json";

    public GeneratorOptions(
        string idlPath,
        string managedMetadataPath,
        string managedApiMetadataPath,
        string namespaceName,
        string className,
        string constantsClassName,
        string accessModifier,
        string callingConvention,
        string libraryExpression)
    {
        IdlPath = idlPath;
        ManagedMetadataPath = managedMetadataPath;
        ManagedApiMetadataPath = managedApiMetadataPath;
        NamespaceName = namespaceName;
        ClassName = className;
        ConstantsClassName = constantsClassName;
        AccessModifier = accessModifier;
        CallingConvention = callingConvention;
        LibraryExpression = libraryExpression;
    }

    public string IdlPath { get; }

    public string ManagedMetadataPath { get; }

    public string ManagedApiMetadataPath { get; }

    public string NamespaceName { get; }

    public string ClassName { get; }

    public string ConstantsClassName { get; }

    public string AccessModifier { get; }

    public string CallingConvention { get; }

    public string LibraryExpression { get; }

    public static GeneratorOptions From(AnalyzerConfigOptions options)
    {
        return new GeneratorOptions(
            idlPath: ReadBuildProperty(
                options,
                DefaultIdlPath,
                "AbiIdlPath"),
            managedMetadataPath: ReadBuildProperty(
                options,
                DefaultManagedMetadataPath,
                "AbiManagedMetadataPath"),
            managedApiMetadataPath: ReadBuildProperty(
                options,
                DefaultManagedApiMetadataPath,
                "AbiManagedApiMetadataPath"),
            namespaceName: ReadBuildProperty(
                options,
                DefaultNamespace,
                "AbiNamespace"),
            className: ReadBuildProperty(
                options,
                DefaultClassName,
                "AbiClassName"),
            constantsClassName: ReadBuildProperty(
                options,
                DefaultConstantsClassName,
                "AbiConstantsClassName"),
            accessModifier: ReadBuildProperty(
                options,
                DefaultAccessModifier,
                "AbiAccessModifier"),
            callingConvention: ReadBuildProperty(
                options,
                DefaultCallingConvention,
                "AbiCallingConvention"),
            libraryExpression: ReadBuildProperty(
                options,
                DefaultLibraryExpression,
                "AbiLibraryExpression")
        );
    }

    public bool MatchesIdlPath(string candidatePath)
    {
        return MatchesConfiguredPath(candidatePath, IdlPath);
    }

    public bool MatchesManagedMetadataPath(string candidatePath)
    {
        return MatchesConfiguredPath(candidatePath, ManagedMetadataPath);
    }

    public bool MatchesManagedApiMetadataPath(string candidatePath)
    {
        return MatchesConfiguredPath(candidatePath, ManagedApiMetadataPath);
    }

    private static bool MatchesConfiguredPath(string candidatePath, string configuredPath)
    {
        if (string.IsNullOrWhiteSpace(candidatePath))
        {
            return false;
        }

        if (Path.IsPathRooted(configuredPath))
        {
            var configuredAbsolute = NormalizePath(SafeGetFullPath(configuredPath));
            var candidateAbsolute = NormalizePath(SafeGetFullPath(candidatePath));
            return string.Equals(candidateAbsolute, configuredAbsolute, PathComparison);
        }

        var candidateNormalized = NormalizePath(candidatePath);
        var configuredRelative = NormalizePath(configuredPath).TrimStart('/');
        if (string.IsNullOrWhiteSpace(configuredRelative))
        {
            return false;
        }

        if (string.Equals(candidateNormalized.TrimStart('/'), configuredRelative, PathComparison))
        {
            return true;
        }

        return candidateNormalized.EndsWith("/" + configuredRelative, PathComparison);
    }

    private static string ReadBuildProperty(
        AnalyzerConfigOptions options,
        string fallback,
        params string[] propertyNames)
    {
        foreach (var propertyName in propertyNames)
        {
            if (string.IsNullOrWhiteSpace(propertyName))
            {
                continue;
            }

            var key = "build_property." + propertyName;
            if (options.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
            {
                return value.Trim();
            }
        }

        return fallback;
    }

    private static string NormalizePath(string value)
    {
        return value.Replace('\\', '/').Trim();
    }

    private static string SafeGetFullPath(string path)
    {
        try
        {
            return Path.GetFullPath(path);
        }
        catch
        {
            return path;
        }
    }
}

internal sealed class IdlModel
{
    public IdlModel(
        IReadOnlyList<FunctionSpec> functions,
        HashSet<string> enumNames,
        HashSet<string> structNames,
        string abiVersion,
        Dictionary<string, FunctionParameterOverrideSpec> functionParameterOverrides,
        string symbolPrefix = "")
    {
        Functions = functions;
        EnumNames = enumNames;
        StructNames = structNames;
        AbiVersion = abiVersion;
        FunctionParameterOverrides = functionParameterOverrides;
        SymbolPrefix = symbolPrefix;
    }

    public IReadOnlyList<FunctionSpec> Functions { get; }

    public HashSet<string> EnumNames { get; }

    public HashSet<string> StructNames { get; }

    public string AbiVersion { get; }

    public Dictionary<string, FunctionParameterOverrideSpec> FunctionParameterOverrides { get; }

    public string SymbolPrefix { get; }
}

internal sealed class FunctionSpec
{
    public FunctionSpec(
        string name,
        string cReturnType,
        IReadOnlyList<ParameterSpec> parameters,
        string documentation,
        bool deprecated)
    {
        Name = name;
        CReturnType = cReturnType;
        Parameters = parameters;
        Documentation = documentation;
        Deprecated = deprecated;
    }

    public string Name { get; }

    public string CReturnType { get; }

    public IReadOnlyList<ParameterSpec> Parameters { get; }

    public string Documentation { get; }

    public bool Deprecated { get; }
}

internal sealed class ParameterSpec
{
    public ParameterSpec(string name, string cType, bool variadic)
    {
        Name = name;
        CType = cType;
        Variadic = variadic;
    }

    public string Name { get; }

    public string CType { get; }

    public bool Variadic { get; }
}

internal sealed class CTypeInfo
{
    public CTypeInfo(string baseType, int pointerDepth)
    {
        BaseType = baseType;
        PointerDepth = pointerDepth;
    }

    public string BaseType { get; }

    public int PointerDepth { get; }
}

internal sealed class ParameterRenderSpec
{
    public ParameterRenderSpec(string typeName, string? modifier, bool marshalAsI1)
    {
        TypeName = typeName;
        Modifier = modifier;
        MarshalAsI1 = marshalAsI1;
    }

    public string TypeName { get; }

    public string? Modifier { get; }

    public bool MarshalAsI1 { get; }
}

internal sealed class FunctionParameterOverrideSpec
{
    public FunctionParameterOverrideSpec(string managedType, string modifier, bool? marshalAsI1)
    {
        ManagedType = string.IsNullOrWhiteSpace(managedType) ? null : managedType.Trim();
        Modifier = string.IsNullOrWhiteSpace(modifier) ? null : modifier.Trim();
        MarshalAsI1 = marshalAsI1;
    }

    public string? ManagedType { get; }

    public string? Modifier { get; }

    public bool? MarshalAsI1 { get; }
}

internal sealed class GeneratorException : Exception
{
    public GeneratorException(string message)
        : base(message)
    {
    }
}
