/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.structuralsearch;

import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.codeInsight.template.JavaCodeContextType;
import com.intellij.codeInsight.template.TemplateContextType;
import com.intellij.codeInsight.template.TemplateManager;
import com.intellij.dupLocator.iterators.NodeIterator;
import com.intellij.lang.Language;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.structuralsearch.impl.matcher.*;
import com.intellij.structuralsearch.impl.matcher.compiler.GlobalCompilingVisitor;
import com.intellij.structuralsearch.impl.matcher.compiler.JavaCompilingVisitor;
import com.intellij.structuralsearch.impl.matcher.compiler.PatternCompiler;
import com.intellij.structuralsearch.impl.matcher.filters.JavaLexicalNodesFilter;
import com.intellij.structuralsearch.impl.matcher.filters.LexicalNodesFilter;
import com.intellij.structuralsearch.plugin.replace.ReplaceOptions;
import com.intellij.structuralsearch.plugin.replace.impl.ParameterInfo;
import com.intellij.structuralsearch.plugin.replace.impl.ReplacementBuilder;
import com.intellij.structuralsearch.plugin.replace.impl.ReplacementContext;
import com.intellij.structuralsearch.plugin.replace.impl.Replacer;
import com.intellij.structuralsearch.plugin.ui.Configuration;
import com.intellij.structuralsearch.plugin.ui.SearchContext;
import com.intellij.structuralsearch.plugin.ui.UIUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

/**
 * @author Eugene.Kudelevsky
 */
public class JavaStructuralSearchProfile extends StructuralSearchProfile {
  private JavaLexicalNodesFilter myJavaLexicalNodesFilter;

  public String getText(PsiElement match, int start,int end) {
    if (match instanceof PsiIdentifier) {
      PsiElement parent = match.getParent();
      if (parent instanceof PsiJavaCodeReferenceElement && !(parent instanceof PsiExpression)) {
        match = parent; // care about generic
      }
    }
    final String matchText = match.getText();
    if (start==0 && end==-1) return matchText;
    return matchText.substring(start,end == -1? matchText.length():end);
  }

  public Class getElementContextByPsi(PsiElement element) {
    if (element instanceof PsiIdentifier) {
      element = element.getParent();
    }

    if (element instanceof PsiMember) {
      return PsiMember.class;
    } else {
      return PsiExpression.class;
    }
  }

  @NotNull
  public String getTypedVarString(final PsiElement element) {
    String text;

    if (element instanceof PsiNamedElement) {
      text = ((PsiNamedElement)element).getName();
    }
    else if (element instanceof PsiAnnotation) {
      PsiJavaCodeReferenceElement referenceElement = ((PsiAnnotation)element).getNameReferenceElement();
      text = referenceElement == null ? null : referenceElement.getQualifiedName();
    }
    else if (element instanceof PsiNameValuePair) {
      text = ((PsiNameValuePair)element).getName();
    }
    else {
      text = element.getText();
      if (StringUtil.startsWithChar(text, '@')) {
        text = text.substring(1);
      }
      if (StringUtil.endsWithChar(text, ';')) text = text.substring(0, text.length() - 1);
      else if (element instanceof PsiExpressionStatement) {
        int i = text.indexOf(';');
        if (i != -1) text = text.substring(0, i);
      }
    }

    if (text==null) text = element.getText();

    return text;
  }

  @Override
  public String getMeaningfulText(PsiElement element) {
    if (element instanceof PsiReferenceExpression &&
        ((PsiReferenceExpression)element).getQualifierExpression() != null) {
      final PsiElement resolve = ((PsiReferenceExpression)element).resolve();
      if (resolve instanceof PsiClass) return element.getText();

      final PsiElement referencedElement = ((PsiReferenceExpression)element).getReferenceNameElement();
      String text = referencedElement != null ? referencedElement.getText() : "";

      if (resolve == null && text.length() > 0 && Character.isUpperCase(text.charAt(0))) {
        return element.getText();
      }
      return text;
    }
    return super.getMeaningfulText(element);
  }

