/*
 * jNPad v0.3 - jNPad's an Simple Text Editor written in Java
 *
 * Copyright (C) 2014-2017  rgs
 *
 * Require JDK 1.6 (or later)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 *
 * Info, Questions, Suggestions & Bugs Report to rgsevero@gmail.com
 */

package jnpad.text;

import static jnpad.util.Utilities.EMPTY_STRING;
import static jnpad.util.Utilities.LF_STRING;
import static jnpad.util.Utilities.TAB_STRING;

import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;
import javax.swing.text.StyledDocument;

import jnpad.GUIUtilities;
import jnpad.config.Config;
import jnpad.util.LinePosition;
import jnpad.util.Utilities;

/**
 * The Class TextUtilities.
 *
 * @version 0.3
 * @since   jNPad v0.1
 */
public final class TextUtilities {
  /** Logger */
  private static final Logger LOGGER = Logger.getLogger(TextUtilities.class.getName());

  /** no instances */
  private TextUtilities() {
    super();
  }

  /**
   * Indent lines.
   *
   * @param doc the doc
   * @param fromPos the from pos
   * @param toPos the to pos
   * @param tabSize the tab size
   */
  public static void indentLines(Document doc, int fromPos, int toPos, int tabSize) {
    int lineStart = getLineNumber(doc, fromPos);
    int lineEnd = getLineNumber(doc, toPos);

    for (int line = lineStart; line <= lineEnd; line++) {
      try {
        Element lineElem = doc.getDefaultRootElement().getElement(line);
        doc.insertString(lineElem.getStartOffset(), Utilities.spaces(tabSize), lineElem.getAttributes());
      }
      catch (Exception ex) {
        LOGGER.log(Level.WARNING, ex.getMessage(), ex);
      }
    }
  }

  /**
   * Indent lines.
   *
   * @param doc the doc
   * @param fromPos the from pos
   * @param toPos the to pos
   */
  public static void indentLines(Document doc, int fromPos, int toPos) {
    int lineStart = getLineNumber(doc, fromPos);
    int lineEnd = getLineNumber(doc, toPos);

    for (int line = lineStart; line <= lineEnd; line++) {
      try {
        Element lineElem = doc.getDefaultRootElement().getElement(line);
        doc.insertString(lineElem.getStartOffset(), TAB_STRING, lineElem.getAttributes());
      }
      catch (Exception ex) {
        LOGGER.log(Level.WARNING, ex.getMessage(), ex);
      }
    }
  }

  /**
   * Unindent lines.
   *
   * @param doc the doc
   * @param fromPos the from pos
   * @param toPos the to pos
   */
  public static void unindentLines(Document doc, int fromPos, int toPos) {
    int lineStart = getLineNumber(doc, fromPos);
    int lineEnd = getLineNumber(doc, toPos);

    for (int line = lineStart; line <= lineEnd; line++) {
      try {
        Element lineElem = doc.getDefaultRootElement().getElement(line);
        char ci = doc.getText(lineElem.getStartOffset(), 1).charAt(0);
        if (Character.isWhitespace(ci) && ci != Utilities.LF) {
          doc.remove(lineElem.getStartOffset(), 1);
        }
      }
      catch (Exception ex) {
        LOGGER.log(Level.WARNING, ex.getMessage(), ex);
      }
    }
  }

  /**
   * Unindent lines.
   *
   * @param doc the doc
   * @param fromPos the from pos
   * @param toPos the to pos
   * @param tabSize the tab size
   */
  public static void unindentLines(Document doc, int fromPos, int toPos, int tabSize) {
    int lineStart = getLineNumber(doc, fromPos);
    int lineEnd = getLineNumber(doc, toPos);

    for (int line = lineStart; line <= lineEnd; line++) {
      try {
        Element lineElem = doc.getDefaultRootElement().getElement(line);

        int lineElemStart = lineElem.getStartOffset();
        int lineElemEnd = lineElem.getEndOffset();
        String lineText = doc.getText(lineElemStart, lineElemEnd - lineElemStart);
        String spacedTab = Utilities.spaces(tabSize);
        char ci = doc.getText(lineElemStart, 1).charAt(0);

        if (lineText.startsWith(spacedTab)) {
          doc.remove(lineElemStart, tabSize); // remove spaced tab
        }
        else if (Character.isWhitespace(ci) && ci != Utilities.LF) {
          doc.remove(lineElemStart, 1);
        }
      }
      catch (Exception ex) {
        LOGGER.log(Level.WARNING, ex.getMessage(), ex);
      }
    }
  }

