// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// Generates the file `api.txt`, which describes the analyzer public API.
library;

import 'dart:collection';

import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/utilities/extensions/element.dart';
import 'package:analyzer_utilities/package_root.dart' as pkg_root;
import 'package:analyzer_utilities/package_root.dart';
import 'package:analyzer_utilities/tools.dart';
import 'package:collection/collection.dart';
import 'package:path/path.dart';

Future<void> main() async {
  await GeneratedContent.generateAll(analyzerPkgPath, allTargets);
}

/// A list of all targets generated by this code generator.
final List<GeneratedContent> allTargets = [
  GeneratedFile('api.txt', computeApiTxtContents)
];

/// The path to the `analyzer` package.
final String analyzerPkgPath =
    normalize(join(pkg_root.packageRoot, 'analyzer'));

/// Computes what should be written to the `api.txt` file.
Future<String> computeApiTxtContents(String pkgPath) async {
  var provider = PhysicalResourceProvider.INSTANCE;
  var pathContext = provider.pathContext;
  var pkgRootPath = provider.pathContext.normalize(packageRoot);
  var packagePath = pathContext.join(pkgRootPath, 'analyzer');
  var collection = AnalysisContextCollection(
      includedPaths: [packagePath], resourceProvider: provider);
  var context = collection.contexts.single;
  var publicApi = ApiDescription();
  var stringBuffer = StringBuffer();
  printNodes(stringBuffer, await publicApi.build(context));
  return stringBuffer.toString();
}

/// Outputs the contents of [nodes] to [sink], prepending [prefix] to every
/// line.
void printNodes<SortKey extends Comparable<SortKey>>(
    StringSink sink, List<(SortKey, Node)> nodes,
    {String prefix = ''}) {
  for (var entry in nodes.sortedBy((n) => n.$1)) {
    var node = entry.$2;
    sink.writeln('$prefix${node.text.join()}');
    node.printChildren(sink, prefix: '$prefix  ');
  }
}

/// Data structure keeping track of the analyzer API while walking it to produce
/// `api.txt`.
class ApiDescription {
  /// Top level elements that have already had their child elements dumped.
  ///
  /// If an element is seen again in a different library, it will be followed
  /// with `(see above)` (rather than having its child elements dumped twice).
  final _dumpedTopLevelElements = <Element2>{};

  /// Top level elements that have been referenced so far and haven't yet been
  /// processed by [build].
  ///
  /// This is used to ensure that all elements referred to by the public API
  /// (e.g., by being mentioned in the type of an API element) also show up in
  /// the output.
  final _potentiallyDanglingReferences = Queue<Element2>();

  final _uniqueNamer = UniqueNamer();

  /// Cache of values returned by [_getOrComputeImmediateSubinterfaceMap], to
  /// avoid unnecessary recomputation.
  final _immediateSubinterfaceCache =
      <LibraryElement2, Map<ClassElement2, Set<InterfaceElement2>>>{};

  /// Builds a list of [Node] objects representing all the libraries that are
  /// relevant to the analyzer public API.
  ///
  /// This includes libraries that are in the analyzer public API as well as
  /// libraries that are referenced by the analyzer public API (either by being
  /// re-exported as part of the analyzer public API, or by being used as part
  /// of the type of something in the public API).
  ///
  /// Each library node is pared with a [UriSortKey] indicating the order in
  /// which the nodes should be output.
  Future<List<(UriSortKey, Node)>> build(AnalysisContext context) async {
    var nodes = <Uri, Node<MemberSortKey>>{};
    for (var file in context.contextRoot.analyzedFiles().sorted()) {
      if (!file.endsWith('.dart')) continue;
      var fileResult = context.currentSession.getFile(file) as FileResult;
      var uri = fileResult.uri;
      if (fileResult.isLibrary && uri.isInAnalyzerPublicLib) {
        var resolvedLibraryResult = (await context.currentSession
            .getResolvedLibrary(file)) as ResolvedLibraryResult;
        var node = nodes[uri] = Node<MemberSortKey>();
        _dumpLibrary(resolvedLibraryResult.element2, node);
      }
    }
    // Then dump anything referenced by public libraries.
    while (_potentiallyDanglingReferences.isNotEmpty) {
      var element = _potentiallyDanglingReferences.removeFirst();
      if (!_dumpedTopLevelElements.add(element)) continue;
      var containingLibraryUri = element.library2!.uri;
      var childNode = Node<MemberSortKey>()
        ..text.add(_uniqueNamer.name(element));
      _dumpElement(element, childNode);
      (nodes[containingLibraryUri] ??= Node<MemberSortKey>()
            ..text.add('$containingLibraryUri:'))
          .childNodes
          .add((MemberSortKey(element), childNode));
    }
    return [
      for (var entry in nodes.entries) (UriSortKey(entry.key), entry.value)
    ];
  }

