/*
 * 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.jetbrains.python.psi;

import com.google.common.collect.Iterables;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PythonCodeStyleService;
import com.jetbrains.python.ast.PyAstStatementList;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;
import java.util.List;

/**
 * Contains various methods for manipulation on indentation found in arbitrary text and individual lines:
 * <ul>
 *   <li>calculating actual and expected indentation</li>
 *   <li>finding common indentation of several lines</li>
 *   <li>replacing and removing indentation of multiple lines</li>
 * </ul>
 *
 * It indented to be used primarily when one needs to modify content of Python files on document level and preserve valid block structure.
 * Note that in most scenarios accurate indentation consistent with the code style settings is provided by automatic formatting pass
 * that is performed each time you modify PSI tree directly.
 *
 * @author Mikhail Golubev
 */
public final class PyIndentUtil {
  public static final @NonNls String TWO_SPACES = "  ";
  public static final @NonNls String FOUR_SPACES = "    ";

  private PyIndentUtil() {
  }

  /**
   * Returns indentation size as number of characters <tt>' '</tt> and <tt>'\t'</tt> in the beginning of a line.
   * It doesn't perform any expansion of tabs.
   */
  public static int getLineIndentSize(@NotNull CharSequence line) {
    int stop;
    for (stop = 0; stop < line.length(); stop++) {
      final char c = line.charAt(stop);
      if (!(c == ' ' || c == '\t')) {
        break;
      }
    }
    return stop;
  }

  public static @NotNull String getLineIndent(@NotNull String line) {
    return line.substring(0, getLineIndentSize(line));
  }

  /**
   * Useful version of {@link #getLineIndent(String)} for custom character sequences like {@link com.jetbrains.python.toolbox.Substring}.
   */
  public static @NotNull CharSequence getLineIndent(@NotNull CharSequence line) {
    return line.subSequence(0, getLineIndentSize(line));
  }

  public static @NotNull String getElementIndent(@NotNull PsiElement anchor) {
    if (anchor instanceof PsiFile) {
      return "";
    }
    final PyAstStatementList statementList = getAnchorStatementList(anchor);
    if (statementList == null) {
      return "";
    }
    final PsiElement prevSibling = statementList.getPrevSibling();
    final String whitespace = prevSibling instanceof PsiWhiteSpace ? prevSibling.getText() : "";
    final int i = whitespace.lastIndexOf("\n");
    if (i >= 0 && statementList.getStatements().length != 0) {
      return whitespace.substring(i + 1);
    }
    else {
      return getExpectedBlockIndent(statementList);
    }
  }

  private static @NotNull String getExpectedBlockIndent(@NotNull PyAstStatementList anchor) {
    final String indentStep = getIndentFromSettings(anchor.getContainingFile());
    final PyAstStatementList parentBlock = PsiTreeUtil.getParentOfType(anchor, PyAstStatementList.class, true);
    if (parentBlock != null) {
      return getElementIndent(parentBlock) + indentStep;
    }
    return indentStep;
  }

  private static @Nullable PyAstStatementList getAnchorStatementList(@NotNull PsiElement element) {
    PyAstStatementList statementList = null;
    // First whitespace right before the statement list (right after ":")
    if (element instanceof PsiWhiteSpace) {
      statementList = ObjectUtils.tryCast(element.getNextSibling(), PyAstStatementList.class);
    }
    if (statementList == null) {
      statementList = PsiTreeUtil.getParentOfType(element, PyAstStatementList.class, false);
    }
    return statementList;
  }

  public static boolean areTabsUsedForIndentation(@NotNull PsiFile file) {
    return PythonCodeStyleService.getInstance().isTabIndentation(file);
  }

  /**
   * Returns indentation size configured in the Python code style settings.
   *
   * @see #getIndentFromSettings(PsiFile)
   */
  public static int getIndentSizeFromSettings(@NotNull PsiFile file) {
    return PythonCodeStyleService.getInstance().getIndentSize(file);
  }

