// Copyright (c) 2021, 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.

import 'dart:async';

import 'package:_macros/src/api.dart';

/// A macro for testing diagnostics reporting, including error handling.
class DiagnosticMacro implements ClassTypesMacro {
  @override
  FutureOr<void> buildTypesForClass(
      ClassDeclaration clazz, ClassTypeBuilder builder) {
    builder.report(Diagnostic(
        DiagnosticMessage('superclass',
            target: clazz.superclass!.asDiagnosticTarget),
        Severity.info,
        contextMessages: [
          DiagnosticMessage(
            'interface',
            target: clazz.interfaces.single.asDiagnosticTarget,
          ),
        ],
        correctionMessage: 'correct me!'));

    // Test general error handling also
    throw 'I threw an error!';
  }
}

/// A very simple macro that augments any declaration it is given, usually
/// adding print statements and inlining values from the declaration object
/// for comparison with expected values in tests.
///
/// When applied to [MethodDeclaration]s there is some extra work that happens
/// to validate the introspection APIs work as expected.
class SimpleMacro
    implements
        ClassTypesMacro,
        ClassDeclarationsMacro,
        ClassDefinitionMacro,
        ConstructorTypesMacro,
        ConstructorDeclarationsMacro,
        ConstructorDefinitionMacro,
        EnumTypesMacro,
        EnumDeclarationsMacro,
        EnumDefinitionMacro,
        EnumValueTypesMacro,
        EnumValueDeclarationsMacro,
        EnumValueDefinitionMacro,
        ExtensionTypesMacro,
        ExtensionDeclarationsMacro,
        ExtensionDefinitionMacro,
        ExtensionTypeTypesMacro,
        ExtensionTypeDeclarationsMacro,
        ExtensionTypeDefinitionMacro,
        FieldTypesMacro,
        FieldDeclarationsMacro,
        FieldDefinitionMacro,
        FunctionTypesMacro,
        FunctionDeclarationsMacro,
        FunctionDefinitionMacro,
        LibraryTypesMacro,
        LibraryDeclarationsMacro,
        LibraryDefinitionMacro,
        MethodTypesMacro,
        MethodDeclarationsMacro,
        MethodDefinitionMacro,
        MixinTypesMacro,
        MixinDeclarationsMacro,
        MixinDefinitionMacro,
        TypeAliasTypesMacro,
        TypeAliasDeclarationsMacro,
        VariableTypesMacro,
        VariableDeclarationsMacro,
        VariableDefinitionMacro {
  final bool? myBool;
  final int? myInt;
  final double? myDouble;
  final Set? mySet;
  final List? myList;
  final Map? myMap;
  final String? myString;

  SimpleMacro([this.myInt])
      : myBool = null,
        myDouble = null,
        mySet = null,
        myList = null,
        myMap = null,
        myString = null;

  SimpleMacro.named(
      {required this.myBool,
      required this.myDouble,
      required this.myInt,
      required this.mySet,
      required this.myList,
      required this.myMap,
      required this.myString});

  @override
  FutureOr<void> buildDeclarationsForClass(
      ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
    var fields = await builder.fieldsOf(clazz);
    builder.declareInType(DeclarationCode.fromParts([
      'static const List<String> fieldNames = [',
      for (var field in fields) "'${field.identifier.name}',",
      '];',
    ]));
  }

  @override
  FutureOr<void> buildDeclarationsForConstructor(
      ConstructorDeclaration constructor, MemberDeclarationBuilder builder) {
    var className = constructor.definingType.name;
    var constructorName = constructor.identifier.name;
    builder.declareInType(DeclarationCode.fromString(
        'factory $className.${constructorName}Delegate() => '
        '$className.$constructorName();'));
  }

  @override
  FutureOr<void> buildDeclarationsForEnum(
      EnumDeclaration enuum, EnumDeclarationBuilder builder) async {
    var values = await builder.valuesOf(enuum);
    builder.declareInType(DeclarationCode.fromParts([
      'static const List<String> valuesByName = {',
      for (var value in values) ...[
        "'${value.identifier.name}': ",
        value.identifier
      ],
      '};',
    ]));
  }

  @override
  FutureOr<void> buildDeclarationsForEnumValue(
      EnumValueDeclaration value, EnumDeclarationBuilder builder) async {
    final parent = await builder.typeDeclarationOf(value.definingEnum);
    builder.declareInType(DeclarationCode.fromParts([
      parent.identifier,
      ' ${value.identifier.name}ToString() => ',
      value.identifier,
      '.toString();',
    ]));
  }

  @override
  FutureOr<void> buildDeclarationsForFunction(
      FunctionDeclaration function, DeclarationBuilder builder) {
    var functionName = function.identifier.name;
    builder.declareInLibrary(DeclarationCode.fromParts([
      function.returnType.code,
      if (function.isGetter) ' get' else if (function.isSetter) ' set ',
      ' delegate${functionName.capitalize()}',
      if (!function.isGetter) ...[
        '(',
        if (function.isSetter) ...[
          function.positionalParameters.first.type.code,
          ' value',
        ],
        ')',
      ],
      ' => $functionName',
      function.isGetter
          ? ''
          : function.isSetter
              ? ' = value'
              : '()',
      ';',
    ]));
  }

  @override
  FutureOr<void> buildDeclarationsForMethod(
      MethodDeclaration method, MemberDeclarationBuilder builder) {
    if (method.positionalParameters.isNotEmpty ||
        method.namedParameters.isNotEmpty) {
      throw UnsupportedError('Can only run on method with no parameters!');
    }
    var methodName = method.identifier.name;
    builder.declareInLibrary(DeclarationCode.fromParts([
      method.returnType.code,
      ' delegateMember${methodName.capitalize()}() => $methodName();',
    ]));
  }

  @override
  FutureOr<void> buildDeclarationsForMixin(
      MixinDeclaration mixin, MemberDeclarationBuilder builder) async {
    var methods = await builder.methodsOf(mixin);
    builder.declareInType(DeclarationCode.fromParts([
      'static const List<String> methodNames = [',
      for (var method in methods) "'${method.identifier.name}',",
      '];',
    ]));
  }

  @override
  FutureOr<void> buildDeclarationsForVariable(
      VariableDeclaration variable, DeclarationBuilder builder) {
    var variableName = variable.identifier.name;
    builder.declareInLibrary(DeclarationCode.fromParts([
      variable.type.code,
      ' get delegate${variableName.capitalize()} => $variableName;',
    ]));
  }

  @override
  FutureOr<void> buildDeclarationsForField(
      FieldDeclaration field, MemberDeclarationBuilder builder) {
    var fieldName = field.identifier.name;
    builder.declareInType(DeclarationCode.fromParts([
      field.type.code,
      ' get delegate${fieldName.capitalize()} => $fieldName;',
    ]));
  }

  @override
  Future<void> buildDefinitionForClass(
      ClassDeclaration clazz, TypeDefinitionBuilder builder) async {
    // Apply ourself to all our members
    var fields = (await builder.fieldsOf(clazz));
    for (var field in fields) {
      await buildDefinitionForField(
          field, await builder.buildField(field.identifier));
    }
    var methods = (await builder.methodsOf(clazz));
    for (var method in methods) {
      await buildDefinitionForMethod(
          method, await builder.buildMethod(method.identifier));
    }
    var constructors = (await builder.constructorsOf(clazz));
    for (var constructor in constructors) {
      await buildDefinitionForConstructor(
          constructor, await builder.buildConstructor(constructor.identifier));
    }
  }

  @override
  Future<void> buildDefinitionForConstructor(ConstructorDeclaration constructor,
      ConstructorDefinitionBuilder builder) async {
    var clazz = await builder.declarationOf(constructor.definingType)
        as TypeDeclaration;
    var fields = (await builder.fieldsOf(clazz));

    builder.augment(
      body: await _buildFunctionAugmentation(constructor, builder),
      initializers: [
        for (var field in fields)
          // TODO: Compare against actual `int` type.
          if (field.hasFinal &&
              (field.type as NamedTypeAnnotation).identifier.name == 'int')
            RawCode.fromParts([field.identifier, ' = ${myInt!}']),
      ],
    );
  }

  @override
  Future<void> buildDefinitionForEnum(
      EnumDeclaration enuum, EnumDefinitionBuilder builder) async {
    // Apply ourself to all our members
    var values = (await builder.valuesOf(enuum));
    for (var value in values) {
      await buildDefinitionForEnumValue(
          value, await builder.buildEnumValue(value.identifier));
    }
    var fields = (await builder.fieldsOf(enuum));
    for (var field in fields) {
      await buildDefinitionForField(
          field, await builder.buildField(field.identifier));
    }
    var methods = (await builder.methodsOf(enuum));
    for (var method in methods) {
      await buildDefinitionForMethod(
          method, await builder.buildMethod(method.identifier));
    }
    var constructors = (await builder.constructorsOf(enuum));
    for (var constructor in constructors) {
      await buildDefinitionForConstructor(
          constructor, await builder.buildConstructor(constructor.identifier));
    }
  }

  @override
  FutureOr<void> buildDefinitionForEnumValue(
      EnumValueDeclaration value, EnumValueDefinitionBuilder builder) async {
    final parent =
        await builder.typeDeclarationOf(value.definingEnum) as EnumDeclaration;
    final constructor = (await builder.constructorsOf(parent)).first;
    final parts = [
      value.identifier,
      '(',
    ];
    final stringType = await builder.resolve(NamedTypeAnnotationCode(
        name:
            // ignore: deprecated_member_use_from_same_package
            await builder.resolveIdentifier(Uri.parse('dart:core'), 'String')));
    for (var positional in constructor.positionalParameters) {
      final resolvedType = await builder.resolve(positional.type.code);
      if (!(await resolvedType.isExactly(stringType))) {
        throw StateError('Expected only string parameters');
      }
      parts.add("'${positional.identifier.name}', ");
    }
    for (var named in constructor.namedParameters) {
      final resolvedType = await builder.resolve(named.type.code);
      if (!(await resolvedType.isExactly(stringType))) {
        throw StateError('Expected only string parameters');
      }
      parts.add("${named.identifier.name}: '${named.identifier.name}', ");
    }
    parts.add('),');
    builder.augment(DeclarationCode.fromParts(parts));
  }

  @override
  Future<void> buildDefinitionForField(
          FieldDeclaration field, VariableDefinitionBuilder builder) async =>
      buildDefinitionForVariable(field, builder);

  @override
  Future<void> buildDefinitionForFunction(
      FunctionDeclaration function, FunctionDefinitionBuilder builder) async {
    builder.augment(await _buildFunctionAugmentation(function, builder),
        docComments: CommentCode.fromString('// A comment!'));
  }

  @override
  Future<void> buildDefinitionForMethod(
      MethodDeclaration method, FunctionDefinitionBuilder builder) async {
    await buildDefinitionForFunction(method, builder);

    // Test the type declaration resolver
    var parentClass = await builder.typeDeclarationOf(method.definingType);
    // Should be able to find ourself in the methods of the parent class.
    (await builder.methodsOf(parentClass))
        .singleWhere((m) => m.identifier == method.identifier);

    TypeDeclaration? superClass;
    final interfaces = <TypeDeclaration>[];
    final mixins = <TypeDeclaration>[];
    final superclassConstraints = <TypeDeclaration>[];
    // Test the class introspector
    if (parentClass is ClassDeclaration) {
      superClass =
          (await builder.typeDeclarationOf(parentClass.superclass!.identifier));
      interfaces.addAll(await Future.wait(parentClass.interfaces.map(
          (interface) => builder.typeDeclarationOf(interface.identifier))));
      mixins.addAll(await Future.wait(parentClass.mixins
          .map((mixins) => builder.typeDeclarationOf(mixins.identifier))));
    } else if (parentClass is MixinDeclaration) {
      superclassConstraints.addAll(await Future.wait(
          parentClass.superclassConstraints.map(
              (interface) => builder.typeDeclarationOf(interface.identifier))));
      interfaces.addAll(await Future.wait(parentClass.interfaces.map(
          (interface) => builder.typeDeclarationOf(interface.identifier))));
    } else if (parentClass is EnumDeclaration) {
      interfaces.addAll(await Future.wait(parentClass.interfaces.map(
          (interface) => builder.typeDeclarationOf(interface.identifier))));
      mixins.addAll(await Future.wait(parentClass.mixins
          .map((mixins) => builder.typeDeclarationOf(mixins.identifier))));
    }
    var fields = (await builder.fieldsOf(parentClass));
    var methods = (await builder.methodsOf(parentClass));
    var constructors = (await builder.constructorsOf(parentClass));

    // Test the type resolver and static type interfaces
    var methodReturnType = method.returnType as RecordTypeAnnotation;
    var staticReturnType = await builder
        .resolve(methodReturnType.positionalFields.first.type.code);
    if (!(await staticReturnType.isExactly(staticReturnType))) {
      throw StateError('The return type should be exactly equal to itself!');
    }
    if (!(await staticReturnType.isSubtypeOf(staticReturnType))) {
      throw StateError('The return type should be a subtype of itself!');
    }

    // TODO: Use `builder.instantiateCode` instead once implemented.
    if (constructors.isNotEmpty) {
      var classType = await builder.resolve(constructors.first.returnType.code);
      if (await staticReturnType.isExactly(classType)) {
        throw StateError(
            'The return type should not be exactly equal to the class type');
      }
      if (await staticReturnType.isSubtypeOf(classType)) {
        throw StateError(
            'The return type should not be a subtype of the class type!');
      }
    }

    builder.augment(FunctionBodyCode.fromParts([
      '''{
      print('myBool: $myBool');
      print('myDouble: $myDouble');
      print('myInt: $myInt');
      print('myList: $myList');
      print('mySet: $mySet');
      print('myMap: $myMap');
      print('myString: $myString');
      print('parentClass: ${parentClass.identifier.name}');
      print('superClass: ${superClass?.identifier.name}');''',
      for (var interface in interfaces)
        "\n      print('interface: ${interface.identifier.name}');",
      for (var mixin in mixins)
        "\n      print('mixin: ${mixin.identifier.name}');",
      for (var field in fields)
        "\n      print('field: ${field.identifier.name}');",
      for (var method in methods)
        "\n      print('method: ${method.identifier.name}');",
      for (var constructor in constructors)
        "\n      print('constructor: ${constructor.identifier.name}');",
      '''
\n      return augmented();
    }''',
    ]));
  }

  @override
  Future<void> buildDefinitionForMixin(
      MixinDeclaration mixin, TypeDefinitionBuilder builder) async {
    // Apply ourself to all our members
    var fields = (await builder.fieldsOf(mixin));
    for (var field in fields) {
      await buildDefinitionForField(
          field, await builder.buildField(field.identifier));
    }
    var methods = (await builder.methodsOf(mixin));
    for (var method in methods) {
      await buildDefinitionForMethod(
          method, await builder.buildMethod(method.identifier));
    }
  }

  @override
  Future<void> buildDefinitionForVariable(
      VariableDeclaration variable, VariableDefinitionBuilder builder) async {
    var definingClass =
        variable is FieldDeclaration ? variable.definingType.name : '';
    builder.augment(
      getter: DeclarationCode.fromParts([
        variable.type.code,
        ' get ',
        variable.identifier.name,
        ''' {
          print('parentClass: $definingClass');
        ''',
        if (variable is FieldDeclaration)
          "print('isAbstract: ${variable.hasAbstract}');\n",
        '''print('isExternal: ${variable.hasExternal}');
          print('isFinal: ${variable.hasFinal}');
          print('isLate: ${variable.hasLate}');
          return augmented;
        }''',
      ]),
      setter: DeclarationCode.fromParts([
        'set ',
        variable.identifier.name,
        '(',
        variable.type.code,
        ' value) { augmented = value; }'
      ]),
      initializer: ExpressionCode.fromString("'new initial value' + augmented"),
    );
  }

  @override
  FutureOr<void> buildTypesForClass(
      ClassDeclaration clazz, ClassTypeBuilder builder) async {
    List<Object> buildTypeParam(
        TypeParameterDeclaration typeParam, bool isFirst) {
      return [
        if (!isFirst) ', ',
        typeParam.identifier.name,
        if (typeParam.bound != null) ...[
          ' extends ',
          typeParam.bound!.code,
        ]
      ];
    }

    var name = '${clazz.identifier.name}Builder';
    builder.declareType(
        name,
        DeclarationCode.fromParts([
          'class $name',
          if (clazz.typeParameters.isNotEmpty) ...[
            '<',
            ...buildTypeParam(clazz.typeParameters.first, true),
            for (var typeParam in clazz.typeParameters.skip(1))
              ...buildTypeParam(typeParam, false),
            '>',
          ],
          ' implements Builder<',
          clazz.identifier,
          if (clazz.typeParameters.isNotEmpty) ...[
            '<',
            clazz.typeParameters.first.identifier.name,
            for (var typeParam in clazz.typeParameters)
              ', ${typeParam.identifier.name}',
            '>',
          ],
          '> {}'
        ]));

    final interfaceName = 'HasX';
    builder.declareType(interfaceName, DeclarationCode.fromString('''
abstract interface class $interfaceName {
  int get x;
}'''));

    final mixinName = 'GetX';
    builder.declareType(mixinName, DeclarationCode.fromString('''
mixin $mixinName implements $interfaceName {
  int get x => 1;
}'''));

    // ignore: deprecated_member_use_from_same_package
    final mySuperClass = await builder.resolveIdentifier(
        Uri.parse('package:foo/bar.dart'), 'MySuperclass');
    builder.extendsType(NamedTypeAnnotationCode(name: mySuperClass));
    builder.appendInterfaces([RawTypeAnnotationCode.fromString(interfaceName)]);
    builder.appendMixins([RawTypeAnnotationCode.fromString(mixinName)]);
  }

  @override
  FutureOr<void> buildTypesForConstructor(
      ConstructorDeclaration constructor, TypeBuilder builder) {
    var name = 'GeneratedBy${constructor.identifier.name.capitalize()}';
    builder.declareType(name, DeclarationCode.fromString('class $name {}'));
  }

  @override
  FutureOr<void> buildTypesForEnum(EnumDeclaration enuum, TypeBuilder builder) {
    final name = 'GeneratedBy${enuum.identifier.name.capitalize()}';
    builder.declareType(name, DeclarationCode.fromString('class $name {}'));
  }

  @override
  FutureOr<void> buildTypesForEnumValue(
      EnumValueDeclaration value, TypeBuilder builder) {
    final name = 'GeneratedBy${value.definingEnum.name}_'
        '${value.identifier.name.capitalize()}';
    builder.declareType(name, DeclarationCode.fromString('class $name {}'));
  }

  @override
  FutureOr<void> buildTypesForField(
      FieldDeclaration field, TypeBuilder builder) {
    var name = 'GeneratedBy${field.identifier.name.capitalize()}';
    builder.declareType(name, DeclarationCode.fromString('class $name {}'));
  }

  @override
  FutureOr<void> buildTypesForFunction(
      FunctionDeclaration function, TypeBuilder builder) {
    var suffix = function.isGetter
        ? 'Getter'
        : function.isSetter
            ? 'Setter'
            : '';
    var name = 'GeneratedBy${function.identifier.name.capitalize()}$suffix';
    builder.declareType(name, DeclarationCode.fromString('class $name {}'));
  }

  @override
  FutureOr<void> buildTypesForMethod(
      MethodDeclaration method, TypeBuilder builder) {
    var name = 'GeneratedBy${method.identifier.name.capitalize()}';
    builder.declareType(name, DeclarationCode.fromString('class $name {}'));
  }

  @override
  FutureOr<void> buildTypesForMixin(
      MixinDeclaration mixin, TypeBuilder builder) {
    final onNames = mixin.superclassConstraints
        .map((type) => type.identifier.name.capitalize())
        .join('');
    final name = 'GeneratedBy${mixin.identifier.name.capitalize()}On$onNames';
    builder.declareType(name, DeclarationCode.fromString('class $name {}'));
  }

  @override
  FutureOr<void> buildTypesForVariable(
      VariableDeclaration variable, TypeBuilder builder) {
    var name = 'GeneratedBy${variable.identifier.name.capitalize()}';
    builder.declareType(name, DeclarationCode.fromString('class $name {}'));
  }

  @override
  void buildDeclarationsForLibrary(
      Library library, DeclarationBuilder builder) {
    builder.declareInLibrary(
        DeclarationCode.fromString("final LibraryInfo library;"));
  }

  @override
  Future<void> buildDefinitionForLibrary(
      Library library, LibraryDefinitionBuilder builder) async {
    var languageVersion = library.languageVersion;
    var allDeclarations = await builder.topLevelDeclarationsOf(library);
    var variableDeclaration =
        allDeclarations.singleWhere((d) => d.identifier.name == 'library');
    var variableBuilder =
        await builder.buildVariable(variableDeclaration.identifier);
    variableBuilder.augment(
        initializer: ExpressionCode.fromParts([
      'LibraryInfo(',
      "Uri.parse('${library.uri}'), ",
      "'${languageVersion.major}.${languageVersion.minor}', ",
      "[",
      for (var type in allDeclarations)
        if (type is TypeDeclaration) ...[type.identifier, ', '],
      "])",
    ]));
  }

  @override
  void buildTypesForLibrary(Library library, TypeBuilder builder) {
    builder.declareType('LibraryInfo', DeclarationCode.fromString('''
class LibraryInfo {
  final Uri uri;
  final String languageVersion;
  final List<Type> definedTypes;
  const LibraryInfo(this.uri, this.languageVersion, this.definedTypes);
}'''));
  }

  @override
  FutureOr<void> buildTypesForExtension(
      ExtensionDeclaration extension, TypeBuilder builder) {
    final onType = extension.onType as NamedTypeAnnotation;
    final name = '${extension.identifier.name}On${onType.identifier.name}';
    builder.declareType(name, DeclarationCode.fromString('class $name {}'));
  }

  @override
  FutureOr<void> buildDeclarationsForExtension(
      ExtensionDeclaration extension, MemberDeclarationBuilder builder) async {
    final dartCoreList =
        // ignore: deprecated_member_use_from_same_package
        await builder.resolveIdentifier(Uri.parse('dart:core'), 'List');
    final dartCoreString =
        // ignore: deprecated_member_use_from_same_package
        await builder.resolveIdentifier(Uri.parse('dart:core'), 'String');
    builder.declareInType(DeclarationCode.fromParts([
      NamedTypeAnnotationCode(name: dartCoreList, typeArguments: [
        NamedTypeAnnotationCode(name: dartCoreString),
      ]),
      ' get onTypeFieldNames;',
    ]));
  }

  @override
  FutureOr<void> buildDefinitionForExtension(
      ExtensionDeclaration extension, TypeDefinitionBuilder builder) async {
    // Get a builder for the getter we added earlier.
    final extensionMethods = await builder.methodsOf(extension);
    final getterBuilder = await builder.buildMethod(extensionMethods
        .singleWhere((m) => m.identifier.name == 'onTypeFieldNames')
        .identifier);

    // Introspect on our `on` type.
    final onType = (await builder.typeDeclarationOf(
        (extension.onType as NamedTypeAnnotation).identifier));
    final onTypeFields = await builder.fieldsOf(onType);

    getterBuilder.augment(FunctionBodyCode.fromParts([
      '=> [',
      for (var field in onTypeFields) "'${field.identifier.name}',",
      '];',
    ]));
  }

  @override
  FutureOr<void> buildTypesForExtensionType(
      ExtensionTypeDeclaration extensionType, TypeBuilder builder) {
    final representationType =
        extensionType.representationType as NamedTypeAnnotation;
    final name = '${extensionType.identifier.name}On'
        '${representationType.identifier.name}';
    builder.declareType(name, DeclarationCode.fromString('class $name {}'));
  }

  @override
  FutureOr<void> buildDeclarationsForExtensionType(
      ExtensionTypeDeclaration extensionType,
      MemberDeclarationBuilder builder) async {
    final dartCoreList =
        // ignore: deprecated_member_use_from_same_package
        await builder.resolveIdentifier(Uri.parse('dart:core'), 'List');
    final dartCoreString =
        // ignore: deprecated_member_use_from_same_package
        await builder.resolveIdentifier(Uri.parse('dart:core'), 'String');
    builder.declareInType(DeclarationCode.fromParts([
      NamedTypeAnnotationCode(name: dartCoreList, typeArguments: [
        NamedTypeAnnotationCode(name: dartCoreString),
      ]),
      ' get onTypeFieldNames;',
    ]));
  }

  @override
  FutureOr<void> buildDefinitionForExtensionType(
      ExtensionTypeDeclaration extensionType,
      TypeDefinitionBuilder builder) async {
    // Get a builder for the getter we added earlier.
    final extensionTypeMethods = await builder.methodsOf(extensionType);
    final getterBuilder = await builder.buildMethod(extensionTypeMethods
        .singleWhere((m) => m.identifier.name == 'onTypeFieldNames')
        .identifier);

    // Introspect on our "representation" type.
    final onType = (await builder.typeDeclarationOf(
        (extensionType.representationType as NamedTypeAnnotation).identifier));
    final onTypeFields = await builder.fieldsOf(onType);

    getterBuilder.augment(FunctionBodyCode.fromParts([
      '=> [',
      for (var field in onTypeFields) "'${field.identifier.name}',",
      '];',
    ]));
  }

  @override
  FutureOr<void> buildTypesForTypeAlias(
      TypeAliasDeclaration extensionType, TypeBuilder builder) {
    final representationType = extensionType.aliasedType as NamedTypeAnnotation;
    final name = '${extensionType.identifier.name}AliasedType'
        '${representationType.identifier.name}';
    builder.declareType(name, DeclarationCode.fromString('class $name {}'));
  }

  @override
  FutureOr<void> buildDeclarationsForTypeAlias(
      TypeAliasDeclaration extensionType, DeclarationBuilder builder) async {
    final dartCoreList =
        // ignore: deprecated_member_use_from_same_package
        await builder.resolveIdentifier(Uri.parse('dart:core'), 'List');
    final dartCoreString =
        // ignore: deprecated_member_use_from_same_package
        await builder.resolveIdentifier(Uri.parse('dart:core'), 'String');
    builder.declareInLibrary(DeclarationCode.fromParts([
      NamedTypeAnnotationCode(name: dartCoreList, typeArguments: [
        NamedTypeAnnotationCode(name: dartCoreString),
      ]),
      ' get aliasedTypeFieldNames;',
    ]));
  }
}

