// Copyright (c) 2019, 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/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/scope.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/scope.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/summary2/function_type_builder.dart';
import 'package:analyzer/src/summary2/link.dart';
import 'package:analyzer/src/summary2/linking_node_scope.dart';
import 'package:analyzer/src/summary2/named_type_builder.dart';
import 'package:analyzer/src/summary2/record_type_builder.dart';
import 'package:analyzer/src/summary2/types_builder.dart';
import 'package:analyzer/src/utilities/extensions/element.dart';

/// Recursive visitor of LinkedNodes that resolves explicit type annotations
/// in outlines.  This includes resolving element references in identifiers
/// in type annotation, and setting LinkedNodeTypes for corresponding type
/// annotation nodes.
///
/// Declarations that have type annotations, e.g. return types of methods, get
/// the corresponding type set (so, if there is an explicit type annotation,
/// the type is set, otherwise we keep it empty, so we will attempt to infer
/// it later).
class ReferenceResolver extends ThrowingAstVisitor<void> {
  final Linker linker;
  final TypeSystemImpl typeSystem;
  final NodesToBuildType nodesToBuildType;

  Scope scope;

  ReferenceResolver(
    this.linker,
    this.nodesToBuildType,
    this.typeSystem,
    this.scope,
  );

  @override
  void visitAnnotation(covariant AnnotationImpl node) {
    if (node.arguments != null) {
      var identifier = node.name;
      if (identifier is PrefixedIdentifierImpl) {
        var prefixNode = identifier.prefix;
        var prefixElement = scope.lookup(prefixNode.name).getter;
        prefixNode.element = prefixElement;

        if (prefixElement is PrefixElement) {
          var name = identifier.identifier.name;
          var element = prefixElement.scope.lookup(name).getter;
          identifier.identifier.element = element;
        }
      } else if (identifier is SimpleIdentifierImpl) {
        var element = scope.lookup(identifier.name).getter;
        identifier.element = element;
        return;
      }
    }
  }

  @override
  void visitBlockFunctionBody(BlockFunctionBody node) {}

  @override
  void visitClassDeclaration(covariant ClassDeclarationImpl node) {
    var outerScope = scope;

    var element = node.declaredFragment!;

    scope = TypeParameterScope(
      scope,
      element.typeParameters.map((e) => e.asElement2).toList(),
    );

    node.metadata.accept(this);
    node.typeParameters?.accept(this);
    node.extendsClause?.accept(this);
    node.withClause?.accept(this);
    node.implementsClause?.accept(this);

    scope = InstanceScope(scope, element.asElement2);
    LinkingNodeContext(node, scope);

    node.members.accept(this);
    nodesToBuildType.addDeclaration(node);

    scope = outerScope;
  }

  @override
  void visitClassTypeAlias(covariant ClassTypeAliasImpl node) {
    var outerScope = scope;

    var element = node.declaredFragment!;

    scope = TypeParameterScope(
      scope,
      element.typeParameters.map((e) => e.asElement2).toList(),
    );
    LinkingNodeContext(node, scope);

    node.metadata.accept(this);
    node.typeParameters?.accept(this);
    node.superclass.accept(this);
    node.withClause.accept(this);
    node.implementsClause?.accept(this);
    nodesToBuildType.addDeclaration(node);

    scope = outerScope;
  }

  @override
  void visitCompilationUnit(CompilationUnit node) {
    LinkingNodeContext(node, scope);
    node.declarations.accept(this);
  }

  @override
  void visitConstructorDeclaration(covariant ConstructorDeclarationImpl node) {
    var outerScope = scope;

    var element = node.declaredFragment!.element;

    scope = TypeParameterScope(scope, element.typeParameters);
    LinkingNodeContext(node, scope);

    node.metadata.accept(this);
    node.parameters.accept(this);

    scope = outerScope;
  }

  @override
  void visitDefaultFormalParameter(DefaultFormalParameter node) {
    LinkingNodeContext(node, scope);
    node.parameter.accept(this);
  }

  @override
  void visitEnumDeclaration(covariant EnumDeclarationImpl node) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;
    var element = fragment.element;

    scope = TypeParameterScope(
      scope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );

    node.metadata.accept(this);
    node.typeParameters?.accept(this);
    node.implementsClause?.accept(this);
    node.withClause?.accept(this);

    scope = InstanceScope(scope, element);
    LinkingNodeContext(node, scope);

    node.members.accept(this);
    nodesToBuildType.addDeclaration(node);

    for (var field in fragment.fields) {
      var node = linker.elementNodes[field];
      if (node != null) {
        LinkingNodeContext(node, scope);
      }
    }

