/**
   * Finds a closing parenthesis to the left of <code>position</code> in document, where that
   * parenthesis is only separated by whitespace from <code>position</code>. If no such parenthesis
   * can be found, <code>position</code> is returned.
   *
   * @param scanner the java heuristic scanner set up on the document
   * @param position the first character position in <code>document</code> to be considered
   * @return the position of a closing parenthesis left to <code>position</code> separated only by
   *     whitespace, or <code>position</code> if no parenthesis can be found
   */
  private static int findClosingParenToLeft(JavaHeuristicScanner scanner, int position) {
    if (position < 1) return position;

    if (scanner.previousToken(position - 1, JavaHeuristicScanner.UNBOUND) == Symbols.TokenRPAREN)
      return scanner.getPosition() + 1;
    return position;
  }
  /**
   * Skips the scope opened by <code>token</code>.
   *
   * @param scanner the scanner
   * @param start the start position
   * @param token the token
   * @return the position after the scope or <code>JavaHeuristicScanner.NOT_FOUND</code>
   */
  private static int skipScope(JavaHeuristicScanner scanner, int start, int token) {
    int openToken = token;
    int closeToken;
    switch (token) {
      case Symbols.TokenLPAREN:
        closeToken = Symbols.TokenRPAREN;
        break;
      case Symbols.TokenLBRACKET:
        closeToken = Symbols.TokenRBRACKET;
        break;
      case Symbols.TokenLBRACE:
        closeToken = Symbols.TokenRBRACE;
        break;
      default:
        Assert.isTrue(false);
        return -1; // dummy
    }

    int depth = 1;
    int p = start;

    while (true) {
      int tok = scanner.nextToken(p, JavaHeuristicScanner.UNBOUND);
      p = scanner.getPosition();

      if (tok == openToken) {
        depth++;
      } else if (tok == closeToken) {
        depth--;
        if (depth == 0) return p + 1;
      } else if (tok == Symbols.TokenEOF) {
        return JavaHeuristicScanner.NOT_FOUND;
      }
    }
  }
  /**
   * Returns the block balance, i.e. zero if the blocks are balanced at <code>offset</code>, a
   * negative number if there are more closing than opening braces, and a positive number if there
   * are more opening than closing braces.
   *
   * @param document the document
   * @param offset the offset
   * @param partitioning the partitioning
   * @return the block balance
   */
  private static int getBlockBalance(IDocument document, int offset, String partitioning) {
    if (offset < 1) return -1;
    if (offset >= document.getLength()) return 1;

    int begin = offset;
    int end = offset - 1;

    JavaHeuristicScanner scanner = new JavaHeuristicScanner(document);

    while (true) {
      begin = scanner.findOpeningPeer(begin - 1, '{', '}');
      end = scanner.findClosingPeer(end + 1, '{', '}');
      if (begin == -1 && end == -1) return 0;
      if (begin == -1) return -1;
      if (end == -1) return 1;
    }
  }
  /**
   * Computes an insert position for an opening brace if <code>offset</code> maps to a position in
   * <code>document</code> with a expression in parenthesis that will take a block after the closing
   * parenthesis.
   *
   * @param document the document being modified
   * @param offset the offset of the caret position, relative to the line start.
   * @param partitioning the document partitioning
   * @param max the max position
   * @return an insert position relative to the line start if <code>line</code> contains a
   *     parenthesized expression that can be followed by a block, -1 otherwise
   */
  private static int computeAnonymousPosition(
      IDocument document, int offset, String partitioning, int max) {
    // find the opening parenthesis for every closing parenthesis on the current line after offset
    // return the position behind the closing parenthesis if it looks like a method declaration
    // or an expression for an if, while, for, catch statement

    JavaHeuristicScanner scanner = new JavaHeuristicScanner(document);
    int pos = offset;
    int length = max;
    int scanTo = scanner.scanForward(pos, length, '}');
    if (scanTo == -1) scanTo = length;

    int closingParen = findClosingParenToLeft(scanner, pos) - 1;
    boolean hasNewToken = looksLikeAnonymousClassDef(document, partitioning, scanner, pos);
    int openingParen = -1;
    while (true) {
      int startScan = closingParen + 1;
      closingParen = scanner.scanForward(startScan, scanTo, ')');
      if (closingParen == -1) {
        if (hasNewToken && openingParen != -1) return openingParen + 1;
        break;
      }

      openingParen = scanner.findOpeningPeer(closingParen - 1, '(', ')');

      // no way an expression at the beginning of the document can mean anything
      if (openingParen < 1) break;

      // only select insert positions for parenthesis currently embracing the caret
      if (openingParen > pos) continue;

      if (looksLikeAnonymousClassDef(document, partitioning, scanner, openingParen - 1))
        return closingParen + 1;
    }

    return -1;
  }
  private void smartIndentAfterOpeningBracket(IDocument d, DocumentCommand c) {
    if (c.offset < 1 || d.getLength() == 0) return;

    JavaHeuristicScanner scanner = new JavaHeuristicScanner(d);

    int p = (c.offset == d.getLength() ? c.offset - 1 : c.offset);

    try {
      // current line
      int line = d.getLineOfOffset(p);
      int lineOffset = d.getLineOffset(line);

      // make sure we don't have any leading comments etc.
      if (d.get(lineOffset, p - lineOffset).trim().length() != 0) return;

      // line of last Java code
      int pos = scanner.findNonWhitespaceBackward(p, JavaHeuristicScanner.UNBOUND);
      if (pos == -1) return;
      int lastLine = d.getLineOfOffset(pos);

      // only shift if the last java line is further up and is a braceless block candidate
      if (lastLine < line) {

        JavaIndenter indenter = new JavaIndenter(d, scanner, fProject);
        StringBuffer indent = indenter.computeIndentation(p, true);
        String toDelete = d.get(lineOffset, c.offset - lineOffset);
        if (indent != null && !indent.toString().equals(toDelete)) {
          c.text = indent.append(c.text).toString();
          c.length += c.offset - lineOffset;
          c.offset = lineOffset;
        }
      }

    } catch (BadLocationException e) {
      JavaPlugin.log(e);
    }
  }
  private static CompilationUnitInfo getCompilationUnitForMethod(IDocument document, int offset) {
    try {
      JavaHeuristicScanner scanner = new JavaHeuristicScanner(document);

      IRegion sourceRange = scanner.findSurroundingBlock(offset);
      if (sourceRange == null) return null;
      String source = document.get(sourceRange.getOffset(), sourceRange.getLength());

      StringBuffer contents = new StringBuffer();
      contents.append("class ____C{void ____m()"); // $NON-NLS-1$
      final int methodOffset = contents.length();
      contents.append(source);
      contents.append('}');

      char[] buffer = contents.toString().toCharArray();

      return new CompilationUnitInfo(buffer, sourceRange.getOffset() - methodOffset);

    } catch (BadLocationException e) {
      JavaPlugin.log(e);
    }

    return null;
  }
  /**
   * Checks whether the content of <code>document</code> at <code>position</code> looks like an
   * anonymous class definition. <code>position</code> must be to the left of the opening
   * parenthesis of the definition's parameter list.
   *
   * @param document the document being modified
   * @param partitioning the document partitioning
   * @param scanner the scanner
   * @param position the first character position in <code>document</code> to be considered
   * @return <code>true</code> if the content of <code>document</code> looks like an anonymous class
   *     definition, <code>false</code> otherwise
   */
  private static boolean looksLikeAnonymousClassDef(
      IDocument document, String partitioning, JavaHeuristicScanner scanner, int position) {
    int previousCommaParenEqual =
        scanner.scanBackward(
            position - 1, JavaHeuristicScanner.UNBOUND, new char[] {',', '(', '='});
    if (previousCommaParenEqual == -1
        || position < previousCommaParenEqual + 5) // 2 for borders, 3 for "new"
    return false;

    if (isNewMatch(
        document,
        previousCommaParenEqual + 1,
        position - previousCommaParenEqual - 2,
        partitioning)) return true;

    return false;
  }
  private int getPeerPosition(IDocument document, DocumentCommand command) {
    if (document.getLength() == 0) return 0;
    /*
     * Search for scope closers in the pasted text and find their opening peers
     * in the document.
     */
    Document pasted = new Document(command.text);
    installJavaStuff(pasted);
    int firstPeer = command.offset;

    JavaHeuristicScanner pScanner = new JavaHeuristicScanner(pasted);
    JavaHeuristicScanner dScanner = new JavaHeuristicScanner(document);

    // add scope relevant after context to peer search
    int afterToken =
        dScanner.nextToken(command.offset + command.length, JavaHeuristicScanner.UNBOUND);
    try {
      switch (afterToken) {
        case Symbols.TokenRBRACE:
          pasted.replace(pasted.getLength(), 0, "}"); // $NON-NLS-1$
          break;
        case Symbols.TokenRPAREN:
          pasted.replace(pasted.getLength(), 0, ")"); // $NON-NLS-1$
          break;
        case Symbols.TokenRBRACKET:
          pasted.replace(pasted.getLength(), 0, "]"); // $NON-NLS-1$
          break;
      }
    } catch (BadLocationException e) {
      // cannot happen
      Assert.isTrue(false);
    }

    int pPos = 0; // paste text position (increasing from 0)
    int dPos = Math.max(0, command.offset - 1); // document position (decreasing from paste offset)
    while (true) {
      int token = pScanner.nextToken(pPos, JavaHeuristicScanner.UNBOUND);
      pPos = pScanner.getPosition();
      switch (token) {
        case Symbols.TokenLBRACE:
        case Symbols.TokenLBRACKET:
        case Symbols.TokenLPAREN:
          pPos = skipScope(pScanner, pPos, token);
          if (pPos == JavaHeuristicScanner.NOT_FOUND) return firstPeer;
          break; // closed scope -> keep searching
        case Symbols.TokenRBRACE:
          int peer = dScanner.findOpeningPeer(dPos, '{', '}');
          dPos = peer - 1;
          if (peer == JavaHeuristicScanner.NOT_FOUND) return firstPeer;
          firstPeer = peer;
          break; // keep searching
        case Symbols.TokenRBRACKET:
          peer = dScanner.findOpeningPeer(dPos, '[', ']');
          dPos = peer - 1;
          if (peer == JavaHeuristicScanner.NOT_FOUND) return firstPeer;
          firstPeer = peer;
          break; // keep searching
        case Symbols.TokenRPAREN:
          peer = dScanner.findOpeningPeer(dPos, '(', ')');
          dPos = peer - 1;
          if (peer == JavaHeuristicScanner.NOT_FOUND) return firstPeer;
          firstPeer = peer;
          break; // keep searching
        case Symbols.TokenCASE:
        case Symbols.TokenDEFAULT:
          JavaIndenter indenter = new JavaIndenter(document, dScanner, fProject);
          peer = indenter.findReferencePosition(dPos, false, false, false, true);
          if (peer == JavaHeuristicScanner.NOT_FOUND) return firstPeer;
          firstPeer = peer;
          break; // keep searching

        case Symbols.TokenEOF:
          return firstPeer;
        default:
          // keep searching
      }
    }
  }
  private void smartIndentAfterNewLine(IDocument d, DocumentCommand c) {
    JavaHeuristicScanner scanner = new JavaHeuristicScanner(d);
    JavaIndenter indenter = new JavaIndenter(d, scanner, fProject);
    StringBuffer indent = indenter.computeIndentation(c.offset);
    if (indent == null) indent = new StringBuffer();

    int docLength = d.getLength();
    if (c.offset == -1 || docLength == 0) return;

    try {
      int p = (c.offset == docLength ? c.offset - 1 : c.offset);
      int line = d.getLineOfOffset(p);

      StringBuffer buf = new StringBuffer(c.text + indent);

      IRegion reg = d.getLineInformation(line);
      int lineEnd = reg.getOffset() + reg.getLength();

      int contentStart = findEndOfWhiteSpace(d, c.offset, lineEnd);
      c.length = Math.max(contentStart - c.offset, 0);

      int start = reg.getOffset();
      ITypedRegion region = TextUtilities.getPartition(d, fPartitioning, start, true);
      if (IJavaPartitions.JAVA_DOC.equals(region.getType()))
        start = d.getLineInformationOfOffset(region.getOffset()).getOffset();

      // insert closing brace on new line after an unclosed opening brace
      if (getBracketCount(d, start, c.offset, true) > 0
          && closeBrace()
          && !isClosed(d, c.offset, c.length)) {
        c.caretOffset = c.offset + buf.length();
        c.shiftsCaret = false;

        // copy old content of line behind insertion point to new line
        // unless we think we are inserting an anonymous type definition

        if (c.offset == 0
            || computeAnonymousPosition(d, c.offset - 1, fPartitioning, lineEnd) == -1) {
          if (lineEnd - contentStart > 0) {
            c.length = lineEnd - c.offset;
            buf.append(d.get(contentStart, lineEnd - contentStart).toCharArray());
          }
        }

        buf.append(TextUtilities.getDefaultLineDelimiter(d));
        StringBuffer reference = null;
        int nonWS = findEndOfWhiteSpace(d, start, lineEnd);
        if (nonWS < c.offset && d.getChar(nonWS) == '{')
          reference = new StringBuffer(d.get(start, nonWS - start));
        else reference = indenter.getReferenceIndentation(c.offset);
        if (reference != null) buf.append(reference);
        buf.append('}');
      }
      // insert extra line upon new line between two braces
      else if (c.offset > start && contentStart < lineEnd && d.getChar(contentStart) == '}') {
        int firstCharPos = scanner.findNonWhitespaceBackward(c.offset - 1, start);
        if (firstCharPos != JavaHeuristicScanner.NOT_FOUND && d.getChar(firstCharPos) == '{') {
          c.caretOffset = c.offset + buf.length();
          c.shiftsCaret = false;

          StringBuffer reference = null;
          int nonWS = findEndOfWhiteSpace(d, start, lineEnd);
          if (nonWS < c.offset && d.getChar(nonWS) == '{')
            reference = new StringBuffer(d.get(start, nonWS - start));
          else reference = indenter.getReferenceIndentation(c.offset);

          buf.append(TextUtilities.getDefaultLineDelimiter(d));

          if (reference != null) buf.append(reference);
        }
      }
      c.text = buf.toString();

    } catch (BadLocationException e) {
      JavaPlugin.log(e);
    }
  }
  private void smartIndentUponE(IDocument d, DocumentCommand c) {
    if (c.offset < 4 || d.getLength() == 0) return;

    try {
      String content = d.get(c.offset - 3, 3);
      if (content.equals("els")) { // $NON-NLS-1$
        JavaHeuristicScanner scanner = new JavaHeuristicScanner(d);
        int p = c.offset - 3;

        // current line
        int line = d.getLineOfOffset(p);
        int lineOffset = d.getLineOffset(line);

        // make sure we don't have any leading comments etc.
        if (d.get(lineOffset, p - lineOffset).trim().length() != 0) return;

        // line of last Java code
        int pos = scanner.findNonWhitespaceBackward(p - 1, JavaHeuristicScanner.UNBOUND);
        if (pos == -1) return;
        int lastLine = d.getLineOfOffset(pos);

        // only shift if the last java line is further up and is a braceless block candidate
        if (lastLine < line) {

          JavaIndenter indenter = new JavaIndenter(d, scanner, fProject);
          int ref = indenter.findReferencePosition(p, true, false, false, false);
          if (ref == JavaHeuristicScanner.NOT_FOUND) return;
          int refLine = d.getLineOfOffset(ref);
          String indent = getIndentOfLine(d, refLine);

          if (indent != null) {
            c.text = indent.toString() + "else"; // $NON-NLS-1$
            c.length += c.offset - lineOffset;
            c.offset = lineOffset;
          }
        }

        return;
      }

      if (content.equals("cas")) { // $NON-NLS-1$
        JavaHeuristicScanner scanner = new JavaHeuristicScanner(d);
        int p = c.offset - 3;

        // current line
        int line = d.getLineOfOffset(p);
        int lineOffset = d.getLineOffset(line);

        // make sure we don't have any leading comments etc.
        if (d.get(lineOffset, p - lineOffset).trim().length() != 0) return;

        // line of last Java code
        int pos = scanner.findNonWhitespaceBackward(p - 1, JavaHeuristicScanner.UNBOUND);
        if (pos == -1) return;
        int lastLine = d.getLineOfOffset(pos);

        // only shift if the last java line is further up and is a braceless block candidate
        if (lastLine < line) {

          JavaIndenter indenter = new JavaIndenter(d, scanner, fProject);
          int ref = indenter.findReferencePosition(p, false, false, false, true);
          if (ref == JavaHeuristicScanner.NOT_FOUND) return;
          int refLine = d.getLineOfOffset(ref);
          int nextToken = scanner.nextToken(ref, JavaHeuristicScanner.UNBOUND);
          String indent;
          if (nextToken == Symbols.TokenCASE || nextToken == Symbols.TokenDEFAULT)
            indent = getIndentOfLine(d, refLine);
          else // at the brace of the switch
          indent = indenter.computeIndentation(p).toString();

          if (indent != null) {
            c.text = indent.toString() + "case"; // $NON-NLS-1$
            c.length += c.offset - lineOffset;
            c.offset = lineOffset;
          }
        }

        return;
      }

    } catch (BadLocationException e) {
      JavaPlugin.log(e);
    }
  }