// 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/element/element.dart';
import 'package:analyzer/dart/element/scope.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/error/codes.dart';

/// Handles possible rewrites of AST.
///
/// When code is initially parsed, many assumptions are made which may be
/// incorrect given newer Dart syntax. For example, `new a.b()` is parsed as an
/// [InstanceCreationExpression], but `a.b()` (without `new`) is parsed as a
/// [MethodInvocation]. The public methods of this class carry out the minimal
/// amount of resolution in order to determine whether a node (and its
/// descendants) should be replaced by another, and perform such replacements.
///
/// The public methods of this class form a complete accounting of possible
/// node replacements.
class AstRewriter {
  final DiagnosticReporter _diagnosticReporter;

  AstRewriter(this._diagnosticReporter);

  /// Possibly rewrites [node] as a [MethodInvocation] with a
  /// [FunctionReference] target.
  ///
  /// Code such as `a<...>.b(...);` (or with a prefix such as `p.a<...>.b(...)`)
  /// is parsed as an [ExpressionStatement] with an [InstanceCreationExpression]
  /// with `a<...>.b` as the [ConstructorName] (which has 'type' of `a<...>`
  /// and 'name' of `b`). The [InstanceCreationExpression] is rewritten as a
  /// [MethodInvocation] if `a` resolves to a function.
  AstNode instanceCreationExpression(
    Scope nameScope,
    InstanceCreationExpressionImpl node,
  ) {
    if (node.keyword != null) {
      // Either `new` or `const` has been specified.
      return node;
    }
    var typeNode = node.constructorName.type;
    var importPrefix = typeNode.importPrefix;
    if (importPrefix == null) {
      var element = nameScope.lookup(typeNode.name.lexeme).getter;
      if (element is ExecutableElement) {
        return _toMethodInvocationOfFunctionReference(
          node: node,
          function: SimpleIdentifierImpl(token: typeNode.name),
        );
      } else if (element is TypeAliasElementImpl &&
          element.aliasedType is FunctionType) {
        return _toMethodInvocationOfAliasedTypeLiteral(
          node: node,
          function: SimpleIdentifierImpl(token: typeNode.name),
          element: element,
        );
      }
    } else {
      var prefixName = importPrefix.name.lexeme;
      var prefixElement = nameScope.lookup(prefixName).getter;
      if (prefixElement is PrefixElement) {
        var prefixedName = typeNode.name.lexeme;
        var element = prefixElement.scope.lookup(prefixedName).getter;
        if (element is TopLevelFunctionElement) {
          return _toMethodInvocationOfFunctionReference(
            node: node,
            function: PrefixedIdentifierImpl(
              prefix: SimpleIdentifierImpl(token: importPrefix.name),
              period: importPrefix.period,
              identifier: SimpleIdentifierImpl(token: typeNode.name),
            ),
          );
        } else if (element is TypeAliasElementImpl &&
            element.aliasedType is FunctionType) {
          return _toMethodInvocationOfAliasedTypeLiteral(
            node: node,
            function: PrefixedIdentifierImpl(
              prefix: SimpleIdentifierImpl(token: importPrefix.name),
              period: importPrefix.period,
              identifier: SimpleIdentifierImpl(token: typeNode.name),
            ),
            element: element,
          );
        }

        // If `element` is a [ClassElement], or a [TypeAliasElement] aliasing
        // an interface type, then this indeed looks like a constructor call; do
        // not rewrite `node`.

        // If `element` is a [TypeAliasElement] aliasing a function type, then
        // this looks like an attempt type instantiate a function type alias
        // (which is not a feature), and then call a method on the resulting
        // [Type] object; no not rewrite `node`.

        // If `typeName.identifier` cannot be resolved, do not rewrite `node`.
        return node;
      } else {
        // In the case that `prefixElement` is not a [PrefixElement], then
        // `typeName`, as a [PrefixedIdentifier], cannot refer to a class or an
        // aliased type; rewrite `node` as a [MethodInvocation].
        return _toMethodInvocationOfFunctionReference(
          node: node,
          function: PrefixedIdentifierImpl(
            prefix: SimpleIdentifierImpl(token: importPrefix.name),
            period: importPrefix.period,
            identifier: SimpleIdentifierImpl(token: typeNode.name),
          ),
        );
      }
    }

    return node;
  }