  /**
   * Returns indentation configured in the Python code style settings either as space character repeated number times specified there
   * or a single tab character if tabs are set to use for indentation.
   *
   * @see #getIndentSizeFromSettings(PsiFile)
   * @see #areTabsUsedForIndentation(PsiFile)
   */
  public static @NotNull String getIndentFromSettings(@NotNull PsiFile file) {
    final boolean useTabs = areTabsUsedForIndentation(file);
    return useTabs ? "\t" : StringUtil.repeatSymbol(' ', getIndentSizeFromSettings(file));
  }

  public static @NotNull List<String> removeCommonIndent(@NotNull Iterable<String> lines, boolean ignoreFirstLine) {
    return changeIndent(lines, ignoreFirstLine, "");
  }

  public static @NotNull String removeCommonIndent(@NotNull String s, boolean ignoreFirstLine) {
    final List<String> trimmed = removeCommonIndent(LineTokenizer.tokenizeIntoList(s, false, false), ignoreFirstLine);
    return StringUtil.join(trimmed, "\n");
  }

  public static @NotNull String changeIndent(@NotNull String s, boolean ignoreFirstLine, String newIndent) {
    final List<String> trimmed = changeIndent(LineTokenizer.tokenizeIntoList(s, false, false), ignoreFirstLine, newIndent);
    return StringUtil.join(trimmed, "\n");
  }

  /**
   * Note that all empty lines will be trimmed regardless of their actual indentation.
   */
  public static @NotNull List<String> changeIndent(@NotNull Iterable<String> lines, boolean ignoreFirstLine, final String newIndent) {
    final String oldIndent = findCommonIndent(lines, ignoreFirstLine);
    if (Iterables.isEmpty(lines)) {
      return Collections.emptyList();
    }

    final List<String> result = ContainerUtil.map(Iterables.skip(lines, ignoreFirstLine ? 1 : 0), line -> {
      if (StringUtil.isEmptyOrSpaces(line)) {
        return "";
      }
      else {
        return newIndent + line.substring(oldIndent.length());
      }
    });
    if (ignoreFirstLine) {
      return ContainerUtil.prepend(result, Iterables.get(lines, 0));
    }
    return result;
  }

  public static @NotNull String findCommonIndent(@NotNull String s, boolean ignoreFirstLine) {
    return findCommonIndent(LineTokenizer.tokenizeIntoList(s, false, false), ignoreFirstLine);
  }

  /**
   * Finds maximum common indentation of the given lines. Indentation of empty lines and lines containing only whitespaces is ignored unless
   * they're the only lines provided. In the latter case common indentation for such lines is returned. If mix of tabs and spaces was used
   * for indentation and any two of lines taken into account contain incompatible combination of these symbols, i.e. it's impossible to
   * decide which one can be used as prefix for another, empty string is returned.
   *
   * @param ignoreFirstLine whether the first line should be considered (useful for multiline string literals)
   */
  public static @NotNull String findCommonIndent(@NotNull Iterable<String> lines, boolean ignoreFirstLine) {
    String minIndent = null;
    boolean allLinesEmpty = true;
    if (Iterables.isEmpty(lines)) {
      return "";
    }
    boolean hasBadEmptyLineIndent = false;
    for (String line : Iterables.skip(lines, ignoreFirstLine ? 1 : 0)) {
      final boolean lineEmpty = StringUtil.isEmptyOrSpaces(line);
      if (lineEmpty && !allLinesEmpty) {
        continue;
      }
      final String indent = getLineIndent(line);
      if (minIndent == null || (!lineEmpty && allLinesEmpty) || minIndent.startsWith(indent)) {
        minIndent = indent;
      }
      else if (!indent.startsWith(minIndent)) {
        if (lineEmpty) {
          hasBadEmptyLineIndent = true;
        }
        else {
          return "";
        }
      }
      allLinesEmpty &= lineEmpty;
    }
    if (allLinesEmpty && hasBadEmptyLineIndent) {
      return "";
    }
    return StringUtil.notNullize(minIndent);
  }

  public static @NotNull String getLineIndent(@NotNull Document document, int lineNumber) {
    final TextRange lineRange = TextRange.create(document.getLineStartOffset(lineNumber), document.getLineEndOffset(lineNumber));
    final String line = document.getText(lineRange);
    return getLineIndent(line);
  }
}
