/*
 * 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 java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.AffineTransform;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.Timer;
import javax.swing.UIDefaults;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;

import jnpad.GUIUtilities;
import jnpad.JNPadFrame;
import jnpad.JNPadKeyboardHandler;
import jnpad.config.Accelerators;
import jnpad.config.Config;
import jnpad.config.KeyEventWorkaround;
import jnpad.text.syntax.ContentTypes;
import jnpad.ui.plaf.LAFUtils;
import jnpad.util.Utilities;

/**
 * The Class JNPadTextArea.
 *
 * @version 0.3
 * @since   jNPad v0.1
 */
public class JNPadTextArea extends AbstractTextArea {
  /** The Constant PROPERTY_CARET_WIDTH. */
  private static final String    PROPERTY_CARET_WIDTH    = "caretWidth";                                  //$NON-NLS-1$

  private boolean                isRightMarginLineVisible;
  private Color                  rightMarginLineColor;
  private int                    rightMarginLineWidth;
  private boolean                isOverwriteTextMode;
  private Color                  oldCaretColor;
  private Color                  overwriteCaretColor;

  // --- auto-completion ---
  private boolean                isAutoCompletionEnabled;
  private boolean                isAutoCompletionAllViewsEnabled;
  private int                    autoCompletionTrigger   = 4;                                             // [1 - 9]
  private AutoCompletionListener autoCompletionListener  = new AutoCompletionListener();
  private CompletionPopup        completionPopup;
  // ---

  /** Logger */
  private static final Logger    LOGGER                  = Logger.getLogger(JNPadTextArea.class.getName());

  /** UID */
  private static final long      serialVersionUID        = -8840535070140806189L;