  /**
   * Gets the line number.
   *
   * @param doc the doc
   * @param pos the pos
   * @return the line number
   */
  public static int getLineNumber(Document doc, int pos) {
    Element map = doc.getDefaultRootElement();
    return map.getElementIndex(pos);
  }

  /**
   * Gets the line.
   *
   * @param doc the doc
   * @param lineNo the line no
   * @return the line
   */
  public static Element getLine(Document doc, int lineNo) {
    return doc.getDefaultRootElement().getElement(lineNo);
  }

  /**
   * Gets the current line index.
   *
   * @param textComponent the text component
   * @return the current line index
   */
  public static int getCurrentLineIndex(JTextComponent textComponent) {
    Document document = textComponent.getDocument();
    return document.getDefaultRootElement().getElementIndex(textComponent.getCaretPosition());
  }

  /**
   * Gets the number of lines.
   *
   * @param doc the doc
   * @return the number of lines
   */
  public static int getNumberOfLines(Document doc) {
    return doc.getDefaultRootElement().getElementCount();
  }

  /**
   * Gets the line column numbers.
   *
   * @param doc the doc
   * @param pos the pos
   * @return the line column numbers
   */
  public static int[] getLineColumnNumbers(Document doc, int pos) {
    Element map = doc.getDefaultRootElement();
    int line = map.getElementIndex(pos);
    Element lineElem = map.getElement(line);
    return new int[] {line, pos - lineElem.getStartOffset()};
  }

  /**
   * Gets the line position.
   *
   * @param textArea the text area
   * @param pos the pos
   * @return the line position
   */
  public static LinePosition getLinePosition(JNPadTextArea textArea, int pos) {
    Document doc = textArea.getDocument();
    Element map = doc.getDefaultRootElement();
    int lines = map.getElementCount();
    int line = map.getElementIndex(pos);
    Element lineElem = map.getElement(line);
    int ch = pos - lineElem.getStartOffset();
    int chs = lineElem.getEndOffset() - lineElem.getStartOffset();
    int col = ch;
    try {
      String text = doc.getText(lineElem.getStartOffset(), ch);
      if (Utilities.countMatches(text, TAB_STRING) > 0 &&
          GUIUtilities.isMonoSpaceFont(textArea.getFont())) {
        col = getColumnAtCaret(textArea);
      }
    }
    catch (Exception ex) {
      // no deberķa pasar
    }
    return new LinePosition(line + 1, col + 1, ch + 1, lines, chs);
  }

  /**
   *  Return the column number at the Caret position.
   *
   *  The column returned will only make sense when using a
   *  Monospaced font.
   *
   * @param textComponent JTextComponent
   * @return int
   */
  public static int getColumnAtCaret(JTextComponent textComponent) {
    // Since we assume a monospaced font we can use the width of a single
    // character to represent the width of each character
    FontMetrics fm = textComponent.getFontMetrics(textComponent.getFont());
    int characterWidth = fm.stringWidth("0"); //$NON-NLS-1$
    int column = 0;

    try {
      Rectangle r = textComponent.modelToView(textComponent.getCaretPosition());
      int width = r.x - textComponent.getInsets().left;
      column = width / characterWidth;
    }
    catch (BadLocationException ex) {
      //ignored
    }

    return column/* + 1*/;
  }

  /**
   * Gets the line start offset for pos.
   *
   * @param doc the doc
   * @param pos the pos
   * @return the line start offset for pos
   */
  public static int getLineStartOffsetForPos(PlainDocument doc, int pos) {
    // a document is modelled as a list of lines (Element)=> index = line number
    Element line = doc.getParagraphElement(pos);
    return line.getStartOffset();
  }
  
  /**
   * Gets the line start offset for pos.
   *
   * @param doc the doc
   * @param pos the pos
   * @return the line start offset for pos
   */
  public static int getLineStartOffsetForPos(StyledDocument doc, int pos) {
    // a document is modelled as a list of lines (Element)=> index = line number
    Element line = doc.getParagraphElement(pos);
    return line.getStartOffset();
  }

