// Copyright (c) 2019, 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/scope.dart';
import 'package:analyzer/src/dart/analysis/analysis_options.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/error/inference_error.dart';
import 'package:analyzer/src/summary2/ast_resolver.dart';
import 'package:analyzer/src/summary2/instance_member_inferrer.dart';
import 'package:analyzer/src/summary2/library_builder.dart';
import 'package:analyzer/src/summary2/link.dart';
import 'package:analyzer/src/summary2/linking_node_scope.dart';
import 'package:analyzer/src/utilities/extensions/object.dart';
import 'package:collection/collection.dart';

/// Resolver for typed constant top-level variables and fields initializers.
///
/// Initializers of untyped variables are resolved during [TopLevelInference].
class ConstantInitializersResolver {
  final Linker linker;

  ConstantInitializersResolver(this.linker);

  void perform() {
    for (var builder in linker.builders.values) {
      var analysisOptions = builder.kind.file.analysisOptions;
      var libraryElement = builder.element;

      var instanceElementListList = [
        libraryElement.classes,
        libraryElement.enums,
        libraryElement.extensions,
        libraryElement.extensionTypes,
        libraryElement.mixins,
      ];
      for (var instanceElementList in instanceElementListList) {
        for (var instanceElement in instanceElementList) {
          for (var field in instanceElement.fields) {
            _resolveVariable(analysisOptions, field);
          }
        }
      }

      for (var variable in libraryElement.topLevelVariables) {
        _resolveVariable(analysisOptions, variable);
      }
    }
  }

  void _resolveVariable(
    AnalysisOptionsImpl analysisOptions,
    PropertyInducingElementImpl element,
  ) {
    if (element is FieldElementImpl && element.isEnumConstant) {
      return;
    }

    var constantInitializer = element.constantInitializer2;
    if (constantInitializer == null) {
      return;
    }

    var fragment = constantInitializer.fragment;
    var node = linker.elementNodes[fragment] as VariableDeclarationImpl;
    var scope = LinkingNodeContext.get(node).scope;

    var astResolver = AstResolver(
      linker,
      fragment.libraryFragment as LibraryFragmentImpl,
      scope,
      analysisOptions,
    );

    List<FormalParameterElementImpl>? inScopePrimaryConstructorParameters;
    if (element case FieldElementImpl field) {
      if (field.isInstanceField && !field.isLate) {
        inScopePrimaryConstructorParameters = field.enclosingElement
            .tryCast<InterfaceElementImpl>()
            ?.primaryConstructor
            ?.formalParameters;
      }
    }

    astResolver.resolveExpression(
      () => node.initializer!,
      contextType: element.type,
      inScopePrimaryConstructorParameters: inScopePrimaryConstructorParameters,
    );

    // We could have rewritten the initializer.
    fragment.constantInitializer = node.initializer;
  }
}

class TopLevelInference {
  final Linker linker;

  TopLevelInference(this.linker);

  void infer() {
    var initializerInference = _InitializerInference(linker);
    initializerInference.createNodes();

    _performOverrideInference();

    initializerInference.perform();
  }

  void _performOverrideInference() {
    var interfacesToInfer = linker.builders.values.expand((builder) {
      return builder.element.children.whereType<InterfaceElementImpl>();
    }).toList();

    var inferrer = InstanceMemberInferrer(linker.inheritance);
    inferrer.perform(interfacesToInfer);
  }
}

enum _InferenceStatus { notInferred, beingInferred, inferred }

class _InitializerInference {
  final Linker _linker;
  final List<PropertyInducingElementImpl> _toInfer = [];
  final List<_PropertyInducingElementTypeInference> _inferring = [];

  late LibraryBuilder _libraryBuilder;

  _InitializerInference(this._linker);

  void createNodes() {
    for (var builder in _linker.builders.values) {
      _libraryBuilder = builder;
      var libraryElement = builder.element;

      var instanceElementListList = [
        libraryElement.classes,
        libraryElement.enums,
        libraryElement.extensions,
        libraryElement.extensionTypes,
        libraryElement.mixins,
      ];
      for (var instanceElementList in instanceElementListList) {
        for (var instanceElement in instanceElementList) {
          for (var field in instanceElement.fields) {
            _addVariableNode(field);
          }
        }
      }

      for (var variable in libraryElement.topLevelVariables) {
        _addVariableNode(variable);
      }
    }
  }

  /// Perform type inference for variables for which it was not done yet.
  void perform() {
    for (var element in _toInfer) {
      // Will perform inference, if not done yet.
      element.type;
    }
  }

  void _addVariableNode(PropertyInducingElementImpl element) {
    if (element.isOriginGetterSetter) {
      return;
    }

    if (!element.hasImplicitType) return;

    _toInfer.add(element);

    element.typeInference = _PropertyInducingElementTypeInference(
      _linker,
      _inferring,
      element,
      _libraryBuilder,
    );
  }
}

