// Copyright (c) 2022, 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 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/ast/ast.dart' as ast;
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/summary2/library_builder.dart';
import 'package:analyzer/src/summary2/linked_element_factory.dart';
import 'package:analyzer/src/summary2/macro.dart';
import 'package:analyzer/src/summary2/macro_application_error.dart';
import 'package:analyzer/src/summary2/macro_declarations.dart';
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer/src/utilities/extensions/collection.dart';
import 'package:analyzer/src/utilities/extensions/object.dart';
import 'package:collection/collection.dart';
import 'package:macros/macros.dart' as macro;
import 'package:macros/src/executor.dart' as macro;
import 'package:macros/src/executor/exception_impls.dart' as macro;
import 'package:macros/src/executor/introspection_impls.dart' as macro;
import 'package:macros/src/executor/multi_executor.dart';
import 'package:macros/src/executor/remote_instance.dart' as macro;
import 'package:meta/meta.dart';

/// The full list of [macro.ArgumentKind]s for this dart type, with type
/// arguments for [InterfaceType]s, if [includeTop] is `true` also including
/// the [InterfaceType] itself, with [macro.ArgumentKind.nullable] preceding
/// nullable types, depth first.
List<macro.ArgumentKind> _argumentKindsOfType(
  DartType type, {
  bool includeTop = true,
}) {
  return [
    if (type.nullabilitySuffix == NullabilitySuffix.question)
      macro.ArgumentKind.nullable,
    if (includeTop)
      switch (type) {
        DartType(isDartCoreBool: true) => macro.ArgumentKind.bool,
        DartType(isDartCoreDouble: true) => macro.ArgumentKind.double,
        DartType(isDartCoreInt: true) => macro.ArgumentKind.int,
        DartType(isDartCoreNum: true) => macro.ArgumentKind.num,
        DartType(isDartCoreNull: true) => macro.ArgumentKind.nil,
        DartType(isDartCoreObject: true) => macro.ArgumentKind.object,
        DartType(isDartCoreString: true) => macro.ArgumentKind.string,
        DartType(isDartCoreList: true) => macro.ArgumentKind.list,
        DartType(isDartCoreMap: true) => macro.ArgumentKind.map,
        DartType(isDartCoreSet: true) => macro.ArgumentKind.set,
        DynamicType() => macro.ArgumentKind.dynamic,
        // TODO(jakemac): Support type annotations and code objects
        _ => throw UnsupportedError('Unsupported macro type argument $type'),
      },
    if (type is InterfaceType) ...[
      for (var typeArgument in type.typeArguments)
        ..._argumentKindsOfType(typeArgument),
    ]
  ];
}

class ApplicationResult {
  final _MacroApplication application;
  final List<macro.MacroExecutionResult> results;

  ApplicationResult(this.application, this.results);

  MacroTargetElement get targetElement {
    return application.target.element;
  }
}

// TODO(scheglov): The name is deceptive, this is not for a single library.
class LibraryMacroApplier {
  @visibleForTesting
  static bool testThrowExceptionTypes = false;

  @visibleForTesting
  static bool testThrowExceptionDeclarations = false;

  @visibleForTesting
  static bool testThrowExceptionDefinitions = false;

  @visibleForTesting
  static bool testThrowExceptionIntrospection = false;

  final LinkedElementFactory elementFactory;
  final MultiMacroExecutor macroExecutor;
  final bool Function(Uri) isLibraryBeingLinked;
  final DeclarationBuilder declarationBuilder;

  /// The callback to run declarations phase if the type.
  /// We do it out-of-order when the type is introspected.
  final Future<void> Function({
    required ElementImpl? targetElement,
    required OperationPerformanceImpl performance,
  }) runDeclarationsPhase;

  /// The applications that currently run.
  ///
  /// There can be more than one, because during the declarations phase we
  /// can start introspecting another declaration, which runs the declarations
  /// phase for that another declaration.
  final List<_MacroApplication> _runningApplications = [];

  /// The map from a declaration that has declarations phase introspection
  /// cycle, to the cycle exception.
  final Map<MacroTargetElement, _MacroIntrospectionCycleException>
      _elementToIntrospectionCycleException = {};

  late final macro.TypePhaseIntrospector _typesPhaseIntrospector =
      _TypePhaseIntrospector(
    this,
    elementFactory,
    declarationBuilder,
    OperationPerformanceImpl('<typesPhaseIntrospector>'),
  );

  LibraryMacroApplier({
    required this.elementFactory,
    required this.macroExecutor,
    required this.isLibraryBeingLinked,
    required this.declarationBuilder,
    required this.runDeclarationsPhase,
  });

  /// The currently running macro application.
  _MacroApplication? get currentApplication {
    return _runningApplications.lastOrNull;
  }

