// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.psi;

import com.google.common.collect.Maps;
import com.intellij.codeInsight.completion.PrioritizedLookupElement;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.codeInspection.SuppressionUtil;
import com.intellij.lang.ASTFactory;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.*;
import com.intellij.ui.IconManager;
import com.intellij.util.*;
import com.intellij.util.concurrency.annotations.RequiresEdt;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonCodeStyleService;
import com.jetbrains.python.ast.impl.PyUtilCore;
import com.jetbrains.python.codeInsight.completion.OverwriteEqualsInsertHandler;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.codeInsight.mlcompletion.PyCompletionMlElementInfo;
import com.jetbrains.python.codeInsight.mlcompletion.PyCompletionMlElementKind;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyExpressionCodeFragmentImpl;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.resolve.QualifiedNameFinder;
import com.jetbrains.python.psi.resolve.RatedResolveResult;
import com.jetbrains.python.psi.stubs.PyLiteralKind;
import com.jetbrains.python.psi.stubs.PySetuptoolsNamespaceIndex;
import com.jetbrains.python.psi.types.*;
import com.jetbrains.python.pyi.PyiStubSuppressor;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.*;
import org.jetbrains.jps.model.java.JavaSourceRootType;

import javax.swing.*;
import java.io.File;
import java.util.*;
import java.util.regex.Pattern;

import static com.jetbrains.python.ast.PyAstFunction.Modifier.CLASSMETHOD;
import static com.jetbrains.python.ast.PyAstFunction.Modifier.STATICMETHOD;

/**
 * Assorted utility methods for Python code insight.
 * <p>
 * These methods don't depend on the Python runtime.
 *
 * @see PyPsiUtils for utilities used in Python PSI API
 * @see PyUiUtil for UI-related utilities for Python (available in PythonCore plugin)
 */
public final class PyUtil {

  private static final boolean VERBOSE_MODE = System.getenv("_PYCHARM_VERBOSE_MODE") != null;

  private PyUtil() {
  }

  /**
   * Flattens the representation of every element in targets, and puts all results together.
   * Elements of every tuple nested in target item are brought to the top level: (a, (b, (c, d))) -> (a, b, c, d)
   * Typical usage: {@code flattenedParensAndTuples(some_tuple.getExpressions())}.
   *
   * @param targets target elements.
   * @return the list of flattened expressions.
   */
  public static @NotNull List<PyExpression> flattenedParensAndTuples(PyExpression... targets) {
    //noinspection unchecked,rawtypes
    return (List)PyUtilCore.flattenedParensAndTuples(targets);
  }

  public static @NotNull List<PyExpression> flattenedParensAndLists(PyExpression... targets) {
    //noinspection unchecked
    return (List)PyUtilCore.flattenedParensAndLists(targets);
  }

  public static @NotNull List<PyExpression> flattenedParensAndStars(PyExpression... targets) {
    //noinspection unchecked,rawtypes
    return (List)PyUtilCore.flattenedParensAndStars(targets);
  }

  /**
   * Produce a reasonable representation of a PSI element, good for debugging.
   *
   * @param elt      element to represent; nulls and invalid nodes are ok.
   * @param cutAtEOL if true, representation stops at nearest EOL inside the element.
   * @return the representation.
   */
  public static @NotNull @NlsSafe String getReadableRepr(PsiElement elt, final boolean cutAtEOL) {
    if (elt == null) return "null!";
    ASTNode node = elt.getNode();
    if (node == null) {
      return "null";
    }
    else {
      String s = node.getText();
      int cut_pos;
      if (cutAtEOL) {
        cut_pos = s.indexOf('\n');
      }
      else {
        cut_pos = -1;
      }
      if (cut_pos < 0) cut_pos = s.length();
      return s.substring(0, Math.min(cut_pos, s.length()));
    }
  }

  public static @Nullable PyClass getContainingClassOrSelf(final PsiElement element) {
    PsiElement current = element;
    while (current != null && !(current instanceof PyClass)) {
      current = current.getParent();
    }
    return (PyClass)current;
  }

  /**
   * @param element for which to obtain the file
   * @return PyFile, or null, if there's no containing file, or it is not a PyFile.
   */
  public static @Nullable PyFile getContainingPyFile(PyElement element) {
    final PsiFile containingFile = element.getContainingFile();
    return containingFile instanceof PyFile ? (PyFile)containingFile : null;
  }