  /**
   * Delete line at pos.
   *
   * @param doc the doc
   * @param pos the pos
   * @throws Exception the exception
   */
  public static void deleteLineAtPos(PlainDocument doc, int pos) throws Exception {
    Element lineElem = doc.getParagraphElement(pos);
    doc.remove(lineElem.getStartOffset(), lineElem.getEndOffset() - lineElem.getStartOffset());
  }
  
  /**
   * Delete line at pos.
   *
   * @param doc the doc
   * @param pos the pos
   * @throws Exception the exception
   */
  public static void deleteLineAtPos(StyledDocument doc, int pos) throws Exception {
    Element lineElem = doc.getParagraphElement(pos);
    doc.remove(lineElem.getStartOffset(), lineElem.getEndOffset() - lineElem.getStartOffset());
  }

  /**
   * Delete line after pos.
   *
   * @param doc the doc
   * @param pos the pos
   * @throws Exception the exception
   */
  public static void deleteLineAfterPos(PlainDocument doc, int pos) throws Exception {
    Element lineElem = doc.getParagraphElement(pos);
    int len = lineElem.getEndOffset() - pos - 1; // subtle
    if (len > 0) {
      doc.remove(pos, len);
    }
  }
  
  /**
   * Delete line after pos.
   *
   * @param doc the doc
   * @param pos the pos
   * @throws Exception the exception
   */
  public static void deleteLineAfterPos(StyledDocument doc, int pos) throws Exception {
    Element lineElem = doc.getParagraphElement(pos);
    int len = lineElem.getEndOffset() - pos - 1; // subtle
    if (len > 0) {
      doc.remove(pos, len);
    }
  }

  /**
   * Gets the text of line at position.
   *
   * @param doc the doc
   * @param pos the pos
   * @return the text of line at position
   */
  public static String getTextOfLineAtPosition(PlainDocument doc, int pos) {
    // a document is modelled as a list of lines (Element)=> index = line number
    Element lineElem = doc.getParagraphElement(pos);
    try {
      return doc.getText(lineElem.getStartOffset(), lineElem.getEndOffset() - lineElem.getStartOffset());
    }
    catch (Exception e) {
      return null;
    }
  }
  
  /**
   * Gets the text of line at position.
   *
   * @param doc the doc
   * @param pos the pos
   * @return the text of line at position
   */
  public static String getTextOfLineAtPosition(StyledDocument doc, int pos) {
    // a document is modelled as a list of lines (Element)=> index = line number
    Element lineElem = doc.getParagraphElement(pos);
    try {
      return doc.getText(lineElem.getStartOffset(), lineElem.getEndOffset() - lineElem.getStartOffset());
    }
    catch (Exception e) {
      return null;
    }
  }

  /**
   * Gets the text of line at position_only up to pos.
   *
   * @param doc the doc
   * @param pos the pos
   * @return the text of line at position_only up to pos
   */
  public static String getTextOfLineAtPosition_onlyUpToPos(PlainDocument doc, int pos) {
    // a document is modelled as a list of lines (Element)=> index = line number
    Element line = doc.getParagraphElement(pos);
    try {
      return doc.getText(line.getStartOffset(), pos - line.getStartOffset());
    }
    catch (Exception e) {
      return null;
    }
  }

  /**
   * Gets the text of line at position_only up to pos.
   *
   * @param doc the doc
   * @param pos the pos
   * @return the text of line at position_only up to pos
   */
  public static String getTextOfLineAtPosition_onlyUpToPos(StyledDocument doc, int pos) {
    // a document is modelled as a list of lines (Element)=> index = line number
    Element line = doc.getParagraphElement(pos);
    try {
      return doc.getText(line.getStartOffset(), pos - line.getStartOffset());
    }
    catch (Exception e) {
      return null;
    }
  }

  /**
   * Gets the text of line.
   *
   * @param doc the doc
   * @param line the line
   * @return the text of line
   */
  public static String getTextOfLine(Document doc, int line) {
    Element map = doc.getDefaultRootElement();
    Element lineElem = map.getElement(line);
    try {
      return doc.getText(lineElem.getStartOffset(), lineElem.getEndOffset() - lineElem.getStartOffset());
    }
    catch (Exception e) {
      return null;
    }
  }

