// 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/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/type.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/assignment_expression_resolver.dart';
import 'package:analyzer/src/dart/resolver/typed_literal_resolver.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/generated/resolver.dart';

/// Helper for resolving [ForStatement]s and [ForElement]s.
class ForResolver {
  final ResolverVisitor _resolver;

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

  TypeSystemImpl get _typeSystem => _resolver.typeSystem;

  void resolveElement(ForElementImpl node, CollectionLiteralContext? context) {
    var forLoopParts = node.forLoopParts;
    void visitBody() {
      node.body.resolveElement(_resolver, context);
      _resolver.popRewrite();
    }

    if (forLoopParts is ForPartsImpl) {
      _forParts(node, forLoopParts, visitBody);
    } else if (forLoopParts is ForEachPartsWithPatternImpl) {
      _analyzePatternForIn(
        node: node,
        awaitKeyword: node.awaitKeyword,
        forLoopParts: forLoopParts,
        dispatchBody: () {
          _resolver.dispatchCollectionElement(node.body, context);
        },
      );
    } else if (forLoopParts is ForEachPartsImpl) {
      _forEachParts(node, node.awaitKeyword != null, forLoopParts, visitBody);
    }
  }

  void resolveStatement(ForStatementImpl node) {
    var forLoopParts = node.forLoopParts;
    void visitBody() {
      node.body.accept(_resolver);
    }

    if (forLoopParts is ForPartsImpl) {
      _forParts(node, forLoopParts, visitBody);
    } else if (forLoopParts is ForEachPartsWithPatternImpl) {
      _analyzePatternForIn(
        node: node,
        awaitKeyword: node.awaitKeyword,
        forLoopParts: forLoopParts,
        dispatchBody: () {
          _resolver.dispatchStatement(node.body);
        },
      );
    } else if (forLoopParts is ForEachPartsImpl) {
      _forEachParts(node, node.awaitKeyword != null, forLoopParts, visitBody);
    }
  }

  void _analyzePatternForIn({
    required AstNodeImpl node,
    required Token? awaitKeyword,
    required ForEachPartsWithPatternImpl forLoopParts,
    required void Function() dispatchBody,
  }) {
    _resolver.analyzePatternForIn(
      node: node,
      hasAwait: awaitKeyword != null,
      pattern: forLoopParts.pattern,
      expression: forLoopParts.iterable,
      dispatchBody: dispatchBody,
    );
    _resolver.popRewrite();
    _resolver.nullableDereferenceVerifier.expression(
      CompileTimeErrorCode.UNCHECKED_USE_OF_NULLABLE_VALUE_AS_ITERATOR,
      forLoopParts.iterable,
    );
  }

  /// Given an iterable expression from a foreach loop, attempt to infer
  /// a type for the elements being iterated over.  Inference is based
  /// on the type of the iterator or stream over which the foreach loop
  /// is defined.
  DartType _computeForEachElementType(Expression iterable, bool isAsync) {
    var iterableType = iterable.staticType;
    if (iterableType == null) {
      return InvalidTypeImpl.instance;
    }

    iterableType = _typeSystem.resolveToBound(iterableType);
    if (iterableType is DynamicType) {
      return DynamicTypeImpl.instance;
    }

    ClassElement iteratedElement = isAsync
        ? _resolver.typeProvider.streamElement
        : _resolver.typeProvider.iterableElement;

    var iteratedType = iterableType.asInstanceOf(iteratedElement);
    if (iteratedType == null) {
      return InvalidTypeImpl.instance;
    }

    return iteratedType.typeArguments.single;
  }

  void _forEachParts(AstNode node, bool isAsync, ForEachParts forEachParts,
      void Function() visitBody) {
    Expression iterable = forEachParts.iterable;
    DeclaredIdentifier? loopVariable;
    SimpleIdentifier? identifier;
    Element? identifierElement;
    if (forEachParts is ForEachPartsWithDeclaration) {
      loopVariable = forEachParts.loopVariable;
    } else if (forEachParts is ForEachPartsWithIdentifier) {
      identifier = forEachParts.identifier;
      // TODO(scheglov): replace with lexical lookup
      identifier.accept(_resolver);
      AssignmentExpressionShared(
        resolver: _resolver,
      ).checkFinalAlreadyAssigned(identifier, isForEachIdentifier: true);
    }

    DartType? valueType;
    if (loopVariable != null) {
      var typeAnnotation = loopVariable.type;
      valueType = typeAnnotation?.type ?? UnknownInferredType.instance;
    }
    if (identifier != null) {
      identifierElement = identifier.staticElement;
      if (identifierElement is VariableElement) {
        valueType = _resolver.localVariableTypeProvider
            .getType(identifier, isRead: false);
      } else if (identifierElement is PropertyAccessorElement) {
        var parameters = identifierElement.parameters;
        if (parameters.isNotEmpty) {
          valueType = parameters[0].type;
        }
      }
    }
    InterfaceType? targetType;
    if (valueType != null) {
      targetType = isAsync
          ? _resolver.typeProvider.streamType(valueType)
          : _resolver.typeProvider.iterableType(valueType);
    }

    _resolver.analyzeExpression(iterable,
        SharedTypeSchemaView(targetType ?? UnknownInferredType.instance));
    iterable = _resolver.popRewrite()!;

    _resolver.nullableDereferenceVerifier.expression(
      CompileTimeErrorCode.UNCHECKED_USE_OF_NULLABLE_VALUE_AS_ITERATOR,
      iterable,
    );

    loopVariable?.accept(_resolver);
    var elementType = _computeForEachElementType(iterable, isAsync);
    if (loopVariable != null && loopVariable.type == null) {
      var loopVariableElement =
          loopVariable.declaredElement as LocalVariableElementImpl;
      loopVariableElement.type = elementType;
    }

    if (loopVariable != null) {
      var declaredElement = loopVariable.declaredElement!;
      _resolver.flowAnalysis.flow?.declare(
          declaredElement, SharedTypeView(declaredElement.type),
          initialized: true);
    }

    _resolver.flowAnalysis.flow?.forEach_bodyBegin(node);
    if (identifierElement is PromotableElement &&
        forEachParts is ForEachPartsWithIdentifier) {
      _resolver.flowAnalysis.flow?.write(
          forEachParts, identifierElement, SharedTypeView(elementType), null);
    }

    visitBody();

    _resolver.flowAnalysis.flow?.forEach_end();
  }

  void _forParts(AstNode node, ForParts forParts, void Function() visitBody) {
    if (forParts is ForPartsWithDeclarations) {
      forParts.variables.accept(_resolver);
    } else if (forParts is ForPartsWithExpression) {
      forParts.initialization?.accept(_resolver);
    } else if (forParts is ForPartsWithPattern) {
      forParts.variables.accept(_resolver);
    } else {
      throw StateError('Unrecognized for loop parts');
    }

    _resolver.flowAnalysis.for_conditionBegin(node);

    var condition = forParts.condition;
    if (condition != null) {
      _resolver.analyzeExpression(
          condition, SharedTypeSchemaView(_resolver.typeProvider.boolType));
      condition = _resolver.popRewrite()!;
      var whyNotPromoted =
          _resolver.flowAnalysis.flow?.whyNotPromoted(condition);
      _resolver.boolExpressionVerifier
          .checkForNonBoolCondition(condition, whyNotPromoted: whyNotPromoted);
    }

    _resolver.flowAnalysis.for_bodyBegin(node, condition);
    visitBody();

    _resolver.flowAnalysis.flow?.for_updaterBegin();
    forParts.updaters.accept(_resolver);

    _resolver.flowAnalysis.flow?.for_end();
  }
}