  Future<void> add({
    required LibraryBuilder libraryBuilder,
    required LibraryOrAugmentationElementImpl container,
    required ast.CompilationUnitImpl unit,
  }) async {
    for (var directive in unit.directives.reversed) {
      switch (directive) {
        case ast.LibraryDirectiveImpl():
          await _addAnnotations(
            libraryBuilder: libraryBuilder,
            container: container,
            targetNode: directive,
            targetNodeElement: libraryBuilder.element,
            targetDeclarationKind: macro.DeclarationKind.library,
            annotations: directive.metadata,
          );
        default:
          break;
      }
    }

    for (var declaration in unit.declarations.reversed) {
      switch (declaration) {
        case ast.ClassDeclarationImpl():
          var element = declaration.declaredElement!;
          var declarationElement = element.augmented.declaration;
          await _addClassLike(
            libraryBuilder: libraryBuilder,
            container: container,
            targetElement: declarationElement,
            classNode: declaration,
            classDeclarationKind: macro.DeclarationKind.classType,
            classAnnotations: declaration.metadata,
            members: declaration.members,
          );
        case ast.ClassTypeAliasImpl():
          var element = declaration.declaredElement!;
          var declarationElement = element.augmented.declaration;
          await _addClassLike(
            libraryBuilder: libraryBuilder,
            container: container,
            targetElement: declarationElement,
            classNode: declaration,
            classDeclarationKind: macro.DeclarationKind.classType,
            classAnnotations: declaration.metadata,
            members: const [],
          );
        case ast.EnumDeclarationImpl():
          var element = declaration.declaredElement!;
          var declarationElement = element.augmented.declaration;
          await _addClassLike(
            libraryBuilder: libraryBuilder,
            container: container,
            targetElement: declarationElement,
            classNode: declaration,
            classDeclarationKind: macro.DeclarationKind.enumType,
            classAnnotations: declaration.metadata,
            members: declaration.members,
          );
          for (var constant in declaration.constants.reversed) {
            await _addAnnotations(
              libraryBuilder: libraryBuilder,
              container: container,
              targetNode: constant,
              targetNodeElement: constant.declaredElement,
              targetDeclarationKind: macro.DeclarationKind.enumValue,
              annotations: constant.metadata,
            );
          }
        case ast.ExtensionDeclarationImpl():
          var element = declaration.declaredElement!;
          var declarationElement = element.augmented.declaration;
          await _addClassLike(
            libraryBuilder: libraryBuilder,
            container: container,
            targetElement: declarationElement,
            classNode: declaration,
            classDeclarationKind: macro.DeclarationKind.extension,
            classAnnotations: declaration.metadata,
            members: declaration.members,
          );
        case ast.ExtensionTypeDeclarationImpl():
          var element = declaration.declaredElement!;
          var declarationElement = element.augmented.declaration;
          await _addClassLike(
            libraryBuilder: libraryBuilder,
            container: container,
            targetElement: declarationElement,
            classNode: declaration,
            classDeclarationKind: macro.DeclarationKind.extensionType,
            classAnnotations: declaration.metadata,
            members: declaration.members,
          );
        case ast.FunctionDeclarationImpl():
          await _addAnnotations(
            libraryBuilder: libraryBuilder,
            container: container,
            targetNode: declaration,
            targetNodeElement: declaration.declaredElement,
            targetDeclarationKind: macro.DeclarationKind.function,
            annotations: declaration.metadata,
          );
        case ast.FunctionTypeAliasImpl():
          // TODO(scheglov): implement it
          break;
        case ast.GenericTypeAliasImpl():
          await _addAnnotations(
            libraryBuilder: libraryBuilder,
            container: container,
            targetNode: declaration,
            targetNodeElement: declaration.declaredElement,
            targetDeclarationKind: macro.DeclarationKind.typeAlias,
            annotations: declaration.metadata,
          );
        case ast.MixinDeclarationImpl():
          var element = declaration.declaredElement!;
          var declarationElement = element.augmented.declaration;
          await _addClassLike(
            libraryBuilder: libraryBuilder,
            container: container,
            targetElement: declarationElement,
            classNode: declaration,
            classDeclarationKind: macro.DeclarationKind.mixinType,
            classAnnotations: declaration.metadata,
            members: declaration.members,
          );
        case ast.TopLevelVariableDeclarationImpl():
          var variables = declaration.variables.variables;
          for (var variable in variables.reversed) {
            await _addAnnotations(
              libraryBuilder: libraryBuilder,
              container: container,
              targetNode: variable,
              targetNodeElement: variable.declaredElement,
              targetDeclarationKind: macro.DeclarationKind.variable,
              annotations: declaration.metadata,
            );
          }
      }
    }
  }

  /// Builds the augmentation library code for [results].
  String? buildAugmentationLibraryCode(
    Uri augmentedLibraryUri,
    List<macro.MacroExecutionResult> results,
  ) {
    if (results.isEmpty) {
      return null;
    }

    return macroExecutor.buildAugmentationLibrary(
      augmentedLibraryUri,
      results,
      declarationBuilder.typeDeclarationOf,
      declarationBuilder.resolveIdentifier,
      declarationBuilder.inferOmittedType,
    );
  }

  void disposeMacroApplications({
    required LibraryBuilder libraryBuilder,
  }) {
    for (var application in libraryBuilder._applications) {
      macroExecutor.disposeMacro(application.instance);
    }
  }