  /**
   * Gets the text.
   *
   * @param doc the doc
   * @return the text
   */
  public static String getText(Document doc) {
    try {
      return doc.getText(0, doc.getLength());
    }
    catch (Exception ex) {
      LOGGER.log(Level.FINE, ex.getMessage(), ex);
    }
    return EMPTY_STRING;
  }

  /**
   * Gets the text.
   *
   * @param doc the doc
   * @param offs the offs
   * @param len the len
   * @return the text
   */
  public static String getText(Document doc, int offs, int len) {
    try {
      return doc.getText(offs, len);
    }
    catch (Exception ex) {
      LOGGER.log(Level.FINE, ex.getMessage(), ex);
    }
    return EMPTY_STRING;
  }
  
  /**
   * Gets the text from to.
   *
   * @param doc the doc
   * @param start the start
   * @param end the end
   * @return the text from to
   */
  public static String getTextFromTo(Document doc, int start, int end) {
    if (start == -1) {
      return EMPTY_STRING;
    }
    if (end <= start) {
      return EMPTY_STRING;
    }
    try {
      return doc.getText(start, end - start);
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
    return EMPTY_STRING;
  }

  /**
   * Gets the char at.
   *
   * @param doc the doc
   * @param pos the pos
   * @return the char at
   */
  public static char getCharAt(Document doc, int pos) {
    try {
      return doc.getText(pos, 1).charAt(0);
    }
    catch (Exception ex) {
      LOGGER.warning("Cannot read char at " + pos + " in doc: " + ex.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$
      return 0;
    }
  }

  /**
   * Gets the doc position for.
   *
   * @param doc the doc
   * @param line the line
   * @param column the column
   * @return the doc position for
   */
  public static int getDocPositionFor(Document doc, int line, int column) {
    if (line < 0) {
      return -1;
    }
    Element map = doc.getDefaultRootElement();
    Element lineElem = map.getElement(line - 1);
    if (lineElem == null) {
      return -1;
    }
    int pos = lineElem.getStartOffset() + (column > 0 ? column : 0);
    if (pos < 0) {
      return 0;
    }
    if (pos > doc.getLength()) {
      return doc.getLength();
    }
    return pos;
  }

  /**
   * Have selection.
   *
   * @param textComponent the text component
   * @return true, if successful
   */
  public static boolean haveSelection(JTextComponent textComponent) {
    Caret caret = textComponent.getCaret();
    return caret.getMark() != caret.getDot();
  }

  /**
   * Do tabs to spaces.
   *
   * @param in the in
   * @param tabSize the tab size
   * @return the string
   */
  public static String doTabsToSpaces(String in, int tabSize) {
    StringBuilder buf = new StringBuilder();
    int width = 0;
    for (int i = 0; i < in.length(); i++) {
      switch (in.charAt(i)) {
        case Utilities.TAB:
          int count = tabSize - (width % tabSize);
          width += count;
          while (--count >= 0) {
            buf.append(Utilities.SPACE);
          }
          break;
        case Utilities.LF:
          width = 0;
          buf.append(in.charAt(i));
          break;
        default:
          width++;
          buf.append(in.charAt(i));
          break;
      }
    }
    return buf.toString();
  }

  /**
   * Do spaces to tabs.
   *
   * @param in the string
   * @param tabSize the tab size
   * @return the string
   */
  public static String doSpacesToTabs(String in, int tabSize) {
    StringBuilder buf = new StringBuilder();
    for (int i = 0, width = 0, whitespace = 0; i < in.length(); i++) {
      switch (in.charAt(i)) {
        case Utilities.SPACE:
          whitespace++;
          width++;
          break;
        case Utilities.TAB:
          int tab = tabSize - (width % tabSize);
          width += tab;
          whitespace += tab;
          break;
        case Utilities.LF:
          whitespace = 0;
          width = 0;
          buf.append(Utilities.LF);
          break;
        default:
          if (whitespace != 0) {
            if (whitespace >= tabSize / 2 && whitespace > 1) {
              int indent = whitespace + ( (width - whitespace) % tabSize);
              int tabs = indent / tabSize;
              int spaces = indent % tabSize;
              while (tabs-- > 0) {
                buf.append(Utilities.TAB);
              } while (spaces-- > 0) {
                buf.append(Utilities.SPACE);
              }
            }
            else {
              while (whitespace-- > 0) {
                buf.append(Utilities.SPACE);
              }
            }
            whitespace = 0;
          }
          buf.append(in.charAt(i));
          width++;
          break;
      }
    }
    return buf.toString();
  }

  /**
   * Sort.
   *
   * @param doc the doc
   * @param reverse the reverse
   */
  public static void sort(Document doc, boolean reverse) {
    sort(doc, 0, doc.getLength(), reverse);
  }

  /**
   * Sort.
   *
   * @param doc the doc
   * @param offset the offset
   * @param length the length
   * @param reverse the reverse
   */
  public static void sort(Document doc, int offset, int length, boolean reverse) {
    if (doc == null) {
      return;
    }

    Element map = doc.getDefaultRootElement();
    Element lineElem;
    int fromIndex = map.getElementIndex(offset);
    int toIndex = map.getElementIndex(offset + length);
    String[] lines = new String[toIndex - fromIndex + 1];

    try {
      for (int i = 0; i < lines.length; i++) {
        lineElem = map.getElement(fromIndex + i);
        lines[i] = doc.getText(lineElem.getStartOffset(),
                               lineElem.getEndOffset() - lineElem.getStartOffset());
        if (lines[i].endsWith(LF_STRING)) {
          lines[i] = lines[i].substring(0, lines[i].length() - 1);
        }
      }
      Arrays.sort(lines);

      StringBuilder buf = new StringBuilder();
      if (reverse) {
        for (int i = lines.length - 1; i > 0; i--) {
          buf.append(lines[i].concat(LF_STRING));
        }
        buf.append(lines[0]);
      }
      else {
        for (int i = 0; i < lines.length - 1; i++) {
          buf.append(lines[i].concat(LF_STRING));
        }
        buf.append(lines[lines.length - 1]);
      }

      int selStart = map.getElement(fromIndex).getStartOffset();
      int selLength = map.getElement(toIndex).getEndOffset() - selStart - 1;

      doc.remove(selStart, selLength);
      doc.insertString(selStart, buf.toString(), null);
    }
    catch (BadLocationException ex) {
      //ignored
    }
  }

  /**
   * Gets the visible doc pos bounds.
   *
   * @param textComponent the text component
   * @param scrollPane the scroll pane
   * @return the visible doc pos bounds
   */
  public static int[] getVisibleDocPosBounds(JTextComponent textComponent, JScrollPane scrollPane) {
    int[] pos = new int[2];
    try {
      Point pt = scrollPane.getViewport().getViewPosition();
      pos[0] = textComponent.viewToModel(pt);
      Dimension dim = scrollPane.getViewport().getExtentSize();

      Point pt2 = new Point(pt.x + dim.width, pt.y + dim.height);
      pos[1] = textComponent.viewToModel(pt2);
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
    return pos;
  }
  
  /**
   * In fact, 1/4 below upper limit is nicer. Scrolls the start of the line to
   * the middle of the screen.
   * 
   * @param textComponent JTextComponent
   * @param pos int
   */
  public static void scrollToMiddle(JTextComponent textComponent, int pos) {
    JViewport vp = null;
    if (textComponent.getParent() instanceof JViewport)
      vp = (JViewport) textComponent.getParent();
    if (vp != null) {
      try {
        Rectangle viewRect = vp.getViewRect();
        Rectangle r = textComponent.modelToView(pos);
        r.width = r.width == 0 ? 1 : r.width; // arreglo
        if (!viewRect.contains(r)) {
          int h = r.y - vp.getHeight() / 4;
          if (h < 0) h = 0;
          vp.setViewPosition(new Point(0, h));
        }
      }
      catch (Exception ex) {
        LOGGER.log(Level.FINE, ex.getMessage(), ex);
      }
    }
  }

  /**
   * Scroll to middle.
   *
   * @param textComponent the text component
   * @param pos the pos
   * @param ratio the ratio
   * @since 0.3
   */
  public static void scrollToMiddle(JTextComponent textComponent, int pos, float ratio) {
    JViewport vp = null;
    if (textComponent.getParent() instanceof JViewport)
      vp = (JViewport) textComponent.getParent();
    if (vp != null) {
      try {
        Rectangle r = textComponent.modelToView(pos);
        int h = r.y - (int) (vp.getHeight() * ratio);
        if (h < 0) h = 0;
        vp.setViewPosition(new Point(0, h));
      }
      catch (Exception ex) {
        LOGGER.log(Level.FINE, ex.getMessage(), ex);
      }
    }
  }
  
  /**
   * Gets the single java word at.
   *
   * @param doc the doc
   * @param pos the pos
   * @return the single java word at
   */
  public static String getSingleJavaWordAt(Document doc, int pos) {
    int[] range = getJavaWordBoundsAt(doc, pos, false);
    if (range == null) {
      return null;
    }
    return getTextFromTo(doc, range[0], range[1]);
  }

  /**
   * Gets the single word at.
   *
   * @param doc the doc
   * @param pos the pos
   * @param delimiters the delimiters
   * @return the single word at
   */
  public static String getSingleWordAt(Document doc, int pos, String delimiters) {
    int[] range = getWordBoundsAt(doc, pos, delimiters);
    if (range == null) {
      return null;
    }
    return getTextFromTo(doc, range[0], range[1]);
  }

  /**
   * Gets the java word bounds at.
   *
   * @param doc Document
   * @param pos int
   * @param acceptDots boolean
   * @return int[] {start, end} position, null if not found.
   */
  public static int[] getJavaWordBoundsAt(Document doc, int pos, boolean acceptDots) {
    int start;
    int end;

    // search for the start, i.e. iterate backward from the given position until failing
    for (start = pos; start >= 0; start--) {
      char c = getCharAt(doc, start);
      if (!Character.isJavaIdentifierPart(c)) {
        if (acceptDots && c == '.') {
          continue;
        }

        if (Character.isJavaIdentifierStart(c)) { // can be part if not start ?
          // take it as part of the ID
          break;
        }
        // dont keep as ID part.
        start++;
        break;
      }
    }

    // search for the end
    for (end = pos; end < doc.getLength(); end++) {
      char c = getCharAt(doc, end);
      if (acceptDots && c == '.') {
        continue;
      }
      if (!Character.isJavaIdentifierPart(c)) {
        break;
      }
    }

    if (start < 0) {
      start = 0;
    }
    if (start >= end) {
      return null; // not found
    }

    return new int[] { start, end };
  }

  /**
   * Gets the word bounds at.
   *
   * @param doc the doc
   * @param pos the pos
   * @param delimiters the delimiters
   * @return the word bounds at
   */
  public static int[] getWordBoundsAt(Document doc, int pos, String delimiters) {
    if (delimiters == null) {
      delimiters = Config.TEXT_DELIMITERS.getValue();
    }

    int start;
    int end;

    // search for the start, i.e. iterate backward from the given position until failing
    for (start = pos; start >= 0; start--) {
      char c = getCharAt(doc, start);
      if ((delimiters.indexOf(c) >= 0) || Character.isWhitespace(c)) {
        // dont keep as ID part.
        start++;
        break;
      }
    }

    // search for the end
    for (end = pos; end < doc.getLength(); end++) {
      char c = getCharAt(doc, end);
      if ((delimiters.indexOf(c) >= 0) || Character.isWhitespace(c)) {
        break;
      }
    }

    if (start < 0) {
      start = 0;
    }
    if (start >= end) {
      return null; // not found
    }

    return new int[] { start, end };
  }

  /**
   * Find word start.
   *
   * @param line the line
   * @param pos the pos
   * @param delimiters the delimiters
   * @return the int
   */
  public static int findWordStart(String line, int pos, String delimiters) {
    if (delimiters == null) {
      delimiters = Config.TEXT_DELIMITERS.getValue();
    }

    char c = line.charAt(pos);

    boolean selectNoLetter = ((delimiters.indexOf(c) >= 0) || Character.isWhitespace(c));

    int wordStart = 0;
    for (int i = pos; i >= 0; i--) {
      c = line.charAt(i);
      if (selectNoLetter ^ ((delimiters.indexOf(c) >= 0) || Character.isWhitespace(c))) {
        wordStart = i + 1;
        break;
      }
    }

    return wordStart;
  }

  /**
   * Find word end.
   *
   * @param line the line
   * @param pos the pos
   * @param delimiters the delimiters
   * @return the int
   */
  public static int findWordEnd(String line, int pos, String delimiters) {
    if (delimiters == null) {
      delimiters = Config.TEXT_DELIMITERS.getValue();
    }

    if (pos != 0) {
      pos--;
    }

    char c = line.charAt(pos);

    boolean selectNoLetter = ((delimiters.indexOf(c) >= 0) || Character.isWhitespace(c));

    int wordEnd = line.length();
    for (int i = pos; i < line.length(); i++) {
      c = line.charAt(i);
      if (selectNoLetter ^ ((delimiters.indexOf(c) >= 0) || Character.isWhitespace(c))) {
        wordEnd = i;
        break;
      }
    }

    return wordEnd;
  }

  /**
   * Locate bracket backward.
   *
   * @param doc the doc
   * @param dot the dot
   * @param openBracket the open bracket
   * @param closeBracket the close bracket
   * @return the int
   * @throws BadLocationException the bad location exception
   */
  public static int locateBracketBackward(Document doc, int dot, char openBracket, char closeBracket) throws BadLocationException {
    int count;
    Element map = doc.getDefaultRootElement();

    // check current line
    int lineNo = map.getElementIndex(dot);
    Element lineElement = map.getElement(lineNo);
    int start = lineElement.getStartOffset();
    int offset = scanBackwardLine(doc.getText(start, dot - start), openBracket, closeBracket, 0);
    count = -offset - 1;
    if (offset >= 0)
      return start + offset;

    // check previous lines
    for (int i = lineNo - 1; i >= 0; i--) {
      lineElement = map.getElement(i);
      start = lineElement.getStartOffset();
      offset = scanBackwardLine(doc.getText(start, lineElement.getEndOffset() - start), openBracket, closeBracket, count);
      count = -offset - 1;
      if (offset >= 0)
        return start + offset;
    }

    // not found
    return -1;
  }
  
  /**
   * Locate bracket forward.
   *
   * @param doc the doc
   * @param dot the dot
   * @param openBracket the open bracket
   * @param closeBracket the close bracket
   * @return the int
   * @throws BadLocationException the bad location exception
   */
  public static int locateBracketForward(Document doc, int dot, char openBracket, char closeBracket) throws BadLocationException {
    int count;
    Element map = doc.getDefaultRootElement();

    // check current line
    int lineNo = map.getElementIndex(dot);
    Element lineElement = map.getElement(lineNo);
    int start = lineElement.getStartOffset();
    int end = lineElement.getEndOffset();
    int offset = scanForwardLine(doc.getText(dot + 1, end - (dot + 1)), openBracket, closeBracket, 0);
    count = -offset - 1;
    if (offset >= 0)
      return dot + offset + 1;

    // check following lines
    for (int i = lineNo + 1; i < map.getElementCount(); i++) {
      lineElement = map.getElement(i);
      start = lineElement.getStartOffset();
      offset = scanForwardLine(doc.getText(start, lineElement.getEndOffset() - start), openBracket, closeBracket, count);
      count = -offset - 1;
      if (offset >= 0)
        return start + offset;
    }

    // not found
    return -1;
  }
  
  
  /**
   * Scan backward line.
   *
   * @param line the line
   * @param openBracket the open bracket
   * @param closeBracket the close bracket
   * @param count the count
   * @return the int
   */
  private static int scanBackwardLine(String line, char openBracket, char closeBracket, int count) {
    for (int i = line.length() - 1; i >= 0; i--) {
      char c = line.charAt(i);
      if (c == closeBracket)
        count++;
      else if (c == openBracket) {
        if (--count < 0)
          return i;
      }
    }
    return -1 - count;
  }
  
  /**
   * Scan forward line.
   *
   * @param line the line
   * @param openBracket the open bracket
   * @param closeBracket the close bracket
   * @param count the count
   * @return the int
   */
  private static int scanForwardLine(String line, char openBracket, char closeBracket, int count) {
    for (int i = 0; i < line.length(); i++) {
      char c = line.charAt(i);
      if (c == openBracket)
        count++;
      else if (c == closeBracket) {
        if (--count < 0)
          return i;
      }
    }
    return -1 - count;
  }
  
}
