@NotNull
  private TypeProjection unsafeSubstitute(
      @NotNull TypeProjection originalProjection, int recursionDepth) throws SubstitutionException {
    assertRecursionDepth(recursionDepth, originalProjection, substitution);

    if (originalProjection.isStarProjection()) return originalProjection;

    // The type is within the substitution range, i.e. T or T?
    JetType type = originalProjection.getType();
    TypeProjection replacement = substitution.get(type);
    Variance originalProjectionKind = originalProjection.getProjectionKind();
    if (replacement == null
        && FlexibleTypesKt.isFlexible(type)
        && !TypeCapabilitiesKt.isCustomTypeVariable(type)) {
      Flexibility flexibility = FlexibleTypesKt.flexibility(type);
      TypeProjection substitutedLower =
          unsafeSubstitute(
              new TypeProjectionImpl(originalProjectionKind, flexibility.getLowerBound()),
              recursionDepth + 1);
      TypeProjection substitutedUpper =
          unsafeSubstitute(
              new TypeProjectionImpl(originalProjectionKind, flexibility.getUpperBound()),
              recursionDepth + 1);

      Variance substitutedProjectionKind = substitutedLower.getProjectionKind();
      assert (substitutedProjectionKind == substitutedUpper.getProjectionKind())
                  && originalProjectionKind == Variance.INVARIANT
              || originalProjectionKind == substitutedProjectionKind
          : "Unexpected substituted projection kind: "
              + substitutedProjectionKind
              + "; original: "
              + originalProjectionKind;

      JetType substitutedFlexibleType =
          DelegatingFlexibleType.create(
              substitutedLower.getType(),
              substitutedUpper.getType(),
              flexibility.getExtraCapabilities());
      return new TypeProjectionImpl(substitutedProjectionKind, substitutedFlexibleType);
    }

    if (KotlinBuiltIns.isNothing(type) || type.isError()) return originalProjection;

    if (replacement != null) {
      VarianceConflictType varianceConflict =
          conflictType(originalProjectionKind, replacement.getProjectionKind());

      // Captured type might be substituted in an opposite projection:
      // out 'Captured (in Int)' = out Int
      // in 'Captured (out Int)' = in Int
      boolean allowVarianceConflict = CapturedTypeConstructorKt.isCaptured(type);
      if (!allowVarianceConflict) {
        //noinspection EnumSwitchStatementWhichMissesCases
        switch (varianceConflict) {
          case OUT_IN_IN_POSITION:
            throw new SubstitutionException("Out-projection in in-position");
          case IN_IN_OUT_POSITION:
            // todo use the right type parameter variance and upper bound
            return new TypeProjectionImpl(
                Variance.OUT_VARIANCE, type.getConstructor().getBuiltIns().getNullableAnyType());
        }
      }
      JetType substitutedType;
      CustomTypeVariable typeVariable = TypeCapabilitiesKt.getCustomTypeVariable(type);
      if (replacement.isStarProjection()) {
        return replacement;
      } else if (typeVariable != null) {
        substitutedType = typeVariable.substitutionResult(replacement.getType());
      } else {
        // this is a simple type T or T?: if it's T, we should just take replacement, if T? - we
        // make replacement nullable
        substitutedType =
            TypeUtils.makeNullableIfNeeded(replacement.getType(), type.isMarkedNullable());
      }

      // substitutionType.annotations = replacement.annotations ++ type.annotations
      if (!type.getAnnotations().isEmpty()) {
        Annotations typeAnnotations = filterOutUnsafeVariance(type.getAnnotations());
        substitutedType =
            TypeUtilsKt.replaceAnnotations(
                substitutedType,
                new CompositeAnnotations(substitutedType.getAnnotations(), typeAnnotations));
      }

      Variance resultingProjectionKind =
          varianceConflict == VarianceConflictType.NO_CONFLICT
              ? combine(originalProjectionKind, replacement.getProjectionKind())
              : originalProjectionKind;
      return new TypeProjectionImpl(resultingProjectionKind, substitutedType);
    }
    // The type is not within the substitution range, i.e. Foo, Bar<T> etc.
    return substituteCompoundType(originalProjection, recursionDepth);
  }
  @NotNull
  @Override
  protected List<IntentionAction> doCreateActions(@NotNull Diagnostic diagnostic) {
    List<IntentionAction> actions = new LinkedList<IntentionAction>();

    BindingContext context = ResolutionUtils.analyzeFully((JetFile) diagnostic.getPsiFile());

    PsiElement diagnosticElement = diagnostic.getPsiElement();
    if (!(diagnosticElement instanceof JetExpression)) {
      LOG.error("Unexpected element: " + diagnosticElement.getText());
      return Collections.emptyList();
    }

    JetExpression expression = (JetExpression) diagnosticElement;

    JetType expectedType;
    JetType expressionType;
    if (diagnostic.getFactory() == Errors.TYPE_MISMATCH) {
      DiagnosticWithParameters2<JetExpression, JetType, JetType> diagnosticWithParameters =
          Errors.TYPE_MISMATCH.cast(diagnostic);
      expectedType = diagnosticWithParameters.getA();
      expressionType = diagnosticWithParameters.getB();
    } else if (diagnostic.getFactory() == Errors.NULL_FOR_NONNULL_TYPE) {
      DiagnosticWithParameters1<JetConstantExpression, JetType> diagnosticWithParameters =
          Errors.NULL_FOR_NONNULL_TYPE.cast(diagnostic);
      expectedType = diagnosticWithParameters.getA();
      expressionType = TypeUtilsKt.makeNullable(expectedType);
    } else if (diagnostic.getFactory() == Errors.CONSTANT_EXPECTED_TYPE_MISMATCH) {
      DiagnosticWithParameters2<JetConstantExpression, String, JetType> diagnosticWithParameters =
          Errors.CONSTANT_EXPECTED_TYPE_MISMATCH.cast(diagnostic);
      expectedType = diagnosticWithParameters.getB();
      expressionType = context.getType(expression);
      if (expressionType == null) {
        LOG.error("No type inferred: " + expression.getText());
        return Collections.emptyList();
      }
    } else {
      LOG.error("Unexpected diagnostic: " + DefaultErrorMessages.render(diagnostic));
      return Collections.emptyList();
    }

    // We don't want to cast a cast or type-asserted expression:
    if (!(expression instanceof JetBinaryExpressionWithTypeRHS)
        && !(expression.getParent() instanceof JetBinaryExpressionWithTypeRHS)) {
      actions.add(new CastExpressionFix(expression, expectedType));
    }

    // Property initializer type mismatch property type:
    JetProperty property = PsiTreeUtil.getParentOfType(expression, JetProperty.class);
    if (property != null) {
      JetPropertyAccessor getter = property.getGetter();
      JetExpression initializer = property.getInitializer();
      if (QuickFixUtil.canEvaluateTo(initializer, expression)
          || (getter != null
              && QuickFixUtil.canFunctionOrGetterReturnExpression(
                  property.getGetter(), expression))) {
        LexicalScope scope =
            CorePackage.getResolutionScope(
                property, context, ResolutionUtils.getResolutionFacade(property));
        JetType typeToInsert =
            TypeUtils.approximateWithResolvableType(expressionType, scope, false);
        actions.add(new ChangeVariableTypeFix(property, typeToInsert));
      }
    }

    PsiElement expressionParent = expression.getParent();

    // Mismatch in returned expression:

    JetCallableDeclaration function =
        expressionParent instanceof JetReturnExpression
            ? BindingContextUtilPackage.getTargetFunction(
                (JetReturnExpression) expressionParent, context)
            : PsiTreeUtil.getParentOfType(expression, JetFunction.class, true);
    if (function instanceof JetFunction
        && QuickFixUtil.canFunctionOrGetterReturnExpression(function, expression)) {
      LexicalScope scope =
          CorePackage.getResolutionScope(
              function, context, ResolutionUtils.getResolutionFacade(function));
      JetType typeToInsert = TypeUtils.approximateWithResolvableType(expressionType, scope, false);
      actions.add(new ChangeFunctionReturnTypeFix((JetFunction) function, typeToInsert));
    }

    // Fixing overloaded operators:
    if (expression instanceof JetOperationExpression) {
      ResolvedCall<?> resolvedCall = CallUtilPackage.getResolvedCall(expression, context);
      if (resolvedCall != null) {
        JetFunction declaration = getFunctionDeclaration(resolvedCall);
        if (declaration != null) {
          actions.add(new ChangeFunctionReturnTypeFix(declaration, expectedType));
        }
      }
    }

    // Change function return type when TYPE_MISMATCH is reported on call expression:
    if (expression instanceof JetCallExpression) {
      ResolvedCall<?> resolvedCall = CallUtilPackage.getResolvedCall(expression, context);
      if (resolvedCall != null) {
        JetFunction declaration = getFunctionDeclaration(resolvedCall);
        if (declaration != null) {
          actions.add(new ChangeFunctionReturnTypeFix(declaration, expectedType));
        }
      }
    }

    ResolvedCall<? extends CallableDescriptor> resolvedCall =
        CallUtilPackage.getParentResolvedCall(expression, context, true);
    if (resolvedCall != null) {
      // to fix 'type mismatch' on 'if' branches
      // todo: the same with 'when'
      JetExpression parentIf = QuickFixUtil.getParentIfForBranch(expression);
      JetExpression argumentExpression = (parentIf != null) ? parentIf : expression;
      ValueArgument valueArgument =
          CallUtilPackage.getValueArgumentForExpression(resolvedCall.getCall(), argumentExpression);
      if (valueArgument != null) {
        JetParameter correspondingParameter =
            QuickFixUtil.getParameterDeclarationForValueArgument(resolvedCall, valueArgument);
        JetType valueArgumentType =
            diagnostic.getFactory() == Errors.NULL_FOR_NONNULL_TYPE
                ? expressionType
                : context.getType(valueArgument.getArgumentExpression());
        if (correspondingParameter != null && valueArgumentType != null) {
          JetCallableDeclaration callable =
              PsiTreeUtil.getParentOfType(
                  correspondingParameter, JetCallableDeclaration.class, true);
          LexicalScope scope =
              callable != null
                  ? CorePackage.getResolutionScope(
                      callable, context, ResolutionUtils.getResolutionFacade(callable))
                  : null;
          JetType typeToInsert =
              TypeUtils.approximateWithResolvableType(valueArgumentType, scope, true);
          actions.add(new ChangeParameterTypeFix(correspondingParameter, typeToInsert));
        }
      }
    }
    return actions;
  }