using System.Globalization;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Runtime.Loader;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;

if (args is ["--self-test"])
{
    ApiCatalog.SelfTest();
    return;
}

if (args.Length < 10 || args[0] != "--output" || args[2] != "--upstream-commit" ||
    args[4] != "--target-framework" || args[6] != "--generated-root")
{
    throw new ArgumentException(
        "usage: api-catalog --output PATH --upstream-commit HASH " +
        "--target-framework TFM --generated-root PATH ASSEMBLY...");
}

ApiCatalog.Write(
    Path.GetFullPath(args[1]),
    args[3],
    args[5],
    Path.GetFullPath(args[7]),
    args[8..].Select(Path.GetFullPath).ToArray());

internal static class ApiCatalog
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
        WriteIndented = true,
    };

    internal static void Write(
        string output,
        string upstreamCommit,
        string targetFramework,
        string generatedRoot,
        string[] assemblyPaths)
    {
        if (assemblyPaths.Length != 13)
            throw new InvalidOperationException($"expected 13 assemblies, found {assemblyPaths.Length}");

        var pathsByName = Directory
            .EnumerateFiles(CommonDirectory(assemblyPaths), "*.dll", SearchOption.AllDirectories)
            .Concat(assemblyPaths)
            .Distinct(StringComparer.Ordinal)
            .Select(path => (Name: Path.GetFileNameWithoutExtension(path), Path: path))
            .Where(item => item.Name is not null)
            .GroupBy(item => item.Name!, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(group => group.Key, group => group.Select(item => item.Path).Order(StringComparer.Ordinal).First(), StringComparer.OrdinalIgnoreCase);
        AssemblyLoadContext.Default.Resolving += (_, name) =>
            name.Name is not null && pathsByName.TryGetValue(name.Name, out var path)
                ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path)
                : null;

        var catalogAssemblyNames = assemblyPaths
            .Select(path => AssemblyName.GetAssemblyName(path).Name!)
            .ToHashSet(StringComparer.Ordinal);
        var externalTypes = new Dictionary<(string Assembly, string DocId), ExternalTypeBuilder>();
        var assemblies = assemblyPaths
            .Order(StringComparer.Ordinal)
            .Select(path => ReadAssembly(path, catalogAssemblyNames, externalTypes))
            .OrderBy(assembly => assembly.Identity.Name, StringComparer.Ordinal)
            .ToArray();
        var generators = ReadGenerators(generatedRoot);
        var externalTypeCatalog = externalTypes.Values
            .Select(builder => builder.Build())
            .OrderBy(type => type.Assembly.Name, StringComparer.Ordinal)
            .ThenBy(type => type.DocId, StringComparer.Ordinal)
            .ToArray();
        var catalog = new Catalog(
            1,
            new Upstream(
                "https://github.com/cinderblocks/libremetaverse",
                upstreamCommit,
                targetFramework),
            generators,
            new Summary(
                assemblies.Length,
                generators.Length,
                assemblies.Sum(assembly => assembly.Counts.CatalogTypes),
                assemblies.Sum(assembly => assembly.Counts.CatalogMembers),
                externalTypeCatalog.Length),
            assemblies,
            externalTypeCatalog);

        Directory.CreateDirectory(Path.GetDirectoryName(output)!);
        File.WriteAllText(output, JsonSerializer.Serialize(catalog, JsonOptions) + "\n");
    }

    private static AssemblyCatalog ReadAssembly(
        string path,
        HashSet<string> catalogAssemblyNames,
        Dictionary<(string Assembly, string DocId), ExternalTypeBuilder> externalTypes)
    {
        var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
        var identity = assembly.GetName();
        var types = assembly.GetExportedTypes()
            .Where(IsPubliclyVisible)
            .OrderBy(TypeDocId, StringComparer.Ordinal)
            .Select(type =>
            {
                CollectExternalSurface(type, catalogAssemblyNames, externalTypes);
                return ReadType(type);
            })
            .ToArray();
        var duplicateDocIds = types.Select(type => type.DocId)
            .Concat(types.SelectMany(type => type.Members.Select(member => member.DocId)))
            .GroupBy(docId => docId, StringComparer.Ordinal)
            .Where(group => group.Count() > 1)
            .Select(group => group.Key)
            .ToArray();
        if (duplicateDocIds.Length != 0)
            throw new InvalidOperationException($"{identity.Name}: duplicate documentation IDs: {string.Join(", ", duplicateDocIds)}");
        var metadataCounts = ReadMetadataCounts(path);
        var catalogMembers = types.Sum(type => type.Members.Length);
        if (types.Length != metadataCounts.Types || catalogMembers != metadataCounts.Members)
        {
            throw new InvalidOperationException(
                $"{identity.Name}: catalog/metadata mismatch: " +
                $"types {types.Length}/{metadataCounts.Types}, members {catalogMembers}/{metadataCounts.Members}");
        }

        return new AssemblyCatalog(
            new AssemblyIdentity(
                identity.Name!,
                identity.Version?.ToString() ?? "0.0.0.0",
                string.IsNullOrEmpty(identity.CultureName) ? "neutral" : identity.CultureName,
                Convert.ToHexString(identity.GetPublicKeyToken() ?? []).ToLowerInvariant(),
                assembly.ManifestModule.ModuleVersionId.ToString("D"),
                Path.GetFileName(path),
                Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))).ToLowerInvariant()),
            new Counts(types.Length, catalogMembers, metadataCounts.Types, metadataCounts.Members),
            types);
    }

    private static TypeCatalog ReadType(Type type)
    {
        var nullability = new NullabilityInfoContext();
        var members = new List<MemberCatalog>();

        foreach (var constructor in type.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
            members.Add(ReadMethod(type, constructor, "constructor", nullability));
        foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
                     .Where(method => !IsAccessor(method)))
            members.Add(ReadMethod(type, method, "method", nullability));
        foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
                     .Where(property => IsPublic(property.GetMethod) || IsPublic(property.SetMethod)))
            members.Add(ReadProperty(type, property, nullability));
        foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
                     .Where(field => !field.IsSpecialName))
            members.Add(ReadField(type, field, nullability));
        foreach (var @event in type.GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
                     .Where(@event => IsPublic(@event.AddMethod) || IsPublic(@event.RemoveMethod) || IsPublic(@event.RaiseMethod)))
            members.Add(ReadEvent(type, @event, nullability));

        var delegateInvoke = typeof(MulticastDelegate).IsAssignableFrom(type.BaseType)
            ? type.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
            : null;
        return new TypeCatalog(
            TypeDocId(type),
            type.Namespace ?? "",
            type.Name,
            DisplayType(type),
            TypeKind(type),
            type.IsNested ? "nested_public" : "public",
            type.IsAbstract,
            type.IsSealed,
            type.IsAbstract && type.IsSealed,
            GenericParameters(type.GetGenericArguments().Where(argument => argument.IsGenericParameter)),
            type.BaseType is null ? null : DisplayType(type.BaseType),
            type.GetInterfaces().Select(DisplayType).Order(StringComparer.Ordinal).ToArray(),
            type.IsEnum ? DisplayType(Enum.GetUnderlyingType(type)) : null,
            delegateInvoke is null ? null : ReadMethod(type, delegateInvoke, "delegate_signature", nullability),
            Attributes(type.CustomAttributes),
            members.OrderBy(member => member.DocId, StringComparer.Ordinal).ToArray());
    }

    private static MemberCatalog ReadMethod(Type owner, MethodBase method, string kind, NullabilityInfoContext nullability)
    {
        var methodInfo = method as MethodInfo;
        return new MemberCatalog(
            MemberDocId(owner, method),
            kind,
            method.IsConstructor ? owner.Name : method.Name,
            method.IsStatic,
            Visibility(method),
            methodInfo is null ? null : DisplayType(methodInfo.ReturnType),
            methodInfo is null ? null : Nullability(nullability.Create(methodInfo.ReturnParameter)),
            methodInfo is null ? [] : Attributes(methodInfo.ReturnParameter.CustomAttributes),
            [],
            GenericParameters(method.IsGenericMethodDefinition ? method.GetGenericArguments() : []),
            method.GetParameters().Select(parameter => ReadParameter(parameter, nullability)).ToArray(),
            null,
            null,
            null,
            null,
            null,
            null,
            Attributes(method.CustomAttributes));
    }

    private static MemberCatalog ReadProperty(Type owner, PropertyInfo property, NullabilityInfoContext nullability) => new(
        MemberDocId(owner, property),
        property.GetIndexParameters().Length == 0 ? "property" : "indexer",
        property.Name,
        (property.GetMethod ?? property.SetMethod)!.IsStatic,
        MostVisible(property.GetMethod, property.SetMethod),
        DisplayType(property.PropertyType),
        Nullability(nullability.Create(property)),
        [],
        Accessors(("get", property.GetMethod), ("set", property.SetMethod)),
        [],
        property.GetIndexParameters().Select(parameter => ReadParameter(parameter, nullability)).ToArray(),
        VisibilityOrNull(property.GetMethod),
        VisibilityOrNull(property.SetMethod),
        null,
        null,
        null,
        null,
        Attributes(property.CustomAttributes));

    private static MemberCatalog ReadField(Type owner, FieldInfo field, NullabilityInfoContext nullability)
    {
        var rawValue = field.IsLiteral ? field.GetRawConstantValue() : null;
        return new MemberCatalog(
            MemberDocId(owner, field),
            owner.IsEnum && field.IsLiteral ? "enum_value" : field.IsLiteral ? "constant" : "field",
            field.Name,
            field.IsStatic,
            "public",
            DisplayType(field.FieldType),
            Nullability(nullability.Create(field)),
            [],
            [],
            [],
            [],
            null,
            null,
            field.IsInitOnly,
            field.IsLiteral,
            field.IsLiteral ? Constant(field.FieldType, rawValue) : null,
            owner.IsEnum && field.IsLiteral ? Marshal.SizeOf(Enum.GetUnderlyingType(owner)) * 8 : null,
            Attributes(field.CustomAttributes));
    }

    private static MemberCatalog ReadEvent(Type owner, EventInfo @event, NullabilityInfoContext nullability) => new(
        MemberDocId(owner, @event),
        "event",
        @event.Name,
        (@event.AddMethod ?? @event.RemoveMethod ?? @event.RaiseMethod)!.IsStatic,
        MostVisible(@event.AddMethod, @event.RemoveMethod, @event.RaiseMethod),
        @event.EventHandlerType is null ? null : DisplayType(@event.EventHandlerType),
        @event.EventHandlerType is null ? null : Nullability(nullability.Create(@event)),
        [],
        Accessors(("add", @event.AddMethod), ("remove", @event.RemoveMethod), ("raise", @event.RaiseMethod)),
        [],
        [],
        VisibilityOrNull(@event.AddMethod),
        VisibilityOrNull(@event.RemoveMethod),
        null,
        null,
        null,
        null,
        Attributes(@event.CustomAttributes));

    private static ParameterCatalog ReadParameter(ParameterInfo parameter, NullabilityInfoContext nullability) => new(
        parameter.Name ?? $"arg{parameter.Position}",
        DisplayType(parameter.ParameterType),
        parameter.IsOut ? "out" : parameter.IsIn && parameter.ParameterType.IsByRef ? "in" : parameter.ParameterType.IsByRef ? "ref" : "value",
        parameter.IsOptional,
        parameter.HasDefaultValue ? Constant(parameter.ParameterType, parameter.DefaultValue) : null,
        Nullability(nullability.Create(parameter)),
        Attributes(parameter.CustomAttributes));

    private static AccessorCatalog[] Accessors(params (string Kind, MethodInfo? Method)[] accessors) => accessors
        .Where(accessor => accessor.Method is not null)
        .Select(accessor => new AccessorCatalog(
            accessor.Kind,
            Visibility(accessor.Method!),
            Attributes(accessor.Method!.CustomAttributes),
            Attributes(accessor.Method.ReturnParameter.CustomAttributes)))
        .ToArray();

    private static GenericParameterCatalog[] GenericParameters(IEnumerable<Type> arguments) => arguments
        .Select(argument =>
        {
            var flags = argument.GenericParameterAttributes;
            var constraints = argument.GetGenericParameterConstraints().Select(DisplayType).Order(StringComparer.Ordinal).ToArray();
            return new GenericParameterCatalog(
                argument.Name,
                argument.GenericParameterPosition,
                flags.HasFlag(GenericParameterAttributes.Covariant) ? "out" : flags.HasFlag(GenericParameterAttributes.Contravariant) ? "in" : "invariant",
                flags.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint),
                flags.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint),
                flags.HasFlag(GenericParameterAttributes.DefaultConstructorConstraint),
                flags.HasFlag(GenericParameterAttributes.AllowByRefLike),
                constraints,
                Attributes(argument.CustomAttributes));
        })
        .OrderBy(argument => argument.Position)
        .ToArray();

    private static AttributeCatalog[] Attributes(IEnumerable<CustomAttributeData> attributes) => attributes
        .Select(attribute => new AttributeCatalog(
            DisplayType(attribute.AttributeType),
            attribute.ConstructorArguments.Select(AttributeValue).ToArray(),
            attribute.NamedArguments.OrderBy(argument => argument.MemberName, StringComparer.Ordinal)
                .Select(argument => new NamedAttributeValue(argument.MemberName, AttributeValue(argument.TypedValue))).ToArray()))
        .OrderBy(attribute => attribute.Type, StringComparer.Ordinal)
        .ThenBy(attribute => JsonSerializer.Serialize(attribute, JsonOptions), StringComparer.Ordinal)
        .ToArray();

    private static TypedValue AttributeValue(CustomAttributeTypedArgument argument) => new(
        DisplayType(argument.ArgumentType),
        argument.Value is IEnumerable<CustomAttributeTypedArgument> values
            ? values.Select(AttributeValue).ToArray()
            : ConstantText(argument.Value));

    private static ConstantValue Constant(Type declaredType, object? value) => new(
        DisplayType(declaredType.IsByRef ? declaredType.GetElementType()! : declaredType),
        ConstantText(value));

    private static object? ConstantText(object? value) => value switch
    {
        null or DBNull or Missing => null,
        Type type => DisplayType(type),
        char character => character.ToString(),
        float number => number.ToString("R", CultureInfo.InvariantCulture),
        double number => number.ToString("R", CultureInfo.InvariantCulture),
        IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
        _ => value.ToString(),
    };

    private static NullabilityCatalog Nullability(NullabilityInfo info) => new(
        info.ReadState.ToString().ToLowerInvariant(),
        info.WriteState.ToString().ToLowerInvariant(),
        info.ElementType is null ? null : Nullability(info.ElementType),
        info.GenericTypeArguments.Select(Nullability).ToArray());

    private static GeneratorCatalog[] ReadGenerators(string root)
    {
        var expected = new Dictionary<string, string[]>(StringComparer.Ordinal)
        {
            ["AttentionsGenerator"] = ["LindenAttentions.g.cs"],
            ["GenepoolGenerator"] = ["Genepool.g.cs"],
            ["PacketSourceGenerator"] = ["Packets.g.cs"],
            ["SkeletonGenerator"] = ["LindenSkeletonDefault.g.cs"],
            ["TreesGenerator"] = ["GrassDefinitions.g.cs", "TreeDefinitions.g.cs"],
            ["VisualParamGenerator"] = ["VisualParams.g.cs"],
        };
        var files = Directory.EnumerateFiles(root, "*.cs", SearchOption.AllDirectories).ToArray();
        return expected.Select(pair =>
        {
            var outputs = pair.Value.Select(name => files.SingleOrDefault(path => Path.GetFileName(path) == name)
                ?? throw new InvalidOperationException($"{pair.Key} did not emit {name}"))
                .Select(path => new GeneratorOutput(
                    Path.GetRelativePath(root, path).Replace(Path.DirectorySeparatorChar, '/'),
                    Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))).ToLowerInvariant()))
                .OrderBy(output => output.Path, StringComparer.Ordinal)
                .ToArray();
            return new GeneratorCatalog(pair.Key, outputs);
        }).OrderBy(generator => generator.Name, StringComparer.Ordinal).ToArray();
    }

    private static void CollectExternalSurface(
        Type type,
        HashSet<string> catalogAssemblyNames,
        Dictionary<(string Assembly, string DocId), ExternalTypeBuilder> externalTypes)
    {
        var typeDocId = TypeDocId(type);
        CollectExternal(type.BaseType, typeDocId, "base_type", catalogAssemblyNames, externalTypes);
        foreach (var @interface in type.GetInterfaces())
            CollectExternal(@interface, typeDocId, "interface", catalogAssemblyNames, externalTypes);
        foreach (var argument in type.GetGenericArguments().Where(argument => argument.IsGenericParameter))
            foreach (var constraint in argument.GetGenericParameterConstraints())
                CollectExternal(constraint, typeDocId, $"generic_constraint:{argument.Name}", catalogAssemblyNames, externalTypes);
        foreach (var constructor in type.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
            CollectExternalMethod(type, constructor, catalogAssemblyNames, externalTypes);
        foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
                     .Where(method => !IsAccessor(method)))
            CollectExternalMethod(type, method, catalogAssemblyNames, externalTypes);
        foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
                     .Where(property => IsPublic(property.GetMethod) || IsPublic(property.SetMethod)))
        {
            var docId = MemberDocId(type, property);
            CollectExternal(property.PropertyType, docId, "property_type", catalogAssemblyNames, externalTypes);
            foreach (var parameter in property.GetIndexParameters())
                CollectExternal(parameter.ParameterType, docId, $"parameter:{parameter.Name}", catalogAssemblyNames, externalTypes);
        }
        foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
                     .Where(field => !field.IsSpecialName))
        {
            var docId = MemberDocId(type, field);
            CollectExternal(field.FieldType, docId, "field_type", catalogAssemblyNames, externalTypes);
        }
        foreach (var @event in type.GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
                     .Where(@event => IsPublic(@event.AddMethod) || IsPublic(@event.RemoveMethod) || IsPublic(@event.RaiseMethod)))
        {
            var docId = MemberDocId(type, @event);
            CollectExternal(@event.EventHandlerType, docId, "event_type", catalogAssemblyNames, externalTypes);
        }
    }

    private static void CollectExternalMethod(
        Type owner,
        MethodBase method,
        HashSet<string> catalogAssemblyNames,
        Dictionary<(string Assembly, string DocId), ExternalTypeBuilder> externalTypes)
    {
        var docId = MemberDocId(owner, method);
        if (method is MethodInfo methodInfo)
        {
            CollectExternal(methodInfo.ReturnType, docId, "return_type", catalogAssemblyNames, externalTypes);
        }
        foreach (var parameter in method.GetParameters())
        {
            CollectExternal(parameter.ParameterType, docId, $"parameter:{parameter.Name}", catalogAssemblyNames, externalTypes);
        }
        if (method is MethodInfo genericMethod)
            foreach (var argument in genericMethod.GetGenericArguments().Where(argument => argument.IsGenericParameter))
                foreach (var constraint in argument.GetGenericParameterConstraints())
                    CollectExternal(constraint, docId, $"generic_constraint:{argument.Name}", catalogAssemblyNames, externalTypes);
    }

    private static void CollectExternal(
        Type? type,
        string ownerDocId,
        string position,
        HashSet<string> catalogAssemblyNames,
        Dictionary<(string Assembly, string DocId), ExternalTypeBuilder> externalTypes)
    {
        if (type is null || type.IsGenericParameter) return;
        if (type.HasElementType)
        {
            CollectExternal(type.GetElementType(), ownerDocId, position, catalogAssemblyNames, externalTypes);
            return;
        }
        var usageSignature = DisplayType(type);
        if (type.IsGenericType)
            foreach (var argument in type.GetGenericArguments())
                CollectExternal(argument, ownerDocId, position, catalogAssemblyNames, externalTypes);
        var definition = type.IsGenericType ? type.GetGenericTypeDefinition() : type;
        var assemblyName = definition.Assembly.GetName();
        if (assemblyName.Name is null || catalogAssemblyNames.Contains(assemblyName.Name)) return;
        var key = (assemblyName.Name, TypeDocId(definition));
        if (!externalTypes.TryGetValue(key, out var builder))
        {
            builder = new ExternalTypeBuilder(definition);
            externalTypes.Add(key, builder);
        }
        builder.Usages.Add(new ExternalTypeUsage(ownerDocId, position, usageSignature));
    }

    private static MetadataCounts ReadMetadataCounts(string path)
    {
        using var stream = File.OpenRead(path);
        using var peReader = new PEReader(stream);
        var metadata = peReader.GetMetadataReader();
        var visibleTypes = metadata.TypeDefinitions.Where(handle => IsPubliclyVisible(metadata, handle)).ToHashSet();
        var members = 0;
        foreach (var handle in visibleTypes)
        {
            var definition = metadata.GetTypeDefinition(handle);
            var accessors = definition.GetProperties()
                .SelectMany(property => PropertyAccessors(metadata.GetPropertyDefinition(property)))
                .Concat(definition.GetEvents().SelectMany(@event => EventAccessors(metadata.GetEventDefinition(@event))))
                .ToHashSet();
            members += definition.GetMethods().Count(method =>
                IsPublic(metadata.GetMethodDefinition(method).Attributes) && !accessors.Contains(method));
            members += definition.GetFields().Count(field =>
            {
                var attributes = metadata.GetFieldDefinition(field).Attributes;
                return (attributes & FieldAttributes.FieldAccessMask) == FieldAttributes.Public &&
                       !attributes.HasFlag(FieldAttributes.SpecialName);
            });
            members += definition.GetProperties().Count(property =>
                PropertyAccessors(metadata.GetPropertyDefinition(property)).Any(accessor => IsPublic(metadata.GetMethodDefinition(accessor).Attributes)));
            members += definition.GetEvents().Count(@event =>
                EventAccessors(metadata.GetEventDefinition(@event)).Any(accessor => IsPublic(metadata.GetMethodDefinition(accessor).Attributes)));
        }
        return new MetadataCounts(visibleTypes.Count, members);
    }

    private static IEnumerable<MethodDefinitionHandle> PropertyAccessors(PropertyDefinition property)
    {
        var accessors = property.GetAccessors();
        if (!accessors.Getter.IsNil) yield return accessors.Getter;
        if (!accessors.Setter.IsNil) yield return accessors.Setter;
        foreach (var other in accessors.Others) yield return other;
    }

    private static IEnumerable<MethodDefinitionHandle> EventAccessors(EventDefinition @event)
    {
        var accessors = @event.GetAccessors();
        if (!accessors.Adder.IsNil) yield return accessors.Adder;
        if (!accessors.Remover.IsNil) yield return accessors.Remover;
        if (!accessors.Raiser.IsNil) yield return accessors.Raiser;
        foreach (var other in accessors.Others) yield return other;
    }

    private static bool IsPubliclyVisible(MetadataReader metadata, TypeDefinitionHandle handle)
    {
        var definition = metadata.GetTypeDefinition(handle);
        var visibility = definition.Attributes & TypeAttributes.VisibilityMask;
        if (visibility == TypeAttributes.Public) return true;
        if (visibility != TypeAttributes.NestedPublic) return false;
        var parent = definition.GetDeclaringType();
        return !parent.IsNil && IsPubliclyVisible(metadata, parent);
    }

    private static bool IsPubliclyVisible(Type type) => type.IsPublic || type.IsNestedPublic && type.DeclaringType is not null && IsPubliclyVisible(type.DeclaringType);
    private static bool IsPublic(MethodBase? method) => method?.IsPublic == true;
    private static bool IsPublic(MethodAttributes attributes) => (attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public;
    private static bool IsAccessor(MethodInfo method) => method.IsSpecialName &&
        (method.Name.StartsWith("get_", StringComparison.Ordinal) || method.Name.StartsWith("set_", StringComparison.Ordinal) ||
         method.Name.StartsWith("add_", StringComparison.Ordinal) || method.Name.StartsWith("remove_", StringComparison.Ordinal) ||
         method.Name.StartsWith("raise_", StringComparison.Ordinal));

    private static string TypeKind(Type type) => type.IsEnum ? "enum" :
        typeof(MulticastDelegate).IsAssignableFrom(type.BaseType) ? "delegate" :
        type.IsInterface ? "interface" : type.IsValueType ? "struct" : "class";

    private static string CommonDirectory(string[] paths)
    {
        var common = Path.GetDirectoryName(paths[0])!;
        while (paths.Any(path => !Path.GetFullPath(path).StartsWith(common + Path.DirectorySeparatorChar, StringComparison.Ordinal)))
            common = Path.GetDirectoryName(common) ?? throw new InvalidOperationException("assemblies have no common directory");
        return common;
    }

    private static string Visibility(MethodBase method) => method.IsPublic ? "public" : method.IsFamily ? "protected" :
        method.IsFamilyOrAssembly ? "protected_internal" : method.IsAssembly ? "internal" : "private";
    private static string? VisibilityOrNull(MethodBase? method) => method is null ? null : Visibility(method);
    private static string MostVisible(params MethodBase?[] methods) => methods.Any(IsPublic) ? "public" :
        methods.Any(method => method?.IsFamilyOrAssembly == true) ? "protected_internal" :
        methods.Any(method => method?.IsFamily == true) ? "protected" : "private";

    private static string DisplayType(Type type)
    {
        if (type.IsByRef) return $"{DisplayType(type.GetElementType()!)}&";
        if (type.IsPointer) return $"{DisplayType(type.GetElementType()!)}*";
        if (type.IsArray) return $"{DisplayType(type.GetElementType()!)}[{new string(',', type.GetArrayRank() - 1)}]";
        if (type.IsGenericParameter) return type.Name;
        var name = (type.IsNested ? $"{DisplayType(type.DeclaringType!)}.{type.Name}" : $"{type.Namespace}.{type.Name}").TrimStart('.');
        var tick = name.IndexOf('`');
        if (tick >= 0) name = name[..tick];
        if (!type.IsGenericType) return name;
        var ownArguments = type.GetGenericArguments();
        if (type.IsNested && type.DeclaringType is { IsGenericType: true } declaring)
            ownArguments = ownArguments.Skip(declaring.GetGenericArguments().Length).ToArray();
        return ownArguments.Length == 0 ? name : $"{name}<{string.Join(", ", ownArguments.Select(DisplayType))}>";
    }

    private static string TypeDocId(Type type) => $"T:{DocTypeDefinitionName(type)}";
    private static string MemberDocId(Type owner, MemberInfo member)
    {
        var prefix = member switch
        {
            PropertyInfo => "P",
            FieldInfo => "F",
            EventInfo => "E",
            _ => "M",
        };
        var name = member is ConstructorInfo ? "#ctor" : member.Name.Replace('.', '#');
        if (member is MethodInfo { IsGenericMethodDefinition: true } method)
            name += $"``{method.GetGenericArguments().Length}";
        var parameters = member switch
        {
            MethodBase methodBase => methodBase.GetParameters(),
            PropertyInfo property => property.GetIndexParameters(),
            _ => [],
        };
        var suffix = parameters.Length == 0 ? "" : $"({string.Join(",", parameters.Select(parameter => DocParameterType(parameter.ParameterType)))})";
        if (member is MethodInfo { Name: "op_Implicit" or "op_Explicit" } conversion)
            suffix += $"~{DocParameterType(conversion.ReturnType)}";
        return $"{prefix}:{DocTypeDefinitionName(owner)}.{name}{suffix}";
    }

    private static string DocTypeDefinitionName(Type type)
    {
        var ownName = type.Name;
        return type.IsNested
            ? $"{DocTypeDefinitionName(type.DeclaringType!)}.{ownName}"
            : string.IsNullOrEmpty(type.Namespace) ? ownName : $"{type.Namespace}.{ownName}";
    }

    private static string DocParameterType(Type type)
    {
        if (type.IsByRef) return $"{DocParameterType(type.GetElementType()!)}@";
        if (type.IsPointer) return $"{DocParameterType(type.GetElementType()!)}*";
        if (type.IsArray)
            return type.GetArrayRank() == 1
                ? $"{DocParameterType(type.GetElementType()!)}[]"
                : $"{DocParameterType(type.GetElementType()!)}[{string.Join(",", Enumerable.Repeat("0:", type.GetArrayRank()))}]";
        if (type.IsGenericParameter)
            return type.DeclaringMethod is null ? $"`{type.GenericParameterPosition}" : $"``{type.GenericParameterPosition}";
        var name = type.IsNested
            ? $"{DocParameterType(type.DeclaringType!)}.{type.Name}"
            : string.IsNullOrEmpty(type.Namespace) ? type.Name : $"{type.Namespace}.{type.Name}";
        var tick = name.IndexOf('`');
        if (tick >= 0) name = name[..tick];
        if (!type.IsGenericType) return name;
        var arguments = type.GetGenericArguments();
        return $"{name}{{{string.Join(",", arguments.Select(DocParameterType))}}}";
    }

    internal static void SelfTest()
    {
        var method = typeof(SelfTestFixture<>).GetMethod(nameof(SelfTestFixture<int>.Convert))!;
        var expected = "M:ApiCatalog.SelfTestFixture`1.Convert``1(System.Collections.Generic.Dictionary{`0,``0[]},System.Int32@)";
        if (MemberDocId(typeof(SelfTestFixture<>), method) != expected)
            throw new InvalidOperationException($"documentation ID mismatch: {MemberDocId(typeof(SelfTestFixture<>), method)}");
        var nullability = new NullabilityInfoContext().Create(method.GetParameters()[0]);
        if (nullability.GenericTypeArguments.Length != 2 || nullability.GenericTypeArguments[1].ElementType?.ReadState != NullabilityState.Nullable)
            throw new InvalidOperationException("nested nullability metadata was not preserved");
    }

    private sealed class SelfTestFixture<T> where T : notnull
    {
        public static explicit operator string(SelfTestFixture<T> _) => "";
        public string Convert<TMethod>(Dictionary<T, TMethod?[]> value, ref int count) where TMethod : class => "";
    }

    private sealed record Catalog(
        int SchemaVersion, Upstream Upstream, GeneratorCatalog[] SourceGenerators, Summary Summary,
        AssemblyCatalog[] Assemblies, ExternalTypeCatalog[] ExternalTypes);
    private sealed record Upstream(string Repository, string Commit, string TargetFramework);
    private sealed record Summary(int AssemblyCount, int SourceGeneratorCount, int PublicTypeCount, int PublicMemberCount, int ExternalTypeCount);
    private sealed record GeneratorCatalog(string Name, GeneratorOutput[] Outputs);
    private sealed record GeneratorOutput(string Path, string Sha256);
    private sealed record AssemblyCatalog(AssemblyIdentity Identity, Counts Counts, TypeCatalog[] Types);
    private sealed record AssemblyIdentity(string Name, string Version, string Culture, string PublicKeyToken, string Mvid, string File, string Sha256);
    private sealed record Counts(int CatalogTypes, int CatalogMembers, int MetadataTypes, int MetadataMembers);
    private sealed record TypeCatalog(
        string DocId, string Namespace, string Name, string Signature, string Kind, string Visibility,
        bool Abstract, bool Sealed, bool Static, GenericParameterCatalog[] GenericParameters,
        string? BaseType, string[] Interfaces, string? EnumUnderlyingType, MemberCatalog? DelegateSignature,
        AttributeCatalog[] Attributes, MemberCatalog[] Members);
    private sealed record MemberCatalog(
        string DocId, string Kind, string Name, bool Static, string Visibility, string? Type,
        NullabilityCatalog? Nullability, AttributeCatalog[] ReturnAttributes, AccessorCatalog[] Accessors,
        GenericParameterCatalog[] GenericParameters, ParameterCatalog[] Parameters,
        string? GetterVisibility, string? SetterVisibility, bool? ReadOnly, bool? Constant,
        ConstantValue? Value, int? NumericWidth, AttributeCatalog[] Attributes);
    private sealed record AccessorCatalog(string Kind, string Visibility, AttributeCatalog[] Attributes, AttributeCatalog[] ReturnAttributes);
    private sealed record ParameterCatalog(
        string Name, string Type, string Passing, bool Optional, ConstantValue? Default,
        NullabilityCatalog Nullability, AttributeCatalog[] Attributes);
    private sealed record GenericParameterCatalog(
        string Name, int Position, string Variance, bool ReferenceType, bool ValueType,
        bool DefaultConstructor, bool AllowByRefLike, string[] Constraints, AttributeCatalog[] Attributes);
    private sealed record NullabilityCatalog(string Read, string Write, NullabilityCatalog? Element, NullabilityCatalog[] GenericArguments);
    private sealed record AttributeCatalog(string Type, TypedValue[] ConstructorArguments, NamedAttributeValue[] NamedArguments);
    private sealed record NamedAttributeValue(string Name, TypedValue Value);
    private sealed record TypedValue(string Type, object? Value);
    private sealed record ConstantValue(string Type, object? Value);
    private sealed record ExternalTypeCatalog(
        string DocId, string Signature, string Kind, ExternalAssemblyIdentity Assembly,
        string MappingStatus, ExternalTypeUsage[] Usages);
    private sealed record ExternalAssemblyIdentity(string Name, string Version, string Culture, string PublicKeyToken);
    private sealed record ExternalTypeUsage(string OwnerDocId, string Position, string Signature);
    private sealed class ExternalTypeBuilder(Type type)
    {
        internal HashSet<ExternalTypeUsage> Usages { get; } = [];

        internal ExternalTypeCatalog Build()
        {
            var identity = type.Assembly.GetName();
            return new ExternalTypeCatalog(
                TypeDocId(type),
                DisplayType(type),
                TypeKind(type),
                new ExternalAssemblyIdentity(
                    identity.Name!,
                    identity.Version?.ToString() ?? "0.0.0.0",
                    string.IsNullOrEmpty(identity.CultureName) ? "neutral" : identity.CultureName,
                    Convert.ToHexString(identity.GetPublicKeyToken() ?? []).ToLowerInvariant()),
                "pending_rust_mapping",
                Usages.OrderBy(usage => usage.OwnerDocId, StringComparer.Ordinal)
                    .ThenBy(usage => usage.Position, StringComparer.Ordinal)
                    .ThenBy(usage => usage.Signature, StringComparer.Ordinal)
                    .ToArray());
        }
    }
    private readonly record struct MetadataCounts(int Types, int Members);
}
