// Copyright (c) 2020, 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/syntactic_entity.dart';
import 'package:analyzer/dart/element/element2.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/error/hint_codes.dart';
import 'package:analyzer/src/utilities/extensions/element.dart';
import 'package:analyzer/src/workspace/workspace.dart';
import 'package:collection/collection.dart';

abstract class BaseDeprecatedMemberUseVerifier {
  /// We push a new value every time when we enter into a scope which
  /// can be marked as deprecated - a class, a method, fields (multiple).
  final List<bool> _inDeprecatedMemberStack = [false];

  final bool _strictCasts;

  BaseDeprecatedMemberUseVerifier({bool strictCasts = false})
      : _strictCasts = strictCasts;

  void assignmentExpression(AssignmentExpression node) {
    _checkForDeprecated(node.readElement2, node.leftHandSide);
    _checkForDeprecated(node.writeElement2, node.leftHandSide);
    _checkForDeprecated(node.element, node);
  }

  void binaryExpression(BinaryExpression node) {
    _checkForDeprecated(node.element, node);
  }

  void constructorName(ConstructorName node) {
    _checkForDeprecated(node.element, node);
  }

  void exportDirective(ExportDirective node) {
    _checkForDeprecated(node.libraryExport?.exportedLibrary2, node);
  }

  void extensionOverride(ExtensionOverride node) {
    _checkForDeprecated(node.element2, node);
  }

  void functionExpressionInvocation(FunctionExpressionInvocation node) {
    var callElement = node.element;
    if (callElement is MethodElement2 &&
        callElement.name3 == MethodElement2.CALL_METHOD_NAME) {
      _checkForDeprecated(callElement, node);
    }
  }

  void importDirective(ImportDirective node) {
    _checkForDeprecated(node.libraryImport?.importedLibrary2, node);
  }

  void indexExpression(IndexExpression node) {
    _checkForDeprecated(node.element, node);
  }

  void instanceCreationExpression(InstanceCreationExpression node) {
    _invocationArguments(
      node.constructorName.element,
      node.argumentList,
    );
  }

  void methodInvocation(MethodInvocation node) {
    _invocationArguments(
      node.methodName.element,
      node.argumentList,
    );
  }

  void namedType(NamedType node) {
    _checkForDeprecated(node.element2, node);
  }

  void patternField(PatternField node) {
    _checkForDeprecated(node.element2, node);
  }

  void popInDeprecated() {
    _inDeprecatedMemberStack.removeLast();
  }

  void postfixExpression(PostfixExpression node) {
    _checkForDeprecated(node.readElement2, node.operand);
    _checkForDeprecated(node.writeElement2, node.operand);
    _checkForDeprecated(node.element, node);
  }

  void prefixExpression(PrefixExpression node) {
    _checkForDeprecated(node.readElement2, node.operand);
    _checkForDeprecated(node.writeElement2, node.operand);
    _checkForDeprecated(node.element, node);
  }

  void pushInDeprecatedMetadata(List<Annotation> metadata) {
    var hasDeprecated = _hasDeprecatedAnnotation(metadata);
    pushInDeprecatedValue(hasDeprecated);
  }

  void pushInDeprecatedValue(bool value) {
    var newValue = _inDeprecatedMemberStack.last || value;
    _inDeprecatedMemberStack.add(newValue);
  }

  void redirectingConstructorInvocation(RedirectingConstructorInvocation node) {
    _checkForDeprecated(node.element, node);
    _invocationArguments(node.element, node.argumentList);
  }

  void reportError(SyntacticEntity errorEntity, Element2 element,
      String displayName, String? message) {
    reportError2(errorEntity, element, displayName, message);
  }

  void reportError2(SyntacticEntity errorEntity, Element2 element,
      String displayName, String? message) {
    reportError(errorEntity, element, displayName, message);
  }

