// Copyright 2000-2017 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.impl;

import com.google.common.collect.ImmutableMap;
import com.intellij.codeInsight.completion.CompletionUtilCoreImpl;
import com.intellij.lang.ASTNode;
import com.intellij.navigation.ItemPresentation;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.NotNullLazyValue;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.*;
import com.intellij.psi.scope.PsiScopeProcessor;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.SearchScope;
import com.intellij.psi.stubs.IStubElementType;
import com.intellij.psi.stubs.StubElement;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.CachedValueProvider.Result;
import com.intellij.psi.util.*;
import com.intellij.ui.IconManager;
import com.intellij.ui.PlatformIcons;
import com.intellij.util.ArrayFactory;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.jetbrains.python.*;
import com.jetbrains.python.ast.PyAstFunction.Modifier;
import com.jetbrains.python.ast.impl.PyUtilCore;
import com.jetbrains.python.codeInsight.PyDataclassParameters;
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.codeInsight.stdlib.PyDataclassTypeProvider;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
import com.jetbrains.python.documentation.docstrings.DocStringUtil;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.stubs.PyClassElementType;
import com.jetbrains.python.psi.impl.stubs.PyVersionSpecificStubBaseKt;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.resolve.QualifiedNameFinder;
import com.jetbrains.python.psi.stubs.PropertyStubStorage;
import com.jetbrains.python.psi.stubs.PyClassStub;
import com.jetbrains.python.psi.stubs.PyFunctionStub;
import com.jetbrains.python.psi.stubs.PyTargetExpressionStub;
import com.jetbrains.python.psi.types.*;
import com.jetbrains.python.pyi.PyiUtil;
import com.jetbrains.python.toolbox.Maybe;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.util.*;
import java.util.stream.Collectors;

import static com.intellij.openapi.util.text.StringUtil.join;
import static com.intellij.openapi.util.text.StringUtil.notNullize;
import static com.jetbrains.python.codeInsight.PyDataclassesKt.parseDataclassParameters;
import static com.jetbrains.python.psi.PyUtil.as;
import static com.jetbrains.python.psi.impl.PyDeprecationUtilKt.extractDeprecationMessageFromDecorator;
import static java.util.Collections.emptySet;


