// 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/types/shared_type.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/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/body_inference_context.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/generated/resolver.dart';

/// Helper for resolving [YieldStatement]s.
class YieldStatementResolver {
  final ResolverVisitor _resolver;

  YieldStatementResolver({
    required ResolverVisitor resolver,
  }) : _resolver = resolver;

  ErrorReporter get _errorReporter => _resolver.errorReporter;

  TypeProviderImpl get _typeProvider => _resolver.typeProvider;

  TypeSystemImpl get _typeSystem => _resolver.typeSystem;

  void resolve(YieldStatementImpl node) {
    var bodyContext = _resolver.bodyContext;
    if (bodyContext != null && bodyContext.isGenerator) {
      _resolve_generator(bodyContext, node);
    } else {
      _resolve_notGenerator(node);
    }
  }

  /// 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.USE_OF_VOID_RESULT].
  ///
  // TODO(scheglov): This is duplicate
  // TODO(scheglov): Also in [BoolExpressionVerifier]
  bool _checkForUseOfVoidResult(Expression expression) {
    if (!identical(expression.staticType, VoidTypeImpl.instance)) {
      return false;
    }

    if (expression is MethodInvocation) {
      _errorReporter.atNode(
        expression.methodName,
        CompileTimeErrorCode.USE_OF_VOID_RESULT,
      );
    } else {
      _errorReporter.atNode(
        expression,
        CompileTimeErrorCode.USE_OF_VOID_RESULT,
      );
    }

    return true;
  }

  /// Check for a type mis-match between the yielded type and the declared
  /// return type of a generator function.
  ///
  /// This method should only be called in generator functions.
  void _checkForYieldOfInvalidType(
    BodyInferenceContext bodyContext,
    YieldStatement node, {
    required bool isYieldEach,
  }) {
    var expression = node.expression;
    var expressionType = expression.typeOrThrow;

    DartType impliedReturnType;
    if (isYieldEach) {
      impliedReturnType = expressionType;
    } else if (bodyContext.isSynchronous) {
      impliedReturnType = _typeProvider.iterableType(expressionType);
    } else {
      impliedReturnType = _typeProvider.streamType(expressionType);
    }

    var imposedReturnType = bodyContext.imposedType;
    if (imposedReturnType != null) {
      if (isYieldEach) {
        if (!_typeSystem.isAssignableTo(impliedReturnType, imposedReturnType,
            strictCasts: _resolver.analysisOptions.strictCasts)) {
          _errorReporter.atNode(
            expression,
            CompileTimeErrorCode.YIELD_EACH_OF_INVALID_TYPE,
            arguments: [impliedReturnType, imposedReturnType],
          );
          return;
        }
      } else {
        var imposedSequenceType = imposedReturnType.asInstanceOf2(
          bodyContext.isSynchronous
              ? _typeProvider.iterableElement2
              : _typeProvider.streamElement2,
        );
        if (imposedSequenceType != null) {
          var imposedValueType = imposedSequenceType.typeArguments[0];
          if (!_typeSystem.isAssignableTo(expressionType, imposedValueType,
              strictCasts: _resolver.analysisOptions.strictCasts)) {
            _errorReporter.atNode(
              expression,
              CompileTimeErrorCode.YIELD_OF_INVALID_TYPE,
              arguments: [expressionType, imposedValueType],
            );
            return;
          }
        }
      }
    }

    if (isYieldEach) {
      // Since the declared return type might have been "dynamic", we need to
      // also check that the implied return type is assignable to generic
      // Iterable/Stream.
      DartType requiredReturnType;
      if (bodyContext.isSynchronous) {
        requiredReturnType = _typeProvider.iterableDynamicType;
      } else {
        requiredReturnType = _typeProvider.streamDynamicType;
      }

      if (!_typeSystem.isAssignableTo(impliedReturnType, requiredReturnType,
          strictCasts: _resolver.analysisOptions.strictCasts)) {
        _errorReporter.atNode(
          expression,
          CompileTimeErrorCode.YIELD_EACH_OF_INVALID_TYPE,
          arguments: [impliedReturnType, requiredReturnType],
        );
      }
    }
  }

  TypeImpl _computeContextType(
    BodyInferenceContext bodyContext,
    YieldStatement node,
  ) {
    var elementType = bodyContext.contextType;
    if (elementType != null) {
      var contextType = elementType;
      if (node.star != null) {
        contextType = bodyContext.isSynchronous
            ? _typeProvider.iterableType(elementType)
            : _typeProvider.streamType(elementType);
      }
      return contextType;
    } else {
      return UnknownInferredType.instance;
    }
  }

  void _resolve_generator(
    BodyInferenceContext bodyContext,
    YieldStatementImpl node,
  ) {
    _resolver.analyzeExpression(node.expression,
        SharedTypeSchemaView(_computeContextType(bodyContext, node)));
    _resolver.popRewrite();

    if (node.star != null) {
      _resolver.nullableDereferenceVerifier.expression(
        CompileTimeErrorCode.UNCHECKED_USE_OF_NULLABLE_VALUE_IN_YIELD_EACH,
        node.expression,
      );
    }

    bodyContext.addYield(node);

    _checkForYieldOfInvalidType(bodyContext, node,
        isYieldEach: node.star != null);
    _checkForUseOfVoidResult(node.expression);
  }

  void _resolve_notGenerator(YieldStatementImpl node) {
    _resolver.analyzeExpression(
        node.expression, _resolver.operations.unknownType);
    _resolver.popRewrite();

    _errorReporter.atNode(
      node,
      node.star != null
          ? CompileTimeErrorCode.YIELD_EACH_IN_NON_GENERATOR
          : CompileTimeErrorCode.YIELD_IN_NON_GENERATOR,
    );

    _checkForUseOfVoidResult(node.expression);
  }
}