  /// Possibly rewrites [node] as an [ExtensionOverride] or as an
  /// [InstanceCreationExpression].
  AstNode methodInvocation(Scope nameScope, MethodInvocationImpl node) {
    var methodName = node.methodName;
    if (methodName.isSynthetic) {
      // This isn't a constructor invocation because the method name is
      // synthetic.
      return node;
    }

    var target = node.target;
    var operator = node.operator;
    if (target == null) {
      // Possible cases: C() or C<>()
      if (node.realTarget != null) {
        // This isn't a constructor invocation because it's in a cascade.
        return node;
      }
      var element = nameScope.lookup(methodName.name).getter;
      if (element is InterfaceElement) {
        return _toInstanceCreation_type(node: node, typeIdentifier: methodName);
      } else if (element is ExtensionElementImpl) {
        var extensionOverride = ExtensionOverrideImpl(
          importPrefix: null,
          name: methodName.token,
          element: element,
          typeArguments: node.typeArguments,
          argumentList: node.argumentList,
        );
        NodeReplacer.replace(node, extensionOverride);
        return extensionOverride;
      } else if (element is TypeAliasElement &&
          element.aliasedType is InterfaceType) {
        return _toInstanceCreation_type(node: node, typeIdentifier: methodName);
      }
    } else if (target is SimpleIdentifierImpl && operator != null) {
      // Possible cases: C.n(), p.C() or p.C<>()
      if (node.isNullAware) {
        // This isn't a constructor invocation because a null aware operator is
        // being used.
      }
      var element = nameScope.lookup(target.name).getter;
      if (element is InterfaceElement) {
        // class C { C.named(); }
        // C.named()
        return _toInstanceCreation_type_constructor(
          node: node,
          typeIdentifier: target,
          constructorIdentifier: methodName,
          classElement: element,
        );
      } else if (element is PrefixElement) {
        // Possible cases: p.C() or p.C<>()
        var prefixedElement = element.scope.lookup(methodName.name).getter;
        if (prefixedElement is InterfaceElement) {
          return _toInstanceCreation_prefix_type(
            node: node,
            prefixIdentifier: target,
            typeIdentifier: methodName,
          );
        } else if (prefixedElement is ExtensionElementImpl) {
          var extensionOverride = ExtensionOverrideImpl(
            importPrefix: ImportPrefixReferenceImpl(
              name: target.token,
              period: operator,
            )..element = element,
            name: node.methodName.token,
            element: prefixedElement,
            typeArguments: node.typeArguments,
            argumentList: node.argumentList,
          );
          NodeReplacer.replace(node, extensionOverride);
          return extensionOverride;
        } else if (prefixedElement is TypeAliasElement &&
            prefixedElement.aliasedType is InterfaceType) {
          return _toInstanceCreation_prefix_type(
            node: node,
            prefixIdentifier: target,
            typeIdentifier: methodName,
          );
        }
      } else if (element is TypeAliasElement) {
        var aliasedType = element.aliasedType;
        if (aliasedType is InterfaceType) {
          // class C { C.named(); }
          // typedef X = C;
          // X.named()
          return _toInstanceCreation_type_constructor(
            node: node,
            typeIdentifier: target,
            constructorIdentifier: methodName,
            classElement: aliasedType.element,
          );
        }
      }
    } else if (target is PrefixedIdentifierImpl) {
      // Possible case: p.C.n()
      var prefixElement = nameScope.lookup(target.prefix.name).getter;
      target.prefix.element = prefixElement;
      if (prefixElement is PrefixElement) {
        var prefixedName = target.identifier.name;
        var element = prefixElement.scope.lookup(prefixedName).getter;
        if (element is InterfaceElement) {
          return _instanceCreation_prefix_type_name(
            node: node,
            typeNameIdentifier: target,
            constructorIdentifier: methodName,
            classElement: element,
          );
        } else if (element is TypeAliasElement) {
          var aliasedType = element.aliasedType;
          if (aliasedType is InterfaceType) {
            return _instanceCreation_prefix_type_name(
              node: node,
              typeNameIdentifier: target,
              constructorIdentifier: methodName,
              classElement: aliasedType.element,
            );
          }
        }
      }
    }
    return node;
  }