  @Override
  public PsiElement updateCurrentNode(PsiElement targetNode) {
    if (targetNode instanceof PsiCodeBlock && ((PsiCodeBlock)targetNode).getStatements().length == 1) {
      PsiElement targetNodeParent = targetNode.getParent();
      if (targetNodeParent instanceof PsiBlockStatement) {
        targetNodeParent = targetNodeParent.getParent();
      }

      if (targetNodeParent instanceof PsiIfStatement || targetNodeParent instanceof PsiLoopStatement) {
        targetNode = targetNodeParent;
      }
    }
    return targetNode;
  }

  @Override
  public PsiElement extendMatchedByDownUp(PsiElement targetNode) {
    if (targetNode instanceof PsiIdentifier) {
      targetNode = targetNode.getParent();
      final PsiElement parent = targetNode.getParent();
      if (parent instanceof PsiTypeElement || parent instanceof PsiStatement) targetNode = parent;
    }
    return targetNode;
  }

  @Override
  public PsiElement extendMatchOnePsiFile(PsiElement file) {
    if (file instanceof PsiIdentifier) {
      // Searching in previous results
      file = file.getParent();
    }
    return file;
  }

  @NotNull
  @Override
  public PsiElement getPresentableElement(PsiElement element) {
    if (element instanceof PsiReferenceExpression) {
      final PsiElement parent = element.getParent();
      if (parent instanceof PsiMethodCallExpression) {
        return parent;
      }
    }
    else if (element instanceof PsiJavaCodeReferenceElement) {
      final PsiElement parent = element.getParent();
      if (parent instanceof PsiTypeElement || parent instanceof PsiNewExpression || parent instanceof PsiAnnotation) {
        return parent;
      }
    }
    return element;
  }

  public void compile(PsiElement[] elements, @NotNull GlobalCompilingVisitor globalVisitor) {
    elements[0].getParent().accept(new JavaCompilingVisitor(globalVisitor));
  }

  @NotNull
  public PsiElementVisitor createMatchingVisitor(@NotNull GlobalMatchingVisitor globalVisitor) {
    return new JavaMatchingVisitor(globalVisitor);
  }

  @NotNull
  @Override
  public PsiElementVisitor getLexicalNodesFilter(@NotNull LexicalNodesFilter filter) {
    if (myJavaLexicalNodesFilter == null) {
      myJavaLexicalNodesFilter = new JavaLexicalNodesFilter(filter);
    }
    return myJavaLexicalNodesFilter;
  }

  @NotNull
  public CompiledPattern createCompiledPattern() {
    return new JavaCompiledPattern();
  }

  public boolean isMyLanguage(@NotNull Language language) {
    return language == JavaLanguage.INSTANCE;
  }

  @Override
  public StructuralReplaceHandler getReplaceHandler(@NotNull ReplacementContext context) {
    return new JavaReplaceHandler(context);
  }