  /// Creates a list of objects which, when their string representations are
  /// concatenated, describes [type].
  ///
  /// The reason we use this method rather than [DartType.toString] is to make
  /// sure that (a) every element mentioned by the type is added to
  /// [_potentiallyDanglingReferences], and (b) if an ambiguous name is used,
  /// the ambiguity will be taken care of by [_uniqueNamer].
  List<Object?> _describeType(DartType type) {
    var suffix = switch (type.nullabilitySuffix) {
      NullabilitySuffix.none => '',
      NullabilitySuffix.star => '*',
      NullabilitySuffix.question => '?'
    };
    switch (type) {
      case DynamicType():
        return ['dynamic'];
      case FunctionType(
          :var returnType,
          :var typeParameters,
          :var formalParameters
        ):
        var params = <List<Object?>>[];
        var optionalParams = <List<Object?>>[];
        var namedParams = <String, List<Object?>>{};
        for (var formalParameter in formalParameters) {
          if (formalParameter.isNamed) {
            namedParams[formalParameter.name3!] = [
              if (formalParameter.isRequired) 'required ',
              ..._describeType(formalParameter.type)
            ];
          } else if (formalParameter.isOptional) {
            optionalParams.add(_describeType(formalParameter.type));
          } else {
            params.add(_describeType(formalParameter.type));
          }
        }
        if (optionalParams.isNotEmpty) {
          params.add(optionalParams.separate(prefix: '[', suffix: ']'));
        }
        if (namedParams.isNotEmpty) {
          params.add(namedParams.entries
              .sortedBy((e) => e.key)
              .map((e) => [...e.value, ' ${e.key}'])
              .separate(prefix: '{', suffix: '}'));
        }
        return <Object?>[
          ..._describeType(returnType),
          ' Function',
          if (typeParameters.isNotEmpty)
            ...typeParameters
                .map(_describeTypeParameter)
                .separate(prefix: '<', suffix: '>'),
          '(',
          ...params.separate(),
          ')',
          suffix
        ];
      case InterfaceType(:var element3, :var typeArguments):
        _potentiallyDanglingReferences.addLast(element3);
        return [
          _uniqueNamer.name(element3),
          if (typeArguments.isNotEmpty)
            ...typeArguments
                .map(_describeType)
                .separate(prefix: '<', suffix: '>'),
          suffix
        ];
      case RecordType(:var positionalFields, :var namedFields):
        if (positionalFields.length == 1 && namedFields.isEmpty) {
          return [
            '(',
            ..._describeType(positionalFields[0].type),
            ',)',
            suffix
          ];
        }
        return [
          ...[
            for (var positionalField in positionalFields)
              _describeType(positionalField.type),
            if (namedFields.isNotEmpty)
              namedFields
                  .sortedBy((f) => f.name)
                  .map((f) => [..._describeType(f.type), ' ', f.name])
                  .separate(prefix: '{', suffix: '}')
          ].separate(prefix: '(', suffix: ')'),
          suffix
        ];
      case TypeParameterType(:var element3):
        return [element3.name3!, suffix];
      case VoidType():
        return ['void'];
      case dynamic(:var runtimeType):
        throw UnimplementedError('Unexpected type: $runtimeType');
    }
  }

