/** @author peter */
public class JavaChainLookupElement extends LookupElementDecorator<LookupElement>
    implements TypedLookupItem {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInsight.completion.JavaChainLookupElement");
  public static final ClassConditionKey<JavaChainLookupElement> CLASS_CONDITION_KEY =
      ClassConditionKey.create(JavaChainLookupElement.class);
  private final LookupElement myQualifier;

  public JavaChainLookupElement(LookupElement qualifier, LookupElement main) {
    super(main);
    myQualifier = qualifier;
  }

  @NotNull
  @Override
  public String getLookupString() {
    return maybeAddParentheses(myQualifier.getLookupString())
        + "."
        + getDelegate().getLookupString();
  }

  public LookupElement getQualifier() {
    return myQualifier;
  }

  @Override
  public Set<String> getAllLookupStrings() {
    final Set<String> strings = getDelegate().getAllLookupStrings();
    final THashSet<String> result = new THashSet<String>();
    result.addAll(strings);
    result.add(getLookupString());
    return result;
  }

  @NotNull
  @Override
  public String toString() {
    return maybeAddParentheses(myQualifier.toString()) + "." + getDelegate();
  }

  private String maybeAddParentheses(String s) {
    return getQualifierObject() instanceof PsiMethod ? s + "()" : s;
  }

  @Nullable
  private Object getQualifierObject() {
    Object qObject = myQualifier.getObject();
    if (qObject instanceof ResolveResult) {
      qObject = ((ResolveResult) qObject).getElement();
    }
    return qObject;
  }

  @Override
  public void renderElement(LookupElementPresentation presentation) {
    super.renderElement(presentation);
    final LookupElementPresentation qualifierPresentation = new LookupElementPresentation();
    myQualifier.renderElement(qualifierPresentation);
    String name = maybeAddParentheses(qualifierPresentation.getItemText());
    final String qualifierText =
        myQualifier.as(CastingLookupElementDecorator.CLASS_CONDITION_KEY) != null
            ? "(" + name + ")"
            : name;
    presentation.setItemText(qualifierText + "." + presentation.getItemText());

    if (myQualifier instanceof LookupItem && getQualifierObject() instanceof PsiClass) {
      String locationString =
          JavaPsiClassReferenceElement.getLocationString((LookupItem) myQualifier);
      presentation.setTailText(StringUtil.notNullize(presentation.getTailText()) + locationString);
    }
  }

  @Override
  public void handleInsert(InsertionContext context) {
    final Document document = context.getEditor().getDocument();
    document.replaceString(context.getStartOffset(), context.getTailOffset(), ";");
    final InsertionContext qualifierContext =
        CompletionUtil.emulateInsertion(context, context.getStartOffset(), myQualifier);
    OffsetKey oldStart = context.trackOffset(context.getStartOffset(), false);

    int start =
        CharArrayUtil.shiftForward(
            context.getDocument().getCharsSequence(), context.getStartOffset(), " \t\n");
    if (shouldParenthesizeQualifier(context.getFile(), start, qualifierContext.getTailOffset())) {
      final String space =
          CodeStyleSettingsManager.getSettings(qualifierContext.getProject())
                  .SPACE_WITHIN_PARENTHESES
              ? " "
              : "";
      document.insertString(start, "(" + space);
      document.insertString(qualifierContext.getTailOffset(), space + ")");
    }

    final char atTail = document.getCharsSequence().charAt(context.getTailOffset() - 1);
    if (atTail != ';') {
      LOG.error(
          LogMessageEx.createEvent(
              "Unexpected character",
              "atTail="
                  + atTail
                  + "\n"
                  + "offset="
                  + context.getTailOffset()
                  + "\n"
                  + "item="
                  + this
                  + "\n"
                  + "item.class="
                  + this.getClass()
                  + "\n"
                  + DebugUtil.currentStackTrace(),
              AttachmentFactory.createAttachment(context.getDocument())));
    }
    document.replaceString(context.getTailOffset() - 1, context.getTailOffset(), ".");

    CompletionUtil.emulateInsertion(getDelegate(), context.getTailOffset(), context);
    context.commitDocument();

    int formatStart = context.getOffset(oldStart);
    int formatEnd = context.getTailOffset();
    if (formatStart >= 0 && formatEnd >= 0) {
      CodeStyleManager.getInstance(context.getProject())
          .reformatText(context.getFile(), formatStart, formatEnd);
    }
  }

  protected boolean shouldParenthesizeQualifier(
      final PsiFile file, final int startOffset, final int endOffset) {
    PsiElement element = file.findElementAt(startOffset);
    if (element == null) {
      return false;
    }

    PsiElement last = element;
    while (element != null
        && element.getTextRange().getStartOffset() >= startOffset
        && element.getTextRange().getEndOffset() <= endOffset
        && !(element instanceof PsiExpressionStatement)) {
      last = element;
      element = element.getParent();
    }
    PsiExpression expr =
        PsiTreeUtil.getParentOfType(last, PsiExpression.class, false, PsiClass.class);
    if (expr == null) {
      return false;
    }
    if (expr.getTextRange().getEndOffset() > endOffset) {
      return true;
    }

    if (expr instanceof PsiJavaCodeReferenceElement
        || expr instanceof PsiMethodCallExpression
        || expr instanceof PsiArrayAccessExpression) {
      return false;
    }

    return true;
  }

  @NotNull
  private LookupElement getComparableQualifier() {
    final CastingLookupElementDecorator casting =
        myQualifier.as(CastingLookupElementDecorator.CLASS_CONDITION_KEY);
    return casting == null ? myQualifier : casting.getDelegate();
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    if (!super.equals(o)) return false;

    return getComparableQualifier().equals(((JavaChainLookupElement) o).getComparableQualifier());
  }

  @Override
  public int hashCode() {
    return 31 * super.hashCode() + getComparableQualifier().hashCode();
  }

  @Override
  public PsiType getType() {
    final Object object = getObject();
    if (object instanceof PsiMember) {
      return JavaCompletionUtil.getQualifiedMemberReferenceType(
          JavaCompletionUtil.getLookupElementType(myQualifier), (PsiMember) object);
    }
    return ((PsiVariable) object).getType();
  }
}
/** @author peter */
public class PsiTypeLookupItem extends LookupItem implements TypedLookupItem {
  private static final InsertHandler<PsiTypeLookupItem> DEFAULT_IMPORT_FIXER =
      new InsertHandler<PsiTypeLookupItem>() {
        @Override
        public void handleInsert(InsertionContext context, PsiTypeLookupItem item) {
          if (item.getObject() instanceof PsiClass) {
            addImportForItem(context, (PsiClass) item.getObject());
          }
        }
      };

  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInsight.lookup.PsiTypeLookupItem");
  public static final ClassConditionKey<PsiTypeLookupItem> CLASS_CONDITION_KEY =
      ClassConditionKey.create(PsiTypeLookupItem.class);
  private final boolean myDiamond;
  private final int myBracketsCount;
  private boolean myIndicateAnonymous;
  private final InsertHandler<PsiTypeLookupItem> myImportFixer;
  @NotNull private final PsiSubstitutor mySubstitutor;
  private boolean myAddArrayInitializer;
  private String myLocationString = "";