Future<FunctionBodyCode> _buildFunctionAugmentation(
    FunctionDeclaration function,
    DefinitionPhaseIntrospector introspector) async {
  Future<List<Object>> typeParts(TypeAnnotation annotation) async {
    if (annotation is OmittedTypeAnnotation) {
      var inferred = await introspector.inferType(annotation);
      return [inferred.code, ' (inferred)'];
    }
    return [annotation.code];
  }

  return FunctionBodyCode.fromParts([
    '{\n',
    if (function is MethodDeclaration)
      "print('definingClass: ${function.definingType.name}');\n",
    if (function is ConstructorDeclaration)
      '''
        print('isConst: ${function.isConst}');
        print('isFactory: ${function.isFactory}');''',
    '''
      print('isExternal: ${function.hasExternal}');
      print('isGetter: ${function.isGetter}');
      print('isSetter: ${function.isSetter}');
      print('returnType: ''',
    function.returnType.code,
    "');\n",
    for (var param in function.positionalParameters) ...[
      "print('positionalParam: ",
      ...await typeParts(param.type),
      ' ${param.identifier.name}',
      "');\n",
    ],
    for (var param in function.namedParameters) ...[
      "print('namedParam: ",
      ...await typeParts(param.type),
      ' ${param.identifier.name}',
      "');\n",
    ],
    for (var param in function.typeParameters) ...[
      "print('typeParam: ${param.identifier.name} ",
      if (param.bound != null) param.bound!.code,
      "');\n",
    ],
    'return augmented',
    if (function.isSetter) ...[
      ' = ',
      function.positionalParameters.first.identifier,
    ],
    if (!function.isGetter && !function.isSetter) '()',
    ''';
    }''',
  ]);
}