  /// Creates a list of objects which, when their string representations are
  /// concatenated, describes [typeParameter].
  List<Object?> _describeTypeParameter(TypeParameterElement2 typeParameter) {
    return [
      typeParameter.name3!,
      if (typeParameter.bound case var bound?) ...[
        ' extends ',
        ..._describeType(bound)
      ]
    ];
  }

  /// Appends information to [node] describing [element].
  void _dumpElement(Element2 element, Node<MemberSortKey> node) {
    var enclosingElement = element.enclosingElement2;
    if (enclosingElement is LibraryElement2 && !element.isInAnalyzerPublicApi) {
      if (!enclosingElement.uri.isInAnalyzer) {
        node.text.add(' (referenced)');
      } else {
        node.text.add(' (non-public)');
      }
      return;
    }
    var parentheticals = <List<Object?>>[];
    switch (element) {
      case InstanceElement2():
        switch (element) {
          case InterfaceElement2(
              :var typeParameters2,
              :var supertype,
              :var interfaces
            ):
            List<Object?> instanceDescription = [
              switch (element) {
                ClassElement2() => 'class',
                EnumElement2() => 'enum',
                MixinElement2() => 'mixin',
                dynamic(:var runtimeType) => 'TODO: $runtimeType',
              }
            ];
            if (typeParameters2.isNotEmpty) {
              instanceDescription.addAll(typeParameters2
                  .map(_describeTypeParameter)
                  .separate(prefix: '<', suffix: '>'));
            }
            if (element is! EnumElement2 && supertype != null) {
              instanceDescription
                  .addAll([' extends ', ..._describeType(supertype)]);
            }
            if (element is MixinElement2 &&
                element.superclassConstraints.isNotEmpty) {
              instanceDescription.addAll(element.superclassConstraints
                  .map(_describeType)
                  .separate(prefix: ' on '));
            }
            if (interfaces.isNotEmpty) {
              instanceDescription.addAll(interfaces
                  .map(_describeType)
                  .separate(prefix: ' implements '));
            }
            parentheticals.add(instanceDescription);
            if (element is ClassElement2 && element.isSealed) {
              var parenthetical = <Object>['sealed'];
              parentheticals.add(parenthetical);
              if (_getOrComputeImmediateSubinterfaceMap(
                      element.library2)[element]
                  case var subinterfaces?) {
                parenthetical.add(' (immediate subtypes: ');
                // Note: it's tempting to just do
                // `subinterfaces.map(_uniqueNamer.name).join(', ')`, but that
                // won't work, because the names returned by
                // `UniqueName.toString()` aren't finalized until we've visited
                // the entire API and seen if there are class names that need to
                // be disambiguated. So we accumulate the `UniqueName` objects
                // into the `parenthetical` list and rely on `printNodes`
                // converting everything to a string when the final API
                // description is being output.
                var commaNeeded = false;
                for (var subinterface in subinterfaces) {
                  if (commaNeeded) {
                    parenthetical.add(', ');
                  } else {
                    commaNeeded = true;
                  }
                  parenthetical.add(_uniqueNamer.name(subinterface));
                }
                parenthetical.add(')');
              }
            }
          case ExtensionElement2(:var extendedType):
            parentheticals
                .add(['extension on ', ..._describeType(extendedType)]);
          case dynamic(:var runtimeType):
            throw UnimplementedError('Unexpected element: $runtimeType');
        }
        for (var member in element.children2.sortedBy((m) => m.name3 ?? '')) {
          if (member.name3 case var name? when name.startsWith('_')) {
            // Ignore private members
            continue;
          }
          if (member is FieldElement2) {
            // Ignore fields; we care about the getters and setters they induce.
            continue;
          }
          if (member is ConstructorElement2 &&
              element is ClassElement2 &&
              element.isAbstract &&
              (element.isFinal || element.isInterface || element.isSealed)) {
            // The class can't be constructed from outside of the library that
            // declares it, so its constructors aren't part of the public API.
            continue;
          }
          if (member is ConstructorElement2 && element is EnumElement2) {
            // Enum constructors can't be called from outside the enum itself,
            // so they aren't part of the public API.
            continue;
          }
          var childNode = Node<MemberSortKey>();
          childNode.text.add(member.apiName);
          _dumpElement(member, childNode);
          node.childNodes.add((MemberSortKey(member), childNode));
        }
      case TopLevelFunctionElement(:var type):
        parentheticals.add(['function: ', ..._describeType(type)]);
      case ExecutableElement2(:var isStatic):
        String maybeStatic = isStatic ? 'static ' : '';
        switch (element) {
          case GetterElement(:var type):
            parentheticals.add(
                ['${maybeStatic}getter: ', ..._describeType(type.returnType)]);
          case SetterElement(:var type):
            parentheticals.add([
              '${maybeStatic}setter: ',
              ..._describeType(type.formalParameters.single.type)
            ]);
          case MethodElement2(:var type):
            parentheticals
                .add(['${maybeStatic}method: ', ..._describeType(type)]);
          case ConstructorElement2(:var type):
            parentheticals.add(['constructor: ', ..._describeType(type)]);
          case dynamic(:var runtimeType):
            throw UnimplementedError('Unexpected element: $runtimeType');
        }
      case dynamic(:var runtimeType):
        throw UnimplementedError('Unexpected element: $runtimeType');
    }

    if (element.metadata.any(_isDeprecated)) {
      parentheticals.add(['deprecated']);
    }
    if (element.metadata.any(_isExperimental)) {
      parentheticals.add(['experimental']);
    }

    if (parentheticals.isNotEmpty) {
      node.text.addAll(parentheticals.separate(prefix: ' (', suffix: ')'));
    }
    if (node.childNodes.isNotEmpty) {
      node.text.add(':');
    }
  }