  /**
   * Returns a quoted string representation, or "null".
   */
  public static @NonNls String nvl(Object s) {
    if (s != null) {
      return "'" + s.toString() + "'";
    }
    else {
      return "null";
    }
  }

  /**
   * Adds an item into a comma-separated list in a PSI tree. E.g. can turn "foo, bar" into "foo, bar, baz", adding commas as needed.
   *
   * @param parent     the element to represent the list; we're adding a child to it.
   * @param newItem    the element we're inserting (the "baz" in the example).
   * @param beforeThis node to mark the insertion point inside the list; must belong to a child of target. Set to null to add first element.
   * @param isFirst    true if we don't need a comma before the element we're adding.
   * @param isLast     true if we don't need a comma after the element we're adding.
   */
  public static void addListNode(PsiElement parent, PsiElement newItem, ASTNode beforeThis,
                                 boolean isFirst, boolean isLast, boolean addWhitespace) {
    ASTNode node = parent.getNode();
    assert node != null;
    ASTNode itemNode = newItem.getNode();
    assert itemNode != null;
    Project project = parent.getProject();
    PyElementGenerator gen = PyElementGenerator.getInstance(project);
    if (!isFirst) node.addChild(gen.createComma(), beforeThis);
    node.addChild(itemNode, beforeThis);
    if (!isLast) node.addChild(gen.createComma(), beforeThis);
    if (addWhitespace) node.addChild(ASTFactory.whitespace(" "), beforeThis);
  }

  // TODO: move to a more proper place?

  /**
   * Makes sure that 'thing' is not null; else throws an {@link IncorrectOperationException}.
   *
   * @param thing what we check.
   * @return thing, if not null.
   */
  public static @NotNull <T> T sure(T thing) {
    if (thing == null) throw new IncorrectOperationException();
    return thing;
  }

  /**
   * Makes sure that the 'thing' is true; else throws an {@link IncorrectOperationException}.
   *
   * @param thing what we check.
   */
  public static void sure(boolean thing) {
    if (!thing) throw new IncorrectOperationException();
  }

  public static boolean isAttribute(PyTargetExpression ex) {
    return isInstanceAttribute(ex) || isClassAttribute(ex);
  }

  public static boolean isInstanceAttribute(PyExpression target) {
    if (!(target instanceof PyTargetExpression)) {
      return false;
    }
    final ScopeOwner owner = ScopeUtil.getScopeOwner(target);
    if (owner instanceof PyFunction method) {
      if (method.getContainingClass() != null) {
        if (method.getStub() != null) {
          return true;
        }
        final PyParameter[] params = method.getParameterList().getParameters();
        if (params.length > 0) {
          final PyTargetExpression targetExpr = (PyTargetExpression)target;
          final PyExpression qualifier = targetExpr.getQualifier();
          return qualifier != null && qualifier.getText().equals(params[0].getName());
        }
      }
    }
    return false;
  }

  public static boolean isClassAttribute(PsiElement element) {
    return element instanceof PyTargetExpression && ScopeUtil.getScopeOwner(element) instanceof PyClass;
  }

  public static boolean hasIfNameEqualsMain(@NotNull PyFile file) {
    final PyIfStatement dunderMain = SyntaxTraverser.psiApi()
      .children(file)
      .filterMap(psi -> psi instanceof PyIfStatement ? ((PyIfStatement)psi) : null)
      .find(ifStatement -> isIfNameEqualsMain(ifStatement));
    return dunderMain != null;
  }

  public static boolean isIfNameEqualsMain(PyIfStatement ifStatement) {
    final PyExpression condition = ifStatement.getIfPart().getCondition();
    return isNameEqualsMain(condition);
  }

  private static boolean isNameEqualsMain(PyExpression condition) {
    if (condition instanceof PyParenthesizedExpression) {
      return isNameEqualsMain(((PyParenthesizedExpression)condition).getContainedExpression());
    }
    if (condition instanceof PyBinaryExpression binaryExpression) {
      if (binaryExpression.getOperator() == PyTokenTypes.OR_KEYWORD) {
        return isNameEqualsMain(binaryExpression.getLeftExpression()) || isNameEqualsMain(binaryExpression.getRightExpression());
      }
      if (binaryExpression.getRightExpression() instanceof PyStringLiteralExpression rhs) {
        return binaryExpression.getOperator() == PyTokenTypes.EQEQ &&
               binaryExpression.getLeftExpression().getText().equals(PyNames.NAME) &&
               rhs.getStringValue().equals("__main__");
      }
    }
    return false;
  }

