/*
 * Copyright 2000-2013 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.psi.impl.source.codeStyle;

import com.intellij.lang.*;
import com.intellij.openapi.command.AbnormalCommandTerminationException;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.Key;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.Factory;
import com.intellij.psi.impl.source.tree.LeafElement;
import com.intellij.psi.impl.source.tree.TreeElement;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.templateLanguages.OuterLanguageElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.NotNullFunction;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class CodeEditUtil {
  private static final Key<Boolean> GENERATED_FLAG = new Key<Boolean>("GENERATED_FLAG");
  private static final Key<Integer> INDENT_INFO = new Key<Integer>("INDENT_INFO");
  private static final Key<Boolean> REFORMAT_BEFORE_KEY = new Key<Boolean>("REFORMAT_BEFORE_KEY");
  private static final Key<Boolean> REFORMAT_KEY = new Key<Boolean>("REFORMAT_KEY");
  private static final ThreadLocal<Boolean> ALLOW_TO_MARK_NODES_TO_REFORMAT = new ThreadLocal<Boolean>() {
    @Override
    protected Boolean initialValue() {
      return Boolean.TRUE;
    }
  };
  private static final ThreadLocal<Boolean> ALLOW_NODES_REFORMATTING = new ThreadLocal<Boolean>() {
    @Override
    protected Boolean initialValue() {
      return Boolean.TRUE;
    }
  };
  private static final ThreadLocal<NotNullFunction<ASTNode, Boolean>> NODE_REFORMAT_STRATEGY = new ThreadLocal<NotNullFunction<ASTNode, Boolean>>();

  public static final Key<Boolean> OUTER_OK = new Key<Boolean>("OUTER_OK");

  private CodeEditUtil() { }

  public static void addChild(ASTNode parent, ASTNode child, ASTNode anchorBefore) {
    addChildren(parent, child, child, anchorBefore);
  }

  public static void removeChild(ASTNode parent, @NotNull ASTNode child) {
    removeChildren(parent, child, child);
  }

  public static ASTNode addChildren(ASTNode parent, @NotNull ASTNode first, @NotNull ASTNode last, ASTNode anchorBefore) {
    ASTNode lastChild = last.getTreeNext();
    ASTNode current = first;
    while (current != lastChild) {
      saveWhitespacesInfo(current);
      checkForOuters(current);
      current = current.getTreeNext();
    }

    if (anchorBefore != null && isComment(anchorBefore.getElementType())) {
      final ASTNode anchorPrev = anchorBefore.getTreePrev();
      if (anchorPrev != null && anchorPrev.getElementType() == TokenType.WHITE_SPACE) {
        anchorBefore = anchorPrev;
      }
    }

    parent.addChildren(first, lastChild, anchorBefore);
    ASTNode firstAddedLeaf = findFirstLeaf(first, last);
    ASTNode prevLeaf = TreeUtil.prevLeaf(first);
    ASTNode result = first;
    if (firstAddedLeaf != null) {
      ASTNode placeHolderEnd = makePlaceHolderBetweenTokens(prevLeaf, firstAddedLeaf, isFormattingRequired(prevLeaf, first), false);
      if (placeHolderEnd != prevLeaf && first == firstAddedLeaf) {
        result = placeHolderEnd;
      }
      ASTNode lastAddedLeaf = findLastLeaf(first, last);
      placeHolderEnd = makePlaceHolderBetweenTokens(lastAddedLeaf, TreeUtil.nextLeaf(last), true, false);
      if (placeHolderEnd != lastAddedLeaf && lastAddedLeaf == first) {
        result = placeHolderEnd;
      }
    }
    else {
      makePlaceHolderBetweenTokens(prevLeaf, TreeUtil.nextLeaf(last), isFormattingRequired(prevLeaf, first), false);
    }
    return result;
  }

  private static boolean isComment(IElementType type) {
    final ParserDefinition def = LanguageParserDefinitions.INSTANCE.forLanguage(type.getLanguage());
    return def != null && def.getCommentTokens().contains(type);
  }

  private static boolean isFormattingRequired(ASTNode prevLeaf, ASTNode first) {
    while (first != null) {
      ASTNode current = prevLeaf;
      while (current != null) {
        if (current.getTreeNext() == first) return true;
        current = current.getTreeParent();
      }
      final ASTNode parent = first.getTreeParent();
      if (parent != null && parent.getTextRange().equals(first.getTextRange())) {
        first = parent;
      }
      else {
        break;
      }
    }
    return false;
  }

  public static void checkForOuters(ASTNode element) {
    if (element instanceof OuterLanguageElement && element.getCopyableUserData(OUTER_OK) == null) {
      throw new AbnormalCommandTerminationException();
    }

    ASTNode child = element.getFirstChildNode();
    while (child != null) {
      checkForOuters(child);
      child = child.getTreeNext();
    }
  }

  public static void saveWhitespacesInfo(ASTNode first) {
    if (first == null || isNodeGenerated(first) || getOldIndentation(first) >= 0) {
      return;
    }

    PsiElement psiElement = first.getPsi();
    if (psiElement == null) {
      return;
    }

    PsiFile file = psiElement.getContainingFile();
    setOldIndentation((TreeElement)first, IndentHelper.getInstance().getIndent(file.getProject(), file.getFileType(), first));
  }

  public static int getOldIndentation(ASTNode node) {
    if (node == null) return -1;
    final Integer stored = node.getCopyableUserData(INDENT_INFO);
    return stored != null ? stored : -1;
  }

  public static void removeChildren(ASTNode parent, @NotNull ASTNode first, @NotNull ASTNode last) {
    final boolean tailingElement = last.getStartOffset() + last.getTextLength() == parent.getStartOffset() + parent.getTextLength();
    final boolean forceReformat = needToForceReformat(parent, first, last);
    saveWhitespacesInfo(first);

    TreeElement child = (TreeElement)first;
    while (child != null) {
      //checkForOuters(child);
      if (child == last) break;
      child = child.getTreeNext();
    }
    assert child == last : last + " is not a successor of " + first + " in the .getTreeNext() chain";

    final ASTNode prevLeaf = TreeUtil.prevLeaf(first);
    final ASTNode nextLeaf = TreeUtil.nextLeaf(first);
    parent.removeRange(first, last.getTreeNext());
    ASTNode nextLeafToAdjust = nextLeaf;
    if (nextLeafToAdjust != null && prevLeaf != null && nextLeafToAdjust.getTreeParent() == null) {
      //next element has invalidated
      nextLeafToAdjust = prevLeaf.getTreeNext();
    }
    makePlaceHolderBetweenTokens(prevLeaf, nextLeafToAdjust, forceReformat, tailingElement);
  }

  private static boolean needToForceReformat(final ASTNode parent, final ASTNode first, final ASTNode last) {
    return parent == null || first.getStartOffset() != parent.getStartOffset() ||
           parent.getText().trim().length() == getTrimmedTextLength(first, last) &&
           needToForceReformat(parent.getTreeParent(), parent, parent);
  }

  private static int getTrimmedTextLength(ASTNode first, final ASTNode last) {
    final StringBuilder buffer = new StringBuilder();
    while (first != last.getTreeNext()) {
      buffer.append(first.getText());
      first = first.getTreeNext();
    }
    return buffer.toString().trim().length();
  }

  public static void replaceChild(ASTNode parent, @NotNull ASTNode oldChild, @NotNull ASTNode newChild) {
    saveWhitespacesInfo(oldChild);
    saveWhitespacesInfo(newChild);
    checkForOuters(oldChild);
    checkForOuters(newChild);

    LeafElement oldFirst = TreeUtil.findFirstLeaf(oldChild);

    parent.replaceChild(oldChild, newChild);
    final LeafElement firstLeaf = TreeUtil.findFirstLeaf(newChild);
    final ASTNode prevToken = TreeUtil.prevLeaf(newChild);
    if (firstLeaf != null) {
      final ASTNode nextLeaf = TreeUtil.nextLeaf(newChild);
      makePlaceHolderBetweenTokens(prevToken, firstLeaf, isFormattingRequired(prevToken, newChild), false);
      if (nextLeaf != null && !CharArrayUtil.containLineBreaks(nextLeaf.getText())) {
        makePlaceHolderBetweenTokens(TreeUtil.prevLeaf(nextLeaf), nextLeaf, false, false);
      }
    }
    else {
      if (oldFirst != null && prevToken == null) {
        ASTNode whitespaceNode = newChild.getTreeNext();
        if (whitespaceNode != null && whitespaceNode.getElementType() == TokenType.WHITE_SPACE) {
          // Replacing non-empty prefix to empty shall remove whitespace
          parent.removeChild(whitespaceNode);
        }
      }

      makePlaceHolderBetweenTokens(prevToken, TreeUtil.nextLeaf(newChild), isFormattingRequired(prevToken, newChild), false);
    }
  }

  @Nullable
  private static ASTNode findFirstLeaf(ASTNode first, ASTNode last) {
    do {
      final LeafElement leaf = TreeUtil.findFirstLeaf(first);
      if (leaf != null) return leaf;
      first = first.getTreeNext();
      if (first == null) return null;
    }
    while (first != last);
    return null;
  }

  @Nullable
  private static ASTNode findLastLeaf(ASTNode first, ASTNode last) {
    do {
      final ASTNode leaf = TreeUtil.findLastLeaf(last);
      if (leaf != null) return leaf;
      last = last.getTreePrev();
      if (last == null) return null;
    }
    while (first != last);
    return null;
  }

  @Nullable
  private static ASTNode makePlaceHolderBetweenTokens(ASTNode left, ASTNode right, boolean forceReformat, boolean normalizeTrailingWS) {
    if (right == null) return left;

    markToReformatBefore(right, false);
    if (left == null) {
      markToReformatBefore(right, true);
    }
    else if (left.getElementType() == TokenType.WHITE_SPACE && left.getTreeNext() == null && normalizeTrailingWS) {
      // handle tailing whitespaces if element on the left has been removed
      final ASTNode prevLeaf = TreeUtil.prevLeaf(left);
      left.getTreeParent().removeChild(left);
      markToReformatBeforeOrInsertWhitespace(prevLeaf, right);
      left = right;
    }
    else if (left.getElementType() == TokenType.WHITE_SPACE && right.getElementType() == TokenType.WHITE_SPACE) {
      final String text;
      final int leftBlankLines = getBlankLines(left.getText());
      final int rightBlankLines = getBlankLines(right.getText());
      final boolean leaveRightText = leftBlankLines < rightBlankLines;
      if (leftBlankLines == 0 && rightBlankLines == 0) {
        text = left.getText() + right.getText();
      }
      else if (leaveRightText) {
        text = right.getText();
      }
      else {
        text = left.getText();
      }
      if (leaveRightText || forceReformat) {
        final LeafElement merged = ASTFactory.whitespace(text);
        if (!leaveRightText) {
          left.getTreeParent().replaceChild(left, merged);
          right.getTreeParent().removeChild(right);
        }
        else {
          right.getTreeParent().replaceChild(right, merged);
          left.getTreeParent().removeChild(left);
        }
        left = merged;
      }
      else {
        right.getTreeParent().removeChild(right);
      }
    }
    else if (left.getElementType() != TokenType.WHITE_SPACE || forceReformat) {
      if (right.getElementType() == TokenType.WHITE_SPACE) {
        markWhitespaceForReformat(right);
      }
      else if (left.getElementType() == TokenType.WHITE_SPACE) {
        markWhitespaceForReformat(left);
      }
      else {
        markToReformatBeforeOrInsertWhitespace(left, right);
      }
    }
    return left;
  }

  private static void markWhitespaceForReformat(final ASTNode right) {
    final String text = right.getText();
    final LeafElement merged = ASTFactory.whitespace(text);
    right.getTreeParent().replaceChild(right, merged);
  }

  private static void markToReformatBeforeOrInsertWhitespace(final ASTNode left, @NotNull final ASTNode right) {
    final Language leftLang = left != null ? PsiUtilCore.getNotAnyLanguage(left) : null;
    final Language rightLang = PsiUtilCore.getNotAnyLanguage(right);

    ASTNode generatedWhitespace = null;
    if (leftLang != null && leftLang.isKindOf(rightLang)) {
      generatedWhitespace = LanguageTokenSeparatorGenerators.INSTANCE.forLanguage(leftLang).generateWhitespaceBetweenTokens(left, right);
    }
    else if (rightLang.isKindOf(leftLang)) {
      generatedWhitespace = LanguageTokenSeparatorGenerators.INSTANCE.forLanguage(rightLang).generateWhitespaceBetweenTokens(left, right);
    }

    if (generatedWhitespace != null) {
      final TreeUtil.CommonParentState parentState = new TreeUtil.CommonParentState();
      TreeUtil.prevLeaf((TreeElement)right, parentState);
      parentState.nextLeafBranchStart.getTreeParent().addChild(generatedWhitespace, parentState.nextLeafBranchStart);
    }
    else {
      markToReformatBefore(right, true);
    }
  }

  public static void markToReformatBefore(final ASTNode right, boolean value) {
    right.putCopyableUserData(REFORMAT_BEFORE_KEY, value ? true : null);
  }

  private static int getBlankLines(final String text) {
    int result = 0;
    int currentIndex = -1;
    while ((currentIndex = text.indexOf('\n', currentIndex + 1)) >= 0) result++;
    return result;
  }

  public static boolean isNodeGenerated(final ASTNode node) {
    return node == null || node.getCopyableUserData(GENERATED_FLAG) != null;
  }

  public static void setNodeGenerated(final ASTNode next, final boolean value) {
    if (next == null) return;
    next.putCopyableUserData(GENERATED_FLAG, value ? true : null);
  }

  public static void setNodeGeneratedRecursively(final ASTNode next, final boolean value) {
    if (next == null) return;
    setNodeGenerated(next, value);
    for (ASTNode child = next.getFirstChildNode(); child != null; child = child.getTreeNext()) {
      setNodeGeneratedRecursively(child, value);
    }
  }

  public static void setOldIndentation(final TreeElement treeElement, final int oldIndentation) {
    if (treeElement == null) return;
    treeElement.putCopyableUserData(INDENT_INFO, oldIndentation >= 0 ? oldIndentation : null);
  }

  public static boolean isMarkedToReformatBefore(final TreeElement element) {
    return element.getCopyableUserData(REFORMAT_BEFORE_KEY) != null;
  }

  @Nullable
  public static PsiElement createLineFeed(final PsiManager manager) {
    return Factory.createSingleLeafElement(TokenType.WHITE_SPACE, "\n", 0, 1, null, manager).getPsi();
  }

  /**
   * Allows to answer if given node is configured to be reformatted.
   *
   * @param node node to check
   * @return <code>true</code> if given node is configured to be reformatted; <code>false</code> otherwise
   */
  public static boolean isMarkedToReformat(final ASTNode node) {
    if (node.getCopyableUserData(REFORMAT_KEY) == null || !isSuspendedNodesReformattingAllowed()) {
      return false;
    }
    final NotNullFunction<ASTNode, Boolean> strategy = NODE_REFORMAT_STRATEGY.get();
    return strategy == null || strategy.fun(node);
  }

  /**
   * Allows to define if given element should be reformatted later.
   *
   * @param node  target element which <code>'reformat'</code> status should be changed
   * @param value <code>true</code> if the element should be reformatted; <code>false</code> otherwise
   */
  public static void markToReformat(final ASTNode node, boolean value) {
    if (ALLOW_TO_MARK_NODES_TO_REFORMAT.get()) {
      node.putCopyableUserData(REFORMAT_KEY, value ? true : null);
    }
  }

  /**
   * We allow to mark particular {@link ASTNode AST nodes} to be reformatted later (e.g. we may want method definition and calls
   * to be reformatted when we perform <code>'change method signature'</code> refactoring. Hence, we mark corresponding expressions
   * to be reformatted).
   * <p/>
   * For convenience that is made automatically on AST/PSI level, i.e. every time target element change it automatically marks itself
   * to be reformatted.
   * <p/>
   * However, there is a possible case that particular element is changed because of formatting, hence, there is no need to mark
   * itself for postponed formatting one more time. This method allows to configure allowance of reformat markers processing
   * for the calling thread. I.e. this method may be called with <code>'false'</code> as an argument, hence, all further attempts
   * to {@link #markToReformat(ASTNode, boolean) mark node for postponed formatting} will have no effect until current method is
   * called with <code>'true'</code> as an argument. Hence, following usage scenario is expected:
   * <ol>
   * <li>This method is called with <code>'false'</code> argument;</li>
   * <li>Formatting is performed at dedicated <code>'try'</code> block;</li>
   * <li>This method is called with <code>'false'</code> argument from <code>'finally'</code> section;</li>
   * </ol>
   *
   * @param allow flag that defines if new reformat markers can be added from the current thread
   * @see #markToReformat(ASTNode, boolean)
   */
  public static void allowToMarkNodesForPostponedFormatting(boolean allow) {
    ALLOW_TO_MARK_NODES_TO_REFORMAT.set(allow);
  }

  /**
   * @return <code>'allow suspended formatting'</code> flag value
   * @see #setAllowSuspendNodesReformatting(boolean)
   */
  public static boolean isSuspendedNodesReformattingAllowed() {
    return ALLOW_NODES_REFORMATTING.get();
  }

  /**
   * There is a possible case that particular PSI tree node is {@link #markToReformat(ASTNode, boolean) marked for reformatting}.
   * That means that there is a big chance that the node will be re-formatted during corresponding document processing
   * (e.g. on call to {@link PsiDocumentManager#doPostponedOperationsAndUnblockDocument(Document)}).
   * <p/>
   * However, there is a possible case that particular activity that triggers such document processing is not ready to the
   * situation when the document is modified because of postponed formatting. Hence, it may ask to suspend postponed formatting
   * for a while. This method allows to do that at thread-local manner. I.e. it's expected to be called as follows:
   * <pre>
   * <ol>
   *   <li>This method is called with <code>'false'</code> argument;</li>
   *   <li>Document is processed at dedicated <code>'try'</code> block;</li>
   *   <li>This method is called with <code>'true'</code> argument from <code>'finally'</code> section;</li>
   * </ol>
   * </pre>
   */
  public static void setAllowSuspendNodesReformatting(boolean allow) {
    ALLOW_NODES_REFORMATTING.set(allow);
  }

  /**
   * Allows to control the same process as {@link #setAllowSuspendNodesReformatting(boolean)} but on a node level. I.e. it allows
   * to answer if particular node can be reformatted if {@link #isSuspendedNodesReformattingAllowed() global reformatting is allowed}.
   *
   * @param strategy strategy to use; <code>null</code> as an indication that no fine-grained checking should be performed
   */
  public static void setNodeReformatStrategy(@Nullable NotNullFunction<ASTNode, Boolean> strategy) {
    NODE_REFORMAT_STRATEGY.set(strategy);
  }
}