  /// Appends information to [node] describing [element].
  void _dumpLibrary(LibraryElement2 library, Node<MemberSortKey> node) {
    var uri = library.uri;
    node.text.addAll([uri, ':']);
    var definedNames = library.exportNamespace.definedNames2;
    for (var key in definedNames.keys.sorted()) {
      var element = definedNames[key]!;
      var childNode = Node<MemberSortKey>()
        ..text.add(_uniqueNamer.name(element));
      if (!_dumpedTopLevelElements.add(element)) {
        childNode.text.add(' (see above)');
      } else {
        _dumpElement(element, childNode);
      }
      node.childNodes.add((MemberSortKey(element), childNode));
    }
  }

  /// Returns a map from each sealed class in [library] to the set of its
  /// immediate sub-interfaces.
  ///
  /// If this method has been called before with the same [library], a cached
  /// map is returned from [_immediateSubinterfaceCache]. Otherwise a fresh map
  /// is computed.
  Map<ClassElement2, Set<InterfaceElement2>>
      _getOrComputeImmediateSubinterfaceMap(LibraryElement2 library) {
    if (_immediateSubinterfaceCache[library] case var m?) return m;
    var result = <ClassElement2, Set<InterfaceElement2>>{};
    for (var interface in [
      ...library.classes,
      ...library.mixins,
      ...library.enums,
      ...library.extensionTypes
    ]..sortBy((e) => e.name3!)) {
      for (var superinterface in [
        interface.supertype,
        ...interface.interfaces,
        ...interface.mixins,
        if (interface is MixinElement2) ...interface.superclassConstraints
      ]) {
        if (superinterface == null) continue;
        var superinterfaceElement = superinterface.element3;
        if (superinterfaceElement is ClassElement2 &&
            superinterfaceElement.isSealed) {
          (result[superinterfaceElement] ??= {}).add(interface);
        }
      }
    }
    _immediateSubinterfaceCache[library] = result;
    return result;
  }