class DanglingTaskMacro
    implements
        LibraryTypesMacro,
        LibraryDeclarationsMacro,
        LibraryDefinitionMacro {
  @override
  void buildTypesForLibrary(Library library, TypeBuilder builder) {
    Future.value('hello').then((_) => Future.value('world'));
  }

  @override
  void buildDeclarationsForLibrary(
      Library library, DeclarationBuilder builder) {
    Future.value('hello').then((_) => Future.value('world'));
  }

  @override
  FutureOr<void> buildDefinitionForLibrary(
      Library library, LibraryDefinitionBuilder builder) {
    Future.value('hello').then((_) => Future.value('world'));
  }
}

class DanglingTimerMacro
    implements
        LibraryTypesMacro,
        LibraryDeclarationsMacro,
        LibraryDefinitionMacro {
  @override
  void buildTypesForLibrary(Library library, TypeBuilder builder) {
    Timer.run(() {});
  }

  @override
  void buildDeclarationsForLibrary(
      Library library, DeclarationBuilder builder) {
    Timer.run(() {});
  }

  @override
  FutureOr<void> buildDefinitionForLibrary(
      Library library, LibraryDefinitionBuilder builder) {
    Timer.run(() {});
  }
}

class DanglingPeriodicTimerMacro
    implements
        LibraryTypesMacro,
        LibraryDeclarationsMacro,
        LibraryDefinitionMacro {
  @override
  void buildTypesForLibrary(Library library, TypeBuilder builder) {
    Timer.periodic(Duration(seconds: 1), (_) {});
  }

  @override
  void buildDeclarationsForLibrary(
      Library library, DeclarationBuilder builder) {
    Timer.periodic(Duration(seconds: 1), (_) {});
  }

  @override
  FutureOr<void> buildDefinitionForLibrary(
      Library library, LibraryDefinitionBuilder builder) {
    Timer.periodic(Duration(seconds: 1), (_) {});
  }
}

extension on String {
  String capitalize() => '${this[0].toUpperCase()}${substring(1)}';
}