  private PsiTypeLookupItem(
      Object o,
      @NotNull @NonNls String lookupString,
      boolean diamond,
      int bracketsCount,
      InsertHandler<PsiTypeLookupItem> fixer,
      @NotNull PsiSubstitutor substitutor) {
    super(o, lookupString);
    myDiamond = diamond;
    myBracketsCount = bracketsCount;
    myImportFixer = fixer;
    mySubstitutor = substitutor;
  }

  @NotNull
  @Override
  public PsiType getType() {
    Object object = getObject();
    PsiType type =
        object instanceof PsiType
            ? getSubstitutor().substitute((PsiType) object)
            : JavaPsiFacade.getElementFactory(((PsiClass) object).getProject())
                .createType((PsiClass) object, getSubstitutor());
    for (int i = 0; i < getBracketsCount(); i++) {
      type = new PsiArrayType(type);
    }
    return type;
  }

  public void setIndicateAnonymous(boolean indicateAnonymous) {
    myIndicateAnonymous = indicateAnonymous;
  }

  public boolean isIndicateAnonymous() {
    return myIndicateAnonymous;
  }

  @Override
  public boolean equals(final Object o) {
    return super.equals(o)
        && o instanceof PsiTypeLookupItem
        && getBracketsCount() == ((PsiTypeLookupItem) o).getBracketsCount()
        && myAddArrayInitializer == ((PsiTypeLookupItem) o).myAddArrayInitializer;
  }