  Future<ApplicationResult?> executeDeclarationsPhase({
    required LibraryBuilder libraryBuilder,
    required ElementImpl? targetElement,
    required OperationPerformanceImpl performance,
  }) async {
    if (targetElement != null) {
      for (var i = 0; i < _runningApplications.length; i++) {
        var running = _runningApplications[i];
        if (running.target.element == targetElement) {
          var applications = _runningApplications.sublist(i);
          var exception = _MacroIntrospectionCycleException(
            'Declarations phase introspection cycle.',
            applications: applications,
          );

          // Mark all applications as having introspection cycle.
          // So, every introspection of these elements will throw.
          for (var application in applications) {
            var element = application.target.element;
            _elementToIntrospectionCycleException[element] = exception;
          }

          throw exception;
        }
      }
    }

    var application = _nextForDeclarationsPhase(
      libraryBuilder: libraryBuilder,
      targetElement: targetElement,
    );
    if (application == null) {
      return null;
    }

    _runningApplications.add(application);
    var results = <macro.MacroExecutionResult>[];

    await _runWithCatchingExceptions(
      () async {
        var target = _buildTarget(application.target.node);

        var introspector = _DeclarationPhaseIntrospector(
          this,
          elementFactory,
          declarationBuilder,
          performance,
          libraryBuilder.element.typeSystem,
        );

        var result = await macroExecutor.executeDeclarationsPhase(
          application.instance,
          target,
          introspector,
        );

        _addDiagnostics(application, result);
        if (result.isNotEmpty) {
          results.add(result);
        }

        if (testThrowExceptionDeclarations) {
          throw 'Intentional exception';
        }
      },
      targetElement: application.target.element,
      annotationIndex: application.annotationIndex,
    );

    _runningApplications.removeLast();
    return ApplicationResult(application, results);
  }

  Future<ApplicationResult?> executeDefinitionsPhase({
    required LibraryBuilder libraryBuilder,
    required OperationPerformanceImpl performance,
  }) async {
    var application = _nextForDefinitionsPhase(
      libraryBuilder: libraryBuilder,
    );
    if (application == null) {
      return null;
    }

    _runningApplications.add(application);
    var results = <macro.MacroExecutionResult>[];

    await _runWithCatchingExceptions(
      () async {
        var target = _buildTarget(application.target.node);

        var introspector = _DefinitionPhaseIntrospector(
          this,
          elementFactory,
          declarationBuilder,
          performance,
          application.target.library.element.typeSystem,
        );

        var result = await macroExecutor.executeDefinitionsPhase(
          application.instance,
          target,
          introspector,
        );

        _addDiagnostics(application, result);
        if (result.isNotEmpty) {
          results.add(result);
        }

        if (testThrowExceptionDefinitions) {
          throw 'Intentional exception';
        }
      },
      targetElement: application.target.element,
      annotationIndex: application.annotationIndex,
    );

    _runningApplications.removeLast();
    return ApplicationResult(application, results);
  }

  Future<ApplicationResult?> executeTypesPhase({
    required LibraryBuilder libraryBuilder,
  }) async {
    var application = _nextForTypesPhase(
      libraryBuilder: libraryBuilder,
    );
    if (application == null) {
      return null;
    }

    _runningApplications.add(application);
    var results = <macro.MacroExecutionResult>[];

    await _runWithCatchingExceptions(
      () async {
        var target = _buildTarget(application.target.node);

        var result = await macroExecutor.executeTypesPhase(
          application.instance,
          target,
          _typesPhaseIntrospector,
        );

        _addDiagnostics(application, result);
        if (result.isNotEmpty) {
          results.add(result);
        }

        if (testThrowExceptionTypes) {
          throw 'Intentional exception';
        }
      },
      targetElement: application.target.element,
      annotationIndex: application.annotationIndex,
    );

    _runningApplications.removeLast();
    return ApplicationResult(application, results);
  }

  Future<void> _addAnnotations({
    required LibraryBuilder libraryBuilder,
    required LibraryOrAugmentationElementImpl container,
    required ast.AstNode targetNode,
    required Element? targetNodeElement,
    required macro.DeclarationKind targetDeclarationKind,
    required List<ast.Annotation> annotations,
  }) async {
    if (targetNode is ast.FieldDeclaration) {
      for (var field in targetNode.fields.variables) {
        await _addAnnotations(
          libraryBuilder: libraryBuilder,
          container: container,
          targetNode: field,
          targetNodeElement: field.declaredElement,
          targetDeclarationKind: macro.DeclarationKind.field,
          annotations: annotations,
        );
      }
      return;
    }

    var targetElement = targetNodeElement.ifTypeOrNull<MacroTargetElement>();
    if (targetElement == null) {
      return;
    }

    var macroTarget = _MacroTarget(
      library: libraryBuilder,
      node: targetNode,
      element: targetElement,
    );

    for (var (annotationIndex, annotation) in annotations.indexed) {
      var importedMacro = _importedMacro(
        container: container,
        annotation: annotation,
      );
      if (importedMacro == null) {
        continue;
      }

      var constructorElement = importedMacro.constructorElement;
      if (constructorElement == null) {
        continue;
      }

      var instance = await _runWithCatchingExceptions(
        () async {
          var arguments = _buildArguments(
            annotationIndex: annotationIndex,
            constructor: constructorElement,
            node: importedMacro.arguments,
          );

          return await importedMacro.bundleExecutor.instantiate(
            libraryUri: importedMacro.macroLibrary.source.uri,
            className: importedMacro.macroClass.name,
            constructorName: importedMacro.constructorName ?? '',
            arguments: arguments,
          );
        },
        targetElement: targetElement,
        annotationIndex: annotationIndex,
      );
      if (instance == null) {
        continue;
      }

      var phasesToExecute = macro.Phase.values.where((phase) {
        return instance.shouldExecute(targetDeclarationKind, phase);
      }).toSet();

      if (!instance.supportsDeclarationKind(targetDeclarationKind)) {
        macroTarget.element.addMacroDiagnostic(
          InvalidMacroTargetDiagnostic(
            annotationIndex: annotationIndex,
            supportedKinds: macro.DeclarationKind.values
                .where(instance.supportsDeclarationKind)
                .map((e) => e.name)
                .toList(),
          ),
        );
        return;
      }

      var application = _MacroApplication(
        target: macroTarget,
        annotationIndex: annotationIndex,
        annotationNode: annotation,
        instance: instance,
        phasesToExecute: phasesToExecute,
      );

      libraryBuilder._applications.add(application);
    }
  }