  @NotNull
  @Override
  public PsiElement[] createPatternTree(@NotNull String text,
                                        @NotNull PatternTreeContext context,
                                        @NotNull FileType fileType,
                                        @Nullable Language language,
                                        String contextName, @Nullable String extension,
                                        @NotNull Project project,
                                        boolean physical) {
    if (physical) {
      throw new UnsupportedOperationException(getClass() + " cannot create physical PSI");
    }
    final PsiElementFactory elementFactory = JavaPsiFacade.getInstance(project).getElementFactory();
    if (context == PatternTreeContext.Block) {
      final PsiElement element = elementFactory.createStatementFromText("{\n" + text + "\n}", null);
      final PsiElement[] children = ((PsiBlockStatement)element).getCodeBlock().getChildren();
      final int extraChildCount = 4;

      if (children.length > extraChildCount) {
        PsiElement[] result = new PsiElement[children.length - extraChildCount];
        System.arraycopy(children, 2, result, 0, children.length - extraChildCount);

        if (shouldTryExpressionPattern(result)) {
          try {
            final PsiElement[] expressionPattern =
              createPatternTree(text, PatternTreeContext.Expression, fileType, language, contextName, extension, project, false);
            if (expressionPattern.length == 1) {
              result = expressionPattern;
            }
          } catch (IncorrectOperationException ignore) {}
        }
        else if (shouldTryClassPattern(result)) {
          final PsiElement[] classPattern =
            createPatternTree(text, PatternTreeContext.Class, fileType, language, contextName, extension, project, false);
          if (classPattern.length == 1) {
            result = classPattern;
          }
        }
        return result;
      }
      else {
        return PsiElement.EMPTY_ARRAY;
      }
    }
    else if (context == PatternTreeContext.Class) {
      final PsiClass clazz = elementFactory.createClassFromText(text, null);
      PsiElement startChild = clazz.getLBrace();
      if (startChild != null) startChild = startChild.getNextSibling();

      PsiElement endChild = clazz.getRBrace();
      if (endChild != null) endChild = endChild.getPrevSibling();
      if (startChild == endChild) return PsiElement.EMPTY_ARRAY; // nothing produced

      final PsiCodeBlock codeBlock = elementFactory.createCodeBlock();
      final List<PsiElement> result = new ArrayList<PsiElement>(3);
      assert startChild != null;
      for (PsiElement el = startChild.getNextSibling(); el != endChild && el != null; el = el.getNextSibling()) {
        if (el instanceof PsiErrorElement) continue;
        result.add(codeBlock.add(el));
      }

      return PsiUtilCore.toPsiElementArray(result);
    }
    else if (context == PatternTreeContext.Expression) {
      final PsiExpression expression = elementFactory.createExpressionFromText(text, null);
      final PsiBlockStatement statement = (PsiBlockStatement)elementFactory.createStatementFromText("{\na\n}", null);
      final PsiElement[] children = statement.getCodeBlock().getChildren();
      if (children.length != 5) return PsiElement.EMPTY_ARRAY;
      final PsiExpressionStatement childStatement = (PsiExpressionStatement)children[2];
      childStatement.getExpression().replace(expression);
      return new PsiElement[] { childStatement };
    }
    else {
      return PsiFileFactory.getInstance(project).createFileFromText("__dummy.java", text).getChildren();
    }
  }

  private static boolean shouldTryExpressionPattern(PsiElement[] elements) {
    if (elements.length >= 1 && elements.length <= 3) {
      final PsiElement firstElement = elements[0];
      if (firstElement instanceof PsiDeclarationStatement) {
        final PsiElement lastChild = firstElement.getLastChild();
        if (lastChild instanceof PsiErrorElement && PsiTreeUtil.prevLeaf(lastChild) instanceof PsiErrorElement) {
          // Because an identifier followed by < (less than) is parsed as the start of a declaration
          // in com.intellij.lang.java.parser.StatementParser.parseStatement() line 236
          // but it could just be a comparison
          return true;
        }
      }
    }
    return false;
  }

  private static boolean shouldTryClassPattern(PsiElement[] result) {
    if (result.length == 3 && PsiModifier.STATIC.equals(result[0].getText()) && result[1] instanceof PsiWhiteSpace &&
        result[2] instanceof PsiBlockStatement) {
      // looks like static initializer
      return true;
    }
    else if (result.length > 1 && result[0] instanceof PsiDeclarationStatement && !result[0].getText().endsWith(";")) {
      // might be method
      return true;
    }
    return false;
  }

  @NotNull
  @Override
  public Editor createEditor(@NotNull SearchContext searchContext,
                             @NotNull FileType fileType,
                             Language dialect,
                             String text,
                             boolean useLastConfiguration) {
    // provides autocompletion

    PsiElement element = searchContext.getFile();

    if (element != null && !useLastConfiguration) {
      final Editor selectedEditor = FileEditorManager.getInstance(searchContext.getProject()).getSelectedTextEditor();

      if (selectedEditor != null) {
        int caretPosition = selectedEditor.getCaretModel().getOffset();
        PsiElement positionedElement = searchContext.getFile().findElementAt(caretPosition);

        if (positionedElement == null) {
          positionedElement = searchContext.getFile().findElementAt(caretPosition + 1);
        }

        if (positionedElement != null) {
          element = PsiTreeUtil.getParentOfType(
            positionedElement,
            PsiClass.class, PsiCodeBlock.class
          );
        }
      }
    }

    final PsiManager psimanager = PsiManager.getInstance(searchContext.getProject());
    final Project project = psimanager.getProject();
    final PsiCodeFragment file = createCodeFragment(project, text, element);
    final Document doc = PsiDocumentManager.getInstance(searchContext.getProject()).getDocument(file);
    DaemonCodeAnalyzer.getInstance(searchContext.getProject()).setHighlightingEnabled(file, false);
    return UIUtil.createEditor(doc, searchContext.getProject(), true, true, getTemplateContextType());
  }