  public boolean isAddArrayInitializer() {
    return myAddArrayInitializer;
  }

  public void setAddArrayInitializer() {
    myAddArrayInitializer = true;
  }

  @Override
  public void handleInsert(InsertionContext context) {
    myImportFixer.handleInsert(context, this);

    PsiElement position = context.getFile().findElementAt(context.getStartOffset());
    if (position != null) {
      int genericsStart = context.getTailOffset();
      context
          .getDocument()
          .insertString(
              genericsStart,
              JavaCompletionUtil.escapeXmlIfNeeded(context, calcGenerics(position, context)));
      JavaCompletionUtil.shortenReference(context.getFile(), genericsStart - 1);
    }

    int tail = context.getTailOffset();
    String braces = StringUtil.repeat("[]", getBracketsCount());
    Editor editor = context.getEditor();
    if (!braces.isEmpty()) {
      if (myAddArrayInitializer) {
        context.getDocument().insertString(tail, braces + "{}");
        editor.getCaretModel().moveToOffset(tail + braces.length() + 1);
      } else {
        context.getDocument().insertString(tail, braces);
        editor.getCaretModel().moveToOffset(tail + 1);
        if (context.getCompletionChar() == '[') {
          context.setAddCompletionChar(false);
        }
      }
    } else {
      editor.getCaretModel().moveToOffset(tail);
    }
    editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);