  Future<void> _addClassLike({
    required LibraryBuilder libraryBuilder,
    required LibraryOrAugmentationElementImpl container,
    required MacroTargetElement targetElement,
    required ast.Declaration classNode,
    required macro.DeclarationKind classDeclarationKind,
    required List<ast.Annotation> classAnnotations,
    required List<ast.ClassMember> members,
  }) async {
    await _addAnnotations(
      libraryBuilder: libraryBuilder,
      container: container,
      targetNode: classNode,
      targetNodeElement: classNode.declaredElement,
      targetDeclarationKind: classDeclarationKind,
      annotations: classAnnotations,
    );

    for (var member in members.reversed) {
      var memberDeclarationKind = switch (member) {
        ast.ConstructorDeclaration() => macro.DeclarationKind.constructor,
        ast.FieldDeclaration() => macro.DeclarationKind.field,
        ast.MethodDeclaration() => macro.DeclarationKind.method,
      };
      await _addAnnotations(
        libraryBuilder: libraryBuilder,
        container: container,
        targetNode: member,
        targetNodeElement: member.declaredElement,
        targetDeclarationKind: memberDeclarationKind,
        annotations: member.metadata,
      );
    }
  }

  void _addDiagnostics(
    _MacroApplication application,
    macro.MacroExecutionResult result,
  ) {
    MacroDiagnosticMessage convertMessage(
      macro.DiagnosticMessage message,
    ) {
      MacroDiagnosticTarget target;
      switch (message.target) {
        case macro.DeclarationDiagnosticTarget macroTarget:
          var element = (macroTarget.declaration as HasElement).element;
          target = ElementMacroDiagnosticTarget(element: element);
        case macro.TypeAnnotationDiagnosticTarget macroTarget:
          target = _typeAnnotationTarget(application, macroTarget);
        case macro.MetadataAnnotationDiagnosticTarget macroTarget:
          var annotation =
              macroTarget.metadataAnnotation as MetadataAnnotationImpl;
          target = ElementAnnotationMacroDiagnosticTarget(
            element: annotation.element,
            annotationIndex: annotation.annotationIndex,
          );
        case null:
          target = ApplicationMacroDiagnosticTarget(
            annotationIndex: application.annotationIndex,
          );
      }

      return MacroDiagnosticMessage(
        target: target,
        message: message.message,
      );
    }

    for (var diagnostic in result.diagnostics) {
      application.target.element.addMacroDiagnostic(
        MacroDiagnostic(
          severity: diagnostic.severity,
          message: convertMessage(diagnostic.message),
          contextMessages:
              diagnostic.contextMessages.map(convertMessage).toList(),
          correctionMessage: diagnostic.correctionMessage,
        ),
      );
    }

    bool addIntrospectionCycle(macro.MacroException? exception) {
      if (exception is! _MacroIntrospectionCycleException) {
        return false;
      }

      var introspectedElement = application.lastIntrospectedElement;
      if (introspectedElement == null) {
        return false;
      }

      var applications = exception.applications;
      var components = applications.map((application) {
        return DeclarationsIntrospectionCycleComponent(
          element: application.target.element,
          annotationIndex: application.annotationIndex,
          introspectedElement: application.lastIntrospectedElement!,
        );
      }).toList();

      // Report only for this application.
      // Introspections of every cycle component will report it too.
      // Macro implementations can catch and ignore introspection exceptions.
      application.target.element.addMacroDiagnostic(
        DeclarationsIntrospectionCycleDiagnostic(
          annotationIndex: application.annotationIndex,
          introspectedElement: introspectedElement,
          components: components,
        ),
      );
      return true;
    }

    if (result.exception case var exception?) {
      var reported = addIntrospectionCycle(exception);
      if (!reported) {
        application.target.element.addMacroDiagnostic(
          ExceptionMacroDiagnostic(
            annotationIndex: application.annotationIndex,
            message: exception.message,
            stackTrace: exception.stackTrace ?? '<null>',
          ),
        );
      }
    }
  }

  macro.MacroTarget _buildTarget(ast.AstNode node) {
    return declarationBuilder.buildTarget(node);
  }