class _PropertyInducingElementTypeInference
    implements PropertyInducingElementTypeInference {
  final Linker _linker;

  /// The stack of objects performing inference now. A new object is pushed
  /// when we start resolving the initializer, and popped when we are done.
  final List<_PropertyInducingElementTypeInference> _inferring;

  /// The status is used to identify a cycle, when we are asked to infer the
  /// type, but the status is already [_InferenceStatus.beingInferred].
  _InferenceStatus _status = _InferenceStatus.notInferred;

  final LibraryBuilder _libraryBuilder;
  final PropertyInducingElementImpl _element;

  _PropertyInducingElementTypeInference(
    this._linker,
    this._inferring,
    this._element,
    this._libraryBuilder,
  );

  @override
  TypeImpl perform() {
    LibraryFragmentImpl? initializerLibraryFragment;
    Scope? scope;
    ExpressionImpl Function()? getInitializer;
    List<FormalParameterElementImpl>? inScopePrimaryConstructorParameters;
    for (var fragment in _element.fragments) {
      var node = _linker.elementNodes[fragment];
      switch (node) {
        case VariableDeclarationImpl():
          if (node.initializer != null) {
            initializerLibraryFragment = fragment.libraryFragment;
            scope = LinkingNodeContext.get(node).scope;
            getInitializer = () => node.initializer!;
            if (_element case FieldElementImpl field) {
              if (field.isInstanceField && !field.isLate) {
                inScopePrimaryConstructorParameters = field.enclosingElement
                    .tryCast<InterfaceElementImpl>()
                    ?.primaryConstructor
                    ?.formalParameters;
              }
            }
          }
        case DefaultFormalParameterImpl():
          _assertElementFieldOriginDeclaringFormalParameter();
          if (node.defaultValue != null) {
            initializerLibraryFragment = fragment.libraryFragment;
            scope = LinkingNodeContext.get(node).scope;
            getInitializer = () => node.defaultValue!;
          } else {
            _status = _InferenceStatus.inferred;
            return _element.library.typeSystem.objectQuestion;
          }
        case SimpleFormalParameterImpl():
          _assertElementFieldOriginDeclaringFormalParameter();
          _status = _InferenceStatus.inferred;
          return _element.library.typeSystem.objectQuestion;
      }
    }

    if (initializerLibraryFragment == null ||
        scope == null ||
        getInitializer == null) {
      _status = _InferenceStatus.inferred;
      return DynamicTypeImpl.instance;
    }

    // With this status the type must be already set.
    // So, the element knows the type, ans should not call the inferrer.
    if (_status == _InferenceStatus.inferred) {
      assert(false, 'Should not happen: $_element');
      return DynamicTypeImpl.instance;
    }

    // If we are already inferring this element, we found a cycle.
    if (_status == _InferenceStatus.beingInferred) {
      var startIndex = _inferring.indexOf(this);
      var cycle = _inferring.slice(startIndex);
      var inferenceError = TopLevelInferenceErrorDependencyCycle(
        cycle: cycle.map((e) => e._element.name ?? '').sorted(),
      );
      for (var inference in cycle) {
        if (inference._status == _InferenceStatus.beingInferred) {
          var element = inference._element;
          element.typeInferenceError = inferenceError;
          element.type = DynamicTypeImpl.instance;
          inference._status = _InferenceStatus.inferred;
        }
      }
      return DynamicTypeImpl.instance;
    }

    assert(_status == _InferenceStatus.notInferred);

    // Push self into the stack, and mark.
    _inferring.add(this);
    _status = _InferenceStatus.beingInferred;

    var enclosingElement = _element.enclosingElement;
    var enclosingInterfaceElement = enclosingElement
        .tryCast<InterfaceElementImpl>();

    var analysisOptions = _libraryBuilder.kind.file.analysisOptions;
    var astResolver = AstResolver(
      _linker,
      initializerLibraryFragment,
      scope,
      analysisOptions,
      enclosingClassElement: enclosingInterfaceElement,
    );
    astResolver.resolveExpression(
      getInitializer,
      inScopePrimaryConstructorParameters: inScopePrimaryConstructorParameters,
    );

    // Pop self from the stack.
    var self = _inferring.removeLast();
    assert(identical(self, this));

    // We might have found a cycle, and already set the type.
    // Anyway, we are done.
    if (_status == _InferenceStatus.inferred) {
      return _element.type;
    } else {
      _status = _InferenceStatus.inferred;
    }

    var initializerType = getInitializer().typeOrThrow;
    return _refineType(initializerType);
  }

  void _assertElementFieldOriginDeclaringFormalParameter() {
    assert(() {
      if (_element case FieldElementImpl element) {
        return element.isOriginDeclaringFormalParameter;
      }
      return false;
    }());
  }

  TypeImpl _refineType(TypeImpl type) {
    if (type.isDartCoreNull) {
      // When `T` is `Null`, `p` has declared type `Object?`.
      if (_element case FieldElementImpl field) {
        if (field.declaringFormalParameter != null) {
          return _element.library.typeSystem.objectQuestion;
        }
      }
      // Logic for older language versions.
      return DynamicTypeImpl.instance;
    }

    return type;
  }
}