    InsertHandler handler = getInsertHandler();
    if (handler != null) {
      //noinspection unchecked
      handler.handleInsert(context, this);
    }
  }

  public String calcGenerics(@NotNull PsiElement context, InsertionContext insertionContext) {
    if (insertionContext.getCompletionChar() == '<') {
      return "";
    }

    assert context.isValid();
    if (myDiamond) {
      return "<>";
    }

    if (getObject() instanceof PsiClass) {
      PsiClass psiClass = (PsiClass) getObject();
      PsiResolveHelper resolveHelper =
          JavaPsiFacade.getInstance(psiClass.getProject()).getResolveHelper();
      PsiSubstitutor substitutor = getSubstitutor();
      StringBuilder builder = new StringBuilder();
      for (PsiTypeParameter parameter : psiClass.getTypeParameters()) {
        PsiType substitute = substitutor.substitute(parameter);
        if (substitute == null
            || (PsiUtil.resolveClassInType(substitute) == parameter
                && resolveHelper.resolveReferencedClass(parameter.getName(), context)
                    != CompletionUtil.getOriginalOrSelf(parameter))) {
          return "";
        }
        if (builder.length() > 0) {
          builder.append(", ");
        }
        builder.append(substitute.getCanonicalText());
      }
      if (builder.length() > 0) {
        return "<" + builder + ">";
      }
    }
    return "";
  }

  @Override
  public int hashCode() {
    final int fromSuper = super.hashCode();
    final int dim = getBracketsCount();
    return fromSuper + dim * 31;
  }

  public int getBracketsCount() {
    return myBracketsCount;
  }

  public static PsiTypeLookupItem createLookupItem(
      @NotNull PsiType type, @Nullable PsiElement context) {
    final boolean diamond = isDiamond(type);
    return createLookupItem(type, context, diamond);
  }

  public static PsiTypeLookupItem createLookupItem(
      @NotNull PsiType type, @Nullable PsiElement context, boolean isDiamond) {
    return createLookupItem(type, context, isDiamond, DEFAULT_IMPORT_FIXER);
  }

  public static PsiTypeLookupItem createLookupItem(
      @NotNull PsiType type,
      @Nullable PsiElement context,
      boolean isDiamond,
      InsertHandler<PsiTypeLookupItem> importFixer) {
    int dim = 0;
    while (type instanceof PsiArrayType) {
      type = ((PsiArrayType) type).getComponentType();
      dim++;
    }

    return doCreateItem(type, context, dim, isDiamond, importFixer);
  }

  private static PsiTypeLookupItem doCreateItem(
      final PsiType type,
      PsiElement context,
      int bracketsCount,
      boolean diamond,
      InsertHandler<PsiTypeLookupItem> importFixer) {
    if (type instanceof PsiClassType) {
      PsiClassType.ClassResolveResult classResolveResult = ((PsiClassType) type).resolveGenerics();
      final PsiClass psiClass = classResolveResult.getElement();

      if (psiClass != null) {
        String name = psiClass.getName();
        if (name != null) {
          final PsiSubstitutor substitutor = classResolveResult.getSubstitutor();

          PsiClass resolved =
              JavaPsiFacade.getInstance(psiClass.getProject())
                  .getResolveHelper()
                  .resolveReferencedClass(name, context);

          Set<String> allStrings = new HashSet<String>();
          allStrings.add(name);
          if (!psiClass.getManager().areElementsEquivalent(resolved, psiClass)
              && !PsiUtil.isInnerClass(psiClass)) {
            // inner class name should be shown qualified if its not accessible by single name
            PsiClass aClass = psiClass.getContainingClass();
            while (aClass != null && !PsiUtil.isInnerClass(aClass) && aClass.getName() != null) {
              name = aClass.getName() + '.' + name;
              allStrings.add(name);
              aClass = aClass.getContainingClass();
            }
          }

          PsiTypeLookupItem item =
              new PsiTypeLookupItem(
                  psiClass, name, diamond, bracketsCount, importFixer, substitutor);
          item.addLookupStrings(ArrayUtil.toStringArray(allStrings));
          return item;
        }
      }
    }
    return new PsiTypeLookupItem(
        type, type.getPresentableText(), false, bracketsCount, importFixer, PsiSubstitutor.EMPTY);
  }

  public static boolean isDiamond(PsiType type) {
    boolean diamond = false;
    if (type instanceof PsiClassReferenceType) {
      final PsiReferenceParameterList parameterList =
          ((PsiClassReferenceType) type).getReference().getParameterList();
      if (parameterList != null) {
        final PsiTypeElement[] typeParameterElements = parameterList.getTypeParameterElements();
        diamond =
            typeParameterElements.length == 1
                && typeParameterElements[0].getType() instanceof PsiDiamondType;
      }
    }
    return diamond;
  }

  @NotNull
  private PsiSubstitutor getSubstitutor() {
    return mySubstitutor;
  }

  @Override
  public void renderElement(LookupElementPresentation presentation) {
    final Object object = getObject();
    if (object instanceof PsiClass) {
      JavaPsiClassReferenceElement.renderClassItem(
          presentation, this, (PsiClass) object, myDiamond, myLocationString, mySubstitutor);
    } else {
      assert object instanceof PsiType;

      if (!(object instanceof PsiPrimitiveType)) {
        presentation.setIcon(DefaultLookupItemRenderer.getRawIcon(this, presentation.isReal()));
      }

      presentation.setItemText(((PsiType) object).getCanonicalText());
      presentation.setItemTextBold(object instanceof PsiPrimitiveType);
      if (isAddArrayInitializer()) {
        presentation.setTailText("{...}");
      }
    }
    if (myBracketsCount > 0) {
      presentation.setTailText(
          StringUtil.repeat("[]", myBracketsCount)
              + StringUtil.notNullize(presentation.getTailText()),
          true);
    }
  }

  public PsiTypeLookupItem setShowPackage() {
    Object object = getObject();
    if (object instanceof PsiClass) {
      myLocationString = " (" + PsiFormatUtil.getPackageDisplayName((PsiClass) object) + ")";
    }
    return this;
  }

  public static void addImportForItem(InsertionContext context, PsiClass aClass) {
    if (aClass.getQualifiedName() == null) return;
    PsiFile file = context.getFile();

    int startOffset = context.getStartOffset();
    int tail = context.getTailOffset();
    int newTail = JavaCompletionUtil.insertClassReference(aClass, file, startOffset, tail);
    if (newTail > context.getDocument().getTextLength() || newTail < 0) {
      LOG.error(
          LogMessageEx.createEvent(
              "Invalid offset after insertion ",
              "offset="
                  + newTail
                  + "\n"
                  + "start="
                  + startOffset
                  + "\n"
                  + "tail="
                  + tail
                  + "\n"
                  + "file.length="
                  + file.getTextLength()
                  + "\n"
                  + "document="
                  + context.getDocument()
                  + "\n"
                  + DebugUtil.currentStackTrace(),
              AttachmentFactory.createAttachment(context.getDocument())));
      return;
    }
    context.setTailOffset(newTail);
    JavaCompletionUtil.shortenReference(file, context.getStartOffset());
    PostprocessReformattingAspect.getInstance(context.getProject()).doPostponedFormatting();
  }
}
/** @author peter */
public class JavaPsiClassReferenceElement extends LookupItem<Object> {
  public static final ClassConditionKey<JavaPsiClassReferenceElement> CLASS_CONDITION_KEY =
      ClassConditionKey.create(JavaPsiClassReferenceElement.class);
  private final Object myClass;
  private volatile Reference<PsiClass> myCache;
  private final String myQualifiedName;
  private String myForcedPresentableName;