  /// If [annotation] references a macro, invokes the right callback.
  _AnnotationMacro? _importedMacro({
    required LibraryOrAugmentationElementImpl container,
    required ast.Annotation annotation,
  }) {
    var arguments = annotation.arguments;
    if (arguments == null) {
      return null;
    }

    String? prefix;
    String name;
    String? constructorName;
    var nameNode = annotation.name;
    if (nameNode is ast.SimpleIdentifier) {
      prefix = null;
      name = nameNode.name;
      constructorName = annotation.constructorName?.name;
    } else if (nameNode is ast.PrefixedIdentifier) {
      var importPrefixCandidate = nameNode.prefix.name;
      var hasImportPrefix = container.libraryImports.any(
          (import) => import.prefix?.element.name == importPrefixCandidate);
      if (hasImportPrefix) {
        prefix = importPrefixCandidate;
        name = nameNode.identifier.name;
        constructorName = annotation.constructorName?.name;
      } else {
        prefix = null;
        name = nameNode.prefix.name;
        constructorName = nameNode.identifier.name;
      }
    } else {
      throw StateError('${nameNode.runtimeType} $nameNode');
    }

    for (var import in container.libraryImports) {
      if (import.prefix?.element.name != prefix) {
        continue;
      }

      var importedLibrary = import.importedLibrary;
      if (importedLibrary == null) {
        continue;
      }

      // Skip if a library that is being linked.
      var importedUri = importedLibrary.source.uri;
      if (isLibraryBeingLinked(importedUri)) {
        continue;
      }

      var macroClass = importedLibrary.exportNamespace.get(name);
      if (macroClass is! ClassElementImpl) {
        continue;
      }

      var macroLibrary = macroClass.library;
      var bundleExecutor = macroLibrary.bundleMacroExecutor;
      if (bundleExecutor == null) {
        continue;
      }

      if (macroClass.isMacro) {
        return _AnnotationMacro(
          macroLibrary: macroLibrary,
          bundleExecutor: bundleExecutor,
          macroClass: macroClass,
          constructorName: constructorName,
          arguments: arguments,
        );
      }
    }
    return null;
  }

  _MacroApplication? _nextForDeclarationsPhase({
    required LibraryBuilder libraryBuilder,
    required Element? targetElement,
  }) {
    var applications = libraryBuilder._applications;
    for (var i = applications.length - 1; i >= 0; i--) {
      var application = applications[i];
      if (targetElement != null) {
        var applicationElement = application.target.element;
        if (!identical(applicationElement, targetElement) &&
            !identical(applicationElement.enclosingElement, targetElement)) {
          continue;
        }
      }
      if (application.phasesToExecute.remove(macro.Phase.declarations)) {
        return application;
      }
    }

    return null;
  }

  _MacroApplication? _nextForDefinitionsPhase({
    required LibraryBuilder libraryBuilder,
  }) {
    var applications = libraryBuilder._applications;
    for (var i = applications.length - 1; i >= 0; i--) {
      var application = applications[i];
      if (application.phasesToExecute.remove(macro.Phase.definitions)) {
        return application;
      }
    }
    return null;
  }

  _MacroApplication? _nextForTypesPhase({
    required LibraryBuilder libraryBuilder,
  }) {
    var applications = libraryBuilder._applications;
    for (var i = applications.length - 1; i >= 0; i--) {
      var application = applications[i];
      if (application.phasesToExecute.remove(macro.Phase.types)) {
        return application;
      }
    }
    return null;
  }

  MacroDiagnosticTarget _typeAnnotationTarget(
    _MacroApplication application,
    macro.TypeAnnotationDiagnosticTarget macroTarget,
  ) {
    switch (macroTarget.typeAnnotation) {
      case TypeAnnotationWithLocation typeAnnotation:
        return TypeAnnotationMacroDiagnosticTarget(
          location: typeAnnotation.location,
        );
    }

    // We don't know anything better.
    return ApplicationMacroDiagnosticTarget(
      annotationIndex: application.annotationIndex,
    );
  }

  static macro.Arguments _buildArguments({
    required int annotationIndex,
    required ConstructorElement constructor,
    required ast.ArgumentList node,
  }) {
    var allParameters = constructor.parameters;
    var namedParameters = allParameters
        .where((e) => e.isNamed)
        .map((e) => MapEntry(e.name, e))
        .mapFromEntries;
    var positionalParameters =
        allParameters.where((e) => e.isPositional).toList();
    var positionalParameterIndex = 0;

    var positional = <macro.Argument>[];
    var named = <String, macro.Argument>{};
    for (var i = 0; i < node.arguments.length; ++i) {
      ParameterElement? parameter;
      String? namedArgumentName;
      ast.Expression expressionToEvaluate;
      var argument = node.arguments[i];
      if (argument is ast.NamedExpression) {
        namedArgumentName = argument.name.label.name;
        expressionToEvaluate = argument.expression;
        parameter = namedParameters.remove(namedArgumentName);
      } else {
        expressionToEvaluate = argument;
        parameter = positionalParameters.elementAtOrNull(
          positionalParameterIndex++,
        );
      }

      var contextType = parameter?.type ?? DynamicTypeImpl.instance;
      var evaluation = _ArgumentEvaluation(
        annotationIndex: annotationIndex,
        argumentIndex: i,
      );
      var value = evaluation.evaluate(contextType, expressionToEvaluate);

      if (namedArgumentName != null) {
        named[namedArgumentName] = value;
      } else {
        positional.add(value);
      }
    }
    return macro.Arguments(positional, named);
  }

