// 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:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
import 'package:_fe_analyzer_shared/src/types/shared_type.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.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/element/element.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_provider.dart';
import 'package:analyzer/src/dart/element/type_schema.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/dart/resolver/type_property_resolver.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/generated/resolver.dart';

/// Helper for resolving [AssignmentExpression]s.
class AssignmentExpressionResolver {
  final ResolverVisitor _resolver;
  final TypePropertyResolver _typePropertyResolver;
  final AssignmentExpressionShared _assignmentShared;

  AssignmentExpressionResolver({required ResolverVisitor resolver})
    : _resolver = resolver,
      _typePropertyResolver = resolver.typePropertyResolver,
      _assignmentShared = AssignmentExpressionShared(resolver: resolver);

  DiagnosticReporter get _diagnosticReporter => _resolver.diagnosticReporter;

  TypeProviderImpl get _typeProvider => _resolver.typeProvider;

  TypeSystemImpl get _typeSystem => _resolver.typeSystem;

  void resolve(AssignmentExpressionImpl node, {required TypeImpl contextType}) {
    var operator = node.operator.type;
    var hasRead = operator != TokenType.EQ;
    var isIfNull = operator == TokenType.QUESTION_QUESTION_EQ;

    var leftResolution = _resolver.resolveForWrite(
      node: node.leftHandSide,
      hasRead: hasRead,
    );

    var left = node.leftHandSide;
    var right = node.rightHandSide;

    var readElement = leftResolution.readElement2;
    var writeElement = leftResolution.writeElement2;
    var writeElement2 = leftResolution.writeElement2;

    if (hasRead) {
      _resolver.setReadElement(
        left,
        readElement,
        atDynamicTarget: leftResolution.atDynamicTarget,
      );
      {
        var recordField = leftResolution.recordField;
        if (recordField != null) {
          node.readType = recordField.type;
        }
      }
      _resolveOperator(node);
    }
    _resolver.setWriteElement(
      left,
      writeElement,
      atDynamicTarget: leftResolution.atDynamicTarget,
    );

    // TODO(scheglov): Use VariableElement and do in resolveForWrite() ?
    _assignmentShared.checkFinalAlreadyAssigned(left);

    TypeImpl rhsContext;
    {
      var leftType = node.writeType;
      if (writeElement is VariableElement) {
        leftType = _resolver.localVariableTypeProvider.getType(
          left as SimpleIdentifierImpl,
          isRead: false,
        );
      }
      rhsContext = _computeRhsContext(node, leftType!, operator, right);
    }

    var flow = _resolver.flowAnalysis.flow;
    if (flow != null && isIfNull) {
      flow.ifNullExpression_rightBegin(left, SharedTypeView(node.readType!));
    }

    _resolver.analyzeExpression(right, SharedTypeSchemaView(rhsContext));
    right = _resolver.popRewrite()!;
    var whyNotPromoted = flow?.whyNotPromoted(right);

    _resolveTypes(
      node,
      whyNotPromoted: whyNotPromoted,
      contextType: contextType,
    );

    if (flow != null) {
      if (writeElement2 is PromotableElementImpl) {
        flow.write(
          node,
          writeElement2,
          SharedTypeView(node.typeOrThrow),
          hasRead ? null : right,
        );
      }
      if (isIfNull) {
        flow.ifNullExpression_end();
      }
    }
  }

  void _checkForInvalidAssignment(
    TypeImpl writeType,
    Expression right,
    TypeImpl rightType, {
    required Map<SharedTypeView, NonPromotionReason> Function()? whyNotPromoted,
  }) {
    if (writeType is! VoidType && _checkForUseOfVoidResult(right)) {
      return;
    }

    var strictCasts = _resolver.analysisOptions.strictCasts;
    if (_typeSystem.isAssignableTo(
      rightType,
      writeType,
      strictCasts: strictCasts,
    )) {
      return;
    }

    if (writeType is RecordTypeImpl &&
        writeType.positionalFields.length == 1 &&
        rightType is! RecordType &&
        right is ParenthesizedExpressionImpl) {
      var field = writeType.positionalFields.first;
      if (_typeSystem.isAssignableTo(
        field.type,
        rightType,
        strictCasts: strictCasts,
      )) {
        _diagnosticReporter.atNode(
          right,
          CompileTimeErrorCode.recordLiteralOnePositionalNoTrailingComma,
        );
        return;
      }
    }

    _diagnosticReporter.atNode(
      right,
      CompileTimeErrorCode.invalidAssignment,
      arguments: [rightType, writeType],
      contextMessages: _resolver.computeWhyNotPromotedMessages(
        right,
        whyNotPromoted?.call(),
      ),
    );
  }