  /**
   * Searches for a method wrapping given element.
   *
   * @param start element presumably inside a method
   * @param deep  if true, allow 'start' to be inside functions nested in a method; else, 'start' must be directly inside a method.
   * @return if not 'deep', [0] is the method and [1] is the class; if 'deep', first several elements may be the nested functions,
   * the last but one is the method, and the last is the class.
   */
  public static @Nullable List<PsiElement> searchForWrappingMethod(PsiElement start, boolean deep) {
    PsiElement seeker = start;
    List<PsiElement> ret = new ArrayList<>(2);
    while (seeker != null) {
      PyFunction func = PsiTreeUtil.getParentOfType(seeker, PyFunction.class, true, PyClass.class);
      if (func != null) {
        PyClass cls = func.getContainingClass();
        if (cls != null) {
          ret.add(func);
          ret.add(cls);
          return ret;
        }
        else if (deep) {
          ret.add(func);
          seeker = func;
        }
        else {
          return null; // no immediate class
        }
      }
      else {
        return null; // no function
      }
    }
    return null;
  }

  public static boolean inSameFile(@NotNull PsiElement e1, @NotNull PsiElement e2) {
    final PsiFile f1 = e1.getContainingFile();
    final PsiFile f2 = e2.getContainingFile();
    if (f1 == null || f2 == null) {
      return false;
    }
    return f1 == f2;
  }

  public static boolean isTopLevel(@NotNull PsiElement element) {
    return PyUtilCore.isTopLevel(element);
  }

  public static void deletePycFiles(String pyFilePath) {
    if (pyFilePath.endsWith(PyNames.DOT_PY)) {
      List<File> filesToDelete = new ArrayList<>();
      File pyc = new File(pyFilePath + "c");
      if (pyc.exists()) {
        filesToDelete.add(pyc);
      }
      File pyo = new File(pyFilePath + "o");
      if (pyo.exists()) {
        filesToDelete.add(pyo);
      }
      final File file = new File(pyFilePath);
      File pycache = new File(file.getParentFile(), PyNames.PYCACHE);
      if (pycache.isDirectory()) {
        final String shortName = FileUtilRt.getNameWithoutExtension(file.getName());
        Collections.addAll(filesToDelete, pycache.listFiles(pathname -> {
          if (!FileUtilRt.extensionEquals(pathname.getName(), "pyc")) return false;
          String nameWithMagic = FileUtilRt.getNameWithoutExtension(pathname.getName());
          return FileUtilRt.getNameWithoutExtension(nameWithMagic).equals(shortName);
        }));
      }
      FileUtil.asyncDelete(filesToDelete);
    }
  }

  public static String getElementNameWithoutExtension(PsiNamedElement psiNamedElement) {
    return psiNamedElement instanceof PyFile
           ? FileUtilRt.getNameWithoutExtension(((PyFile)psiNamedElement).getName())
           : psiNamedElement.getName();
  }

  public static boolean hasUnresolvedAncestors(@NotNull PyClass cls, @NotNull TypeEvalContext context) {
    for (PyClassLikeType type : cls.getAncestorTypes(context)) {
      if (type == null) {
        return true;
      }
    }
    return false;
  }

  public static @NotNull AccessDirection getPropertyAccessDirection(@NotNull PyFunction function) {
    final Property property = function.getProperty();
    if (property != null) {
      if (property.getGetter().valueOrNull() == function) {
        return AccessDirection.READ;
      }
      if (property.getSetter().valueOrNull() == function) {
        return AccessDirection.WRITE;
      }
      else if (property.getDeleter().valueOrNull() == function) {
        return AccessDirection.DELETE;
      }
    }
    return AccessDirection.READ;
  }