  /// Possibly rewrites [node] as a [ConstructorReference].
  ///
  /// Code such as `List.filled;` is parsed as (an [ExpressionStatement] with) a
  /// [PrefixedIdentifier] with 'prefix' of `List` and 'identifier' of `filled`.
  /// The [PrefixedIdentifier] may need to be rewritten as a
  /// [ConstructorReference].
  AstNode prefixedIdentifier(Scope nameScope, PrefixedIdentifierImpl node) {
    var parent = node.parent;
    if (parent is Annotation) {
      // An annotations which is a const constructor invocation can initially be
      // represented with a [PrefixedIdentifier]. Do not rewrite such nodes.
      return node;
    }
    if (parent is CommentReferenceImpl) {
      // TODO(srawlins): This probably should be allowed to be rewritten to a
      // [ConstructorReference] at some point.
      return node;
    }
    if (parent is AssignmentExpressionImpl && parent.leftHandSide == node) {
      // A constructor cannot be assigned to, in some expression like
      // `C.new = foo`; do not rewrite.
      return node;
    }
    var identifier = node.identifier;
    if (identifier.isSynthetic) {
      // This isn't a constructor reference.
      return node;
    }
    var prefix = node.prefix;
    var prefixElement = nameScope.lookup(prefix.name).getter;
    if (parent is ConstantPattern && prefixElement is PrefixElement) {
      var element = prefixElement.scope.lookup(node.identifier.name).getter;
      if (element is TypeDefiningElement) {
        return _toPatternTypeLiteral(parent, node);
      }
    }
    if (prefixElement is InterfaceElement) {
      // Example:
      //     class C { C.named(); }
      //     C.named
      return _toConstructorReference_prefixed(
        node: node,
        classElement: prefixElement,
      );
    } else if (prefixElement is TypeAliasElement) {
      var aliasedType = prefixElement.aliasedType;
      if (aliasedType is InterfaceType) {
        // Example:
        //     class C { C.named(); }
        //     typedef X = C;
        //     X.named
        return _toConstructorReference_prefixed(
          node: node,
          classElement: aliasedType.element,
        );
      }
    }
    return node;
  }

  /// Possibly rewrites [node] as a [ConstructorReference].
  ///
  /// Code such as `async.Future.value;` is parsed as (an [ExpressionStatement]
  /// with) a [PropertyAccess] with a 'target' of [PrefixedIdentifier] (with
  /// 'prefix' of `List` and 'identifier' of `filled`) and a 'propertyName' of
  /// `value`. The [PropertyAccess] may need to be rewritten as a
  /// [ConstructorReference].
  AstNode propertyAccess(Scope nameScope, PropertyAccessImpl node) {
    if (node.isCascaded) {
      // For example, `List..filled`: this is a property access on an instance
      // `Type`.
      return node;
    }
    if (node.parent is CommentReferenceImpl) {
      // TODO(srawlins): This probably should be allowed to be rewritten to a
      // [ConstructorReference] at some point.
      return node;
    }
    var receiver = node.target!;

    IdentifierImpl receiverIdentifier;
    TypeArgumentListImpl? typeArguments;
    if (receiver is PrefixedIdentifierImpl) {
      receiverIdentifier = receiver;
    } else if (receiver is FunctionReferenceImpl) {
      // A [ConstructorReference] with explicit type arguments is initially
      // parsed as a [PropertyAccess] with a [FunctionReference] target; for
      // example: `List<int>.filled` or `core.List<int>.filled`.
      var function = receiver.function;
      if (function is! IdentifierImpl) {
        // If [receiverIdentifier] is not an Identifier then [node] is not a
        // ConstructorReference.
        return node;
      }
      receiverIdentifier = function;
      typeArguments = receiver.typeArguments;
    } else {
      // If the receiver is not (initially) a prefixed identifier or a function
      // reference, then [node] is not a constructor reference.
      return node;
    }

    Element? element;
    if (receiverIdentifier is SimpleIdentifierImpl) {
      element = nameScope.lookup(receiverIdentifier.name).getter;
    } else if (receiverIdentifier is PrefixedIdentifierImpl) {
      var prefixElement = nameScope
          .lookup(receiverIdentifier.prefix.name)
          .getter;
      if (prefixElement is PrefixElement) {
        element = prefixElement.scope
            .lookup(receiverIdentifier.identifier.name)
            .getter;
      } else {
        // This expression is something like `foo.List<int>.filled` where `foo`
        // is not an import prefix.
        // TODO(srawlins): Tease out a `null` prefixElement from others for
        // specific errors.
        return node;
      }
    }

    if (element is InterfaceElement) {
      // Example:
      //     class C<T> { C.named(); }
      //     C<int>.named
      return _toConstructorReference_propertyAccess(
        node: node,
        receiver: receiverIdentifier,
        typeArguments: typeArguments,
        classElement: element,
      );
    } else if (element is TypeAliasElement) {
      var aliasedType = element.aliasedType;
      if (aliasedType is InterfaceType) {
        // Example:
        //     class C<T> { C.named(); }
        //     typedef X<T> = C<T>;
        //     X<int>.named
        return _toConstructorReference_propertyAccess(
          node: node,
          receiver: receiverIdentifier,
          typeArguments: typeArguments,
          classElement: aliasedType.element,
        );
      }
    }

    // If [receiverIdentifier] is an Identifier, but could not be resolved to
    // an Element, we cannot assume [node] is a ConstructorReference.
    //
    // TODO(srawlins): However, take an example like `Lisst<int>.filled;`
    // (where 'Lisst' does not resolve to any element). Possibilities include:
    // the user tried to write a TypeLiteral or a FunctionReference, then access
    // a property on that (these include: hashCode, runtimeType, tearoff of
    // toString, and extension methods on Type); or the user tried to write a
    // ConstructReference. It seems much more likely that the user is trying to
    // do the latter. Consider doing the work so that the user gets an error in
    // this case about `Lisst` not being a type, or `Lisst.filled` not being a
    // known constructor.
    return node;
  }