  /// Check for situations where the result of a method or function is used,
  /// when it returns 'void'. Or, in rare cases, when other types of expressions
  /// are void, such as identifiers.
  ///
  /// See [CompileTimeErrorCode.useOfVoidResult].
  // TODO(scheglov): this is duplicate
  bool _checkForUseOfVoidResult(Expression expression) {
    if (!identical(expression.staticType, VoidTypeImpl.instance)) {
      return false;
    }

    if (expression is MethodInvocation) {
      SimpleIdentifier methodName = expression.methodName;
      _diagnosticReporter.atNode(
        methodName,
        CompileTimeErrorCode.useOfVoidResult,
      );
    } else {
      _diagnosticReporter.atNode(
        expression,
        CompileTimeErrorCode.useOfVoidResult,
      );
    }

    return true;
  }

  TypeImpl _computeRhsContext(
    AssignmentExpressionImpl node,
    TypeImpl leftType,
    TokenType operator,
    Expression right,
  ) {
    switch (operator) {
      case TokenType.EQ:
      case TokenType.QUESTION_QUESTION_EQ:
        return leftType;
      case TokenType.AMPERSAND_AMPERSAND_EQ:
      case TokenType.BAR_BAR_EQ:
        return _typeProvider.boolType;
      default:
        var method = node.element;
        if (method != null) {
          var parameters = method.formalParameters;
          if (parameters.isNotEmpty) {
            return _typeSystem.refineNumericInvocationContext(
              leftType,
              method,
              leftType,
              parameters[0].type,
            );
          }
        }
        return UnknownInferredType.instance;
    }
  }

  void _resolveOperator(AssignmentExpressionImpl node) {
    var left = node.leftHandSide;
    var operator = node.operator;
    var operatorType = operator.type;

    var leftType = node.readType!;
    if (identical(leftType, NeverTypeImpl.instance)) {
      return;
    }

    // Values of the type void cannot be used.
    // Example: `y += 0`, is not allowed.
    if (operatorType != TokenType.EQ) {
      if (leftType is VoidType) {
        _diagnosticReporter.atToken(
          operator,
          CompileTimeErrorCode.useOfVoidResult,
        );
        return;
      }
    }

    if (operatorType == TokenType.AMPERSAND_AMPERSAND_EQ ||
        operatorType == TokenType.BAR_BAR_EQ ||
        operatorType == TokenType.EQ ||
        operatorType == TokenType.QUESTION_QUESTION_EQ) {
      return;
    }

    var binaryOperatorType = operatorType.binaryOperatorOfCompoundAssignment;
    if (binaryOperatorType == null) {
      return;
    }
    var methodName = binaryOperatorType.lexeme;

    var result = _typePropertyResolver.resolve(
      receiver: left,
      receiverType: leftType,
      name: methodName,
      hasRead: operatorType != TokenType.EQ,
      hasWrite: true,
      propertyErrorEntity: operator,
      nameErrorEntity: operator,
    );
    node.element = result.getter2 as InternalMethodElement?;
    if (result.needsGetterError) {
      _diagnosticReporter.atToken(
        operator,
        CompileTimeErrorCode.undefinedOperator,
        arguments: [methodName, leftType],
      );
    }
  }