  bool _isDeprecated(ElementAnnotation annotation) {
    if (annotation.computeConstantValue()!.type
        case InterfaceType(:var element3) when element3.name3 == 'Deprecated') {
      return true;
    } else {
      return false;
    }
  }

  bool _isExperimental(ElementAnnotation annotation) {
    if (annotation.computeConstantValue()!.type
        case InterfaceType(:var element3)
        when element3.name3 == '_Experimental') {
      return true;
    } else {
      return false;
    }
  }
}

/// Element categorization used by [MemberSortKey].
enum MemberCategory {
  constructor,
  propertyAccessor,
  topLevelFunctionOrMethod,
  interface,
  extension
}

/// Sort key used to sort elements in the output.
class MemberSortKey implements Comparable<MemberSortKey> {
  final bool _isInstanceMember;
  final MemberCategory _category;
  final String _name;

  MemberSortKey(Element2 element)
      : _isInstanceMember = _computeIsInstanceMember(element),
        _category = _computeCategory(element),
        _name = element.name3!;

  @override
  int compareTo(MemberSortKey other) {
    if ((_isInstanceMember ? 1 : 0).compareTo(other._isInstanceMember ? 1 : 0)
        case var value when value != 0) {
      return value;
    }
    if (_category.index.compareTo(other._category.index) case var value
        when value != 0) {
      return value;
    }
    return _name.compareTo(other._name);
  }

  static MemberCategory _computeCategory(Element2 element) => switch (element) {
        ConstructorElement2() => MemberCategory.constructor,
        PropertyAccessorElement2() => MemberCategory.propertyAccessor,
        TopLevelFunctionElement() => MemberCategory.topLevelFunctionOrMethod,
        MethodElement2() => MemberCategory.topLevelFunctionOrMethod,
        InterfaceElement2() => MemberCategory.interface,
        ExtensionElement2() => MemberCategory.extension,
        dynamic(:var runtimeType) =>
          throw UnimplementedError('Unexpected element: $runtimeType')
      };

  static bool _computeIsInstanceMember(Element2 element) =>
      element.enclosingElement2 is InstanceElement2 &&
      switch (element) {
        ExecutableElement2(:var isStatic) => !isStatic,
        dynamic(:var runtimeType) =>
          throw UnimplementedError('Unexpected element: $runtimeType')
      };
}

/// A node to be printed to the output.
class Node<ChildSortKey extends Comparable<ChildSortKey>> {
  /// A list of objects which, when their string representations are
  /// concatenated, is the text that should be displayed on the first line of
  /// the node.
  ///
  /// The reason this is a list rather than a single string is to allow elements
  /// of the list to be [UniqueName] objects, which may acquire a disambiguation
  /// suffix at a later time.
  final text = <Object?>[];

  /// A list of child nodes, paired with a sort key indicating the order in
  /// which they should be output.
  final childNodes = <(ChildSortKey, Node)>[];

  /// Outputs [childNodes], prepending [prefix] to every line.
  void printChildren(StringSink sink, {required String prefix}) {
    printNodes(sink, childNodes, prefix: prefix);
  }
}

/// Object that will have a unique string representation within the context of a
/// given [UniqueNamer] instance.
///
/// If two or more [UniqueName] objects are constructed with reference to the
/// same [UniqueNamer], and they have the same [_nameHint], then all such
/// objects' [toString] methods will append a unique suffix of the form
/// `@INTEGER`, so that the resulting strings are unique.
class UniqueName {
  /// The name that will be returned by [toString] if no disambiguation is
  /// needed.
  final String _nameHint;

  /// If not `Null`, the integer that [toString] will use to disambiguate this
  /// [UniqueName] from other ones with the same [_nameHint].
  int? _disambiguator;