  /// Run the [body], report [AnalyzerMacroDiagnostic]s to [onDiagnostic].
  static Future<T?> _runWithCatchingExceptions<T>(
    Future<T> Function() body, {
    required MacroTargetElement targetElement,
    required int annotationIndex,
  }) async {
    try {
      return await body();
    } on AnalyzerMacroDiagnostic catch (e) {
      targetElement.addMacroDiagnostic(e);
    } on macro.MacroException catch (e) {
      targetElement.addMacroDiagnostic(
        ExceptionMacroDiagnostic(
          annotationIndex: annotationIndex,
          message: e.message,
          stackTrace: e.stackTrace ?? '<null>',
        ),
      );
    } catch (e, stackTrace) {
      targetElement.addMacroDiagnostic(
        ExceptionMacroDiagnostic(
          annotationIndex: annotationIndex,
          message: '$e',
          stackTrace: '$stackTrace',
        ),
      );
    }
    return null;
  }
}

/// This mixin is added to [LibraryBuilder] to make it a container with
/// macro applications, but at the same time don't expose internals of
/// [_MacroApplication].
mixin MacroApplicationsContainer {
  /// The reversed queue of macro applications to apply.
  ///
  /// We add classes before methods, and methods in the reverse order,
  /// classes in the reverse order, annotations in the direct order.
  ///
  /// We iterate from the end looking for the next application to apply.
  /// This way we ensure two ordering rules:
  /// 1. inner before outer
  /// 2. right to left
  /// 3. source order
  final List<_MacroApplication> _applications = [];
}

/// Facts about applying macros in a library.
class MacroProcessing {
  bool hasAnyIntrospection = false;
}

class _AnnotationMacro {
  final LibraryElementImpl macroLibrary;
  final BundleMacroExecutor bundleExecutor;
  final ClassElementImpl macroClass;
  final String? constructorName;
  final ast.ArgumentList arguments;

  _AnnotationMacro({
    required this.macroLibrary,
    required this.bundleExecutor,
    required this.macroClass,
    required this.constructorName,
    required this.arguments,
  });

  ConstructorElement? get constructorElement {
    return macroClass.getNamedConstructor(constructorName ?? '');
  }
}

/// Helper class for evaluating arguments for a single constructor based
/// macro application.
class _ArgumentEvaluation {
  final int annotationIndex;
  final int argumentIndex;

  _ArgumentEvaluation({
    required this.annotationIndex,
    required this.argumentIndex,
  });

  macro.Argument evaluate(DartType contextType, ast.Expression node) {
    if (node is ast.AdjacentStrings) {
      return macro.StringArgument(node.strings
          .map((e) => evaluate(contextType, e))
          .map((arg) => arg.value)
          .join());
    } else if (node is ast.BooleanLiteral) {
      return macro.BoolArgument(node.value);
    } else if (node is ast.DoubleLiteral) {
      return macro.DoubleArgument(node.value);
    } else if (node is ast.IntegerLiteral) {
      return macro.IntArgument(node.value!);
    } else if (node is ast.ListLiteral) {
      return _listLiteral(contextType, node);
    } else if (node is ast.NullLiteral) {
      return macro.NullArgument();
    } else if (node is ast.PrefixExpression &&
        node.operator.type == TokenType.MINUS) {
      var operandValue = evaluate(contextType, node.operand);
      if (operandValue is macro.DoubleArgument) {
        return macro.DoubleArgument(-operandValue.value);
      } else if (operandValue is macro.IntArgument) {
        return macro.IntArgument(-operandValue.value);
      }
    } else if (node is ast.SetOrMapLiteral) {
      return _setOrMapLiteral(contextType, node);
    } else if (node is ast.SimpleStringLiteral) {
      return macro.StringArgument(node.value);
    }
    _throwError(node, 'Not supported: ${node.runtimeType}');
  }

  macro.ListArgument _listLiteral(
    DartType contextType,
    ast.ListLiteral node,
  ) {
    DartType elementType;
    switch (contextType) {
      case InterfaceType(isDartCoreList: true):
        elementType = contextType.typeArguments[0];
      default:
        _throwError(node, 'Expected context type List');
    }

    var typeArguments = _argumentKindsOfType(
      contextType,
      includeTop: false,
    );

    return macro.ListArgument(
      node.elements
          .cast<ast.Expression>()
          .map((e) => evaluate(elementType, e))
          .toList(),
      typeArguments,
    );
  }

  macro.Argument _setOrMapLiteral(
    DartType contextType,
    ast.SetOrMapLiteral node,
  ) {
    var typeArguments = _argumentKindsOfType(
      contextType,
      includeTop: false,
    );

    switch (contextType) {
      case InterfaceType(isDartCoreMap: true):
        var keyType = contextType.typeArguments[0];
        var valueType = contextType.typeArguments[1];
        var result = <macro.Argument, macro.Argument>{};
        for (var element in node.elements) {
          if (element is! ast.MapLiteralEntry) {
            _throwError(element, 'MapLiteralEntry expected');
          }
          var key = evaluate(keyType, element.key);
          var value = evaluate(valueType, element.value);
          result[key] = value;
        }
        return macro.MapArgument(result, typeArguments);
      case InterfaceType(isDartCoreSet: true):
        var elementType = contextType.typeArguments[0];
        var result = <macro.Argument>[];
        for (var element in node.elements) {
          if (element is! ast.Expression) {
            _throwError(element, 'Expression expected');
          }
          var value = evaluate(elementType, element);
          result.add(value);
        }
        return macro.SetArgument(result, typeArguments);
      default:
        _throwError(node, 'Expected context type Map or Set');
    }
  }

  Never _throwError(ast.AstNode node, String message) {
    throw ArgumentMacroDiagnostic(
      annotationIndex: annotationIndex,
      argumentIndex: argumentIndex,
      message: message,
    );
  }
}