public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyClass {
  public static class MROException extends Exception {
    public MROException(String s) {
      super(s);
    }
  }

  public static final PyClass[] EMPTY_ARRAY = new PyClassImpl[0];

  private volatile @Nullable List<PyTargetExpression> myInstanceAttributes;
  private volatile @Nullable List<PyTargetExpression> myFallbackInstanceAttributes;
  // Class attributes initialized in @classmethod-decorated methods
  private volatile @Nullable List<PyTargetExpression> myClassAttributesFromClassMethods;

  private volatile Map<String, Property> myLocalPropertyCache;

  @Override
  public PyType getType(@NotNull TypeEvalContext context, @NotNull TypeEvalContext.Key key) {
    if (PyTypingTypeProvider.ANY.equals(getQualifiedName())) {
      return null;
    }
    return new PyClassTypeImpl(this, true);
  }

  public PyClassImpl(@NotNull ASTNode astNode) {
    super(astNode);
  }

  public PyClassImpl(final @NotNull PyClassStub stub) {
    this(stub, PyStubElementTypes.CLASS_DECLARATION);
  }

  public PyClassImpl(final @NotNull PyClassStub stub, @NotNull IStubElementType nodeType) {
    super(stub, nodeType);
  }


  @Override
  public PsiElement setName(@NotNull String name) throws IncorrectOperationException {
    final ASTNode nameElement = PyUtil.createNewName(this, name);
    final ASTNode node = getNameNode();
    if (node != null) {
      getNode().replaceChild(node, nameElement);
    }
    return this;
  }

  @Override
  public @Nullable String getName() {
    final PyClassStub stub = getStub();
    if (stub != null) {
      return stub.getName();
    }
    else {
      return PyClass.super.getName();
    }
  }

  @Override
  public Icon getIcon(int flags) {
    return IconManager.getInstance().getPlatformIcon(PlatformIcons.Class);
  }

  @Override
  protected void acceptPyVisitor(PyElementVisitor pyVisitor) {
    pyVisitor.visitPyClass(this);
  }

  @Override
  public PyExpression @NotNull [] getSuperClassExpressions() {
    final PyArgumentList argList = getSuperClassExpressionList();
    if (argList != null) {
      return argList.getArguments();
    }
    return PyExpression.EMPTY_ARRAY;
  }

  public static @NotNull List<PyExpression> getUnfoldedSuperClassExpressions(@NotNull PyClass pyClass) {
    return StreamEx
      .of(pyClass.getSuperClassExpressions())
      .filter(expression -> !(expression instanceof PyKeywordArgument))
      .flatCollection(PyClassImpl::unfoldSuperClassExpression)
      .toList();
  }

  private static @NotNull List<PyExpression> unfoldSuperClassExpression(@NotNull PyExpression expression) {
    if (isSixWithMetaclassCall(expression)) {
      final PyExpression[] arguments = ((PyCallExpression)expression).getArguments();
      if (arguments.length > 1) {
        return ContainerUtil.subArrayAsList(arguments, 1, arguments.length);
      }
      else {
        return Collections.emptyList();
      }
    }
    // Heuristic: unfold Foo[Bar] to Foo for subscription expressions for superclasses
    else if (expression instanceof PySubscriptionExpression subscriptionExpr) {
      return Collections.singletonList(subscriptionExpr.getOperand());
    }

    return Collections.singletonList(expression);
  }

  public static boolean isSixWithMetaclassCall(@NotNull PyExpression expression) {
    if (expression instanceof PyCallExpression) {
      final PyExpression callee = ((PyCallExpression)expression).getCallee();

      if (callee instanceof PyReferenceExpression) {
        final QualifiedName sixWithMetaclass = QualifiedName.fromComponents("six", "with_metaclass");
        final QualifiedName djangoWithMetaclass = QualifiedName.fromDottedString("django.utils.six.with_metaclass");

        return ContainerUtil.exists(PyResolveUtil.resolveImportedElementQNameLocally((PyReferenceExpression)callee),
                                    name -> name.equals(sixWithMetaclass) || name.equals(djangoWithMetaclass));
      }
    }

    return false;
  }

  @Override
  public final @NotNull List<PyClass> getAncestorClasses(final @Nullable TypeEvalContext context) {
    final List<PyClass> results = new ArrayList<>();
    for (final PyClassLikeType type : getAncestorTypes(notNullizeContext(context))) {
      if (type instanceof PyClassType) {
        results.add(((PyClassType)type).getPyClass());
      }
    }
    return results;
  }

  @Override
  public boolean isSubclass(PyClass parent, @Nullable TypeEvalContext context) {
    if (this == parent) {
      return true;
    }
    for (PyClass superclass : getAncestorClasses(context)) {
      if (parent == superclass) return true;
    }
    return false;
  }

  @Override
  public boolean isSubclass(@NotNull String superClassQName, @Nullable TypeEvalContext context) {
    if (superClassQName.equals(getQualifiedName())) {
      return true;
    }
    for (PyClassLikeType type : getAncestorTypes(notNullizeContext(context))) {
      if (type != null && superClassQName.equals(type.getClassQName())) {
        return true;
      }
    }
    return false;
  }

  @Override
  public @Nullable String getQualifiedName() {
    return QualifiedNameFinder.getQualifiedName(this);
  }

  @Override
  public @Nullable List<String> getSlots(@Nullable TypeEvalContext context) {
    final Set<String> result = new LinkedHashSet<>();

    final PyClassType currentType = new PyClassTypeImpl(this, true);
    final TypeEvalContext contextToUse = notNullizeContext(context);

    List<PyClassLikeType> meAndAncestors = ContainerUtil.concat(Collections.singletonList(currentType), getAncestorTypes(contextToUse));
    for (PyClassLikeType type : ContainerUtil.reverse(meAndAncestors)) {
      if (!(type instanceof PyClassType)) return null;

      final PyClass cls = ((PyClassType)type).getPyClass();
      if (PyUtil.isObjectClass(cls)) {
        continue;
      }

      if (!cls.isNewStyleClass(contextToUse)) return null;

      result.removeAll(getNamesOfOwnAttributesThatHaveAssignedValues()); // remove slots that are shadowed by class attributes

      List<String> ownSlots = cls.getOwnSlots();
      if (ownSlots != null && ownSlots.contains(PyNames.DUNDER_DICT)) {
        return null; // not "viably slotted" due to __dict__ in __slots__
      }
      if (ownSlots == null) {
        // check @dataclass(slots=True)
        ownSlots = getOwnSlotsSynthesized(contextToUse);
      }
      if (ownSlots == null) {
        return null; // not "viably slotted"
      }

      result.addAll(ownSlots);
    }

    return new ArrayList<>(result);
  }

  @Override
  public @Nullable List<String> getOwnSlots() {
    final PyClassStub stub = getStub();
    if (stub != null) {
      return stub.getSlots();
    }

    final PyTargetExpression slotsTargetExpr = ContainerUtil.find(getClassAttributes(), target -> PyNames.SLOTS.equals(target.getName()));
    if (slotsTargetExpr != null) {
      final PyExpression slotsValue = slotsTargetExpr.findAssignedValue();
      final LinkedHashSet<String> slots = new LinkedHashSet<>();
      if (slotsValue instanceof PyStringLiteralExpression) {
        slots.add(((PyStringLiteralExpression)slotsValue).getStringValue());
      }
      else {
        List<@NotNull String> slotsValues = PyUtilCore.strListValue(slotsValue);
        if (slotsValues != null) {
          slots.addAll(slotsValues);
        }
      }

      slots.removeAll(getNamesOfOwnAttributesThatHaveAssignedValues()); // remove slots that are shadowed by class attributes

      return new ArrayList<>(slots);
    }

    return null;
  }

  private Set<String> getNamesOfOwnAttributesThatHaveAssignedValues() {
    return getClassAttributes().stream()
      .filter(target -> target.hasAssignedValue() && target.getName() != null)
      .map(PyTargetExpression::getName).collect(Collectors.toSet());
  }


  /**
   * Returns null if this class is not made "viably slotted" via <code>@dataclass(slots=True)</code>.
   * Returns a list of all valid slotted attribute names otherwise.
   */
  private @Nullable List<@NotNull String> getOwnSlotsSynthesized(@NotNull TypeEvalContext context) {
    PyDataclassParameters dcParams = parseDataclassParameters(this, context);
    if (dcParams != null && dcParams.getSlots()) {
      List<String> result = new ArrayList<>();
      var initVars = PyDataclassTypeProvider.Companion.getInitVars(this, dcParams, context);
      var initVarTargets = initVars == null ? emptySet() : ContainerUtil.map2Set(initVars, iv -> iv.getTargetExpression());
      var attributes = getClassAttributes();

      for (PyTargetExpression target : attributes) {
        final String name = target.getName();
        if (name == null) continue;

        // Include only dataclass instance fields:
        // - must be an annotated field (PEP 526), not an unannotated class attribute
        // - must not be typing.ClassVar
        // - must not be dataclasses.InitVar
        final boolean isAnnotatedField = target.getAnnotation() != null || target.getParent() instanceof PyTypeDeclarationStatement;
        if (!isAnnotatedField) continue;
        if (PyTypingTypeProvider.isClassVar(target, context)) continue;
        if (initVarTargets.contains(target)) continue;

        result.add(name);
      }
      return result;
    }
    return null;
  }

  @Override
  public @Nullable List<@NotNull String> getOwnMatchArgs() {
    final PyClassStub stub = getStub();
    if (stub != null) {
      return stub.getMatchArgs();
    }

    final PyTargetExpression matchArgs = ContainerUtil.find(getClassAttributes(), target -> PyNames.MATCH_ARGS.equals(target.getName()));
    if (matchArgs != null) {
      final PyExpression value = matchArgs.findAssignedValue();
      return PyUtilCore.strListValue(value);
    }

    return null;
  }

  @Override
  public PyClass @NotNull [] getSuperClasses(@Nullable TypeEvalContext context) {
    final List<PyClassLikeType> superTypes = getSuperClassTypes(notNullizeContext(context));
    if (superTypes.isEmpty()) {
      return EMPTY_ARRAY;
    }
    final List<PyClass> result = new ArrayList<>();
    for (PyClassLikeType type : superTypes) {
      if (type instanceof PyClassType) {
        result.add(((PyClassType)type).getPyClass());
      }
    }
    return result.toArray(PyClass.EMPTY_ARRAY);
  }

  @Override
  public ItemPresentation getPresentation() {
    return new PyElementPresentation(this) {
      @Override
      public @NotNull String getPresentableText() {
        PyPsiUtils.assertValid(PyClassImpl.this);
        final StringBuilder result = new StringBuilder(notNullize(getName(), PyNames.UNNAMED_ELEMENT));
        final List<String> superClassesText = getSuperClassesText();
        if (!superClassesText.isEmpty()) {
          result.append("(");
          result.append(join(superClassesText, expr -> notNullize(expr, PyNames.UNNAMED_ELEMENT), ", "));
          result.append(")");
        }
        return result.toString();
      }
    };
  }

  private List<String> getSuperClassesText() {
    PyClassStub stub = getGreenStub();
    if (stub == null) {
      return ContainerUtil.map(getSuperClassExpressions(), PsiElement::getText);
    }
    else {
      return stub.getSuperClassesText();
    }
  }

  private static @NotNull List<PyClassLikeType> mroMerge(@NotNull List<List<PyClassLikeType>> sequences) throws MROException {
    List<PyClassLikeType> result = new LinkedList<>(); // need to insert to 0th position on linearize
    while (true) {
      // filter blank sequences
      final List<List<PyClassLikeType>> nonBlankSequences = new ArrayList<>(sequences.size());
      for (List<PyClassLikeType> item : sequences) {
        if (!item.isEmpty()) nonBlankSequences.add(item);
      }
      if (nonBlankSequences.isEmpty()) return result;
      // find a clean head
      boolean found = false;
      PyClassLikeType head = null; // to keep compiler happy; really head is assigned in the loop at least once.
      for (List<PyClassLikeType> seq : nonBlankSequences) {
        head = seq.getFirst();
        if (head == null) {
          seq.removeFirst();
          found = true;
          break;
        }
        boolean headInTails = false;
        for (List<PyClassLikeType> tailSeq : nonBlankSequences) {
          if (tailSeq.indexOf(head) > 0) { // -1 is not found, 0 is head, >0 is tail.
            headInTails = true;
            break;
          }
        }
        if (!headInTails) {
          found = true;
          break;
        }
        else {
          head = null; // as a signal
        }
      }
      if (!found) {
        // Inconsistent hierarchy results in TypeError
        throw new MROException("Inconsistent class hierarchy");
      }
      // our head is clean;
      result.add(head);
      // remove it from heads of other sequences
      if (head != null) {
        for (List<PyClassLikeType> seq : nonBlankSequences) {
          if (Comparing.equal(seq.getFirst(), head)) {
            seq.removeFirst();
          }
        }
      }
    } // we either return inside the loop or die by assertion
  }


  private static @NotNull List<PyClassLikeType> mroLinearize(@NotNull PyClassLikeType type,
                                                             boolean addThisType,
                                                             @NotNull TypeEvalContext context,
                                                             @NotNull Map<PyClassLikeType, Ref<List<PyClassLikeType>>> cache)
    throws MROException {
    final Ref<List<PyClassLikeType>> computed = cache.get(type);
    if (computed != null) {
      if (computed.isNull()) {
        throw new MROException("Circular class inheritance");
      }
      return computed.get();
    }
    cache.put(type, Ref.create());
    List<PyClassLikeType> result = null;
    try {
      final List<PyClassLikeType> bases = moveTypingGenericToTheEnd(removeNotNullDuplicates(type.getSuperClassTypes(context)));
      final List<List<PyClassLikeType>> lines = new ArrayList<>();
      for (PyClassLikeType base : bases) {
        if (base != null) {
          // Don't include ancestors of a metaclass instance
          final List<PyClassLikeType> baseClassMRO;
          if (base.isDefinition()) {
            baseClassMRO = mroLinearize(base, true, context, cache);
          }
          else {
            List<PyClassLikeType> metaclassInstanceMro = new ArrayList<>();
            metaclassInstanceMro.add(base);
            if (base instanceof PyClassType) {
              final PyClassImpl pyClass = as(((PyClassType)base).getPyClass(), PyClassImpl.class);
              if (pyClass != null) {
                ContainerUtil.addIfNotNull(metaclassInstanceMro, pyClass.getImplicitSuper());
              }
            }
            baseClassMRO = metaclassInstanceMro;
          }
          if (!baseClassMRO.isEmpty()) {
            // mroMerge() updates passed MRO lists internally
            lines.add(new LinkedList<>(baseClassMRO));
          }
        }
      }
      if (!bases.isEmpty()) {
        lines.add(bases);
      }
      result = mroMerge(lines);
      if (addThisType) {
        result.addFirst(type);
      }
      result = Collections.unmodifiableList(result);
    }
    finally {
      cache.put(type, Ref.create(result));
    }
    return result;
  }

  private static @NotNull List<PyClassLikeType> moveTypingGenericToTheEnd(@NotNull List<PyClassLikeType> bases) {
    int genericPos = ContainerUtil.indexOf(bases,
                                           base -> base instanceof PyCustomType customType &&
                                                   PyTypingTypeProvider.GENERIC.equals(customType.getClassQName()));
    if (genericPos >= 0) {
      List<PyClassLikeType> reorderedBases = new ArrayList<>(bases.size());
      reorderedBases.addAll(bases.subList(0, genericPos));
      reorderedBases.addAll(bases.subList(genericPos + 1, bases.size()));
      reorderedBases.add(bases.get(genericPos));
      return reorderedBases;
    }
    return bases;
  }

  private static @NotNull <T> List<T> removeNotNullDuplicates(@NotNull List<T> list) {
    final Set<T> distinct = new HashSet<>();
    final List<T> result = new ArrayList<>();
    for (T elem : list) {
      if (elem != null) {
        final boolean isUnique = distinct.add(elem);
        if (!isUnique) {
          continue;
        }
      }
      result.add(elem);
    }
    return result;
  }

  @Override
  public PyFunction @NotNull [] getMethodsInherited(@Nullable TypeEvalContext context) {
    List<PyFunction> collectedMethods = new ArrayList<>(Arrays.asList(getMethods()));

    for (PyClass superClass : getAncestorClasses(context)) {
      collectedMethods.addAll(Arrays.asList(superClass.getMethods()));
    }
    return collectedMethods.toArray(PyFunction.EMPTY_ARRAY);
  }

  @Override
  public PyFunction @NotNull [] getMethods() {
    final TokenSet functionDeclarationTokens = PythonDialectsTokenSetProvider.getInstance().getFunctionDeclarationTokens();
    return getClassChildren(functionDeclarationTokens, PyFunction.class, PyFunction.ARRAY_FACTORY);
  }

  @Override
  public @NotNull Map<String, Property> getPropertiesInherited(@Nullable TypeEvalContext context) {
    initLocalProperties();
    Map<String, Property> propertiesHashMap = new HashMap<>(myLocalPropertyCache);

    for (PyClass superClass : getAncestorClasses(context)) {
      Map<String, Property> superClassProperties = superClass.getPropertiesInherited(context);
      for (Map.Entry<String, Property> entry : superClassProperties.entrySet()) {
        // Do not replace existing property in case superclass have it, keep only subclass properties
        propertiesHashMap.putIfAbsent(entry.getKey(), entry.getValue());
      }
    }
    return propertiesHashMap;
  }

  @Override
  public @NotNull Map<String, Property> getProperties() {
    initLocalProperties();
    return new HashMap<>(myLocalPropertyCache);
  }

  @Override
  public PyClass[] getNestedClasses() {
    return getClassChildren(TokenSet.create(PyElementTypes.CLASS_DECLARATION), PyClass.class, PyClass.ARRAY_FACTORY);
  }

  private <T extends StubBasedPsiElement<? extends StubElement<T>>> T @NotNull [] getClassChildren(@NotNull TokenSet elementTypes,
                                                                                                   @NotNull Class<T> childrenClass,
                                                                                                   @NotNull ArrayFactory<? extends T> factory) {
    final List<T> result = new ArrayList<>();
    processClassLevelDeclarations(new PsiScopeProcessor() {
      @Override
      public boolean execute(@NotNull PsiElement element, @NotNull ResolveState state) {
        if (childrenClass.isInstance(element) && elementTypes.contains(((StubBasedPsiElement<?>)element).getElementType())) {
          result.add(childrenClass.cast(element));
        }
        return true;
      }
    });
    return ContainerUtil.toArray(result, factory);
  }

  private static class NameFinder<T extends PyElement> implements Processor<T> {
    private final @NotNull TypeEvalContext myContext;
    private T myResult;
    private final String[] myNames;
    private int myLastResultIndex = -1;
    private PyClass myLastVisitedClass = null;

    NameFinder(@NotNull TypeEvalContext context, String... names) {
      myContext = context;
      myNames = names;
      myResult = null;
    }

    public T getResult() {
      return myResult;
    }

    protected @Nullable PyClass getContainingClass(@NotNull T element) {
      return null;
    }

    @Override
    public boolean process(T target) {
      final PyClass currentClass = getContainingClass(target);
      // Stop when the current class changes and there was a result
      if (myLastVisitedClass != null && myLastVisitedClass != currentClass && myResult != null) {
        return false;
      }

      myLastVisitedClass = currentClass;

      final int index = ArrayUtil.indexOf(myNames, target.getName());
      // Do not depend on the order in which elements appear, always try to find the first one
      if (index >= 0) {
        if (myLastResultIndex == -1 ||
            index < myLastResultIndex ||
            index == myLastResultIndex && PyiUtil.isOverload(myResult, myContext) && !PyiUtil.isOverload(target, myContext)) {
          myLastResultIndex = index;
          myResult = target;

          if (index == 0 && !PyiUtil.isOverload(myResult, myContext)) {
            return false;
          }
        }
      }
      return true;
    }
  }

  private static class MultiNameFinder<T extends PyElement> implements Processor<T> {

    private final @NotNull List<T> myResult;

    private final String @NotNull [] myNames;

    private @Nullable PyClass myLastVisitedClass;

    MultiNameFinder(String @NotNull ... names) {
      myResult = new ArrayList<>();
      myNames = names;
      myLastVisitedClass = null;
    }

    @Override
    public boolean process(T t) {
      final PyClass currentClass = t instanceof PyPossibleClassMember ? ((PyPossibleClassMember)t).getContainingClass() : null;
      // Stop when the current class changes and there was a result
      if (myLastVisitedClass != null && currentClass != myLastVisitedClass && !myResult.isEmpty()) {
        return false;
      }

      myLastVisitedClass = currentClass;

      if (ArrayUtil.contains(t.getName(), myNames)) {
        myResult.add(t);
      }

      return true;
    }
  }

  @Override
  public PyFunction findMethodByName(final @Nullable String name, boolean inherited, @Nullable TypeEvalContext context) {
    if (name == null) return null;
    NameFinder<PyFunction> proc = new NameFinder<>(notNullizeContext(context), name);
    visitMethods(proc, inherited, context);
    return proc.getResult();
  }

  @Override
  public @NotNull List<PyFunction> multiFindMethodByName(@NotNull String name, boolean inherited, @Nullable TypeEvalContext context) {
    final MultiNameFinder<PyFunction> processor = new MultiNameFinder<>(name);
    visitMethods(processor, inherited, context);
    return processor.myResult;
  }

  @Override
  public @Nullable PyClass findNestedClass(String name, boolean inherited) {
    if (name == null) return null;
    NameFinder<PyClass> proc = new NameFinder<>(TypeEvalContext.codeInsightFallback(getProject()), name);
    visitNestedClasses(proc, inherited);
    return proc.getResult();
  }

  @Override
  public @Nullable PyFunction findInitOrNew(boolean inherited, final @Nullable TypeEvalContext context) {
    NameFinder<PyFunction> proc;
    if (isNewStyleClass(context)) {
      proc = new NameFinder<>(notNullizeContext(context), PyNames.INIT, PyNames.NEW) {
        @Override
        protected @Nullable PyClass getContainingClass(@NotNull PyFunction element) {
          return element.getContainingClass();
        }
      };
    }
    else {
      proc = new NameFinder<>(notNullizeContext(context), PyNames.INIT);
    }
    visitMethods(proc, inherited, context);
    return proc.getResult();
  }

  @Override
  public @NotNull List<PyFunction> multiFindInitOrNew(boolean inherited, @Nullable TypeEvalContext context) {
    final MultiNameFinder<PyFunction> processor = isNewStyleClass(context)
                                                  ? new MultiNameFinder<>(PyNames.INIT, PyNames.NEW)
                                                  : new MultiNameFinder<>(PyNames.INIT);

    visitMethods(processor, inherited, context);
    return processor.myResult;
  }

  private static final Maybe<PyCallable> UNKNOWN_CALL = new Maybe<>(); // denotes _not_ a PyFunction, actually
  private static final Maybe<PyCallable> NONE = new Maybe<>(null); // denotes an explicit None

  /**
   * @param filter returns true if the property is acceptable
   * @return the first property that filter accepted.
   */
  private @Nullable Property processPropertiesInClass(@Nullable Processor<? super Property> filter) {
    final Property decoratedProperty = processDecoratedProperties(filter);
    if (decoratedProperty != null) return decoratedProperty;

    if (getStub() != null) {
      return processStubProperties(filter);
    }
    else {
      // name = property(...) assignments from PSI
      for (PyTargetExpression target : getClassAttributes()) {
        final Property property = PropertyImpl.fromTarget(target);
        if (property != null && (filter == null || filter.process(property))) {
          return property;
        }
      }
    }

    return null;
  }

  private @Nullable Property processDecoratedProperties(@Nullable Processor<? super Property> filter) {
    // look at @property decorators
    final MultiMap<String, PyFunction> grouped = new MultiMap<>();
    // group suitable same-named methods, each group defines a property
    for (PyFunction method : getMethods()) {
      grouped.putValue(method.getName(), method);
    }
    for (Map.Entry<String, Collection<PyFunction>> entry : grouped.entrySet()) {
      Maybe<PyCallable> getter = NONE;
      Maybe<PyCallable> setter = NONE;
      Maybe<PyCallable> deleter = NONE;
      final String decoratorName = entry.getKey();
      for (PyFunction method : entry.getValue()) {
        final PyDecoratorList decoratorList = method.getDecoratorList();
        if (decoratorList != null) {
          for (PyDecorator deco : decoratorList.getDecorators()) {
            TypeEvalContext context = TypeEvalContext.codeInsightFallback(getProject());
            List<PyKnownDecorator> knownDecorators = PyKnownDecoratorUtil.asKnownDecorators(deco, context);
            for (PyKnownDecorator knownDecorator : knownDecorators) {
              if (knownDecorator.isMutableProperty()) {
                getter = new Maybe<>(method);
                setter = new Maybe<>(method);
                deleter = new Maybe<>(method);
                break;
              }

              if (knownDecorator.isProperty()) {
                getter = new Maybe<>(method);
                break;
              }
            }

            final QualifiedName qname = deco.getQualifiedName();
            if (qname != null) {
              if (qname.matches(decoratorName, PyNames.GETTER)) {
                getter = new Maybe<>(method);
              }
              else if (qname.matches(decoratorName, PyNames.SETTER)) {
                setter = new Maybe<>(method);
              }
              else if (qname.matches(decoratorName, PyNames.DELETER)) {
                deleter = new Maybe<>(method);
              }
            }
          }
        }
        if (getter != NONE && setter != NONE && deleter != NONE) break; // can't improve
      }
      if (getter != NONE || setter != NONE || deleter != NONE) {
        final PropertyImpl prop = new PropertyImpl(decoratorName, getter, setter, deleter, null, null);
        if (filter == null || filter.process(prop)) return prop;
      }
    }
    return null;
  }

  private Maybe<PyCallable> fromPacked(Maybe<String> maybeName) {
    if (maybeName.isDefined()) {
      final String value = maybeName.value();
      if (value == null || PyNames.NONE.equals(value)) {
        return NONE;
      }
      PyFunction method = findMethodByName(value, true, null);
      if (method != null) return new Maybe<>(method);
    }
    return UNKNOWN_CALL;
  }

  private @Nullable Property processStubProperties(@Nullable Processor<? super Property> filter) {
    final PyClassStub stub = getStub();
    if (stub != null) {
      for (StubElement<?> subStub : getVersionSpecificChildrenStubs(stub)) {
        if (subStub.getStubType() == PyElementTypes.TARGET_EXPRESSION) {
          final PyTargetExpressionStub targetStub = (PyTargetExpressionStub)subStub;
          final PropertyStubStorage prop = targetStub.getCustomStub(PropertyStubStorage.class);
          if (prop != null) {
            final Maybe<PyCallable> getter = fromPacked(prop.getGetter());
            final Maybe<PyCallable> setter = fromPacked(prop.getSetter());
            final Maybe<PyCallable> deleter = fromPacked(prop.getDeleter());
            final String doc = prop.getDoc();
            if (getter != NONE || setter != NONE || deleter != NONE) {
              final PropertyImpl property = new PropertyImpl(targetStub.getName(), getter, setter, deleter, doc, targetStub.getPsi());
              if (filter == null || filter.process(property)) return property;
            }
          }
        }
      }
    }
    return null;
  }

  @Override
  public @Nullable Property findProperty(@NotNull String name, boolean inherited, @Nullable TypeEvalContext context) {
    initLocalProperties();

    final Property property = myLocalPropertyCache.get(name);
    if (property != null) {
      return property;
    }

    if (findMethodByName(name, false, context) != null ||
        findClassAttribute(name, false, context) != null ||
        ContainerUtil.notNullize(getOwnSlots()).contains(name)) {
      return null;
    }

    if (inherited) {
      for (PyClass cls : getAncestorClasses(context)) {
        final Property ancestorProperty = cls.findProperty(name, false, context);
        if (ancestorProperty != null) {
          return ancestorProperty;
        }
      }
    }

    return null;
  }

  @Override
  public @Nullable Property findPropertyByCallable(PyCallable callable) {
    initLocalProperties();

    for (Property property : myLocalPropertyCache.values()) {
      if (property.getGetter().valueOrNull() == callable ||
          property.getSetter().valueOrNull() == callable ||
