/*
 * Copyright 2000-2015 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.editorActions.enter;

import com.intellij.codeInsight.CodeInsightSettings;
import com.intellij.codeInsight.highlighting.BraceMatcher;
import com.intellij.codeInsight.highlighting.BraceMatchingUtil;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.TokenType;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;

public class EnterAfterUnmatchedBraceHandler extends EnterHandlerDelegateAdapter {
  private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.editorActions.enter.EnterAfterUnmatchedBraceHandler");

  @Override
  public Result preprocessEnter(@NotNull final PsiFile file,
                                @NotNull final Editor editor,
                                @NotNull final Ref<Integer> caretOffsetRef,
                                @NotNull final Ref<Integer> caretAdvance,
                                @NotNull final DataContext dataContext,
                                final EditorActionHandler originalHandler) {

    int caretOffset = caretOffsetRef.get();
    if (!isApplicable(file, caretOffset)) {
      return Result.Continue;
    }

    int maxRBraceCount = getMaxRBraceCount(file, editor, caretOffset);
    if (maxRBraceCount > 0) {
      insertRBraces(file, editor,
                    caretOffset,
                    getRBraceOffset(file, editor, caretOffset),
                    generateStringToInsert(editor, caretOffset, maxRBraceCount));
      return Result.DefaultForceIndent;
    }
    return Result.Continue;
  }

  /**
   * Checks that the text context is in responsibility of the handler.
   *
   * @param file        target PSI file
   * @param caretOffset target caret offset
   * @return true, if handler is in charge
   */
  public boolean isApplicable(@NotNull PsiFile file, int caretOffset) {
    return true;
  }

  /**
   * Calculates the maximum number of '}' that can be inserted by handler.
   * Can return {@code 0} or less in custom implementation to skip '}' insertion in the {@code preprocessEnter} call
   * and switch to default implementation.
   *
   * @param file        target PSI file
   * @param editor      target editor
   * @param caretOffset target caret offset
   * @return maximum number of '}' that can be inserted by handler, {@code 0} or less to switch to default implementation
   */
  protected int getMaxRBraceCount(@NotNull final PsiFile file, @NotNull final Editor editor, int caretOffset) {
    if (!CodeInsightSettings.getInstance().INSERT_BRACE_ON_ENTER) {
      return 0;
    }
    return Math.max(0, getUnmatchedLBracesNumberBefore(editor, caretOffset, file.getFileType()));
  }

  /**
   * Calculates the string of '}' that have to be inserted by handler.
   * Some languages can expand the string by additional characters (i.e. '\', ';')
   *
   * @param editor         target editor
   * @param caretOffset    target caret offset
   * @param maxRBraceCount the maximum number of '}' for insert at position, it always positive
   * @return the string of '}' that has to be inserted by handler, it must have at least one '}'
   */
  @NotNull
  protected String generateStringToInsert(@NotNull final Editor editor, int caretOffset, int maxRBraceCount) {
    assert maxRBraceCount > 0;

    CharSequence text = editor.getDocument().getCharsSequence();
    int bracesToInsert = 0;
    for (int i = caretOffset - 1; i >= 0 && bracesToInsert < maxRBraceCount; --i) {
      final char c = text.charAt(i);
      if (c == '{') {
        ++bracesToInsert;
      }
      else if (isStopChar(c)) {
        break;
      }
    }
    return StringUtil.repeatSymbol('}', Math.max(bracesToInsert, 1));
  }

  /**
   * Checks the character before the inserted '}' to reduce the count of inserted '}'.
   * The number of inserted '}' will increase for each found '{'.
   *
   * @param c character to check
   * @return true, to stop back iteration
   */
  protected boolean isStopChar(char c) {
    return " \n\t".indexOf(c) < 0;
  }

  /**
   * Calculates the position for insertion of one or more '}'.
   *
   * @param file        target PSI file
   * @param editor      target editor
   * @param caretOffset target caret offset
   * @return the position between {@code caretOffset} and the end of file
   */
  protected int getRBraceOffset(@NotNull final PsiFile file, @NotNull final Editor editor, int caretOffset) {
    CharSequence text = editor.getDocument().getCharsSequence();
    int offset = CharArrayUtil.shiftForward(text, caretOffset, " \t");
    final int fileLength = text.length();
    if (offset < fileLength && ")];,%<?".indexOf(text.charAt(offset)) < 0) {
      offset = calculateOffsetToInsertClosingBrace(file, text, offset).second;
      //offset = CharArrayUtil.shiftForwardUntil(text, caretOffset, "\n");
    }
    return Math.min(offset, fileLength);
  }

  /**
   * Inserts the {@code generatedRBraces} at the {@code rBracesInsertOffset} position and formats the code block.
   * @param file                target PSI file
   * @param editor              target editor
   * @param caretOffset         target caret offset
   * @param rBracesInsertOffset target position to insert
   * @param generatedRBraces    string of '}' to insert
   */
  protected void insertRBraces(@NotNull PsiFile file,
                               @NotNull Editor editor,
                               int caretOffset,
                               int rBracesInsertOffset,
                               String generatedRBraces) {
    final Document document = editor.getDocument();
    insertRBracesAtPosition(document, caretOffset, rBracesInsertOffset, generatedRBraces);
    formatCodeFragmentBetweenBraces(file, document, caretOffset, rBracesInsertOffset, generatedRBraces);
  }

  /**
   * Inserts the {@code rBracesCount} of '}' at the {@code rBracesInsertOffset} position.
   *
   * @param document            target document
   * @param caretOffset         target caret offset
   * @param rBracesInsertOffset target position to insert
   * @param generatedRBraces    string of '}' to insert
   */
  protected void insertRBracesAtPosition(Document document, int caretOffset, int rBracesInsertOffset, String generatedRBraces) {
    document.insertString(rBracesInsertOffset, "\n" + generatedRBraces);
    // We need to adjust indents of the text that will be moved, hence, need to insert preliminary line feed.
    // Example:
    //     if (test1()) {
    //     } else {<caret> if (test2()) {
    //         foo();
    //     }
    // We insert here '\n}' after 'foo();' and have the following:
    //     if (test1()) {
    //     } else { if (test2()) {
    //         foo();
    //         }
    //     }
    // That is formatted incorrectly because line feed between 'else' and 'if' is not inserted yet (whole 'if' block is indent anchor
    // to 'if' code block('{}')). So, we insert temporary line feed between 'if' and 'else', correct indent and remove that temporary
    // line feed.
    document.insertString(caretOffset, "\n");
  }

  /**
   * Formats the code block between caret and inserted braces.
   *
   * @param file                target PSI file
   * @param document            target document
   * @param caretOffset         target caret offset
   * @param rBracesInsertOffset target position to insert
   * @param generatedRBraces    string of '}' to insert
   */
  protected void formatCodeFragmentBetweenBraces(@NotNull PsiFile file,
                                                 @NotNull Document document,
                                                 int caretOffset,
                                                 int rBracesInsertOffset,
                                                 String generatedRBraces) {
    Project project = file.getProject();
    long stamp = document.getModificationStamp();
    boolean closingBraceIndentAdjusted;
    try {
      PsiDocumentManager.getInstance(project).commitDocument(document);
      CodeStyleManager.getInstance(project).adjustLineIndent(file, new TextRange(caretOffset, rBracesInsertOffset + 2));
    }
    catch (IncorrectOperationException e) {
      LOG.error(e);
    }
    finally {
      closingBraceIndentAdjusted = stamp != document.getModificationStamp();
      // do you remember that we insert the '\n'? here we take it back!
      document.deleteString(caretOffset, caretOffset + 1);
    }

    // There is a possible case that formatter was unable to adjust line indent for the closing brace (that is the case for plain text
    // document for example). Hence, we're trying to do the manually.
    if (!closingBraceIndentAdjusted) {
      int line = document.getLineNumber(rBracesInsertOffset);
      StringBuilder buffer = new StringBuilder();
      int start = document.getLineStartOffset(line);
      int end = document.getLineEndOffset(line);
      final CharSequence text = document.getCharsSequence();
      for (int i = start; i < end; i++) {
        char c = text.charAt(i);
        if (c != ' ' && c != '\t') {
          break;
        }
        else {
          buffer.append(c);
        }
      }
      if (buffer.length() > 0) {
        document.insertString(rBracesInsertOffset + 1, buffer);
      }
    }
  }

  /**
   * Current handler inserts closing curly brace (right brace) if necessary. There is a possible case that it should be located
   * more than one line forward.
   * <p/>
   * <b>Example</b>
   * <pre>
   *     if (test1()) {
   *     } else {<caret> if (test2()) {
   *         foo();
   *     }
   * </pre>
   * <p/>
   * We want to get this after the processing:
   * <pre>
   *     if (test1()) {
   *     } else {
   *         if (test2()) {
   *             foo();
   *         }
   *     }
   * </pre>
   * I.e. closing brace should be inserted two lines below current caret line. Hence, we need to calculate correct offset
   * to use for brace inserting. This method is responsible for that.
   * <p/>
   * In essence it inspects PSI structure and finds PSE elements with the max length that starts at caret offset. End offset
   * of that element is used as an insertion point.
   *
   * @param file   target PSI file
   * @param text   text from the given file
   * @param offset target offset where line feed will be inserted
   * @return pair of (element, offset). The element is the '}' owner, if applicable; the offset is the position for inserting closing brace
   */
  protected Pair<PsiElement, Integer> calculateOffsetToInsertClosingBrace(@NotNull PsiFile file, @NotNull CharSequence text, final int offset) {
    PsiElement element = PsiUtilCore.getElementAtOffset(file, offset);
    ASTNode node = element.getNode();
    if (node != null && node.getElementType() == TokenType.WHITE_SPACE) {
      return Pair.create(null, CharArrayUtil.shiftForwardUntil(text, offset, "\n"));
    }
    for (PsiElement parent = element.getParent(); parent != null; parent = parent.getParent()) {
      ASTNode parentNode = parent.getNode();
      if (parentNode == null || parentNode.getStartOffset() != offset) {
        break;
      }
      element = parent;
    }
    if (element.getTextOffset() != offset) {
      return Pair.create(null, CharArrayUtil.shiftForwardUntil(text, offset, "\n"));
    }
    return Pair.create(element, calculateOffsetToInsertClosingBraceInsideElement(element));
  }

  protected int calculateOffsetToInsertClosingBraceInsideElement(PsiElement element) {
    return element.getTextRange().getEndOffset();
  }

  public static boolean isAfterUnmatchedLBrace(Editor editor, int offset, FileType fileType) {
    return getUnmatchedLBracesNumberBefore(editor, offset, fileType) > 0;
  }

  /**
   * Calculates number of unmatched left braces before the given offset.
   *
   * @param editor   target editor
   * @param offset   target offset
   * @param fileType target file type
   * @return number of unmatched braces before the given offset;
   * negative value if it's not possible to perform the calculation or if there are no unmatched left braces before
   * the given offset
   */
  protected static int getUnmatchedLBracesNumberBefore(Editor editor, int offset, FileType fileType) {
    if (offset == 0) {
      return -1;
    }
    CharSequence chars = editor.getDocument().getCharsSequence();
    if (chars.charAt(offset - 1) != '{') {
      return -1;
    }

    EditorHighlighter highlighter = ((EditorEx)editor).getHighlighter();
    HighlighterIterator iterator = highlighter.createIterator(offset - 1);
    BraceMatcher braceMatcher = BraceMatchingUtil.getBraceMatcher(fileType, iterator);

    if (!braceMatcher.isLBraceToken(iterator, chars, fileType) || !braceMatcher.isStructuralBrace(iterator, chars, fileType)) {
      return -1;
    }

    Language language = iterator.getTokenType().getLanguage();

    iterator = highlighter.createIterator(0);
    int lBracesBeforeOffset = 0;
    int lBracesAfterOffset = 0;
    int rBracesBeforeOffset = 0;
    int rBracesAfterOffset = 0;
    for (; !iterator.atEnd(); iterator.advance()) {
      IElementType tokenType = iterator.getTokenType();
      if (!tokenType.getLanguage().equals(language) || !braceMatcher.isStructuralBrace(iterator, chars, fileType)) {
        continue;
      }

      boolean beforeOffset = iterator.getStart() < offset;

      if (braceMatcher.isLBraceToken(iterator, chars, fileType)) {
        if (beforeOffset) {
          lBracesBeforeOffset++;
        }
        else {
          lBracesAfterOffset++;
        }
      }
      else if (braceMatcher.isRBraceToken(iterator, chars, fileType)) {
        if (beforeOffset) {
          rBracesBeforeOffset++;
        }
        else {
          rBracesAfterOffset++;
        }
      }
    }

    return lBracesBeforeOffset - rBracesBeforeOffset - (rBracesAfterOffset - lBracesAfterOffset);
  }
}