  public static void removeQualifier(final @NotNull PyReferenceExpression element) {
    final PyExpression qualifier = element.getQualifier();
    if (qualifier == null) return;

    if (qualifier instanceof PyCallExpression) {
      final PyExpression callee = ((PyCallExpression)qualifier).getCallee();
      if (callee instanceof PyReferenceExpression) {
        final PyExpression calleeQualifier = ((PyReferenceExpression)callee).getQualifier();
        if (calleeQualifier != null) {
          qualifier.replace(calleeQualifier);
          return;
        }
      }
    }
    final PsiElement dot = PyPsiUtils.getNextNonWhitespaceSibling(qualifier);
    if (dot != null) dot.delete();
    qualifier.delete();
  }

  public static boolean isOwnScopeComprehension(@NotNull PyComprehensionElement comprehension) {
    final boolean isAtLeast30 = !LanguageLevel.forElement(comprehension).isPython2();
    final boolean isListComprehension = comprehension instanceof PyListCompExpression;
    return !isListComprehension || isAtLeast30;
  }

  public static ASTNode createNewName(PyElement element, String name) {
    return PyElementGenerator.getInstance(element.getProject()).createNameIdentifier(name, LanguageLevel.forElement(element));
  }

  /**
   * Finds element declaration by resolving its references top the top but not further than file (to prevent un-stubbing)
   *
   * @param elementToResolve element to resolve
   * @return its declaration
   */
  public static @NotNull PsiElement resolveToTheTop(final @NotNull PsiElement elementToResolve) {
    PsiElement currentElement = elementToResolve;
    final Set<PsiElement> checkedElements = new HashSet<>(); // To prevent PY-20553
    while (true) {
      final PsiReference reference = currentElement.getReference();
      if (reference == null) {
        break;
      }
      final PsiElement resolve = reference.resolve();
      if (resolve == null || checkedElements.contains(resolve) || resolve.equals(currentElement) || !inSameFile(resolve, currentElement)) {
        break;
      }
      currentElement = resolve;
      checkedElements.add(resolve);
    }
    return currentElement;
  }

  // Note that returned list may contain null items, e.g. for unresolved import elements, originally wrapped
  //  in `com.jetbrains.python.psi.resolve.ImportedResolveResult`
  //  TODO: it would be a good idea to revise `filterTopPriority` to return the import definer when the element is null
  public static @NotNull List<@Nullable PsiElement> multiResolveTopPriority(@NotNull PsiElement element,
                                                                            @NotNull PyResolveContext resolveContext) {
    if (element instanceof PyReferenceOwner referenceOwner) {
      return multiResolveTopPriority(referenceOwner.getReference(resolveContext));
    }
    else {
      final PsiReference reference = element.getReference();
      return reference != null ? Collections.singletonList(reference.resolve()) : Collections.emptyList();
    }
  }

  public static @NotNull List<@NotNull PsiElement> multiResolveTopPriority(@NotNull PsiPolyVariantReference reference) {
    return filterTopPriorityElements(Arrays.asList(reference.multiResolve(false)));
  }

  /**
   * @deprecated Use {@link #filterTopPriorityElements(List)}
   */
  @Deprecated(forRemoval = true)
  public static @NotNull List<PsiElement> filterTopPriorityResults(ResolveResult @NotNull [] resolveResults) {
    if (resolveResults.length == 0) return Collections.emptyList();

    final int maxRate = getMaxRate(Arrays.asList(resolveResults));
    return StreamEx
      .of(resolveResults)
      .filter(resolveResult -> getRate(resolveResult) >= maxRate)
      .map(ResolveResult::getElement)
      .nonNull()
      .toList();
  }

  public static @NotNull <E extends ResolveResult> List<E> filterTopPriorityResults(@NotNull List<? extends E> resolveResults) {
    if (resolveResults.isEmpty()) return Collections.emptyList();

    final int maxRate = getMaxRate(resolveResults);
    return ContainerUtil.filter(resolveResults, resolveResult -> getRate(resolveResult) >= maxRate);
  }

  public static @NotNull <E extends ResolveResult> List<PsiElement> filterTopPriorityElements(@NotNull List<? extends E> resolveResults) {
    return ContainerUtil.mapNotNull(filterTopPriorityResults(resolveResults), ResolveResult::getElement);
  }

  private static int getMaxRate(@NotNull List<? extends ResolveResult> resolveResults) {
    return resolveResults
      .stream()
      .mapToInt(PyUtil::getRate)
      .max()
      .orElse(Integer.MIN_VALUE);
  }

  private static int getRate(@NotNull ResolveResult resolveResult) {
    return resolveResult instanceof RatedResolveResult ? ((RatedResolveResult)resolveResult).getRate() : RatedResolveResult.RATE_NORMAL;
  }