  void simpleIdentifier(SimpleIdentifier node) {
    // Don't report declared identifiers.
    if (node.inDeclarationContext()) {
      return;
    }

    // Report full ConstructorName, not just the constructor name.
    var parent = node.parent;
    if (parent is ConstructorName && identical(node, parent.name)) {
      return;
    }

    // Report full SuperConstructorInvocation, not just the constructor name.
    if (parent is SuperConstructorInvocation &&
        identical(node, parent.constructorName)) {
      return;
    }

    // HideCombinator is forgiving.
    if (parent is HideCombinator) {
      return;
    }

    _simpleIdentifier(node);
  }

  void superConstructorInvocation(SuperConstructorInvocation node) {
    _checkForDeprecated(node.element, node);
    _invocationArguments(node.element, node.argumentList);
  }

  /// Given some [element], look at the associated metadata and report the use
  /// of the member if it is declared as deprecated. If a diagnostic is reported
  /// it should be reported at the given [node].
  void _checkForDeprecated(Element2? element, AstNode node) {
    if (!_isDeprecated(element)) {
      return;
    }

    if (_inDeprecatedMemberStack.last) {
      return;
    }

    if (_isLocalParameter(element, node)) {
      return;
    }

    if (element is FormalParameterElement && element.isRequired) {
      return;
    }

    SyntacticEntity errorEntity = node;
    var parent = node.parent;
    if (parent is AssignmentExpression && parent.leftHandSide == node) {
      if (node is SimpleIdentifier) {
        errorEntity = node;
      } else if (node is PrefixedIdentifier) {
        errorEntity = node.identifier;
      } else if (node is PropertyAccess) {
        errorEntity = node.propertyName;
      }
    } else if (node is ExtensionOverride) {
      errorEntity = node.name;
    } else if (node is NamedType) {
      errorEntity = node.name2;
    } else if (node is NamedExpression) {
      errorEntity = node.name.label;
    } else if (node is PatternFieldImpl) {
      var fieldName = node.name;
      if (fieldName != null) {
        var name = fieldName.name;
        if (name == null) {
          var variablePattern = node.pattern.variablePattern;
          if (variablePattern != null) {
            errorEntity = variablePattern.name;
          }
        } else {
          errorEntity = name;
        }
      }
    }

    String displayName = element!.displayName;
    if (element is ConstructorElement2) {
      // TODO(jwren): We should modify ConstructorElement.displayName,
      // or have the logic centralized elsewhere, instead of doing this logic
      // here.
      displayName = element.name3 == null
          ? '${element.displayName}.new'
          : element.displayName;
    } else if (element is LibraryElement2) {
      displayName = element.firstFragment.source.uri.toString();
    } else if (node is MethodInvocation &&
        displayName == MethodElement2.CALL_METHOD_NAME) {
      var invokeType = node.staticInvokeType as InterfaceType;
      var invokeClass = invokeType.element3;
      displayName = "${invokeClass.name3}.${element.displayName}";
    }
    var message = _deprecatedMessage(element, strictCasts: _strictCasts);
    reportError(errorEntity, element, displayName, message);
  }

  void _invocationArguments(Element2? element, ArgumentList arguments) {
    element = element?.baseElement;
    if (element is ExecutableElement2) {
      _visitParametersAndArguments(
        element.formalParameters,
        arguments.arguments,
        _checkForDeprecated,
      );
    }
  }

  void _simpleIdentifier(SimpleIdentifier identifier) {
    _checkForDeprecated(identifier.element, identifier);
  }

  /// Return the message in the deprecated annotation on the given [element], or
  /// `null` if the element doesn't have a deprecated annotation or if the
  /// annotation does not have a message.
  static String? _deprecatedMessage(Element2 element,
      {required bool strictCasts}) {
    // Implicit getters/setters.
    if (element.isSynthetic && element is PropertyAccessorElement2) {
      var variable = element.variable3;
      if (variable == null) {
        return null;
      }
      element = variable;
    }
    var annotation = element.metadata.firstWhereOrNull((e) => e.isDeprecated);
    if (annotation == null || annotation.element2 is PropertyAccessorElement2) {
      return null;
    }
    var constantValue = annotation.computeConstantValue();
    return constantValue?.getField('message')?.toStringValue() ??
        constantValue?.getField('expires')?.toStringValue();
  }