  AstNode simpleIdentifier(Scope nameScope, SimpleIdentifierImpl node) {
    var parent = node.parent;
    if (parent is ConstantPattern) {
      var element = nameScope.lookup(node.name).getter;
      if (element is TypeDefiningElement) {
        return _toPatternTypeLiteral(parent, node);
      }
    }

    return node;
  }

  AstNode _instanceCreation_prefix_type_name({
    required MethodInvocationImpl node,
    required PrefixedIdentifierImpl typeNameIdentifier,
    required SimpleIdentifierImpl constructorIdentifier,
    required InterfaceElement classElement,
  }) {
    var constructorElement = classElement.getNamedConstructor(
      constructorIdentifier.name,
    );
    if (constructorElement == null) {
      return node;
    }

    var typeArguments = node.typeArguments;
    if (typeArguments != null) {
      _diagnosticReporter.atNode(
        typeArguments,
        CompileTimeErrorCode.wrongNumberOfTypeArgumentsConstructor,
        arguments: [typeNameIdentifier.toString(), constructorIdentifier.name],
      );
    }

    var typeName = NamedTypeImpl(
      importPrefix: ImportPrefixReferenceImpl(
        name: typeNameIdentifier.prefix.token,
        period: typeNameIdentifier.period,
      ),
      name: typeNameIdentifier.identifier.token,
      typeArguments: typeArguments,
      question: null,
    );
    var constructorName = ConstructorNameImpl(
      type: typeName,
      period: node.operator,
      name: constructorIdentifier,
    );
    var instanceCreationExpression = InstanceCreationExpressionImpl(
      keyword: null,
      constructorName: constructorName,
      argumentList: node.argumentList,
      typeArguments: null,
    );
    NodeReplacer.replace(node, instanceCreationExpression);
    return instanceCreationExpression;
  }

  AstNode _toConstructorReference_prefixed({
    required PrefixedIdentifierImpl node,
    required InterfaceElement classElement,
  }) {
    var name = node.identifier.name;
    var constructorElement = name == 'new'
        ? classElement.unnamedConstructor
        : classElement.getNamedConstructor(name);
    if (constructorElement == null) {
      return node;
    }

    var typeName = NamedTypeImpl(
      importPrefix: null,
      name: node.prefix.token,
      typeArguments: null,
      question: null,
    );
    var constructorName = ConstructorNameImpl(
      type: typeName,
      period: node.period,
      name: node.identifier,
    );
    var constructorReference = ConstructorReferenceImpl(
      constructorName: constructorName,
    );
    NodeReplacer.replace(node, constructorReference);
    return constructorReference;
  }

  AstNode _toConstructorReference_propertyAccess({
    required PropertyAccessImpl node,
    required IdentifierImpl receiver,
    required TypeArgumentListImpl? typeArguments,
    required InterfaceElement classElement,
  }) {
    var name = node.propertyName.name;
    var constructorElement = name == 'new'
        ? classElement.unnamedConstructor
        : classElement.getNamedConstructor(name);
    if (constructorElement == null && typeArguments == null) {
      // If there is no constructor by this name, and no type arguments,
      // do not rewrite the node. If there _are_ type arguments (like
      // `prefix.C<int>.name`, then it looks more like a constructor tearoff
      // than anything else, so continue with the rewrite.
      return node;
    }

    var operator = node.operator;

    var typeName = receiver.toNamedType(
      typeArguments: typeArguments,
      question: null,
    );
    var constructorName = ConstructorNameImpl(
      type: typeName,
      period: operator,
      name: node.propertyName,
    );
    var constructorReference = ConstructorReferenceImpl(
      constructorName: constructorName,
    );
    NodeReplacer.replace(node, constructorReference);
    return constructorReference;
  }