  /**
   * Gets class init method
   *
   * @param pyClass class where to find init
   * @return class init method if any
   */
  public static @Nullable PyFunction getInitMethod(final @NotNull PyClass pyClass) {
    return pyClass.findMethodByName(PyNames.INIT, false, null);
  }

  /**
   * Clone of C# "as" operator.
   * Checks if expression has correct type and casts it if it has. Returns null otherwise.
   * It saves coder from "instanceof / cast" chains.
   *
   * @param expression expression to check
   * @param clazz      class to cast
   * @param <T>        class to cast
   * @return expression casted to appropriate type (if could be casted). Null otherwise.
   */
  public static @Nullable <T> T as(final @Nullable Object expression, final @NotNull Class<T> clazz) {
    return ObjectUtils.tryCast(expression, clazz);
  }

  // TODO: Move to PsiElement?

  /**
   * Searches for references injected to element with certain type
   *
   * @param element       element to search injected references for
   * @param expectedClass expected type of element reference resolved to
   * @param <T>           expected type of element reference resolved to
   * @return resolved element if found or null if not found
   */
  public static @Nullable <T extends PsiElement> T findReference(final @NotNull PsiElement element, final @NotNull Class<T> expectedClass) {
    for (final PsiReference reference : element.getReferences()) {
      final T result = as(reference.resolve(), expectedClass);
      if (result != null) {
        return result;
      }
    }
    return null;
  }


  /**
   * Converts collection to list of certain type
   *
   * @param expression   expression of collection type
   * @param elementClass expected element type
   * @param <T>          expected element type
   * @return list of elements of expected element type
   */
  public static @NotNull <T> List<T> asList(final @Nullable Collection<?> expression, final @NotNull Class<? extends T> elementClass) {
    if ((expression == null) || expression.isEmpty()) {
      return Collections.emptyList();
    }
    final List<T> result = new ArrayList<>();
    for (final Object element : expression) {
      final T toAdd = as(element, elementClass);
      if (toAdd != null) {
        result.add(toAdd);
      }
    }
    return result;
  }

  /**
   * Calculates and caches value based on param. Think about it as about map with param as key which flushes on each psi modification.
   * <p>
   * For nullable function see {@link #getNullableParameterizedCachedValue(PsiElement, Object, NullableFunction)}.
   * <p>
   * This function is used instead of {@link CachedValuesManager#createParameterizedCachedValue(ParameterizedCachedValueProvider, boolean)}
   * because parameter is not used as key there but used only for first calculation. Hence this should have functional dependency on element.
   *
   * @param element place to store cache
   * @param param   param to be used as key
   * @param f       function to produce value for key
   * @param <T>     value type
   * @param <P>     key type
   */
  public static @NotNull <T, P> T getParameterizedCachedValue(@NotNull PsiElement element,
                                                              @Nullable P param,
                                                              @NotNull Function<P, @NotNull T> f) {
    final T result = getNullableParameterizedCachedValue(element, param, f);
    assert result != null;
    return result;
  }

  /**
   * Same as {@link #getParameterizedCachedValue(PsiElement, Object, Function)} but allows nulls.
   */
  public static @Nullable <T, P> T getNullableParameterizedCachedValue(@NotNull PsiElement element,
                                                                       @Nullable P param,
                                                                       @NotNull Function<P, @Nullable T> f) {
    final CachedValuesManager manager = CachedValuesManager.getManager(element.getProject());
    final Map<Optional<P>, Optional<T>> cache = CachedValuesManager.getCachedValue(element, manager.getKeyForClass(f.getClass()), () -> {
      // concurrent hash map is a null-hostile collection
      return CachedValueProvider.Result.create(Maps.newConcurrentMap(), PsiModificationTracker.MODIFICATION_COUNT);
    });
    // Don't use ConcurrentHashMap#computeIfAbsent(), it blocks if the function tries to update the cache recursively for the same key
    // during computation. We can accept here that some values will be computed several times due to non-atomic updates.
    final Optional<P> wrappedParam = Optional.ofNullable(param);
    Optional<T> value = cache.get(wrappedParam);
    if (value == null) {
      value = Optional.ofNullable(f.fun(param));
      cache.put(wrappedParam, value);
    }
    return value.orElse(null);
  }

