/*
 * 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.lang.folding;

import com.intellij.lang.ASTNode;
import com.intellij.lang.Commenter;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageCommenters;
import com.intellij.lang.parser.GeneratedParserUtilBase;
import com.intellij.lang.surroundWith.SurroundDescriptor;
import com.intellij.lang.surroundWith.Surrounder;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;

import static com.intellij.psi.util.PsiTreeUtil.skipParentsOfType;

/**
 * @author Rustam Vishnyakov
 */
public class CustomFoldingSurroundDescriptor implements SurroundDescriptor {

  public final static CustomFoldingSurroundDescriptor INSTANCE = new CustomFoldingSurroundDescriptor();
  public final static CustomFoldingRegionSurrounder[] SURROUNDERS;

  private final static String DEFAULT_DESC_TEXT = "Description";

  static {
    List<CustomFoldingRegionSurrounder> surrounderList = new ArrayList<>();
    for (CustomFoldingProvider provider : CustomFoldingProvider.getAllProviders()) {
      surrounderList.add(new CustomFoldingRegionSurrounder(provider));
    }
    SURROUNDERS = surrounderList.toArray(new CustomFoldingRegionSurrounder[surrounderList.size()]);
  }

  @NotNull
  @Override
  public PsiElement[] getElementsToSurround(PsiFile file, int startOffset, int endOffset) {
    if (startOffset >= endOffset) return PsiElement.EMPTY_ARRAY;
    Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(file.getLanguage());
    if (commenter == null || commenter.getLineCommentPrefix() == null && 
                             (commenter.getBlockCommentPrefix() == null || commenter.getBlockCommentSuffix() == null)) {
      return PsiElement.EMPTY_ARRAY;
    }
    PsiElement startElement = file.findElementAt(startOffset);
    PsiElement endElement = file.findElementAt(endOffset - 1);
    if (startElement instanceof PsiWhiteSpace) {
      if (startElement == endElement) return PsiElement.EMPTY_ARRAY;
      startElement = startElement.getNextSibling();
    }
    if (endElement instanceof PsiWhiteSpace) endElement = endElement.getPrevSibling();
    if (startElement != null && endElement != null) {
      startElement = findClosestParentAfterLineBreak(startElement);
      if (startElement != null) {
        endElement = findClosestParentBeforeLineBreak(endElement);
        if (endElement != null) {
          return adjustRange(startElement, endElement);
        }
      }
    }
    return PsiElement.EMPTY_ARRAY;
  }

  @NotNull
  private static PsiElement[] adjustRange(@NotNull PsiElement start, @NotNull PsiElement end) {
    PsiElement newStart = lowerStartElementIfNeeded(start, end);
    PsiElement newEnd = lowerEndElementIfNeeded(start, end);
    if (newStart == null || newEnd == null) {
      return PsiElement.EMPTY_ARRAY;
    }
    PsiElement commonParent = findCommonAncestorForWholeRange(newStart, newEnd);
    if (commonParent != null) {
      return new PsiElement[] {commonParent};
    }
    // If either start or end element is the first/last leaf element in its parent, use the parent itself instead
    // to prevent selection of clearly illegal ranges like the following:
    // [
    //   <selection>1
    // ]</selection>
    // E.g. in case shown, because of that adjustment, closing bracket and number literal won't have the same parent
    // and next test will fail.
    PsiElement newStartParent = getParent(newStart);
    if (newStartParent != null && newStartParent.getFirstChild() == newStart && newStart.getFirstChild() == null) {
      newStart = newStartParent;
    }
    PsiElement newEndParent = getParent(newEnd);
    if (newEndParent != null && newEndParent.getLastChild() == newEnd && newEnd.getFirstChild() == null) {
      newEnd = newEndParent;
    }
    if (getParent(newStart) == getParent(newEnd)) {
      return new PsiElement[] {newStart, newEnd};
    }
    return PsiElement.EMPTY_ARRAY;
  }