    scope = outerScope;
  }

  @override
  void visitExpressionFunctionBody(ExpressionFunctionBody node) {}

  @override
  void visitExtendsClause(ExtendsClause node) {
    node.superclass.accept(this);
  }

  @override
  void visitExtensionDeclaration(covariant ExtensionDeclarationImpl node) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;

    scope = TypeParameterScope(
      scope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );

    node.metadata.accept(this);
    node.typeParameters?.accept(this);
    node.onClause?.accept(this);

    scope = ExtensionScope(scope, fragment.asElement2);
    LinkingNodeContext(node, scope);

    node.members.accept(this);
    nodesToBuildType.addDeclaration(node);

    scope = outerScope;
  }

  @override
  void visitExtensionOnClause(ExtensionOnClause node) {
    node.extendedType.accept(this);
  }

  @override
  void visitExtensionTypeDeclaration(
    covariant ExtensionTypeDeclarationImpl node,
  ) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;

    scope = TypeParameterScope(
      scope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );

    node.metadata.accept(this);
    node.typeParameters?.accept(this);
    node.representation.accept(this);
    node.implementsClause?.accept(this);

    scope = InstanceScope(scope, fragment.asElement2);
    LinkingNodeContext(node, scope);
    LinkingNodeContext(node.representation, scope);

    node.members.accept(this);
    nodesToBuildType.addDeclaration(node);

    scope = outerScope;
  }

  @override
  void visitFieldDeclaration(FieldDeclaration node) {
    node.metadata.accept(this);
    node.fields.accept(this);
  }

  @override
  void visitFieldFormalParameter(covariant FieldFormalParameterImpl node) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;

    scope = TypeParameterScope(
      scope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );

    node.type?.accept(this);
    node.typeParameters?.accept(this);
    node.parameters?.accept(this);
    nodesToBuildType.addDeclaration(node);

    scope = outerScope;
  }

  @override
  void visitFormalParameterList(FormalParameterList node) {
    node.parameters.accept(this);
  }

  @override
  void visitFunctionDeclaration(covariant FunctionDeclarationImpl node) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;

    scope = TypeParameterScope(
      outerScope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );
    LinkingNodeContext(node, scope);

    node.metadata.accept(this);
    node.returnType?.accept(this);
    node.functionExpression.accept(this);
    nodesToBuildType.addDeclaration(node);

    scope = outerScope;
  }

  @override
  void visitFunctionExpression(FunctionExpression node) {
    node.typeParameters?.accept(this);
    node.parameters?.accept(this);
  }

  @override
  void visitFunctionTypeAlias(covariant FunctionTypeAliasImpl node) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;

    scope = TypeParameterScope(
      outerScope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );

    node.returnType?.accept(this);
    node.typeParameters?.accept(this);
    node.parameters.accept(this);

    nodesToBuildType.addDeclaration(node);

    scope = outerScope;
  }

  @override
  void visitFunctionTypedFormalParameter(
    covariant FunctionTypedFormalParameterImpl node,
  ) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;

    scope = TypeParameterScope(
      scope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );

    node.returnType?.accept(this);
    node.typeParameters?.accept(this);
    node.parameters.accept(this);
    nodesToBuildType.addDeclaration(node);

    scope = outerScope;
  }

  @override
  void visitGenericFunctionType(covariant GenericFunctionTypeImpl node) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;
    scope = TypeParameterScope(
      outerScope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );

    node.returnType?.accept(this);
    node.typeParameters?.accept(this);
    node.parameters.accept(this);

    var nullabilitySuffix = _getNullabilitySuffix(node.question != null);
    var builder = FunctionTypeBuilder.of(node, nullabilitySuffix);
    node.type = builder;
    nodesToBuildType.addDeclaration(node);
    nodesToBuildType.addTypeBuilder(builder);

    scope = outerScope;
  }

  @override
  void visitGenericTypeAlias(covariant GenericTypeAliasImpl node) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;

    scope = TypeParameterScope(
      outerScope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );

    node.metadata.accept(this);
    node.typeParameters?.accept(this);
    node.type.accept(this);
    nodesToBuildType.addDeclaration(node);

    var aliasedType = node.type;
    if (aliasedType is GenericFunctionTypeImpl) {
      fragment.encloseElement(
        aliasedType.declaredFragment as GenericFunctionTypeFragmentImpl,
      );
    }

    scope = outerScope;
  }

  @override
  void visitImplementsClause(ImplementsClause node) {
    node.interfaces.accept(this);
  }

  @override
  void visitMethodDeclaration(covariant MethodDeclarationImpl node) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;

    scope = TypeParameterScope(
      scope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );
    LinkingNodeContext(node, scope);

    node.metadata.accept(this);
    node.returnType?.accept(this);
    node.typeParameters?.accept(this);
    node.parameters?.accept(this);
    nodesToBuildType.addDeclaration(node);

    scope = outerScope;
  }

  @override
  void visitMixinDeclaration(covariant MixinDeclarationImpl node) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;

    scope = TypeParameterScope(
      scope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );

    node.metadata.accept(this);
    node.typeParameters?.accept(this);
    node.onClause?.accept(this);
    node.implementsClause?.accept(this);

    scope = InstanceScope(scope, fragment.asElement2);
    LinkingNodeContext(node, scope);

    node.members.accept(this);
    nodesToBuildType.addDeclaration(node);

    scope = outerScope;
  }

  @override
  void visitMixinOnClause(MixinOnClause node) {
    node.superclassConstraints.accept(this);
  }

  @override
  void visitNamedType(covariant NamedTypeImpl node) {
    Element? element;
    var importPrefix = node.importPrefix;
    if (importPrefix != null) {
      var prefixToken = importPrefix.name;
      var prefixName = prefixToken.lexeme;
      var prefixElement = scope.lookup(prefixName).getter;
      importPrefix.element = prefixElement;

      if (prefixElement is PrefixElement) {
        var name = node.name.lexeme;
        element = prefixElement.scope.lookup(name).getter;
      }
    } else {
      var name = node.name.lexeme;

      if (name == 'void') {
        node.type = VoidTypeImpl.instance;
        return;
      }

      element = scope.lookup(name).getter;
    }
    node.element = element;

    node.typeArguments?.accept(this);

    var nullabilitySuffix = _getNullabilitySuffix(node.question != null);
    if (element == null) {
      node.type = InvalidTypeImpl.instance;
    } else if (element is TypeParameterElementImpl) {
      node.type = TypeParameterTypeImpl(
        element: element,
        nullabilitySuffix: nullabilitySuffix,
      );
    } else {
      var builder = NamedTypeBuilder.of(
        linker: linker,
        typeSystem: typeSystem,
        node: node,
        element: element,
        nullabilitySuffix: nullabilitySuffix,
      );
      node.type = builder;
      nodesToBuildType.addTypeBuilder(builder);
    }
  }

  @override
  void visitRecordTypeAnnotation(covariant RecordTypeAnnotationImpl node) {
    node.positionalFields.accept(this);
    node.namedFields?.accept(this);

    var builder = RecordTypeBuilder.of(typeSystem, node);
    node.type = builder;
    nodesToBuildType.addTypeBuilder(builder);
  }

  @override
  void visitRecordTypeAnnotationNamedField(
    RecordTypeAnnotationNamedField node,
  ) {
    node.type.accept(this);
  }

  @override
  void visitRecordTypeAnnotationNamedFields(
    RecordTypeAnnotationNamedFields node,
  ) {
    node.fields.accept(this);
  }

  @override
  void visitRecordTypeAnnotationPositionalField(
    RecordTypeAnnotationPositionalField node,
  ) {
    node.type.accept(this);
  }

  @override
  void visitRepresentationDeclaration(RepresentationDeclaration node) {
    node.fieldType.accept(this);
  }

  @override
  void visitSimpleFormalParameter(SimpleFormalParameter node) {
    node.type?.accept(this);
    nodesToBuildType.addDeclaration(node);
  }

  @override
  void visitSuperFormalParameter(covariant SuperFormalParameterImpl node) {
    var outerScope = scope;

    var fragment = node.declaredFragment!;

    scope = TypeParameterScope(
      scope,
      fragment.typeParameters.map((e) => e.asElement2).toList(),
    );

    node.type?.accept(this);
    node.typeParameters?.accept(this);
    node.parameters?.accept(this);
    nodesToBuildType.addDeclaration(node);

    scope = outerScope;
  }

  @override
  void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
    node.metadata.accept(this);
    node.variables.accept(this);
  }

  @override
  void visitTypeArgumentList(TypeArgumentList node) {
    node.arguments.accept(this);
  }

  @override
  void visitTypeParameter(covariant TypeParameterImpl node) {
    var bound = node.bound;
    if (bound != null) {
      bound.accept(this);
      var fragment = node.declaredFragment!;
      if (fragment.previousFragment == null) {
        fragment.element.bound = bound.type;
      }
      nodesToBuildType.addDeclaration(node);
    }
  }

  @override
  void visitTypeParameterList(TypeParameterList node) {
    node.typeParameters.accept(this);
  }

  @override
  void visitVariableDeclarationList(
    covariant VariableDeclarationListImpl node,
  ) {
    node.type?.accept(this);
    nodesToBuildType.addDeclaration(node);

    for (var variable in node.variables) {
      var fragment = variable.declaredFragment!;
      var node = linker.elementNodes[fragment]!;
      LinkingNodeContext(node, scope);
    }
  }

  @override
  void visitWithClause(WithClause node) {
    node.mixinTypes.accept(this);
  }

  NullabilitySuffix _getNullabilitySuffix(bool hasQuestion) {
    if (hasQuestion) {
      return NullabilitySuffix.question;
    } else {
      return NullabilitySuffix.none;
    }
  }
}