  public JavaPsiClassReferenceElement(PsiClass psiClass) {
    super(psiClass.getName(), psiClass.getName());
    myClass =
        psiClass.getContainingFile().getVirtualFile() == null
            ? psiClass
            : PsiAnchor.create(psiClass);
    myQualifiedName = psiClass.getQualifiedName();
    JavaCompletionUtil.setShowFQN(this);
    setInsertHandler(AllClassesGetter.TRY_SHORTENING);
    setTailType(TailType.NONE);
  }

  public String getForcedPresentableName() {
    return myForcedPresentableName;
  }

  @NotNull
  @Override
  public String getLookupString() {
    if (myForcedPresentableName != null) {
      return myForcedPresentableName;
    }
    return super.getLookupString();
  }

  @Override
  public Set<String> getAllLookupStrings() {
    if (myForcedPresentableName != null) {
      return Collections.singleton(myForcedPresentableName);
    }

    return super.getAllLookupStrings();
  }

  public void setForcedPresentableName(String forcedPresentableName) {
    myForcedPresentableName = forcedPresentableName;
  }

  @NotNull
  @Override
  public PsiClass getObject() {
    if (myClass instanceof PsiAnchor) {
      Reference<PsiClass> cache = myCache;
      if (cache != null) {
        PsiClass psiClass = cache.get();
        if (psiClass != null) {
          return psiClass;
        }
      }

      final PsiClass retrieve = (PsiClass) ((PsiAnchor) myClass).retrieve();
      assert retrieve != null : myQualifiedName;
      myCache = new WeakReference<PsiClass>(retrieve);
      return retrieve;
    }
    return (PsiClass) myClass;
  }

  @Override
  public boolean isValid() {
    if (myClass instanceof PsiClass) {
      return ((PsiClass) myClass).isValid();
    }

    return ((PsiAnchor) myClass).retrieve() != null;
  }

  @Override
  public boolean equals(final Object o) {
    if (this == o) return true;
    if (!(o instanceof JavaPsiClassReferenceElement)) return false;

    final JavaPsiClassReferenceElement that = (JavaPsiClassReferenceElement) o;

    if (myQualifiedName != null) {
      return myQualifiedName.equals(that.myQualifiedName);
    }

    return Comparing.equal(myClass, that.myClass);
  }

  public String getQualifiedName() {
    return myQualifiedName;
  }

  @Override
  public int hashCode() {
    final String s = myQualifiedName;
    return s == null ? 239 : s.hashCode();
  }