  @Nullable
  private static PsiElement getParent(@Nullable PsiElement e) {
    return e instanceof PsiFile ? e : skipParentsOfType(e, GeneratedParserUtilBase.DummyBlock.class);
  }

  @Nullable
  private static PsiElement lowerEndElementIfNeeded(@NotNull PsiElement start, @NotNull PsiElement end) {
    if (PsiTreeUtil.isAncestor(end, start, true)) {
      PsiElement o = end.getLastChild();
      while (o != null && o.getParent() != start.getParent()) {
        PsiElement last = o.getLastChild();
        if (last == null) return o;
        o = last;
      }
      return o;
    }
    return end;
  }

  @Nullable
  private static PsiElement lowerStartElementIfNeeded(@NotNull PsiElement start, @NotNull PsiElement end) {
    if (PsiTreeUtil.isAncestor(start, end, true)) {
      PsiElement o = start.getFirstChild();
      while (o != null && o.getParent() != end.getParent()) {
        PsiElement first = o.getFirstChild();
        if (first == null) return o;
        o = first;
      }
      return o;
    }
    return start;
  }

  @Nullable
  private static PsiElement findCommonAncestorForWholeRange(@NotNull PsiElement start, @NotNull PsiElement end) {
    if (start.getContainingFile() != end.getContainingFile()) {
      return null;
    }
    final PsiElement parent = PsiTreeUtil.findCommonParent(start, end);
    if (parent == null) {
      return null;
    }
    final TextRange parentRange = parent.getTextRange();
    if (parentRange.getStartOffset() == start.getTextRange().getStartOffset() &&
        parentRange.getEndOffset() == end.getTextRange().getEndOffset()) {
      return parent;
    }
    return null;
  }

  @Nullable
  private static PsiElement findClosestParentAfterLineBreak(PsiElement element) {
    PsiElement parent = element;
    while (parent != null && !(parent instanceof PsiFileSystemItem)) {
      PsiElement prev = parent.getPrevSibling();
      while (prev != null && prev.getTextLength() <= 0) {
        prev = prev.getPrevSibling();
      }
      if (firstElementInFile(parent)) {
        return parent.getContainingFile();
      }
      else if (isWhiteSpaceWithLineFeed(prev)) {
        return parent;
      }
      parent = parent.getParent();
    }
    return null;
  }

  private static boolean firstElementInFile(@NotNull PsiElement element) {
    return element.getTextOffset() == 0;
  }

  @Nullable
  private static PsiElement findClosestParentBeforeLineBreak(PsiElement element) {
    PsiElement parent = element;
    while (parent != null && !(parent instanceof PsiFileSystemItem)) {
      final PsiElement next = parent.getNextSibling();
      if (lastElementInFile(parent)) {
        return parent.getContainingFile();
      }
      else if (isWhiteSpaceWithLineFeed(next)) {
        return parent;
      }
      parent = parent.getParent();
    }
    return null;
  }

  private static boolean lastElementInFile(@NotNull PsiElement element) {
    return element.getTextRange().getEndOffset() == element.getContainingFile().getTextRange().getEndOffset();
  }

  private static boolean isWhiteSpaceWithLineFeed(@Nullable PsiElement element) {
    if (element == null) {
      return false;
    }
    if (element instanceof PsiWhiteSpace) {
      return element.textContains('\n');
    }
    final ASTNode node = element.getNode();
    if (node == null) {
      return false;
    }
    final CharSequence text = node.getChars();
    boolean lineFeedFound = false;
    for (int i = 0; i < text.length(); i++) {
      final char c = text.charAt(i);
      if (!StringUtil.isWhiteSpace(c)) {
        return false;
      }
      lineFeedFound |= c == '\n';
    }
    return lineFeedFound;
  }

  @NotNull
  @Override
  public Surrounder[] getSurrounders() {
    return SURROUNDERS;
  }

  @Override
  public boolean isExclusive() {
    return false;
  }