  UniqueName(UniqueNamer uniqueNamer, this._nameHint) {
    // The uniqueness guarantee depends on `_nameHint` not containing an `@`.
    assert(!_nameHint.contains('@'));
    var conflicts = uniqueNamer._conflicts[_nameHint] ??= [];
    if (conflicts.length == 1) {
      conflicts[0]._disambiguator = 1;
    }
    conflicts.add(this);
    if (conflicts.length > 1) {
      _disambiguator = conflicts.length;
    }
  }

  @override
  String toString() => [
        _nameHint,
        if (_disambiguator case var disambiguator?) '@$disambiguator'
      ].join();
}

/// Manager of unique names for elements.
class UniqueNamer {
  final _names = <Element2, UniqueName>{};
  final _conflicts = <String, List<UniqueName>>{};

  /// Returns a [UniqueName] object whose [toString] method will produce a
  /// unique name for [element].
  UniqueName name(Element2 element) =>
      _names[element] ??= UniqueName(this, element.apiName);
}

/// URI categorization used by [UriSortKey].
enum UriCategory { inAnalyzer, notInAnalyzer }

/// Sort key used to sort libraries in the output.
///
/// Libraries in `package:analyzer` will be output first (sorted by URI),
/// followed by libraries not in `package:analyzer`.
class UriSortKey implements Comparable<UriSortKey> {
  final UriCategory _category;
  final String _uriString;

  UriSortKey(Uri uri)
      : _category = uri.isInAnalyzer
            ? UriCategory.inAnalyzer
            : UriCategory.notInAnalyzer,
        _uriString = uri.toString();

  @override
  int compareTo(UriSortKey other) {
    if (_category.index.compareTo(other._category.index) case var value
        when value != 0) {
      return value;
    }
    return _uriString.compareTo(other._uriString);
  }
}

extension on Iterable<Iterable<Object?>> {
  /// Forms a list containing [prefix], followed by the elements of `this`
  /// (separated by [separator]), followed by [suffix].
  List<Object?> separate(
      {String separator = ', ', String prefix = '', String suffix = ''}) {
    var result = <Object?>[prefix];
    var first = true;
    for (var item in this) {
      if (first) {
        first = false;
      } else {
        result.add(separator);
      }
      result.addAll(item);
    }
    result.add(suffix);
    return result;
  }
}

extension on Element2 {
  /// Returns the appropriate name for describing the element in `api.txt`.
  ///
  /// The name is the same as [name3], but with `=` appended for setters.
  String get apiName {
    var apiName = name3!;
    if (this is SetterElement) {
      apiName += '=';
    }
    return apiName;
  }

  bool get isInAnalyzerPublicApi {
    if (this
        case PropertyAccessorElement2(
          isSynthetic: true,
          :var variable3?,
        ) when variable3.isInAnalyzerPublicApi) {
      return true;
    }
    if (this
        case Annotatable(
          metadata2: Metadata(:var annotations),
        ) when annotations.any(_isPublicApiAnnotation)) {
      return true;
    }
    if (name3 case var name? when !name.isPublic) return false;
    if (library2!.uri.isInAnalyzerPublicLib) return true;
    return false;
  }

  bool _isPublicApiAnnotation(ElementAnnotation annotation) {
    if (annotation.computeConstantValue()
        case DartObject(
          type: InterfaceType(
            element3: InterfaceElement2(name3: 'AnalyzerPublicApi'),
          ),
        )) {
      return true;
    } else {
      return false;
    }
  }
}

extension on String {
  bool get isPublic => !startsWith('_');
}

extension on Uri {
  bool get isInAnalyzer =>
      scheme == 'package' &&
      switch (pathSegments) {
        ['analyzer', ...] => true,
        _ => false,
      };

  bool get isInAnalyzerPublicLib =>
      scheme == 'package' &&
      switch (pathSegments) {
        ['analyzer', 'src', ...] => false,
        ['analyzer', ...] => true,
        _ => false,
      };
}