  /**
   * Instantiates a new jNPad text area.
   *
   * @param editPane the edit pane
   */
  public JNPadTextArea(EditPane editPane) {
    super(editPane);
    try {
      configure(CFG_ALL);
      jbInit();
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Instantiates a new jNPad's text area.
   *
   * @param editPane the edit pane
   * @param textArea the text area
   */
  JNPadTextArea(EditPane editPane, JNPadTextArea textArea) {
    super(editPane);
    try {
      Color bg = textArea.getBackground();
      if (LAFUtils.isNimbusLAF()) {
        UIDefaults overrides = new UIDefaults();
        overrides.put("TextPane[Enabled].backgroundPainter", bg); //$NON-NLS-1$
        putClientProperty("Nimbus.Overrides", overrides); //$NON-NLS-1$
        putClientProperty("Nimbus.Overrides.InheritDefaults", Boolean.TRUE); //$NON-NLS-1$
      }
      setBackground(bg);
      setForeground(textArea.getForeground());
      setSelectionColor(textArea.getSelectionColor());
      setSelectedTextColor(textArea.getSelectedTextColor());
      setRightMarginLineColor(textArea.getRightMarginLineColor());
      setFont(textArea.getFont());
      setRightMarginLineVisible(textArea.isRightMarginLineVisible());
      setRightMarginLineWidth(textArea.getRightMarginLineWidth());
      setLineWrap(textArea.getLineWrap());
      setWrapStyleWord(textArea.getWrapStyleWord());
      setTabSize(textArea.getTabSize());
      setUseTabs(textArea.getUseTabs());
      setDelimiters(textArea.getDelimiters());
      setOverwriteCaretColor(textArea.getOverwriteCaretColor());
      setCaretColor(oldCaretColor = textArea.oldCaretColor);
      setAutoCompletionEnabled(textArea.isAutoCompletionEnabled());
      setAutoCompletionAllViewsEnabled(textArea.isAutoCompletionAllViewsEnabled());
      setAutoCompletionTrigger(textArea.getAutoCompletionTrigger());

      jbInit();
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Component initialization.
   *
   * @throws Exception the exception
   */
  private void jbInit() throws Exception {
    setUI(new JNPadTextAreaUI());
    
    setFilePath(editPane.getFilePath());

    if(Accelerators.isUsingCompositeShortcuts()) {
      enableEvents(AWTEvent.KEY_EVENT_MASK);
    }
    
    setBorder(GUIUtilities.createEmptyBorder(Config.isDistractionFreeMode() ? 10 : 5));
    
    MyCaret c = new MyCaret();
    c.setBlinkRate(getCaret().getBlinkRate());
    setCaret(c);
    oldCaretColor = getCaretColor();
    addCaretListener(new CaretListener() {
      @Override
      public void caretUpdate(final CaretEvent e) {
        if (isOverwriteTextMode()) {
          processCaretWidth();
        }
      }
    });
  }

  //public EditPane getEditPane() {return editPane;}
  
  /**
   * Sets the file path.
   *
   * @param path the new file path
   */
  @SuppressWarnings("nls")
  void setFilePath(String path) {
    if (Utilities.isFileExtension(path, "java"))
      setContentType(ContentTypes.JAVA);
    else if (Utilities.isFileExtension(path, new String[] { "properties", "props" }))
      setContentType(ContentTypes.PROPERTIES);
    else if (Utilities.isFileExtension(path, new String[] { "asn", "asn1" }))
      setContentType(ContentTypes.ASN1);
    else if (Utilities.isFileExtension(path, new String[] { "c"}))
      setContentType(ContentTypes.C);
    else if (Utilities.isFileExtension(path, new String[] { "cpp", "cc", "h", "hpp" }))
      setContentType(ContentTypes.CPP);
    else
      setContentType(ContentTypes.PLAIN);
  }
  
  /**
   * Sets the font.
   *
   * @param f the new font
   * @see javax.swing.JTextArea#setFont(java.awt.Font)
   */
  @Override
  public void setFont(Font f) {
    super.setFont(f);
    Scheme scheme = getScheme();
    if(scheme != null) {
      scheme.setTextFont(f);
    }
  }

  /**
   * Sets the foreground.
   *
   * @param fg the new foreground
   * @see javax.swing.JComponent#setForeground(java.awt.Color)
   */
  @Override
  public void setForeground(Color fg) {
    super.setForeground(fg);
    Scheme scheme = getScheme();
    if(scheme != null) {
      scheme.setTextColor(fg);
    }
  }
  
  /**
   * Gets the scheme.
   *
   * @return the scheme
   */
  public Scheme getScheme() {
    Document doc = getDocument();
    if (doc != null && doc instanceof JNPadDocument) {
      return ((JNPadDocument) doc).getScheme();
    }
    return null;
  }
  
  /**
   * Gets the content type.
   *
   * @return the content type
   * @see jnpad.text.AbstractTextArea#getContentType()
   */
  @Override
  public String getContentType() {
    Document doc = getDocument();
    if (doc != null && doc instanceof JNPadDocument) {
      return ((JNPadDocument) doc).getContentType();
    }
    return ContentTypes.PLAIN;
  }

  /**
   * Sets the content type.
   *
   * @param contentType the new content type
   */
  public void setContentType(String contentType) {
    contentType = Utilities.defaultString(contentType, ContentTypes.PLAIN);
    Document doc = getDocument();
    if (doc != null && doc instanceof JNPadDocument) {
      String old = ((JNPadDocument) doc).getContentType();
      if (!contentType.equals(old)) {
        final boolean b = ContentTypes.PLAIN.equals(contentType);
        setLineWrap(b ? Config.TEXT_LINE_WRAP.getValue() : false);
        setWrapStyleWord(b ? Config.TEXT_LINE_WRAP.getValue() : false);
        ((JNPadDocument) doc).setContentType(contentType);
        firePropertyChange("jnpad.contentType", old, contentType); //$NON-NLS-1$
      }
    }
  }
  
  /**
   * Process key event.
   *
   * @param e KeyEvent
   * @see javax.swing.JComponent#processKeyEvent(java.awt.event.KeyEvent)
   */
  @Override
  protected void processKeyEvent(KeyEvent e) {
    if(!Accelerators.isUsingCompositeShortcuts()) {
      super.processKeyEvent(e);
      return;
    }

    e = KeyEventWorkaround.processKeyEvent(e);
    if (e == null) {
      return;
    }

    JNPadKeyboardHandler keyboardHandler     = getKeyboardHandler();
    KeyListener          keyEventInterceptor = getKeyEventInterceptor();

    switch (e.getID()) {
      case KeyEvent.KEY_TYPED:
        if (keyEventInterceptor != null) {
          keyEventInterceptor.keyTyped(e);
        }
        else if (keyboardHandler != null) {
          keyboardHandler.keyTyped(e);
        }
        break;
      
      case KeyEvent.KEY_PRESSED:
        if (keyEventInterceptor != null) {
          keyEventInterceptor.keyPressed(e);
        }
        else if (keyboardHandler != null) {
          keyboardHandler.keyPressed(e);
        }
        break;
      
      case KeyEvent.KEY_RELEASED:
        if (keyEventInterceptor != null) {
          keyEventInterceptor.keyReleased(e);
        }
        else if (keyboardHandler != null) {
          keyboardHandler.keyReleased(e);
        }
        break;
        
      default: // Keep FindBugs happy
        break;
    }

    if (!e.isConsumed()) {
      super.processKeyEvent(e);
    }
  }
  
  /**
   * Configure.
   *
   * @param cfg the cfg
   * @see jnpad.config.Configurable#configure(int)
   */
  @Override
  public void configure(final int cfg) {
    Document doc = getDocument();
    if (doc != null && doc instanceof JNPadDocument) {
      Scheme scheme = ((JNPadDocument) doc).getScheme();
      if (scheme != null)
        scheme.configure(cfg);
      // --- [added v0.3] ---
      Scheme mscheme = ((JNPadDocument) doc).getMiniScheme();
      if (mscheme != null)
        mscheme.configure(cfg);
      // ---
    }
    
    if ( (cfg & CFG_COLOR) != 0) {
      Color bg = Config.TEXT_BACKGROUND.getValue();
      if (LAFUtils.isNimbusLAF()) {
        UIDefaults overrides = new UIDefaults();
        overrides.put("TextPane[Enabled].backgroundPainter", bg); //$NON-NLS-1$
        putClientProperty("Nimbus.Overrides", overrides); //$NON-NLS-1$
        putClientProperty("Nimbus.Overrides.InheritDefaults", Boolean.TRUE); //$NON-NLS-1$
      }
      setBackground(bg);
      setForeground(Config.TEXT_FOREGROUND.getValue());
      setSelectionColor(Config.TEXT_SELECTION_BACKGROUND.getValue());
      setSelectedTextColor(Config.TEXT_SELECTION_FOREGROUND.getValue());
      setRightMarginLineColor(Config.TEXT_RIGHT_MARGIN_LINE_COLOR.getValue());
      oldCaretColor = Config.TEXT_CARET_INS_COLOR.getValue();
      setOverwriteCaretColor(Config.TEXT_CARET_OVR_COLOR.getValue());
      setCaretColor(isOverwriteTextMode() ? getOverwriteCaretColor() : oldCaretColor);
    }

    if ( (cfg & CFG_FONT) != 0) {
      setFont(Config.TEXT_FONT.getValue());
    }

    if ( (cfg & CFG_VIEW) != 0) {
      setRightMarginLineVisible(Config.TEXT_RIGHT_MARGIN_LINE_VISIBLE.getValue());
      setRightMarginLineWidth(Config.TEXT_RIGHT_MARGIN_LINE_WIDTH.getValue());
      setLineWrap(Config.TEXT_LINE_WRAP.getValue());
      setWrapStyleWord(Config.TEXT_LINE_WRAP.getValue());
      setTabSize(Config.TEXT_TAB_SIZE.getValue());
      setAutoIndent(Config.TEXT_AUTO_INDENT.getValue());
      setUseTabs(Config.TEXT_TAB_ENABLED.getValue());
      setAutoCompletionEnabled(Config.TEXT_AUTOCOMPLETION_ENABLED.getValue());
      setAutoCompletionAllViewsEnabled(Config.TEXT_AUTOCOMPLETION_ALL.getValue());
      setAutoCompletionTrigger(Config.TEXT_AUTOCOMPLETION_TRIGGER.getValue());
    }
  }

  /**
   * Sets the auto indent.
   *
   * @param b boolean
   */
  public void setAutoIndent(boolean b) {
    Document doc = getDocument();
    if (doc != null) {
      boolean old = getAutoIndent();
      doc.putProperty(JNPadDocument.autoIndentAttribute, Boolean.valueOf(b));
      firePropertyChange(JNPadDocument.autoIndentAttribute, old, b);
    }
  }

  /**
   * Gets the auto indent.
   *
   * @return boolean
   */
  public boolean getAutoIndent() {
    Document doc = getDocument();
    if (doc != null) {
      Boolean b = (Boolean) doc.getProperty(JNPadDocument.autoIndentAttribute);
      if (b != null) {
        return b.booleanValue();
      }
    }
    return Config.TEXT_AUTO_INDENT.getValue();
  }

  /**
   * 
   * @param delimiters String
   */
  public void setDelimiters(String delimiters) {
    Document doc = getDocument();
    if (doc != null) {
      String old = getDelimiters();
      doc.putProperty(JNPadDocument.delimitersAttribute, delimiters);
      firePropertyChange(JNPadDocument.delimitersAttribute, old, delimiters);
    }
  }

  /**
   * Gets the delimiters.
   *
   * @return String
   */
  public String getDelimiters() {
    Document doc = getDocument();
    if (doc != null) {
      String delimiters = (String) doc.getProperty(JNPadDocument.delimitersAttribute);
      if (delimiters != null) {
        return delimiters;
      }
    }
    return Config.TEXT_DELIMITERS.getValue();
  }

  /**
   * Sets the use tabs.
   *
   * @param b boolean
   */
  public void setUseTabs(boolean b) {
    Document doc = getDocument();
    if (doc != null) {
      boolean old = getUseTabs();
      doc.putProperty(JNPadDocument.useTabsAttribute, Boolean.valueOf(b));
      firePropertyChange(JNPadDocument.useTabsAttribute, old, b);
    }
  }

  /**
   * Gets the use tabs.
   *
   * @return boolean
   */
  public boolean getUseTabs() {
    Document doc = getDocument();
    if (doc != null) {
      Boolean b = (Boolean) doc.getProperty(JNPadDocument.useTabsAttribute);
      if (b != null) {
        return b.booleanValue();
      }
    }
    return Config.TEXT_TAB_ENABLED.getValue();
  }

  /**
   * Sets the right margin line width.
   *
   * @param width the new right margin line width
   * @see jnpad.text.AbstractTextArea#setRightMarginLineWidth(int)
   */
  @Override
  public void setRightMarginLineWidth(int width) {
    rightMarginLineWidth = width;
    repaint();
  }

  /**
   * Gets the right margin line width.
   *
   * @return the right margin line width
   * @see jnpad.text.AbstractTextArea#getRightMarginLineWidth()
   */
  @Override
  public final int getRightMarginLineWidth() {
    return rightMarginLineWidth;
  }

  /**
   * Sets the right margin line visible.
   *
   * @param b the new right margin line visible
   * @see jnpad.text.AbstractTextArea#setRightMarginLineVisible(boolean)
   */
  @Override
  public void setRightMarginLineVisible(boolean b) {
    isRightMarginLineVisible = b;
    repaint();
  }

  /**
   * Checks if is right margin line visible.
   *
   * @return true, if is right margin line visible
   * @see jnpad.text.AbstractTextArea#isRightMarginLineVisible()
   */
  @Override
  public final boolean isRightMarginLineVisible() {
    return isRightMarginLineVisible;
  }

  /**
   * Sets the right margin line color.
   *
   * @param c the new right margin line color
   * @see jnpad.text.AbstractTextArea#setRightMarginLineColor(java.awt.Color)
   */
  @Override
  public void setRightMarginLineColor(Color c) {
    rightMarginLineColor = c;
    repaint();
  }

  /**
   * Gets the right margin line color.
   *
   * @return the right margin line color
   * @see jnpad.text.AbstractTextArea#getRightMarginLineColor()
   */
  @Override
  public final Color getRightMarginLineColor() {
    return rightMarginLineColor;
  }

  /**
   * Gets the overwrite caret color.
   *
   * @return the overwrite caret color
   */
  public Color getOverwriteCaretColor() {
    return overwriteCaretColor;
  }

  /**
   * Sets the overwrite caret color.
   *
   * @param color the new overwrite caret color
   */
  public void setOverwriteCaretColor(Color color) {
    if (color != null) {
      overwriteCaretColor = color;
    }
  }

  /**
   * Sets the overwrite text mode.
   *
   * @param b the new overwrite text mode
   */
  public void setOverwriteTextMode(boolean b) {
    if (isOverwriteTextMode != b) {
      isOverwriteTextMode = b;
      processMode();
    }
  }

  /**
   * Checks if is overwrite text mode.
   *
   * @return true, if is overwrite text mode
   */
  public boolean isOverwriteTextMode() {
    return isOverwriteTextMode;
  }

  /**
   * Process mode.
   */
  private void processMode() {
    if (isOverwriteTextMode()) {
      processCaretWidth();
      setCaretColor(overwriteCaretColor);
    }
    else {
      setCaretColor(oldCaretColor);
      putClientProperty(PROPERTY_CARET_WIDTH, 1);
    }
  }

  /**
   * Process caret width.
   */
  private void processCaretWidth() {
    try {
      int pos = getCaretPosition();
      Rectangle rPos = modelToView(pos) != null ? modelToView(pos).getBounds() : new Rectangle();
      int caretX = rPos.x;
      int caretEndX = rPos.x;
      if (pos < getDocument().getLength()) {
        Rectangle rNextPos = modelToView(pos + 1) != null ? modelToView(pos + 1).getBounds() : new Rectangle();
        if (rPos.y == rNextPos.y) {
          caretEndX = rNextPos.x;
        }
      }
      putClientProperty(PROPERTY_CARET_WIDTH, Math.max(1, caretEndX - caretX + 1));
    }
    catch (BadLocationException ex) {
      //LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Replace selection.
   *
   * @param content the content
   * @see javax.swing.text.JTextComponent#replaceSelection(java.lang.String)
   */
  @Override
  public void replaceSelection(String content) {
    if (isEditable() && isOverwriteTextMode() && getSelectionStart() == getSelectionEnd()) {
      int pos = getCaretPosition();
      int lastPos = Math.min(getDocument().getLength(), pos + content.length());
      select(pos, lastPos);
    }
    super.replaceSelection(content);
  }
  
  /**
   * To upper case.
   */
  public void toUpperCase() {
    if (!isEditable()) {
      return;
    }
    String selectedText = getSelectedText();
    if (selectedText == null) {
      return;
    }
    replaceSelection(selectedText.toUpperCase());
  }

  /**
   * To lower case.
   */
  public void toLowerCase() {
    if (!isEditable()) {
      return;
    }
    String selectedText = getSelectedText();
    if (selectedText == null) {
      return;
    }
    replaceSelection(selectedText.toLowerCase());
  }

  /**
   * Invert upper lower.
   */
  public void invertUpperLower() {
    if (!isEditable()) {
      return;
    }
    String selectedText = getSelectedText();
    if (selectedText == null) {
      return;
    }
    char[] charArray = selectedText.toCharArray();
    for (int i = 0; i < charArray.length; i++) {
      if (charArray[i] >= 65 && charArray[i] <= 90) {
        charArray[i] = (charArray[i] += 32);
      }
      else if (charArray[i] >= 97 && charArray[i] <= 122) {
        charArray[i] = (charArray[i] -= 32);
      }
    }
    replaceSelection(new String(charArray));
  }

  /**
   * Capitalize.
   */
  public void capitalize() {
    if (!isEditable()) {
      return;
    }
    String selectedText = getSelectedText();
    if (selectedText == null) {
      return;
    }
    replaceSelection(Utilities.capitalizeString(selectedText));
  }

  /**
   * To title.
   */
  public void toTitle() {
    if (!isEditable()) {
      return;
    }
    String selectedText = getSelectedText();
    if (selectedText == null) {
      return;
    }
    replaceSelection(Utilities.toTitleString(selectedText));
  }
  
  // --- support ---
  /**
   * Checks if is main.
   *
   * @return true, if is main
   * @see jnpad.text.AbstractTextArea#isMain()
   */
  @Override
  boolean isMain() {
    return this == getMain();
  }
  
  
  /**
   * Gets the main.
   *
   * @return the main
   */
  JNPadTextArea getMain() {
    try {
      return editPane.buffer.bufferSet.getViewer().getActiveBuffer().getSelectedTextArea();
    }
    catch (Exception ex) {
      JNPadFrame jNPad = GUIUtilities.getJNPadFrame(this);
      if (jNPad != null) {
        return jNPad.getActiveTextArea();
      }
      return this;
    }
  }

  /**
   * Gets the keyboard handler.
   *
   * @return the keyboard handler
   */
  JNPadKeyboardHandler getKeyboardHandler() {
    return editPane.buffer.getKeyboardHandler();
  }

  /**
   * Gets the key event interceptor.
   *
   * @return the key event interceptor
   */
  KeyListener getKeyEventInterceptor() {
    return editPane.buffer.getKeyEventInterceptor();
  }
  
  /**
   * Sets the key event interceptor.
   *
   * @param listener the new key event interceptor
   */
  void setKeyEventInterceptor(KeyListener listener) {
    editPane.buffer.setKeyEventInterceptor(listener);
  }
  // ---
  
  /**
   * Gets the auto completion trigger.
   *
   * @return the auto completion trigger
   */
  public int getAutoCompletionTrigger() {
    return autoCompletionTrigger;
  }

  /**
   * Sets the auto completion trigger.
   *
   * @param i the new auto completion trigger
   */
  public void setAutoCompletionTrigger(int i) {
    autoCompletionTrigger = i > 9 ? 9 : i < 1 ? 1 : i;
  }

  /**
   * Checks if is auto completion enabled.
   *
   * @return true, if is auto completion enabled
   */
  public boolean isAutoCompletionEnabled() {
    return isAutoCompletionEnabled;
  }

  /**
   * Sets the auto completion enabled.
   *
   * @param b the new auto completion enabled
   */
  public void setAutoCompletionEnabled(boolean b) {
    if (b != isAutoCompletionEnabled) {
      isAutoCompletionEnabled = b;
      if (b) {
        autoCompletionListener.addTo(this);
      }
      else {
        autoCompletionListener.removeFrom(this);
      }
    }
  }

  /**
   * Checks if is auto completion all views enabled.
   *
   * @return true, if is auto completion all views enabled
   */
  public boolean isAutoCompletionAllViewsEnabled() {
    return isAutoCompletionAllViewsEnabled;
  }

  /**
   * Sets the auto completion all views enabled.
   *
   * @param b the new auto completion all views enabled
   */
  public void setAutoCompletionAllViewsEnabled(boolean b) {
    if (b != isAutoCompletionAllViewsEnabled) {
      isAutoCompletionAllViewsEnabled = b;
    }
  }

  /**
   * Gets the auto completion delay.
   *
   * @return the auto completion delay
   */
  public int getAutoCompletionDelay() {
    return autoCompletionListener.timer.getDelay();
  }

  /**
   * Sets the auto completion delay.
   *
   * @param ms the new auto completion delay
   */
  public void setAutoCompletionDelay(int ms) {
    ms = Math.max(0, ms);
    autoCompletionListener.timer.stop();
    autoCompletionListener.timer.setInitialDelay(ms);
  }

  /**
   * Checks if is completion popup visible.
   *
   * @return true, if is completion popup visible
   */
  private boolean isCompletionPopupVisible() {
    return completionPopup != null && completionPopup.isVisible();
  }

  /**
   * Checks if is auto activate okay.
   *
   * @return true, if is auto activate okay
   */
  private boolean isAutoActivateOkay() {
    try {
      String word = CompletionUtilities.getWord(this, getCaretPosition() + 1);
      return !Utilities.isBlankString(word) && word.trim().length() >= getAutoCompletionTrigger();
    }
    catch (BadLocationException blex) {
      LOGGER.log(Level.WARNING, blex.getMessage(), blex);
    }
    return false;
  }

  /**
   * Do completion.
   */
  private void doCompletion() {
    try {
      String word = CompletionUtilities.getWord(this, getCaretPosition());
      if (Utilities.isBlankString(word))
        return;

      TreeSet<String> completions = new TreeSet<String>();

      int wordLen = word.length();

      String delimiters = getDelimiters();

      if (isAutoCompletionAllViewsEnabled()) {
        JNPadFrame jNPad = GUIUtilities.getJNPadFrame(this);
        if (jNPad != null) {
          for (JNPadTextArea etxarea : jNPad.getViewer().getTextAreas()) {
            CompletionUtilities.set(etxarea, word, wordLen, delimiters, completions);
          }
        }
        else {
          CompletionUtilities.set(this, word, wordLen, delimiters, completions);
        }
      }
      else {
        CompletionUtilities.set(this, word, wordLen, delimiters, completions);
      }

      if (completions.size() > 0) {
        String[] sCompletions = completions.toArray(new String[completions.size()]);
        if (completionPopup == null) {
          completionPopup = new CompletionPopup(this);
        }
        completionPopup.showCompletions(word, sCompletions);
      }
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  //////////////////////////////////////////////////////////////////////////////
  /**
   * The Class MyCaret.
   */
  private class MyCaret extends DefaultCaret {
    /** UID */
    private static final long serialVersionUID = -374425523031518455L;

    /**
     * Paint.
     *
     * @param g the Graphics
     * @see javax.swing.text.DefaultCaret#paint(java.awt.Graphics)
     */
    @Override
    public void paint(Graphics g) {
      if (isOverwriteTextMode()) {
        //we should shift to half width because of DefaultCaret rendering algorithm
        AffineTransform old = ( (Graphics2D) g).getTransform();
        int w = (Integer) getClientProperty(PROPERTY_CARET_WIDTH);
        Color textAreaBg = getBackground();
        if (textAreaBg == null) {
          textAreaBg = Color.white;
        }
        g.setXORMode(textAreaBg);
        g.translate(w / 2, 0);
        super.paint(g);
        ( (Graphics2D) g).setTransform(old);
      }
      else {
        super.paint(g);
      }
    }

    /**
     * Damage.
     *
     * @param r the Rectangle
     * @see javax.swing.text.DefaultCaret#damage(java.awt.Rectangle)
     */
    @Override
    protected synchronized void damage(Rectangle r) {
      if (isOverwriteTextMode()) {
        if (r != null) {
          int damageWidth = (Integer) getClientProperty(PROPERTY_CARET_WIDTH);
          x = r.x - 4 - (damageWidth / 2);
          y = r.y;
          width = 9 + 3 * damageWidth / 2;
          height = r.height;
          repaint();
        }
      }
      else {
        super.damage(r);
      }
    }
  }

  //////////////////////////////////////////////////////////////////////////////

  //////////////////////////////////////////////////////////////////////////////
  /**
   * The Class AutoCompletionListener.
   */
  private class AutoCompletionListener extends FocusAdapter implements DocumentListener, CaretListener, ActionListener {
    private Timer   timer;
    private boolean justInserted;

    /**
     * Instantiates a new auto completion listener.
     */
    AutoCompletionListener() {
      timer = new Timer(200, this);
      timer.setRepeats(false);
    }

    /**
     * Adds the to.
     *
     * @param tc the tc
     */
    void addTo(JTextComponent tc) {
      tc.addFocusListener(this);
      tc.getDocument().addDocumentListener(this);
      tc.addCaretListener(this);
    }

    /**
     * Removes the from.
     *
     * @param tc the tc
     */
    void removeFrom(JTextComponent tc) {
      tc.removeFocusListener(this);
      tc.getDocument().removeDocumentListener(this);
      tc.removeCaretListener(this);
      timer.stop();
      justInserted = false;
    }

    /**
     * Action performed.
     *
     * @param e the ActionEvent
     * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
     */
    @Override
    public void actionPerformed(ActionEvent e) {
      if(!isCompletionPopupVisible()) {
        doCompletion();
      }
    }

    /**
     * Caret update.
     *
     * @param e the CaretEvent
     * @see javax.swing.event.CaretListener#caretUpdate(javax.swing.event.CaretEvent)
     */
    @Override
    public void caretUpdate(CaretEvent e) {
      if (justInserted) {
        justInserted = false;
      }
      else {
        timer.stop();
      }
    }

    /**
     * Changed update.
     *
     * @param e the DocumentEvent
     * @see javax.swing.event.DocumentListener#changedUpdate(javax.swing.event.DocumentEvent)
     */
    @Override
    public void changedUpdate(DocumentEvent e) {
      // Ignore
    }

    /**
     * Focus lost.
     *
     * @param e the FocusEvent
     * @see java.awt.event.FocusAdapter#focusLost(java.awt.event.FocusEvent)
     */
    @Override
    public void focusLost(FocusEvent e) {
      timer.stop();
    }

    /**
     * Insert update.
     *
     * @param e the DocumentEvent
     * @see javax.swing.event.DocumentListener#insertUpdate(javax.swing.event.DocumentEvent)
     */
    @Override
    public void insertUpdate(DocumentEvent e) {
      justInserted = false;
      if (!isCompletionPopupVisible() && isAutoCompletionEnabled() && e.getLength() == 1) {
        if (isAutoActivateOkay()) {
          timer.restart();
          justInserted = true;
        }
        else {
          timer.stop();
        }
      }
      else {
        timer.stop();
      }
    }

    /**
     * Removes the update.
     *
     * @param e the DocumentEvent
     * @see javax.swing.event.DocumentListener#removeUpdate(javax.swing.event.DocumentEvent)
     */
    @Override
    public void removeUpdate(DocumentEvent e) {
      timer.stop();
    }
  }
  //////////////////////////////////////////////////////////////////////////////

}