  void _resolveTypes(
    AssignmentExpressionImpl node, {
    required Map<SharedTypeView, NonPromotionReason> Function()? whyNotPromoted,
    required TypeImpl contextType,
  }) {
    TypeImpl assignedType;

    var rightHandSide = node.rightHandSide;
    var operator = node.operator.type;
    if (operator == TokenType.EQ) {
      assignedType = rightHandSide.typeOrThrow;
    } else if (operator == TokenType.QUESTION_QUESTION_EQ) {
      assignedType = rightHandSide.typeOrThrow;
    } else if (operator == TokenType.AMPERSAND_AMPERSAND_EQ ||
        operator == TokenType.BAR_BAR_EQ) {
      assignedType = _typeProvider.boolType;
    } else {
      var leftType = node.readType!;
      var operatorElement = node.element;
      if (leftType is DynamicType) {
        assignedType = DynamicTypeImpl.instance;
      } else if (operatorElement != null) {
        var rightType = rightHandSide.typeOrThrow;
        assignedType = _typeSystem.refineBinaryExpressionType(
          leftType,
          operator,
          rightType,
          operatorElement.returnType,
          operatorElement,
        );
      } else {
        assignedType = InvalidTypeImpl.instance;
      }
    }

    DartType nodeType;
    if (operator == TokenType.QUESTION_QUESTION_EQ) {
      // - An if-null assignment `E` of the form `lvalue ??= e` with context type
      //   `K` is analyzed as follows:
      //
      //   - Let `T1` be the read type the lvalue.
      var t1 = node.readType!;
      //   - Let `T2` be the type of `e` inferred with context type `T1`.
      var t2 = assignedType;
      //   - Let `T` be `UP(NonNull(T1), T2)`.
      var nonNullT1 = _typeSystem.promoteToNonNull(t1);
      var t = _typeSystem.leastUpperBound(nonNullT1, t2);
      //   - Let `S` be the greatest closure of `K`.
      var s = _resolver.operations
          .greatestClosureOfSchema(SharedTypeSchemaView(contextType))
          .unwrapTypeView<TypeImpl>();
      // If `inferenceUpdate3` is not enabled, then the type of `E` is `T`.
      if (!_resolver.definingLibrary.featureSet.isEnabled(
        Feature.inference_update_3,
      )) {
        nodeType = t;
      } else
      //   - If `T <: S`, then the type of `E` is `T`.
      if (_typeSystem.isSubtypeOf(t, s)) {
        nodeType = t;
      } else
      //   - Otherwise, if `NonNull(T1) <: S` and `T2 <: S`, then the type of
      //     `E` is `S`.
      if (_typeSystem.isSubtypeOf(nonNullT1, s) &&
          _typeSystem.isSubtypeOf(t2, s)) {
        nodeType = s;
      } else
      //   - Otherwise, the type of `E` is `T`.
      {
        nodeType = t;
      }
    } else {
      nodeType = assignedType;
    }
    node.recordStaticType(nodeType, resolver: _resolver);

    // TODO(scheglov): Remove from ErrorVerifier?
    _checkForInvalidAssignment(
      node.writeType!,
      node.rightHandSide,
      assignedType,
      whyNotPromoted: operator == TokenType.EQ ? whyNotPromoted : null,
    );
    if (operator != TokenType.EQ &&
        operator != TokenType.QUESTION_QUESTION_EQ) {
      _resolver.checkForArgumentTypeNotAssignableForArgument(
        node.rightHandSide,
        whyNotPromoted: whyNotPromoted,
      );
    }
  }
}

class AssignmentExpressionShared {
  final ResolverVisitor _resolver;

  AssignmentExpressionShared({required ResolverVisitor resolver})
    : _resolver = resolver;

  DiagnosticReporter get _errorReporter => _resolver.diagnosticReporter;

  void checkFinalAlreadyAssigned(
    Expression left, {
    bool isForEachIdentifier = false,
  }) {
    var flowAnalysis = _resolver.flowAnalysis;

    var flow = flowAnalysis.flow;
    if (flow == null) return;

    if (left is SimpleIdentifier) {
      var element = left.element;
      if (element is PromotableElementImpl) {
        var assigned = flowAnalysis.isDefinitelyAssigned(left, element);
        var unassigned = flowAnalysis.isDefinitelyUnassigned(left, element);

        if (element.isFinal) {
          if (element.isLate) {
            if (isForEachIdentifier || assigned) {
              _errorReporter.atNode(
                left,
                CompileTimeErrorCode.lateFinalLocalAlreadyAssigned,
              );
            }
          } else {
            if (isForEachIdentifier || !unassigned) {
              _errorReporter.atNode(
                left,
                CompileTimeErrorCode.assignmentToFinalLocal,
                arguments: [element.name!],
              );
            }
          }
        }
      }
    }
  }
}