  @Override
  public Class<? extends TemplateContextType> getTemplateContextTypeClass() {
    return JavaCodeContextType.class;
  }

  public PsiCodeFragment createCodeFragment(Project project, String text, PsiElement context) {
    final JavaCodeFragmentFactory factory = JavaCodeFragmentFactory.getInstance(project);
    return factory.createCodeBlockCodeFragment(text, context, true);
  }

  @Override
  public void checkSearchPattern(Project project, MatchOptions options) {
    class ValidatingVisitor extends JavaRecursiveElementWalkingVisitor {
      private PsiElement myCurrent;

      @Override public void visitAnnotation(PsiAnnotation annotation) {
        final PsiJavaCodeReferenceElement nameReferenceElement = annotation.getNameReferenceElement();

        if (nameReferenceElement == null ||
            !nameReferenceElement.getText().equals(MatchOptions.MODIFIER_ANNOTATION_NAME)) {
          return;
        }

        for(PsiNameValuePair pair:annotation.getParameterList().getAttributes()) {
          final PsiAnnotationMemberValue value = pair.getValue();

          if (value instanceof PsiArrayInitializerMemberValue) {
            for(PsiAnnotationMemberValue v:((PsiArrayInitializerMemberValue)value).getInitializers()) {
              final String name = StringUtil.stripQuotesAroundValue(v.getText());
              checkModifier(name);
            }

          } else if (value != null) {
            final String name = StringUtil.stripQuotesAroundValue(value.getText());
            checkModifier(name);
          }
        }
      }

      private void checkModifier(final String name) {
        if (!MatchOptions.INSTANCE_MODIFIER_NAME.equals(name) &&
            !PsiModifier.PACKAGE_LOCAL.equals(name) &&
            ArrayUtil.find(JavaMatchingVisitor.MODIFIERS, name) < 0
          ) {
          throw new MalformedPatternException(SSRBundle.message("invalid.modifier.type",name));
        }
      }

      @Override
      public void visitErrorElement(PsiErrorElement element) {
        super.visitErrorElement(element);
        final PsiElement parent = element.getParent();
        final String errorDescription = element.getErrorDescription();
        if (parent instanceof PsiClass && "Identifier expected".equals(errorDescription)) {
          // other class content variable.
          return;
        }
        if (parent instanceof PsiTryStatement && "'catch' or 'finally' expected".equals(errorDescription)) {
          // searching for naked try allowed
          return;
        }
        if (parent == myCurrent) {
          // search for expression, type, annotation or symbol
          if ("';' expected".equals(errorDescription)) {
            // expression
            return;
          }
          if ("Identifier or type expected".equals(errorDescription)) {
            // annotation
            return;
          }
          if ("Identifier expected".equals(errorDescription)) {
            // type
            return;
          }
        }
        throw new MalformedPatternException(errorDescription);
      }

      void setCurrent(PsiElement current) {
        myCurrent = current;
      }
    }
    ValidatingVisitor visitor = new ValidatingVisitor();
    final CompiledPattern compiledPattern = PatternCompiler.compilePattern(project, options);
    final int nodeCount = compiledPattern.getNodeCount();
    final NodeIterator nodes = compiledPattern.getNodes();
    while (nodes.hasNext()) {
      final PsiElement current = nodes.current();
      visitor.setCurrent((nodeCount == 1 && (current instanceof PsiExpressionStatement|| current instanceof PsiDeclarationStatement))
                         ? current : null);
      current.accept(visitor);
      nodes.advance();
    }
    nodes.reset();
  }