  static bool _hasDeprecatedAnnotation(List<Annotation> annotations) {
    for (var i = 0; i < annotations.length; i++) {
      if (annotations[i].elementAnnotation!.isDeprecated) {
        return true;
      }
    }
    return false;
  }

  static bool _isDeprecated(Element2? element) {
    if (element == null) {
      return false;
    }

    if (element is PropertyAccessorElement2 && element.isSynthetic) {
      // TODO(brianwilkerson): Why isn't this the implementation for PropertyAccessorElement?
      var variable = element.variable3;
      return variable != null && variable.metadata2.hasDeprecated;
    }
    if (element is Annotatable) {
      return (element as Annotatable).metadata2.hasDeprecated;
    }
    return false;
  }

  /// Returns whether [element] is a [FormalParameterElement] declared in
  /// [node].
  static bool _isLocalParameter(Element2? element, AstNode? node) {
    if (element is FormalParameterElement) {
      var definingFunction = element.firstFragment.enclosingFragment?.element
          as ExecutableElement2;

      for (; node != null; node = node.parent) {
        if (node is ConstructorDeclaration) {
          if (node.declaredFragment?.element == definingFunction) {
            return true;
          }
        } else if (node is FunctionExpression) {
          if (node.declaredFragment?.element == definingFunction) {
            return true;
          }
        } else if (node is MethodDeclaration) {
          if (node.declaredFragment?.element == definingFunction) {
            return true;
          }
        }
      }
    }
    return false;
  }

  static void _visitParametersAndArguments(
    List<FormalParameterElement> parameters,
    List<Expression> arguments,
    void Function(FormalParameterElement, Expression) f,
  ) {
    Map<String, FormalParameterElement>? namedParameters;

    var positionalIndex = 0;
    for (var argument in arguments) {
      if (argument is NamedExpression) {
        if (namedParameters == null) {
          namedParameters = {};
          for (var parameter in parameters) {
            if (parameter.isNamed) {
              namedParameters[parameter.name3!] = parameter;
            }
          }
        }
        var name = argument.name.label.name;
        var parameter = namedParameters[name];
        if (parameter != null) {
          f(parameter, argument);
        }
      } else {
        if (positionalIndex < parameters.length) {
          var parameter = parameters[positionalIndex++];
          if (parameter.isPositional) {
            f(parameter, argument);
          }
        }
      }
    }
  }
}

class DeprecatedMemberUseVerifier extends BaseDeprecatedMemberUseVerifier {
  final WorkspacePackage? _workspacePackage;
  final ErrorReporter _errorReporter;

  DeprecatedMemberUseVerifier(this._workspacePackage, this._errorReporter,
      {required super.strictCasts});

  @override
  void reportError(SyntacticEntity errorEntity, Element2 element,
      String displayName, String? message) {
    var library = element is LibraryElement2 ? element : element.library2;

    message = message?.trim();
    if (message == null || message.isEmpty || message == '.') {
      _errorReporter.atEntity(
        errorEntity,
        _isLibraryInWorkspacePackage(library)
            ? HintCode.DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE
            : HintCode.DEPRECATED_MEMBER_USE,
        arguments: [displayName],
      );
    } else {
      if (!message.endsWith('.') &&
          !message.endsWith('?') &&
          !message.endsWith('!')) {
        message = '$message.';
      }
      _errorReporter.atEntity(
        errorEntity,
        _isLibraryInWorkspacePackage(library)
            ? HintCode.DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE_WITH_MESSAGE
            : HintCode.DEPRECATED_MEMBER_USE_WITH_MESSAGE,
        arguments: [displayName, message],
      );
    }
  }

  bool _isLibraryInWorkspacePackage(LibraryElement2? library) {
    // Better to not make a big claim that they _are_ in the same package,
    // if we were unable to determine what package [_currentLibrary] is in.
    if (_workspacePackage == null || library == null) {
      return false;
    }
    return _workspacePackage.contains(library.firstFragment.source);
  }
}
