// 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/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_provider.dart';
import 'package:analyzer/src/dart/resolver/invocation_inference_helper.dart';
import 'package:analyzer/src/dart/resolver/property_element_resolver.dart';
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:analyzer/src/error/listener.dart';
import 'package:analyzer/src/generated/inference_log.dart';
import 'package:analyzer/src/generated/resolver.dart';

class PrefixedIdentifierResolver {
  final ResolverVisitor _resolver;

  PrefixedIdentifierResolver(this._resolver);

  InvocationInferenceHelper get _inferenceHelper => _resolver.inferenceHelper;

  TypeProviderImpl get _typeProvider => _resolver.typeProvider;

  PropertyAccessImpl? resolve(
    PrefixedIdentifierImpl node, {
    required DartType contextType,
  }) {
    _resolver.analyzeExpression(node.prefix, _resolver.operations.unknownType);
    _resolver.popRewrite();
    _resolver.checkUnreachableNode(node.identifier);

    var prefixElement = node.prefix.element;
    if (prefixElement is! PrefixElement) {
      var prefixType = node.prefix.staticType;
      // TODO(scheglov): It would be nice to rewrite all such cases.
      if (prefixType != null) {
        var prefixTypeResolved = _resolver.typeSystem.resolveToBound(
          prefixType,
        );
        if (prefixTypeResolved is RecordType) {
          var propertyAccess = PropertyAccessImpl(
            target: node.prefix,
            operator: node.period,
            propertyName: node.identifier,
          );
          _resolver.replaceExpression(node, propertyAccess);
          return propertyAccess;
        }
      }
    }

    var resolver = PropertyElementResolver(_resolver);
    var result = resolver.resolvePrefixedIdentifier(
      node: node,
      hasRead: true,
      hasWrite: false,
    );

    var element = result.readElement2;

    var identifier = node.identifier;
    identifier.element = element;

    if (element is ExtensionElement) {
      _setExtensionIdentifierType(node);
      return null;
    }

    if (identical(node.prefix.staticType, NeverTypeImpl.instance)) {
      identifier.setPseudoExpressionStaticType(NeverTypeImpl.instance);
      node.recordStaticType(NeverTypeImpl.instance, resolver: _resolver);
      return null;
    }

    DartType type = InvalidTypeImpl.instance;
    if (result.readElementRequested2 == null &&
        result.readElementRecovery2 != null) {
      // Since the element came from error recovery logic, its type isn't
      // trustworthy; leave it as `dynamic`.
    } else if (element is InterfaceElement) {
      if (_isExpressionIdentifier(node)) {
        var type = _typeProvider.typeType;
        node.recordStaticType(type, resolver: _resolver);
        identifier.setPseudoExpressionStaticType(type);
      } else {
        inferenceLogWriter?.recordExpressionWithNoType(node);
      }
      return null;
    } else if (element is DynamicElementImpl) {
      var type = _typeProvider.typeType;
      node.recordStaticType(type, resolver: _resolver);
      identifier.setPseudoExpressionStaticType(type);
      return null;
    } else if (element is TypeAliasElement) {
      if (node.parent is NamedType) {
        // no type
      } else {
        var type = _typeProvider.typeType;
        node.recordStaticType(type, resolver: _resolver);
        identifier.setPseudoExpressionStaticType(type);
      }
      return null;
    } else if (element is MethodElement) {
      type = element.type;
    } else if (element is PropertyAccessorElement) {
      type = result.getType!;
    } else if (element is ExecutableElement) {
      type = element.type;
    } else if (element is VariableElement) {
      type = element.type;
    } else if (result.functionTypeCallType != null) {
      type = result.functionTypeCallType!;
    } else if (result.atDynamicTarget) {
      type = DynamicTypeImpl.instance;
    }

    if (!_resolver.isConstructorTearoffsEnabled) {
      // Only perform a generic function instantiation on a [PrefixedIdentifier]
      // in pre-constructor-tearoffs code. In constructor-tearoffs-enabled code,
      // generic function instantiation is performed at assignability check
      // sites.
      // TODO(srawlins): Switch all resolution to use the latter method, in a
      // breaking change release.
      type = _inferenceHelper.inferTearOff(
        node,
        identifier,
        type,
        contextType: contextType,
      );
    }
    identifier.setPseudoExpressionStaticType(type);
    node.recordStaticType(type, resolver: _resolver);
    return null;
  }

  /// Return `true` if the given [node] is not a type literal.
  ///
  // TODO(scheglov): this is duplicate
  bool _isExpressionIdentifier(Identifier node) {
    var parent = node.parent;
    if (node is SimpleIdentifier && node.inDeclarationContext()) {
      return false;
    }
    if (parent is ConstructorDeclaration) {
      if (parent.typeName == node) {
        return false;
      }
    }
    if (parent is ConstructorName ||
        parent is MethodInvocation ||
        parent is PrefixedIdentifier && parent.prefix == node ||
        parent is PropertyAccess ||
        parent is NamedType) {
      return false;
    }
    return true;
  }

  // TODO(scheglov): this is duplicate
  void _setExtensionIdentifierType(IdentifierImpl node) {
    if (node is SimpleIdentifierImpl && node.inDeclarationContext()) {
      return;
    }

    var parent = node.parent;

    if (parent is PrefixedIdentifierImpl && parent.identifier == node) {
      node = parent;
      parent = node.parent;
    }

    if (parent is CommentReference ||
        parent is MethodInvocation && parent.target == node ||
        parent is PrefixedIdentifierImpl && parent.prefix == node ||
        parent is PropertyAccess && parent.target == node) {
      inferenceLogWriter?.recordExpressionWithNoType(node);
      return;
    }

    _resolver.diagnosticReporter.report(
      diag.extensionAsExpression.withArguments(name: node.name).at(node),
    );

    if (node is PrefixedIdentifierImpl) {
      node.identifier.setPseudoExpressionStaticType(DynamicTypeImpl.instance);
      node.recordStaticType(DynamicTypeImpl.instance, resolver: _resolver);
    } else if (node is SimpleIdentifier) {
      node.recordStaticType(DynamicTypeImpl.instance, resolver: _resolver);
    }
  }
}