class _DeclarationPhaseIntrospector extends _TypePhaseIntrospector
    implements macro.DeclarationPhaseIntrospector {
  final TypeSystemImpl typeSystem;

  _DeclarationPhaseIntrospector(
    super.applier,
    super.elementFactory,
    super.declarationBuilder,
    super.performance,
    this.typeSystem,
  );

  @override
  Future<List<macro.ConstructorDeclaration>> constructorsOf(
    covariant macro.TypeDeclaration type,
  ) async {
    performance.getDataInt('constructorsOf').increment();

    var element = (type as HasElement).element;
    await _runDeclarationsPhase(element);

    macroProcessing?.hasAnyIntrospection = true;
    if (element case InterfaceElement(:var augmented)) {
      return augmented.constructors
          .map((e) => e.declaration as ConstructorElementImpl)
          .map(declarationBuilder.declarationOfElement)
          .whereType<macro.ConstructorDeclaration>()
          .toList();
    }
    return [];
  }

  @override
  Future<List<macro.FieldDeclaration>> fieldsOf(
    macro.TypeDeclaration type,
  ) async {
    if (LibraryMacroApplier.testThrowExceptionIntrospection) {
      throw 'Intentional exception';
    }

    var element = (type as HasElement).element;
    await _runDeclarationsPhase(element);

    macroProcessing?.hasAnyIntrospection = true;
    if (element case InstanceElement(:var augmented)) {
      return augmented.fields
          .whereNot((e) => e.isSynthetic)
          .map((e) => e.declaration as FieldElementImpl)
          .map(declarationBuilder.declarationOfElement)
          .whereType<macro.FieldDeclaration>()
          .toList();
    }
    // TODO(scheglov): can we test this?
    throw StateError('Unexpected: ${type.runtimeType}');
  }

  @override
  Future<List<macro.MethodDeclaration>> methodsOf(
    macro.TypeDeclaration type,
  ) async {
    performance.getDataInt('methodsOf').increment();

    var element = (type as HasElement).element;
    await _runDeclarationsPhase(element);

    macroProcessing?.hasAnyIntrospection = true;
    if (element case InstanceElement(:var augmented)) {
      return [
        ...augmented.accessors.whereNot((e) => e.isSynthetic),
        ...augmented.methods,
      ]
          .map((e) => e.declaration as ExecutableElementImpl)
          .map(declarationBuilder.declarationOfElement)
          .whereType<macro.MethodDeclaration>()
          .toList();
    }
    // TODO(scheglov): can we test this?
    throw StateError('Unexpected: ${type.runtimeType}');
  }

  @override
  Future<macro.StaticType> resolve(macro.TypeAnnotationCode typeCode) async {
    performance.getDataInt('resolve').increment();
    macroProcessing?.hasAnyIntrospection = true;
    var type = declarationBuilder.resolveType(typeCode);
    return _dartTypeToStaticType(type);
  }

  @override
  Future<macro.TypeDeclaration> typeDeclarationOf(
    macro.Identifier identifier,
  ) async {
    performance.getDataInt('typeDeclarationOf').increment();
    macroProcessing?.hasAnyIntrospection = true;
    return declarationBuilder.typeDeclarationOf(identifier);
  }

  @override
  Future<List<macro.TypeDeclaration>> typesOf(
    covariant LibraryImplFromElement library,
  ) async {
    macroProcessing?.hasAnyIntrospection = true;
    return library.element.topLevelElements
        .map((e) => declarationBuilder.declarationOfElement(e))
        .whereType<macro.TypeDeclaration>()
        .toList();
  }

  @override
  Future<List<macro.EnumValueDeclaration>> valuesOf(
    covariant macro.EnumDeclaration type,
  ) async {
    var element = (type as HasElement).element;
    await _runDeclarationsPhase(element);

    element as EnumElementImpl;
    macroProcessing?.hasAnyIntrospection = true;

    // TODO(scheglov): use augmented
    return element.constants
        .map(declarationBuilder.declarationOfElement)
        .whereType<macro.EnumValueDeclaration>()
        .toList();
  }

  macro.StaticTypeImpl _dartTypeToStaticType(DartType type) {
    if (type is InterfaceType) {
      var element = type.element;
      var declaration = declarationBuilder.declarationOfElement(element);

      return _InterfaceTypeImpl(
        macro.RemoteInstance.uniqueId,
        typeSystem: typeSystem,
        introspector: this,
        type: type,
        declaration: declaration as macro.ParameterizedTypeDeclarationImpl,
        typeArguments: [
          for (final type in type.typeArguments) _dartTypeToStaticType(type),
        ],
      );
    } else {
      return _StaticTypeImpl(
        macro.RemoteInstance.uniqueId,
        typeSystem: typeSystem,
        introspector: this,
        type: type,
      );
    }
  }

  Future<void> _runDeclarationsPhase(ElementImpl element) async {
    // Don't run for the current element.
    var current = applier._runningApplications.lastOrNull;
    if (current?.target.element == element) {
      return;
    }

    current?.lastIntrospectedElement = element;
    await applier.runDeclarationsPhase(
      targetElement: element,
      performance: performance,
    );

    // We might have detected a cycle for this target element.
    // Either just now, or before.
    var exception = applier._elementToIntrospectionCycleException[element];
    if (exception != null) {
      throw exception;
    }
  }
}