  InstanceCreationExpression _toInstanceCreation_prefix_type({
    required MethodInvocationImpl node,
    required SimpleIdentifierImpl prefixIdentifier,
    required SimpleIdentifierImpl typeIdentifier,
  }) {
    var typeName = NamedTypeImpl(
      importPrefix: ImportPrefixReferenceImpl(
        name: prefixIdentifier.token,
        period: node.operator!,
      ),
      name: typeIdentifier.token,
      typeArguments: node.typeArguments,
      question: null,
    );
    var constructorName = ConstructorNameImpl(
      type: typeName,
      period: null,
      name: null,
    );
    var instanceCreationExpression = InstanceCreationExpressionImpl(
      keyword: null,
      constructorName: constructorName,
      argumentList: node.argumentList,
      typeArguments: null,
    );
    NodeReplacer.replace(node, instanceCreationExpression);
    return instanceCreationExpression;
  }

  InstanceCreationExpression _toInstanceCreation_type({
    required MethodInvocationImpl node,
    required SimpleIdentifierImpl typeIdentifier,
  }) {
    var typeName = NamedTypeImpl(
      importPrefix: null,
      name: typeIdentifier.token,
      typeArguments: node.typeArguments,
      question: null,
    );
    var constructorName = ConstructorNameImpl(
      type: typeName,
      period: null,
      name: null,
    );
    var instanceCreationExpression = InstanceCreationExpressionImpl(
      keyword: null,
      constructorName: constructorName,
      argumentList: node.argumentList,
      typeArguments: null,
    );
    NodeReplacer.replace(node, instanceCreationExpression);
    return instanceCreationExpression;
  }

  AstNode _toInstanceCreation_type_constructor({
    required MethodInvocationImpl node,
    required SimpleIdentifierImpl typeIdentifier,
    required SimpleIdentifierImpl constructorIdentifier,
    required InterfaceElement classElement,
  }) {
    var name = constructorIdentifier.name;
    var constructorElement = classElement.getNamedConstructor(name);
    if (constructorElement == null) {
      return node;
    }

    var typeArguments = node.typeArguments;
    if (typeArguments != null) {
      _diagnosticReporter.atNode(
        typeArguments,
        CompileTimeErrorCode.wrongNumberOfTypeArgumentsConstructor,
        arguments: [typeIdentifier.name, constructorIdentifier.name],
      );
    }
    var typeName = NamedTypeImpl(
      importPrefix: null,
      name: typeIdentifier.token,
      typeArguments: null,
      question: null,
    );
    var constructorName = ConstructorNameImpl(
      type: typeName,
      period: node.operator,
      name: constructorIdentifier,
    );
    // TODO(scheglov): I think we should drop "typeArguments" below.
    var instanceCreationExpression = InstanceCreationExpressionImpl(
      keyword: null,
      constructorName: constructorName,
      argumentList: node.argumentList,
      typeArguments: typeArguments,
    );
    NodeReplacer.replace(node, instanceCreationExpression);
    return instanceCreationExpression;
  }

  MethodInvocation _toMethodInvocationOfAliasedTypeLiteral({
    required InstanceCreationExpressionImpl node,
    required Identifier function,
    required TypeAliasElementImpl element,
  }) {
    var typeName = NamedTypeImpl(
      importPrefix: node.constructorName.type.importPrefix,
      name: node.constructorName.type.name,
      typeArguments: node.constructorName.type.typeArguments,
      question: null,
    );
    typeName.type = element.aliasedType;
    var typeLiteral = TypeLiteralImpl(type: typeName);
    var methodInvocation = MethodInvocationImpl(
      target: typeLiteral,
      operator: node.constructorName.period,
      methodName: node.constructorName.name!,
      typeArguments: null,
      argumentList: node.argumentList,
    );
    NodeReplacer.replace(node, methodInvocation);
    return methodInvocation;
  }

  AstNode _toMethodInvocationOfFunctionReference({
    required InstanceCreationExpressionImpl node,
    required IdentifierImpl function,
  }) {
    var period = node.constructorName.period;
    var constructorId = node.constructorName.name;
    if (period == null || constructorId == null) {
      return node;
    }

    var functionReference = FunctionReferenceImpl(
      function: function,
      typeArguments: node.constructorName.type.typeArguments,
    );
    var methodInvocation = MethodInvocationImpl(
      target: functionReference,
      operator: period,
      methodName: constructorId,
      typeArguments: null,
      argumentList: node.argumentList,
    );
    NodeReplacer.replace(node, methodInvocation);
    return methodInvocation;
  }

  TypeLiteralImpl _toPatternTypeLiteral(
    ConstantPattern parent,
    IdentifierImpl node,
  ) {
    var result = TypeLiteralImpl(
      type: node.toNamedType(typeArguments: null, question: null),
    );
    NodeReplacer.replace(node, result, parent: parent);
    return result;
  }
}