  private static class CustomFoldingRegionSurrounder implements Surrounder {

    private final CustomFoldingProvider myProvider;

    public CustomFoldingRegionSurrounder(@NotNull CustomFoldingProvider provider) {
      myProvider = provider;
    }

    @Override
    public String getTemplateDescription() {
      return myProvider.getDescription();
    }

    @Override
    public boolean isApplicable(@NotNull PsiElement[] elements) {
      if (elements.length == 0) return false;
      if (elements[0].getContainingFile() instanceof PsiCodeFragment) {
        return false;
      }
      for (FoldingBuilder each : LanguageFolding.INSTANCE.allForLanguage(elements[0].getLanguage())) {
        if (each instanceof CustomFoldingBuilder) return true;
      }
      return false;
    }

    @Override
    public TextRange surroundElements(@NotNull Project project, @NotNull Editor editor, @NotNull PsiElement[] elements)
      throws IncorrectOperationException {
      if (elements.length == 0) return null;
      PsiElement firstElement = elements[0];
      PsiElement lastElement = elements[elements.length - 1];
      PsiFile psiFile = firstElement.getContainingFile();
      Language language = psiFile.getLanguage();
      Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(language);
      if (commenter == null) return null;
      String linePrefix = commenter.getLineCommentPrefix();
      String lineSuffix = "";
      if (linePrefix == null) {
        linePrefix = commenter.getBlockCommentPrefix();
        lineSuffix = StringUtil.notNullize(commenter.getBlockCommentSuffix());
      }
      if (linePrefix == null) return null;
      int prefixLength = linePrefix.length();

      int startOffset = firstElement.getTextRange().getStartOffset();
      final Document document = editor.getDocument();
      final int startLineNumber = document.getLineNumber(startOffset);
      final String startIndent = document.getText(new TextRange(document.getLineStartOffset(startLineNumber), startOffset));
      int endOffset = lastElement.getTextRange().getEndOffset();
      int delta = 0;
      TextRange rangeToSelect = TextRange.create(startOffset, startOffset);
      String startText = myProvider.getStartString();
      int descPos = startText.indexOf("?");
      if (descPos >= 0) {
        startText = startText.replace("?", DEFAULT_DESC_TEXT);
        rangeToSelect = TextRange.from(startOffset + descPos, DEFAULT_DESC_TEXT.length());
      }

      String startString = linePrefix + startText + lineSuffix + "\n" + startIndent;
      String endString = "\n" + linePrefix + myProvider.getEndString() + lineSuffix;
      document.insertString(endOffset, endString);
      delta += endString.length();
      document.insertString(startOffset, startString);
      delta += startString.length();
      
      RangeMarker rangeMarkerToSelect = document.createRangeMarker(rangeToSelect.shiftRight(prefixLength));
      PsiDocumentManager.getInstance(project).commitDocument(document);
      adjustLineIndent(project, psiFile, language, TextRange.from(endOffset + delta - endString.length(), endString.length()));
      adjustLineIndent(project, psiFile, language, TextRange.from(startOffset, startString.length()));
      rangeToSelect = TextRange.create(rangeMarkerToSelect.getStartOffset(), rangeMarkerToSelect.getEndOffset());
      rangeMarkerToSelect.dispose();
      return rangeToSelect;
    }

    private static void adjustLineIndent(@NotNull Project project, PsiFile file, Language language, TextRange range) {
      CommonCodeStyleSettings formatSettings = CodeStyleSettingsManager.getSettings(project).getCommonSettings(language);
      boolean keepAtFirstCol = formatSettings.KEEP_FIRST_COLUMN_COMMENT;
      formatSettings.KEEP_FIRST_COLUMN_COMMENT = false;
      CodeStyleManager.getInstance(project).adjustLineIndent(file, range);
      formatSettings.KEEP_FIRST_COLUMN_COMMENT = keepAtFirstCol;
    }
  }
}