class _DefinitionPhaseIntrospector extends _DeclarationPhaseIntrospector
    implements macro.DefinitionPhaseIntrospector {
  _DefinitionPhaseIntrospector(
    super.applier,
    super.elementFactory,
    super.declarationBuilder,
    super.performance,
    super.typeSystem,
  );

  @override
  Future<macro.Declaration> declarationOf(
    covariant macro.Identifier identifier,
  ) async {
    return declarationBuilder.declarationOf(identifier);
  }

  @override
  Future<macro.TypeAnnotation> inferType(
    covariant macro.OmittedTypeAnnotation omittedType,
  ) async {
    return declarationBuilder.inferOmittedType(omittedType);
  }

  @override
  Future<List<macro.Declaration>> topLevelDeclarationsOf(
    covariant LibraryImplFromElement library,
  ) async {
    macroProcessing?.hasAnyIntrospection = true;
    return library.element.topLevelElements
        .whereNot((e) => e.isSynthetic)
        .map((e) => declarationBuilder.declarationOfElement(e))
        .whereType<macro.Declaration>()
        .toList();
  }
}

class _InterfaceTypeImpl extends _StaticTypeImpl
    implements macro.NamedStaticTypeImpl {
  @override
  final macro.ParameterizedTypeDeclarationImpl declaration;

  @override
  final List<macro.StaticTypeImpl> typeArguments;

  _InterfaceTypeImpl(
    super.id, {
    required super.typeSystem,
    required super.introspector,
    required InterfaceType super.type,
    required this.declaration,
    required this.typeArguments,
  });
}

class _MacroApplication {
  final _MacroTarget target;
  final int annotationIndex;
  final ast.Annotation annotationNode;
  final macro.MacroInstanceIdentifier instance;
  final Set<macro.Phase> phasesToExecute;
  ElementImpl? lastIntrospectedElement;

  _MacroApplication({
    required this.target,
    required this.annotationIndex,
    required this.annotationNode,
    required this.instance,
    required this.phasesToExecute,
  });

  bool get hasDeclarationsPhase {
    return phasesToExecute.contains(macro.Phase.declarations);
  }

  void removeDeclarationsPhase() {
    phasesToExecute.remove(macro.Phase.declarations);
  }
}

final class _MacroIntrospectionCycleException
    extends macro.MacroIntrospectionCycleExceptionImpl {
  final List<_MacroApplication> applications;

  _MacroIntrospectionCycleException(
    super.message, {
    required this.applications,
  });
}

class _MacroTarget {
  final LibraryBuilder library;
  final ast.AstNode node;
  final MacroTargetElement element;

  _MacroTarget({
    required this.library,
    required this.node,
    required this.element,
  });
}

class _StaticTypeImpl extends macro.StaticTypeImpl {
  final TypeSystemImpl typeSystem;
  final _DeclarationPhaseIntrospector introspector;
  final DartType type;

  _StaticTypeImpl(
    super.id, {
    required this.typeSystem,
    required this.introspector,
    required this.type,
  });

  @override
  Future<macro.NamedStaticType?> asInstanceOf(
      macro.TypeDeclaration declaration) {
    InterfaceType? result;

    if (declaration case HasElement(:var element)) {
      if (element is InterfaceElementImpl) {
        result = type.asInstanceOf(element);
      }
    }

    return Future.value(switch (result) {
      var type? =>
        introspector._dartTypeToStaticType(type) as _InterfaceTypeImpl,
      _ => null,
    });
  }

  @override
  Future<bool> isExactly(_StaticTypeImpl other) {
    var result = type == other.type;
    return Future.value(result);
  }

  @override
  Future<bool> isSubtypeOf(_StaticTypeImpl other) {
    var result = typeSystem.isSubtypeOf(type, other.type);
    return Future.value(result);
  }
}

class _TypePhaseIntrospector implements macro.TypePhaseIntrospector {
  final LibraryMacroApplier applier;
  final LinkedElementFactory elementFactory;
  final DeclarationBuilder declarationBuilder;
  final OperationPerformanceImpl performance;

  _TypePhaseIntrospector(
    this.applier,
    this.elementFactory,
    this.declarationBuilder,
    this.performance,
  );

  MacroProcessing? get macroProcessing {
    return applier.currentApplication?.target.library.macroProcessing;
  }

  @override
  Future<macro.Identifier> resolveIdentifier(Uri library, String name) async {
    var libraryElement = elementFactory.libraryOfUri2(library);
    macroProcessing?.hasAnyIntrospection = true;

    var element = libraryElement.exportNamespace.get(name);
    if (element is PropertyAccessorElement && element.isSynthetic) {
      element = element.variable2;
    }

    if (element == null) {
      throw macro.MacroImplementationExceptionImpl(
        [
          'Unresolved identifier.',
          'library: $library',
          'name: $name',
        ].join('\n'),
        stackTrace: StackTrace.current.toString(),
      );
    }

    return declarationBuilder.fromElement.identifier(element);
  }
}

extension on macro.MacroExecutionResult {
  bool get isNotEmpty =>
      enumValueAugmentations.isNotEmpty ||
      extendsTypeAugmentations.isNotEmpty ||
      interfaceAugmentations.isNotEmpty ||
      libraryAugmentations.isNotEmpty ||
      mixinAugmentations.isNotEmpty ||
      typeAugmentations.isNotEmpty;
}
