/*
 * 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.codeInsight.completion.util;

import com.intellij.codeInsight.completion.InsertHandler;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * @author peter
 */
public abstract class ParenthesesInsertHandler<T extends LookupElement> implements InsertHandler<T> {
  public static final ParenthesesInsertHandler<LookupElement> WITH_PARAMETERS = new ParenthesesInsertHandler<LookupElement>() {
    @Override
    protected boolean placeCaretInsideParentheses(final InsertionContext context, final LookupElement item) {
      return true;
    }
  };
  public static final ParenthesesInsertHandler<LookupElement> NO_PARAMETERS = new ParenthesesInsertHandler<LookupElement>() {
    @Override
    protected boolean placeCaretInsideParentheses(final InsertionContext context, final LookupElement item) {
      return false;
    }
  };

  public static ParenthesesInsertHandler<LookupElement> getInstance(boolean hasParameters) {
    return hasParameters ? WITH_PARAMETERS : NO_PARAMETERS;
  }

  public static ParenthesesInsertHandler<LookupElement> getInstance(final boolean hasParameters, final boolean spaceBeforeParentheses,
                                                                    final boolean spaceBetweenParentheses,
                                                                    final boolean insertRightParenthesis, boolean allowParametersOnNextLine) {
    return new ParenthesesInsertHandler<LookupElement>(spaceBeforeParentheses, spaceBetweenParentheses, insertRightParenthesis, allowParametersOnNextLine) {
      @Override
      protected boolean placeCaretInsideParentheses(InsertionContext context, LookupElement item) {
        return hasParameters;
      }
    };
  }

  private final boolean mySpaceBeforeParentheses;
  private final boolean mySpaceBetweenParentheses;
  private final boolean myMayInsertRightParenthesis;
  private final boolean myAllowParametersOnNextLine;
  private final char myLeftParenthesis;
  private final char myRightParenthesis;

  protected ParenthesesInsertHandler(final boolean spaceBeforeParentheses,
                                     final boolean spaceBetweenParentheses,
                                     final boolean mayInsertRightParenthesis) {
    this(spaceBeforeParentheses, spaceBetweenParentheses, mayInsertRightParenthesis, false);
  }

  protected ParenthesesInsertHandler(boolean spaceBeforeParentheses,
                                     boolean spaceBetweenParentheses,
                                     boolean mayInsertRightParenthesis,
                                     boolean allowParametersOnNextLine) {
    this(spaceBeforeParentheses, spaceBetweenParentheses, mayInsertRightParenthesis, allowParametersOnNextLine, '(', ')');
  }

  protected ParenthesesInsertHandler(boolean spaceBeforeParentheses, 
                                     boolean spaceBetweenParentheses, 
                                     boolean mayInsertRightParenthesis,
                                     boolean allowParametersOnNextLine, 
                                     char leftParenthesis, 
                                     char rightParenthesis) {
    mySpaceBeforeParentheses = spaceBeforeParentheses;
    mySpaceBetweenParentheses = spaceBetweenParentheses;
    myMayInsertRightParenthesis = mayInsertRightParenthesis;
    myAllowParametersOnNextLine = allowParametersOnNextLine;
    myLeftParenthesis = leftParenthesis;
    myRightParenthesis = rightParenthesis;
  }

  protected ParenthesesInsertHandler() {
    this(false, false, true);
  }

  private static boolean isToken(@Nullable final PsiElement element, final String text) {
    return element != null && text.equals(element.getText());
  }

  protected abstract boolean placeCaretInsideParentheses(final InsertionContext context, final T item);

  @Override
  public void handleInsert(final InsertionContext context, final T item) {
    final Editor editor = context.getEditor();
    final Document document = editor.getDocument();
    context.commitDocument();
    PsiElement lParen = findExistingLeftParenthesis(context);

    final char completionChar = context.getCompletionChar();
    final boolean putCaretInside = completionChar == myLeftParenthesis || placeCaretInsideParentheses(context, item);

    if (completionChar == myLeftParenthesis) {
      context.setAddCompletionChar(false);
    }

    if (lParen != null) {
      int lparenthOffset = lParen.getTextRange().getStartOffset();
      if (mySpaceBeforeParentheses && lparenthOffset == context.getTailOffset()) {
        document.insertString(context.getTailOffset(), " ");
        lparenthOffset++;
      }

      if (completionChar == myLeftParenthesis || completionChar == '\t') {
        editor.getCaretModel().moveToOffset(lparenthOffset + 1);
      } else {
        editor.getCaretModel().moveToOffset(context.getTailOffset());
      }

      context.setTailOffset(lparenthOffset + 1);

      PsiElement list = lParen.getParent();
      PsiElement last = list.getLastChild();
      if (isToken(last, String.valueOf(myRightParenthesis))) {
        int rparenthOffset = last.getTextRange().getStartOffset();
        context.setTailOffset(rparenthOffset + 1);
        if (!putCaretInside) {
          for (int i = lparenthOffset + 1; i < rparenthOffset; i++) {
            if (!Character.isWhitespace(document.getCharsSequence().charAt(i))) {
              return;
            }
          }
          editor.getCaretModel().moveToOffset(context.getTailOffset());
        }
        else if (mySpaceBetweenParentheses && document.getCharsSequence().charAt(lparenthOffset) == ' ') {
          editor.getCaretModel().moveToOffset(lparenthOffset + 2);
        }
        else {
          editor.getCaretModel().moveToOffset(lparenthOffset + 1);
        }
        return;
      }
    } else {
      document.insertString(context.getTailOffset(), getSpace(mySpaceBeforeParentheses) + myLeftParenthesis + getSpace(mySpaceBetweenParentheses));
      editor.getCaretModel().moveToOffset(context.getTailOffset());
    }

    if (!myMayInsertRightParenthesis) return;

    if (context.getCompletionChar() == myLeftParenthesis) {
      //todo use BraceMatchingUtil.isPairedBracesAllowedBeforeTypeInFileType
      int tail = context.getTailOffset();
      if (tail < document.getTextLength() && StringUtil.isJavaIdentifierPart(document.getCharsSequence().charAt(tail))) {
        return;
      }
    }

    document.insertString(context.getTailOffset(), getSpace(mySpaceBetweenParentheses) + myRightParenthesis);
    if (!putCaretInside) {
      editor.getCaretModel().moveToOffset(context.getTailOffset());
    }
  }

  private static String getSpace(boolean needSpace) {
    return needSpace ? " " : "";
  }

  @Nullable
  protected PsiElement findExistingLeftParenthesis(@NotNull InsertionContext context) {
    PsiElement element = findNextToken(context);
    return isToken(element, String.valueOf(myLeftParenthesis)) ? element : null;
  }

  @Nullable
  protected PsiElement findNextToken(@NotNull InsertionContext context) {
    final PsiFile file = context.getFile();
    PsiElement element = file.findElementAt(context.getTailOffset());
    if (element instanceof PsiWhiteSpace) {
      if (!myAllowParametersOnNextLine && element.getText().contains("\n")) {
        return null;
      }
      element = file.findElementAt(element.getTextRange().getEndOffset());
    }
    return element;
  }

}