  /**
   * This method is allowed to be called from any thread, but in general you should not set {@code modal=true} if you're calling it
   * from the write action, because in this case {@code function} will be executed right in the current thread (presumably EDT)
   * without any progress whatsoever to avoid possible deadlock.
   *
   * @see com.intellij.openapi.application.impl.ApplicationImpl#runProcessWithProgressSynchronously(Runnable, String, boolean, boolean, Project, JComponent, String)
   */
  public static void runWithProgress(@Nullable Project project, @Nls(capitalization = Nls.Capitalization.Title) @NotNull String title,
                                     boolean modal, boolean canBeCancelled, final @NotNull Consumer<? super ProgressIndicator> function) {
    Task.Backgroundable task = new Task.Backgroundable(project, title, canBeCancelled) {
      @Override
      public void run(@NotNull ProgressIndicator indicator) {
        function.consume(indicator);
      }
    };
    ProgressManager.getInstance().run(task.toModalIfNeeded(modal));
  }

  /**
   * Executes code only if <pre>_PYCHARM_VERBOSE_MODE</pre> is set in env (which should be done for debug purposes only)
   *
   * @param runnable code to call
   */
  public static void verboseOnly(final @NotNull Runnable runnable) {
    if (VERBOSE_MODE) {
      runnable.run();
    }
  }