  @Override
  public void checkReplacementPattern(Project project, ReplaceOptions options) {
    MatchOptions matchOptions = options.getMatchOptions();
    FileType fileType = matchOptions.getFileType();
    PsiElement[] statements = MatcherImplUtil.createTreeFromText(
      matchOptions.getSearchPattern(),
      PatternTreeContext.Block,
      fileType,
      project
    );
    final boolean searchIsExpression = statements.length == 1 && statements[0].getLastChild() instanceof PsiErrorElement;

    PsiElement[] statements2 = MatcherImplUtil.createTreeFromText(
      options.getReplacement(),
      PatternTreeContext.Block,
      fileType,
      project
    );
    final boolean replaceIsExpression = statements2.length == 1 && statements2[0].getLastChild() instanceof PsiErrorElement;

    if (searchIsExpression && statements[0].getFirstChild() instanceof PsiModifierList && statements2.length == 0) {
      return;
    }
    boolean targetFound = false;
    for (final String name : matchOptions.getVariableConstraintNames()) {
      final MatchVariableConstraint constraint = matchOptions.getVariableConstraint(name);
      if (constraint.isPartOfSearchResults()) {
        targetFound = true;
        break;
      }
    }
    if (!targetFound && searchIsExpression != replaceIsExpression) {
      throw new UnsupportedPatternException(
        searchIsExpression ? SSRBundle.message("replacement.template.is.not.expression.error.message") :
        SSRBundle.message("search.template.is.not.expression.error.message")
      );
    }
  }

  @Override
  public LanguageFileType getDefaultFileType(LanguageFileType currentDefaultFileType) {
    return StdFileTypes.JAVA;
  }

  @Override
  Configuration[] getPredefinedTemplates() {
    return JavaPredefinedConfigurations.createPredefinedTemplates();
  }

  @Override
  public void provideAdditionalReplaceOptions(@NotNull PsiElement node, final ReplaceOptions options, final ReplacementBuilder builder) {
    final String templateText = TemplateManager.getInstance(node.getProject()).createTemplate("", "", options.getReplacement()).getTemplateText();
    node.accept(new JavaRecursiveElementWalkingVisitor() {
      @Override
      public void visitReferenceExpression(PsiReferenceExpression expression) {
        visitElement(expression);
      }

      @Override
      public void visitClass(PsiClass aClass) {
        super.visitClass(aClass);

        MatchVariableConstraint constraint =
          options.getMatchOptions().getVariableConstraint(CompiledPattern.ALL_CLASS_UNMATCHED_CONTENT_VAR_ARTIFICIAL_NAME);
        if (constraint != null) {
          ParameterInfo e = new ParameterInfo();
          e.setName(CompiledPattern.ALL_CLASS_UNMATCHED_CONTENT_VAR_ARTIFICIAL_NAME);
          e.setStartIndex(templateText.lastIndexOf('}'));
          builder.addParametrization(e);
        }
      }

      @Override
      public void visitParameter(PsiParameter parameter) {
        super.visitParameter(parameter);

        String name = parameter.getName();
        String type = parameter.getType().getCanonicalText();

        if (StructuralSearchUtil.isTypedVariable(name)) {
          name = Replacer.stripTypedVariableDecoration(name);

          if (StructuralSearchUtil.isTypedVariable(type)) {
            type = Replacer.stripTypedVariableDecoration(type);
          }
          ParameterInfo nameInfo = builder.findParameterization(name);
          ParameterInfo typeInfo = builder.findParameterization(type);

          final PsiElement scope = parameter.getDeclarationScope();
          if (nameInfo != null && typeInfo != null && !(scope instanceof PsiCatchSection) && !(scope instanceof PsiForeachStatement)) {
            nameInfo.setArgumentContext(false);
            typeInfo.setArgumentContext(false);
            typeInfo.setMethodParameterContext(true);
            nameInfo.setMethodParameterContext(true);
            typeInfo.setElement(parameter.getTypeElement());
          }
        }
      }
    });
  }

