/**
   * Shifts the line range specified by <code>lines</code> in <code>document</code>. The amount that
   * the lines get shifted are determined by the first line in the range, all subsequent lines are
   * adjusted accordingly. The passed Java project may be <code>null</code>, it is used solely to
   * obtain formatter preferences.
   *
   * @param document the document to be changed
   * @param lines the line range to be shifted
   * @param project the Java project to get the formatter preferences from, or <code>null</code> if
   *     global preferences should be used
   * @param result the result from a previous call to <code>shiftLines</code>, in order to maintain
   *     comment line properties, or <code>null</code>. Note that the passed result may be changed
   *     by the call.
   * @return an indent result that may be queried for changes and can be reused in subsequent
   *     indentation operations
   * @throws BadLocationException if <code>lines</code> is not a valid line range on <code>document
   *     </code>
   */
  public static IndentResult shiftLines(
      IDocument document, ILineRange lines, IJavaProject project, IndentResult result)
      throws BadLocationException {
    int numberOfLines = lines.getNumberOfLines();

    if (numberOfLines < 1) return new IndentResult(null);

    result = reuseOrCreateToken(result, numberOfLines);
    result.hasChanged = false;

    JavaHeuristicScanner scanner = new JavaHeuristicScanner(document);
    JavaIndenter indenter = new JavaIndenter(document, scanner, project);

    String current = getCurrentIndent(document, lines.getStartLine());
    StringBuffer correct =
        indenter.computeIndentation(document.getLineOffset(lines.getStartLine()));
    if (correct == null) return result; // bail out

    int tabSize = CodeFormatterUtil.getTabWidth(project);
    StringBuffer addition = new StringBuffer();
    int difference = subtractIndent(correct, current, addition, tabSize);

    if (difference == 0) return result;

    if (result.leftmostLine == -1) result.leftmostLine = getLeftMostLine(document, lines, tabSize);

    int maxReduction =
        computeVisualLength(
            getCurrentIndent(document, result.leftmostLine + lines.getStartLine()), tabSize);

    if (difference > 0) {
      for (int line = lines.getStartLine(), last = line + numberOfLines, i = 0; line < last; line++)
        addIndent(document, line, addition, result.commentLinesAtColumnZero, i++);
    } else {
      int reduction = Math.min(-difference, maxReduction);
      for (int line = lines.getStartLine(), last = line + numberOfLines, i = 0; line < last; line++)
        cutIndent(document, line, reduction, tabSize, result.commentLinesAtColumnZero, i++);
    }

    result.hasChanged = true;

    return result;
  }
  /**
   * Indents a single line using the java heuristic scanner. Javadoc and multi line comments are
   * indented as specified by the <code>JavaDocAutoIndentStrategy</code>.
   *
   * @param document the document
   * @param line the line to be indented
   * @param indenter the java indenter
   * @param scanner the heuristic scanner
   * @param commentLines the indent token comment booleans
   * @param lineIndex the zero-based line index
   * @return <code>true</code> if the document was modified, <code>false</code> if not
   * @throws BadLocationException if the document got changed concurrently
   */
  private static boolean indentLine(
      IDocument document,
      int line,
      JavaIndenter indenter,
      JavaHeuristicScanner scanner,
      boolean[] commentLines,
      int lineIndex,
      int tabSize)
      throws BadLocationException {
    IRegion currentLine = document.getLineInformation(line);
    final int offset = currentLine.getOffset();
    int wsStart =
        offset; // where we start searching for non-WS; after the "//" in single line comments

    String indent = null;
    if (offset < document.getLength()) {
      ITypedRegion partition =
          TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, offset, true);
      ITypedRegion startingPartition =
          TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, offset, false);
      String type = partition.getType();
      if (type.equals(IJavaPartitions.JAVA_DOC)
          || type.equals(IJavaPartitions.JAVA_MULTI_LINE_COMMENT)) {
        indent = computeJavadocIndent(document, line, scanner, startingPartition);
      } else if (!commentLines[lineIndex]
          && startingPartition.getOffset() == offset
          && startingPartition.getType().equals(IJavaPartitions.JAVA_SINGLE_LINE_COMMENT)) {
        return false;
      }
    }

    // standard java indentation
    if (indent == null) {
      StringBuffer computed = indenter.computeIndentation(offset);
      if (computed != null) indent = computed.toString();
      else indent = new String();
    }

    // change document:
    // get current white space
    int lineLength = currentLine.getLength();
    int end = scanner.findNonWhitespaceForwardInAnyPartition(wsStart, offset + lineLength);
    if (end == JavaHeuristicScanner.NOT_FOUND) end = offset + lineLength;
    int length = end - offset;
    String currentIndent = document.get(offset, length);

    // memorize the fact that a line is a single line comment (but not at column 0) and should be
    // treated like code
    // as opposed to commented out code, which should keep its slashes at column 0
    if (length > 0) {
      ITypedRegion partition =
          TextUtilities.getPartition(document, IJavaPartitions.JAVA_PARTITIONING, end, false);
      if (partition.getOffset() == end
          && IJavaPartitions.JAVA_SINGLE_LINE_COMMENT.equals(partition.getType())) {
        commentLines[lineIndex] = true;
      }
    }

    // only change the document if it is a real change
    if (!indent.equals(currentIndent)) {
      document.replace(offset, length, indent);
      return true;
    }

    return false;
  }