  public static boolean isPy2ReservedWord(@NotNull PyReferenceExpression node) {
    if (LanguageLevel.forElement(node).isPython2()) {
      if (!node.isQualified()) {
        final String name = node.getName();
        if (PyNames.NONE.equals(name) || PyNames.FALSE.equals(name) || PyNames.TRUE.equals(name)) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * Retrieves the document from {@link PsiDocumentManager} using the anchor PSI element and, if it's not null,
   * passes it to the consumer function.
   * <p>
   * The document is first released from pending PSI operations and then committed after the function has been applied
   * in a {@code try/finally} block, so that subsequent operations on PSI could be performed.
   *
   * @see PsiDocumentManager#doPostponedOperationsAndUnblockDocument(Document)
   * @see PsiDocumentManager#commitDocument(Document)
   * @see #updateDocumentUnblockedAndCommitted(PsiElement, Function)
   */
  public static void updateDocumentUnblockedAndCommitted(@NotNull PsiElement anchor, @NotNull Consumer<? super Document> consumer) {
    PyUtilCore.updateDocumentUnblockedAndCommitted(anchor, consumer);
  }

  public static @Nullable <T> T updateDocumentUnblockedAndCommitted(@NotNull PsiElement anchor,
                                                                    @NotNull Function<? super Document, ? extends T> func) {
    return PyUtilCore.updateDocumentUnblockedAndCommitted(anchor, func);
  }

  public static @Nullable PyType getReturnTypeToAnalyzeAsCallType(@NotNull PyFunction function, @NotNull TypeEvalContext context) {
    if (isInitMethod(function)) {
      final PyClass cls = function.getContainingClass();
      if (cls != null) {
        PyType providedClassType = PyTypeChecker.findGenericDefinitionType(cls, context);
        if (providedClassType != null) return providedClassType;

        final PyInstantiableType classType = as(context.getType(cls), PyInstantiableType.class);
        if (classType != null) {
          return classType.toInstance();
        }
      }
    }

    return context.getReturnType(function);
  }

  /**
   * Create a new expressions fragment from the given text, setting the specified element as its context,
   * and return the contained expression of the first expression statement in it.
   *
   * @param expressionText text of the expression
   * @param context        context element used to resolve symbols in the expression
   * @return instance of {@link PyExpression} as described
   * @see PyExpressionCodeFragment
   */
  public static @Nullable PyExpression createExpressionFromFragment(@NotNull String expressionText, @NotNull PsiElement context) {
    final PyExpressionCodeFragmentImpl codeFragment =
      new PyExpressionCodeFragmentImpl(context.getProject(), "dummy.py", expressionText, false);
    codeFragment.setContext(context);
    final PyExpressionStatement statement = as(codeFragment.getFirstChild(), PyExpressionStatement.class);
    return statement != null ? statement.getExpression() : null;
  }

  public static boolean isRoot(PsiFileSystemItem directory) {
    if (directory == null) return true;
    VirtualFile vFile = directory.getVirtualFile();
    if (vFile == null) return true;
    Project project = directory.getProject();
    return isRoot(vFile, project);
  }

  public static boolean isRoot(@NotNull VirtualFile directory, @NotNull Project project) {
    ProjectFileIndex fileIndex = ProjectFileIndex.getInstance(project);
    return Comparing.equal(fileIndex.getClassRootForFile(directory), directory) ||
           Comparing.equal(fileIndex.getContentRootForFile(directory), directory) ||
           Comparing.equal(fileIndex.getSourceRootForFile(directory), directory);
  }

  /**
   * Checks whether {@param file} representing Python module or package can be imported into {@param file}.
   */
  public static boolean isImportable(PsiFile targetFile, @NotNull PsiFileSystemItem file) {
    PsiDirectory parent = (PsiDirectory)file.getParent();
    return parent != null && file != targetFile &&
           (isRoot(parent) ||
            parent == targetFile.getParent() ||
            isPackage(parent, false, null));
  }

  public static @NotNull Collection<String> collectUsedNames(final @Nullable PsiElement scope) {
    if (!(scope instanceof PyClass) && !(scope instanceof PyFile) && !(scope instanceof PyFunction)) {
      return Collections.emptyList();
    }
    final Set<String> variables = new HashSet<>() {
      @Override
      public boolean add(String s) {
        return s != null && super.add(s);
      }
    };
    scope.acceptChildren(new PyRecursiveElementVisitor() {
      @Override
      public void visitPyTargetExpression(final @NotNull PyTargetExpression node) {
        variables.add(node.getName());
      }

      @Override
      public void visitPyNamedParameter(final @NotNull PyNamedParameter node) {
        variables.add(node.getName());
      }

      @Override
      public void visitPyReferenceExpression(@NotNull PyReferenceExpression node) {
        if (!node.isQualified()) {
          variables.add(node.getReferencedName());
        }
        else {
          super.visitPyReferenceExpression(node);
        }
      }

      @Override
      public void visitPyFunction(final @NotNull PyFunction node) {
        variables.add(node.getName());
      }

      @Override
      public void visitPyClass(final @NotNull PyClass node) {
        variables.add(node.getName());
      }
    });
    return variables;
  }

  /**
   * If argument is a PsiDirectory, turn it into a PsiFile that points to __init__.py in that directory.
   * If there's no __init__.py there, null is returned, there's no point to resolve to a dir which is not a package.
   * Alas, resolve() and multiResolve() can't return anything but a PyFile or PsiFileImpl.isPsiUpToDate() would fail.
   * This is because isPsiUpToDate() relies on identity of objects returned by FileViewProvider.getPsi().
   * If we ever need to exactly tell a dir from __init__.py, that logic has to change.
   *
   * @param target a resolve candidate.
   * @return a PsiFile if target was a PsiDirectory, or null, or target unchanged.
   */
  public static @Nullable PsiElement turnDirIntoInit(@Nullable PsiElement target) {
    if (target instanceof PsiDirectory dir) {
      final PsiFile initStub = dir.findFile(PyNames.INIT_DOT_PYI);
      if (initStub != null && !PyiStubSuppressor.isIgnoredStub(initStub)) {
        return initStub;
      }
      final PsiFile initFile = dir.findFile(PyNames.INIT_DOT_PY);
      if (initFile != null) {
        return initFile; // ResolveImportUtil will extract directory part as needed, everyone else are better off with a file.
      }
      else {
        return null;
      } // dir without __init__.py does not resolve
    }
    else {
      return target;
    } // don't touch non-dirs
  }

  public static @Nullable PsiElement turnDirIntoInitPy(@Nullable PsiElement target) {
    if (!(target instanceof PsiDirectory psiDirectory)) return target;
    return psiDirectory.findFile(PyNames.INIT_DOT_PY);
  }

  public static @Nullable PsiElement turnDirIntoInitPyi(@Nullable PsiElement target) {
    if (!(target instanceof PsiDirectory psiDirectory)) return target;
    final PsiFile initStub = psiDirectory.findFile(PyNames.INIT_DOT_PYI);
    if (initStub != null && !PyiStubSuppressor.isIgnoredStub(initStub)) {
      return initStub;
    }
    return null;
  }

  /**
   * If directory is a PsiDirectory, that is also a valid Python package, return PsiFile that points to __init__.py,