  @Override
  public int handleSubstitution(final ParameterInfo info,
                                MatchResult match,
                                StringBuilder result,
                                int offset,
                                HashMap<String, MatchResult> matchMap) {
    if (info.getName().equals(match.getName())) {
      String replacementString = match.getMatchImage();
      boolean forceAddingNewLine = false;

      if (info.isMethodParameterContext()) {
        StringBuilder buf = new StringBuilder();
        handleMethodParameter(buf, info, matchMap);
        replacementString = buf.toString();
      }
      else if (match.hasSons() && !match.isScopeMatch()) {
        // compound matches
        StringBuilder buf = new StringBuilder();
        MatchResult r = null;

        for (final MatchResult matchResult : match.getAllSons()) {
          MatchResult previous = r;
          r = matchResult;

          final PsiElement currentElement = r.getMatch();

          if (buf.length() > 0) {
            final PsiElement parent = currentElement.getParent();
            if (parent instanceof PsiVariable) {
              final PsiElement prevSibling = PsiTreeUtil.skipSiblingsBackward(parent, PsiWhiteSpace.class);
              if (prevSibling instanceof PsiJavaToken && JavaTokenType.COMMA.equals(((PsiJavaToken)prevSibling).getTokenType())) {
                buf.append(',');
              }
            }
            else if (info.isStatementContext()) {
              final PsiElement previousElement = previous.getMatch();

              if (!(previousElement instanceof PsiComment) &&
                  ( buf.charAt(buf.length() - 1) != '}' ||
                    previousElement instanceof PsiDeclarationStatement
                  )
                ) {
                buf.append(';');
              }

              final PsiElement prevSibling = currentElement.getPrevSibling();

              if (prevSibling instanceof PsiWhiteSpace &&
                  prevSibling.getPrevSibling() == previous.getMatch()
                ) {
                // consequent statements matched so preserve whitespacing
                buf.append(prevSibling.getText());
              }
              else {
                buf.append('\n');
              }
            }
            else if (info.isArgumentContext()) {
              buf.append(',');
            }
            else if (parent instanceof PsiClass) {
              final PsiElement prevSibling = PsiTreeUtil.skipSiblingsBackward(currentElement, PsiWhiteSpace.class);
              if (prevSibling instanceof PsiJavaToken && JavaTokenType.COMMA.equals(((PsiJavaToken)prevSibling).getTokenType())) {
                buf.append(',');
              }
              else {
                buf.append('\n');
              }
            }
            else if (parent instanceof PsiReferenceList) {
              buf.append(',');
            }
            else if (parent instanceof PsiPolyadicExpression) {
              final PsiPolyadicExpression expression = (PsiPolyadicExpression)parent;
              final PsiJavaToken token = expression.getTokenBeforeOperand(expression.getOperands()[1]);
              if (token != null) {
                buf.append(token.getText());
              }
            }
            else {
              buf.append(' ');
            }
          }

          buf.append(r.getMatchImage());
          removeExtraSemicolonForSingleVarInstanceInMultipleMatch(info, r, buf);
          forceAddingNewLine = currentElement instanceof PsiComment;
        }

        replacementString = buf.toString();
      } else {
        if (info.isStatementContext()) {
          forceAddingNewLine = match.getMatch() instanceof PsiComment;
        }
        StringBuilder buf = new StringBuilder(replacementString);
        removeExtraSemicolonForSingleVarInstanceInMultipleMatch(info, match, buf);
        replacementString = buf.toString();
      }

      offset = Replacer.insertSubstitution(result, offset, info, replacementString);
      offset = removeExtraSemicolon(info, offset, result, match);
      if (forceAddingNewLine && info.isStatementContext()) {
        result.insert(info.getStartIndex() + offset + 1, '\n');
        offset ++;
      }
    }
    return offset;
  }

  @Override
  public int handleNoSubstitution(ParameterInfo info, int offset, StringBuilder result) {
    final PsiElement element = info.getElement();
    final PsiElement prevSibling = PsiTreeUtil.skipSiblingsBackward(element, PsiWhiteSpace.class);
    if (prevSibling instanceof PsiJavaToken && isRemovableToken(prevSibling)) {
      final int start = info.getBeforeDelimiterPos() + offset - (prevSibling.getTextLength() - 1);
      final int end = info.getStartIndex() + offset;
      result.delete(start, end);
      return offset - (end - start);
    }
    final PsiElement nextSibling = PsiTreeUtil.skipSiblingsForward(element, PsiWhiteSpace.class);
    if (nextSibling instanceof PsiJavaToken && isRemovableToken(nextSibling)) {
      final int start = info.getStartIndex() + offset;
      final int end = info.getAfterDelimiterPos() + nextSibling.getTextLength() + offset;
      result.delete(start, end);
      return offset - 1;
    }
    if (element == null || !(element.getParent() instanceof PsiForStatement)) {
      return removeExtraSemicolon(info, offset, result, null);
    }
    return offset;
  }