  @Override
  public void renderElement(LookupElementPresentation presentation) {
    LookupItem item = this;
    PsiClass psiClass = getObject();
    renderClassItem(presentation, item, psiClass, false);
  }

  public static void renderClassItem(
      LookupElementPresentation presentation, LookupItem item, PsiClass psiClass, boolean diamond) {
    if (!(psiClass instanceof PsiTypeParameter)) {
      presentation.setIcon(DefaultLookupItemRenderer.getRawIcon(item, presentation.isReal()));
    }

    final boolean bold = item.getAttribute(LookupItem.HIGHLIGHTED_ATTR) != null;
    boolean strikeout = JavaElementLookupRenderer.isToStrikeout(item);
    presentation.setItemText(getName(psiClass, item, diamond));
    presentation.setStrikeout(strikeout);
    presentation.setItemTextBold(bold);

    String tailText = StringUtil.notNullize((String) item.getAttribute(LookupItem.TAIL_TEXT_ATTR));
    PsiSubstitutor substitutor = (PsiSubstitutor) item.getAttribute(LookupItem.SUBSTITUTOR);

    if (item instanceof PsiTypeLookupItem
        && ((PsiTypeLookupItem) item).isIndicateAnonymous()
        && (psiClass.isInterface() || psiClass.hasModifierProperty(PsiModifier.ABSTRACT))) {
      tailText = "{...}" + tailText;
    }
    if (substitutor == null && !diamond && psiClass.getTypeParameters().length > 0) {
      tailText =
          "<"
              + StringUtil.join(
                  psiClass.getTypeParameters(),
                  new Function<PsiTypeParameter, String>() {
                    @Override
                    public String fun(PsiTypeParameter psiTypeParameter) {
                      return psiTypeParameter.getName();
                    }
                  },
                  "," + (showSpaceAfterComma(psiClass) ? " " : ""))
              + ">"
              + tailText;
    }
    presentation.setTailText(tailText, true);
  }

  private static String getName(
      final PsiClass psiClass, final LookupItem<?> item, boolean diamond) {
    if (item instanceof JavaPsiClassReferenceElement) {
      String forced = ((JavaPsiClassReferenceElement) item).getForcedPresentableName();
      if (forced != null) {
        return forced;
      }
    }

    String name = PsiUtilCore.getName(psiClass);

    if (item.getAttribute(LookupItem.FORCE_QUALIFY) != null) {
      if (psiClass.getContainingClass() != null) {
        name = psiClass.getContainingClass().getName() + "." + name;
      }
    }

    if (diamond) {
      return name + "<>";
    }

    PsiSubstitutor substitutor = (PsiSubstitutor) item.getAttribute(LookupItem.SUBSTITUTOR);
    if (substitutor != null) {
      final PsiTypeParameter[] params = psiClass.getTypeParameters();
      if (params.length > 0) {
        return name + formatTypeParameters(substitutor, params);
      }
    }

    return StringUtil.notNullize(name);
  }

  @Nullable
  private static String formatTypeParameters(
      @NotNull final PsiSubstitutor substitutor, final PsiTypeParameter[] params) {
    final boolean space = showSpaceAfterComma(params[0]);
    StringBuilder buffer = new StringBuilder();
    buffer.append("<");
    for (int i = 0; i < params.length; i++) {
      final PsiTypeParameter param = params[i];
      final PsiType type = substitutor.substitute(param);
      if (type == null) {
        return "";
      }
      if (type instanceof PsiClassType && ((PsiClassType) type).getParameters().length > 0) {
        buffer.append(((PsiClassType) type).rawType().getPresentableText()).append("<...>");
      } else {
        buffer.append(type.getPresentableText());
      }

      if (i < params.length - 1) {
        buffer.append(",");
        if (space) {
          buffer.append(" ");
        }
      }
    }
    buffer.append(">");
    return buffer.toString();
  }

  private static boolean showSpaceAfterComma(PsiClass element) {
    return CodeStyleSettingsManager.getSettings(element.getProject()).SPACE_AFTER_COMMA;
  }
}