  private static boolean isRemovableToken(PsiElement element) {
    final PsiElement parent = element.getParent();
    if (!(parent instanceof PsiAnnotationParameterList || // ',' between annotation parameters
          parent instanceof PsiAssertStatement || // ':' before assertion message
          parent instanceof PsiExpressionList || // ',' between expressions
          parent instanceof PsiParameterList || // ',' between parameters
          parent instanceof PsiPolyadicExpression || // '+', '*', '&&' etcetera
          parent instanceof PsiReferenceList || // ','
          parent instanceof PsiReferenceParameterList || // ','
          parent instanceof PsiResourceList || // ';'
          parent instanceof PsiTypeParameterList || // ','
          parent instanceof PsiVariable)) { // '=' before initializer
      return false;
    }
    final String text = element.getText();
    if (text.length() != 1) {
      return true;
    }
    switch(text.charAt(0)) {
      case '<':
      case '>':
      case '(':
      case ')':
      case '{':
      case '}':
      case '[':
      case ']':
        return false;
      default:
        return true;
    }
  }

  @Override
  public boolean isIdentifier(PsiElement element) {
    return element instanceof PsiIdentifier;
  }

  @NotNull
  @Override
  public Collection<String> getReservedWords() {
    return Collections.singleton(PsiModifier.PACKAGE_LOCAL);
  }

  @Override
  public boolean isDocCommentOwner(PsiElement match) {
    return match instanceof PsiMember;
  }

  private static void handleMethodParameter(StringBuilder buf, ParameterInfo info, HashMap<String, MatchResult> matchMap) {
    if(!(info.getElement() instanceof PsiTypeElement)) {
      // no specific handling for name of method parameter since it is handled with type
      return;
    }

    String name = ((PsiParameter)info.getElement().getParent()).getName();
    name = StructuralSearchUtil.isTypedVariable(name) ? Replacer.stripTypedVariableDecoration(name):name;

    final MatchResult matchResult = matchMap.get(name);
    if (matchResult == null) return;

    if (matchResult.isMultipleMatch()) {
      for (MatchResult result : matchResult.getAllSons()) {
        if (buf.length() > 0) {
          buf.append(',');
        }

        appendParameter(buf, result);
      }
    } else {
      appendParameter(buf, matchResult);
    }
  }

  private static void appendParameter(final StringBuilder buf, final MatchResult matchResult) {
    final List<MatchResult> sons = matchResult.getAllSons();
    assert sons.size() == 1;
    buf.append(sons.get(0).getMatchImage()).append(' ').append(matchResult.getMatchImage());
  }

  private static void removeExtraSemicolonForSingleVarInstanceInMultipleMatch(final ParameterInfo info, MatchResult r, StringBuilder buf) {
    if (info.isStatementContext()) {
      final PsiElement element = r.getMatch();

      // remove extra ;
      if (buf.charAt(buf.length()-1)==';' &&
          r.getMatchImage().charAt(r.getMatchImage().length()-1)==';' &&
          ( element instanceof PsiReturnStatement ||
            element instanceof PsiDeclarationStatement ||
            element instanceof PsiExpressionStatement ||
            element instanceof PsiAssertStatement ||
            element instanceof PsiBreakStatement ||
            element instanceof PsiContinueStatement ||
            element instanceof PsiMember ||
            element instanceof PsiIfStatement && !(((PsiIfStatement)element).getThenBranch() instanceof PsiBlockStatement) ||
            element instanceof PsiLoopStatement && !(((PsiLoopStatement)element).getBody() instanceof PsiBlockStatement)
          )
        ) {
        // contains extra ;
        buf.deleteCharAt(buf.length()-1);
      }
    }
  }

  private static int removeExtraSemicolon(ParameterInfo info, int offset, StringBuilder result, MatchResult match) {
    if (info.isStatementContext()) {
      int index = offset+ info.getStartIndex();
      if (result.charAt(index)==';' &&
          ( match == null ||
            ( result.charAt(index-1)=='}' &&
              !(match.getMatch() instanceof PsiDeclarationStatement) && // array init in dcl
              !(match.getMatch() instanceof PsiNewExpression) // array initializer
            ) ||
            ( !match.isMultipleMatch() &&                                                // ; in comment
              match.getMatch() instanceof PsiComment
            ) ||
            ( match.isMultipleMatch() &&                                                 // ; in comment
              match.getAllSons().get( match.getAllSons().size() - 1 ).getMatch() instanceof PsiComment
            )
          )
        ) {
        result.deleteCharAt(index);
        --offset;
      }
    }

    return offset;
  }
}
