/*
 * Copyright (C) 2012-14 Martin Leopold <m@martinleopold.com> and Manindra Moharana <me@mkmoharana.com>
 *
 * 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., 59 Temple
 * Place - Suite 330, Boston, MA 02111-1307, USA.
 */
package processing.mode.experimental;
import static processing.mode.experimental.ExperimentalMode.log;
import static processing.mode.experimental.ExperimentalMode.log2;
import galsasson.mode.tweak.ColorControlBox;
import galsasson.mode.tweak.Handle;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.FontMetrics;
import java.awt.Point;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import javax.swing.DefaultListModel;
import javax.swing.SwingWorker;

import processing.app.Base;
import processing.app.syntax.JEditTextArea;
import processing.app.syntax.TextAreaDefaults;
/**
 * Customized text area. Adds support for line background colors.
 * 
 * @author Martin Leopold <m@martinleopold.com>
 */
public class TextArea extends JEditTextArea {

  protected MouseListener[] mouseListeners; // cached mouselisteners, these are wrapped by MouseHandler

  protected DebugEditor editor; // the editor

  // line properties
  protected Map<Integer, Color> lineColors = new HashMap<Integer, Color>(); // contains line background colors

  // left-hand gutter properties
  protected int gutterPadding = 3; // [px] space added to the left and right of gutter chars

  protected Color gutterBgColor = new Color(252, 252, 252); // gutter background color

  protected Color gutterLineColor = new Color(233, 233, 233); // color of vertical separation line

  protected String breakpointMarker = "<>"; // the text marker for highlighting breakpoints in the gutter

  protected String currentLineMarker = "->"; // the text marker for highlighting the current line in the gutter

  protected Map<Integer, String> gutterText = new HashMap<Integer, String>(); // maps line index to gutter text

  protected Map<Integer, Color> gutterTextColors = new HashMap<Integer, Color>(); // maps line index to gutter text color

  protected TextAreaPainter customPainter;

  protected ErrorCheckerService errorCheckerService;

  public TextArea(TextAreaDefaults defaults, DebugEditor editor) {
    super(defaults);
    this.editor = editor;

    // replace the painter:
    // first save listeners, these are package-private in JEditTextArea, so not accessible
    ComponentListener[] componentListeners = painter.getComponentListeners();
    mouseListeners = painter.getMouseListeners();
    MouseMotionListener[] mouseMotionListeners = painter
        .getMouseMotionListeners();

    remove(painter);

    // set new painter
    customPainter = new TextAreaPainter(this, defaults);
    painter = customPainter;

    // set listeners
    for (ComponentListener cl : componentListeners) {
      painter.addComponentListener(cl);
    }

    for (MouseMotionListener mml : mouseMotionListeners) {
      painter.addMouseMotionListener(mml);
    }

    // use a custom mouse handler instead of directly using mouseListeners
    MouseHandler mouseHandler = new MouseHandler();
    painter.addMouseListener(mouseHandler);
    painter.addMouseMotionListener(mouseHandler);
    //addCompletionPopupListner();
    add(CENTER, painter);

    // load settings from theme.txt
    ExperimentalMode theme = (ExperimentalMode) editor.getMode();
    gutterBgColor = theme.getThemeColor("gutter.bgcolor", gutterBgColor);
    gutterLineColor = theme.getThemeColor("gutter.linecolor", gutterLineColor);
    gutterPadding = theme.getInteger("gutter.padding");
    breakpointMarker = theme.loadThemeString("breakpoint.marker",
                                             breakpointMarker);
    currentLineMarker = theme.loadThemeString("currentline.marker",
                                              currentLineMarker);

    // TweakMode code

	prevCompListeners = painter
			.getComponentListeners();
	prevMouseListeners = painter.getMouseListeners();
	prevMMotionListeners = painter
			.getMouseMotionListeners();
	prevKeyListeners = editor.getKeyListeners();


	interactiveMode = false;
	addPrevListeners();

  }

  /**
   * Sets ErrorCheckerService and loads theme for TextArea(XQMode)
   * 
   * @param ecs
   * @param mode
   */
  public void setECSandThemeforTextArea(ErrorCheckerService ecs,
                                        ExperimentalMode mode) {
    errorCheckerService = ecs;
    customPainter.setECSandTheme(ecs, mode);
  }

  /**
   * Handles KeyEvents for TextArea
   * Code completion begins from here.
   */
  public void processKeyEvent(KeyEvent evt) {
    if(Base.isMacOS() && evt.isControlDown()) System.out.println("Ctrl down: " + evt);
    if(evt.getKeyCode() == KeyEvent.VK_ESCAPE){
      if(suggestion != null){
        if(suggestion.isVisible()){
          log("esc key");
          hideSuggestion();
          evt.consume();
          return;
        }
      }
    }
    else if(evt.getKeyCode() == KeyEvent.VK_ENTER && evt.getID() == KeyEvent.KEY_PRESSED){
      if (suggestion != null) {
        if (suggestion.isVisible()) {
          if (suggestion.insertSelection(CompletionPanel.KEYBOARD_COMPLETION)) {
            //hideSuggestion(); // Kill it!  
            evt.consume();
            // Still try to show suggestions after inserting if it's
            // the case of overloaded methods. See #2755
            if(suggestion.isVisible())
              prepareSuggestions(evt);
            return;
          }
        }
      }
    }
    
    if (evt.getID() == KeyEvent.KEY_PRESSED) {
      switch (evt.getKeyCode()) {
      case KeyEvent.VK_DOWN:
        if (suggestion != null)
          if (suggestion.isVisible()) {
            //log("KeyDown");
            suggestion.moveDown();
            return;
          }
        break;
      case KeyEvent.VK_UP:
        if (suggestion != null)
          if (suggestion.isVisible()) {
            //log("KeyUp");
            suggestion.moveUp();
            return;
          }
        break;
      case KeyEvent.VK_BACK_SPACE:
        log("BK Key");
        break;
      case KeyEvent.VK_SPACE:
        if (suggestion != null)
          if (suggestion.isVisible()) {
            log("Space bar, hide completion list");
            suggestion.hide();
          }
        break;
      default:
        break;
      }
    }
    super.processKeyEvent(evt);

    if (editor.hasJavaTabs) return; // code completion disabled if java tabs
    
    if (evt.getID() == KeyEvent.KEY_TYPED) {
      char keyChar = evt.getKeyChar();
      if (keyChar == KeyEvent.VK_ENTER || 
          keyChar == KeyEvent.VK_ESCAPE ||
          keyChar == KeyEvent.VK_TAB ||
          keyChar == KeyEvent.CHAR_UNDEFINED) {
        return;
      }
      else if (keyChar == ')') {
        hideSuggestion(); // See #2741 
        return;
      }
      
      final KeyEvent evt2 = evt;
      if (keyChar == ' ') {
        if (!Base.isMacOS() && ExperimentalMode.ccTriggerEnabled &&
        (evt.isControlDown() || evt.isMetaDown())) {
          SwingWorker<Object, Object> worker = new SwingWorker<Object, Object>() {
            protected Object doInBackground() throws Exception {
              // Provide completions only if it's enabled
              if (ExperimentalMode.codeCompletionsEnabled
                  && ExperimentalMode.ccTriggerEnabled) {
                getDocument().remove(getCaretPosition() - 1, 1); // Remove the typed space
                log("[KeyEvent]" + evt2.getKeyChar() + "  |Prediction started");
                log("Typing: " + fetchPhrase(evt2));
              }
              return null;
            }
          };
          worker.execute();
        } else {
          hideSuggestion(); // hide on spacebar
        }
      } else {
        prepareSuggestions(evt2);
      }
    }
    // #2699 - Special case for OS X, where Ctrl-Space is not detected as Key_Typed -_-
    else if (Base.isMacOS() && evt.getID() == KeyEvent.KEY_RELEASED
        && evt.getKeyCode() == KeyEvent.VK_SPACE && evt.isControlDown()) {
      final KeyEvent evt2 = evt;
      SwingWorker<Object, Object> worker = new SwingWorker<Object, Object>() {
        protected Object doInBackground() throws Exception {
          // Provide completions only if it's enabled
          if (ExperimentalMode.codeCompletionsEnabled
              && ExperimentalMode.ccTriggerEnabled) {
            log("[KeyEvent]" + KeyEvent.getKeyText(evt2.getKeyCode()) + "  |Prediction started");
            log("Typing: " + fetchPhrase(evt2));
          }
          return null;
        }
      };
      worker.execute();
    }
  }

  /**
   * Kickstart auto-complete suggestions
   * @param evt - KeyEvent
   */
  private void prepareSuggestions(final KeyEvent evt){
    SwingWorker<Object, Object> worker = new SwingWorker<Object, Object>() {
      protected Object doInBackground() throws Exception {
        // Provide completions only if it's enabled
        if (ExperimentalMode.codeCompletionsEnabled
            && (!ExperimentalMode.ccTriggerEnabled || suggestion.isVisible())) {
          log("[KeyEvent]" + evt.getKeyChar() + "  |Prediction started");
          log("Typing: " + fetchPhrase(evt));
        }
        return null;
      }
    };
    worker.execute();
  }
 
  /**
   * Retrieves the word on which the mouse pointer is present
   * @param evt - the MouseEvent which triggered this method 
   * @return
   */
  private String fetchPhrase(MouseEvent evt) {
    log("--handle Mouse Right Click--");
    int off = xyToOffset(evt.getX(), evt.getY());
    if (off < 0)
      return null;
    int line = getLineOfOffset(off);
    if (line < 0)
      return null;
    String s = getLineText(line);
    if (s == null)
      return null;
    else if (s.length() == 0)
      return null;
    else {
      int x = xToOffset(line, evt.getX()), x2 = x + 1, x1 = x - 1;
      int xLS = off - getLineStartNonWhiteSpaceOffset(line);
      log("x=" + x);
      if (x < 0 || x >= s.length())
        return null;
      String word = s.charAt(x) + "";
      if (s.charAt(x) == ' ')
        return null;
      if (!(Character.isLetterOrDigit(s.charAt(x)) || s.charAt(x) == '_' || s
          .charAt(x) == '$'))
        return null;
      int i = 0;
      while (true) {
        i++;
        if (x1 >= 0 && x1 < s.length()) {
          if (Character.isLetter(s.charAt(x1)) || s.charAt(x1) == '_') {
            word = s.charAt(x1--) + word;
            xLS--;
          } else
            x1 = -1;
        } else
          x1 = -1;

        if (x2 >= 0 && x2 < s.length()) {
          if (Character.isLetterOrDigit(s.charAt(x2)) || s.charAt(x2) == '_'
              || s.charAt(x2) == '$')
            word = word + s.charAt(x2++);
          else
            x2 = -1;
        } else
          x2 = -1;

        if (x1 < 0 && x2 < 0)
          break;
        if (i > 200) {
          // time out!
          break;
        }
      }
      if (Character.isDigit(word.charAt(0)))
        return null;
      log("Mouse click, word: " + word.trim());
      errorCheckerService.getASTGenerator().setLastClickedWord(line, word, xLS);
      return word.trim();
    }
  }
  
  /**
   * Retrieves the current word typed just before the caret.
   * Then triggers code completion for that word.
   * 
   * @param evt - the KeyEvent which triggered this method 
   * @return
   */
  public String fetchPhrase(KeyEvent evt) {
   
    int off = getCaretPosition();
    log2("off " + off);
    if (off < 0)
      return null;
    int line = getCaretLine();
    if (line < 0)
      return null;
    String s = getLineText(line);
    log2("lin " + line);

    //log2(s + " len " + s.length());

    int x = getCaretPosition() - getLineStartOffset(line) - 1, x1 = x - 1;
    if(x >= s.length() || x < 0) {
      //log("X is " + x + ". Returning null");
      hideSuggestion();
      return null; //TODO: Does this check cause problems? Verify.
    }
    
    log2(" x char: " + s.charAt(x));
    
    if (!(Character.isLetterOrDigit(s.charAt(x)) || s.charAt(x) == '_'
        || s.charAt(x) == '(' || s.charAt(x) == '.')) {
      //log("Char before caret isn't a letter/digit/_(. so no predictions");
      hideSuggestion();
      return null;
    } else if (x > 0 && (s.charAt(x - 1) == ' ' || s.charAt(x - 1) == '(')
        && Character.isDigit(s.charAt(x))) {
      //log("Char before caret isn't a letter, but ' ' or '(', so no predictions");
      hideSuggestion(); // See #2755, Option 2 comment
      return null;
    } else if (x == 0){
      //log("X is zero");
      hideSuggestion();
      return null;
    }
    
    //int xLS = off - getLineStartNonWhiteSpaceOffset(line);    

    String word = (x < s.length() ? s.charAt(x) : "") + "";
    if (s.trim().length() == 1) {
//      word = ""
//          + (keyChar == KeyEvent.CHAR_UNDEFINED ? s.charAt(x - 1) : keyChar);
      //word = (x < s.length()?s.charAt(x):"") + "";
      word = word.trim();
      if (word.endsWith("."))
        word = word.substring(0, word.length() - 1);
      
      errorCheckerService.getASTGenerator().preparePredictions(word, line
          + errorCheckerService.mainClassOffset,0);
      return word;
    }

    int i = 0;
    int closeB = 0;

    while (true) {
      i++;
      //TODO: currently works on single line only. "a. <new line> b()" won't be detected
      if (x1 >= 0) {
//        if (s.charAt(x1) != ';' && s.charAt(x1) != ',' && s.charAt(x1) != '(')
        if (Character.isLetterOrDigit(s.charAt(x1)) || s.charAt(x1) == '_'
            || s.charAt(x1) == '.' || s.charAt(x1) == ')' || s.charAt(x1) == ']') {

          if (s.charAt(x1) == ')') {
            word = s.charAt(x1--) + word;
            closeB++;
            while (x1 >= 0 && closeB > 0) {
              word = s.charAt(x1) + word;
              if (s.charAt(x1) == '(')
                closeB--;
              if (s.charAt(x1) == ')')
                closeB++;
              x1--;
            }
          }
          else if (s.charAt(x1) == ']') {
            word = s.charAt(x1--) + word;
            closeB++;
            while (x1 >= 0 && closeB > 0) {
              word = s.charAt(x1) + word;
              if (s.charAt(x1) == '[')
                closeB--;
              if (s.charAt(x1) == ']')
                closeB++;
              x1--;
            }
          }
          else {
            word = s.charAt(x1--) + word;
          }
        } else {
          break;
        }
      } else {
        break;
      }

      if (i > 200) {
        // time out!
        break;
      }
    }

    if (Character.isDigit(word.charAt(0)))
      return null;
    word = word.trim();
    //    if (word.endsWith("."))
    //      word = word.substring(0, word.length() - 1);
    int lineStartNonWSOffset = 0;
    if (word.length() >= ExperimentalMode.codeCompletionTriggerLength) {
      errorCheckerService.getASTGenerator()
          .preparePredictions(word, line + errorCheckerService.mainClassOffset,
                              lineStartNonWSOffset);
    }
    return word;

  }

  /**
   * Retrieve the total width of the gutter area.
   * 
   * @return gutter width in pixels
   */
  protected int getGutterWidth() {
    if(editor.debugToolbarEnabled == null || !editor.debugToolbarEnabled.get()){
      return 0;
    }
    FontMetrics fm = painter.getFontMetrics();
//        log("fm: " + (fm == null));
//        log("editor: " + (editor == null));
    //log("BPBPBPBPB: " + (editor.breakpointMarker == null));

    int textWidth = Math.max(fm.stringWidth(breakpointMarker),
                             fm.stringWidth(currentLineMarker));
    return textWidth + 2 * gutterPadding;
  }

  /**
   * Retrieve the width of margins applied to the left and right of the gutter
   * text.
   * 
   * @return margins in pixels
   */
  protected int getGutterMargins() {
    if(editor.debugToolbarEnabled == null || !editor.debugToolbarEnabled.get()){
      return 0;
    }
    return gutterPadding;
  }

  /**
   * Set the gutter text of a specific line.
   * 
   * @param lineIdx
   *          the line index (0-based)
   * @param text
   *          the text
   */
  public void setGutterText(int lineIdx, String text) {
    gutterText.put(lineIdx, text);
    painter.invalidateLine(lineIdx);
  }

  /**
   * Set the gutter text and color of a specific line.
   * 
   * @param lineIdx
   *          the line index (0-based)
   * @param text
   *          the text
   * @param textColor
   *          the text color
   */
  public void setGutterText(int lineIdx, String text, Color textColor) {
    gutterTextColors.put(lineIdx, textColor);
    setGutterText(lineIdx, text);
  }

  /**
   * Clear the gutter text of a specific line.
   * 
   * @param lineIdx
   *          the line index (0-based)
   */
  public void clearGutterText(int lineIdx) {
    gutterText.remove(lineIdx);
    painter.invalidateLine(lineIdx);
  }

  /**
   * Clear all gutter text.
   */
  public void clearGutterText() {
    for (int lineIdx : gutterText.keySet()) {
      painter.invalidateLine(lineIdx);
    }
    gutterText.clear();
  }

  /**
   * Retrieve the gutter text of a specific line.
   * 
   * @param lineIdx
   *          the line index (0-based)
   * @return the gutter text
   */
  public String getGutterText(int lineIdx) {
    return gutterText.get(lineIdx);
  }

  /**
   * Retrieve the gutter text color for a specific line.
   * 
   * @param lineIdx
   *          the line index
   * @return the gutter text color
   */
  public Color getGutterTextColor(int lineIdx) {
    return gutterTextColors.get(lineIdx);
  }

  /**
   * Set the background color of a line.
   * 
   * @param lineIdx
   *          0-based line number
   * @param col
   *          the background color to set
   */
  public void setLineBgColor(int lineIdx, Color col) {
    lineColors.put(lineIdx, col);
    painter.invalidateLine(lineIdx);
  }

  /**
   * Clear the background color of a line.
   * 
   * @param lineIdx
   *          0-based line number
   */
  public void clearLineBgColor(int lineIdx) {
    lineColors.remove(lineIdx);
    painter.invalidateLine(lineIdx);
  }

  /**
   * Clear all line background colors.
   */
  public void clearLineBgColors() {
    for (int lineIdx : lineColors.keySet()) {
      painter.invalidateLine(lineIdx);
    }
    lineColors.clear();
  }

  /**
   * Get a lines background color.
   * 
   * @param lineIdx
   *          0-based line number
   * @return the color or null if no color was set for the specified line
   */
  public Color getLineBgColor(int lineIdx) {
    return lineColors.get(lineIdx);
  }

  /**
   * Convert a character offset to a horizontal pixel position inside the text
   * area. Overridden to take gutter width into account.
   * 
   * @param line
   *          the 0-based line number
   * @param offset
   *          the character offset (0 is the first character on a line)
   * @return the horizontal position
   */
  @Override
  public int _offsetToX(int line, int offset) {
    return super._offsetToX(line, offset) + getGutterWidth();
  }

  /**
   * Convert a horizontal pixel position to a character offset. Overridden to
   * take gutter width into account.
   * 
   * @param line
   *          the 0-based line number
   * @param x
   *          the horizontal pixel position
   * @return he character offset (0 is the first character on a line)
   */
  @Override
  public int xToOffset(int line, int x) {
    return super.xToOffset(line, x - getGutterWidth());
  }

  /**
   * Custom mouse handler. Implements double clicking in the gutter area to
   * toggle breakpoints, sets default cursor (instead of text cursor) in the
   * gutter area.
   */
  protected class MouseHandler implements MouseListener, MouseMotionListener {

    protected int lastX; // previous horizontal positon of the mouse cursor

    @Override
    public void mouseClicked(MouseEvent me) {
      // forward to standard listeners
      for (MouseListener ml : mouseListeners) {
        ml.mouseClicked(me);
      }
    }

    @Override
    public void mousePressed(MouseEvent me) {
      // check if this happened in the gutter area
      if (me.getX() < getGutterWidth()) {
        if (me.getButton() == MouseEvent.BUTTON1 && me.getClickCount() == 2) {
          int line = me.getY() / painter.getFontMetrics().getHeight()
              + firstLine;
          if (line >= 0 && line <= getLineCount() - 1) {
            editor.gutterDblClicked(line);
          }
        }
        return;
      }
      
      if (me.getButton() == MouseEvent.BUTTON3) {
        if(!editor.hasJavaTabs){ // tooltips, etc disabled for java tabs
          fetchPhrase(me);
        }
      }

      // forward to standard listeners
      for (MouseListener ml : mouseListeners) {
        ml.mousePressed(me);
      }

    }

    @Override
    public void mouseReleased(MouseEvent me) {
      // forward to standard listeners
      for (MouseListener ml : mouseListeners) {
        ml.mouseReleased(me);
      }
    }

    @Override
    public void mouseEntered(MouseEvent me) {
      // forward to standard listeners
      for (MouseListener ml : mouseListeners) {
        ml.mouseEntered(me);
      }
    }

    @Override
    public void mouseExited(MouseEvent me) {
      // forward to standard listeners
      for (MouseListener ml : mouseListeners) {
        ml.mouseExited(me);
      }
    }

    @Override
    public void mouseDragged(MouseEvent me) {
      // No need to forward since the standard MouseMotionListeners are called anyway
      // nop
    }

    @Override
    public void mouseMoved(MouseEvent me) {
      // No need to forward since the standard MouseMotionListeners are called anyway
      if (me.getX() < getGutterWidth()) {
        if (lastX >= getGutterWidth()) {
          painter.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
        }
      } else {
        if (lastX < getGutterWidth()) {
          painter.setCursor(new Cursor(Cursor.TEXT_CURSOR));
        }
      }
      lastX = me.getX();
    }
  }

  private CompletionPanel suggestion;

  //JEditTextArea textarea;

  /* No longer used
  private void addCompletionPopupListner() {
    this.addKeyListener(new KeyListener() {

      @Override
      public void keyTyped(KeyEvent e) {

      }

      @Override
      public void keyReleased(KeyEvent e) {
        if (Character.isLetterOrDigit(e.getKeyChar())
            || e.getKeyChar() == KeyEvent.VK_BACK_SPACE
            || e.getKeyChar() == KeyEvent.VK_DELETE) {
//          SwingUtilities.invokeLater(new Runnable() {
//            @Override
//            public void run() {
//              showSuggestion();
//            }
//
//          });
        } else if (Character.isWhitespace(e.getKeyChar())
            || e.getKeyChar() == KeyEvent.VK_ESCAPE) {
          hideSuggestion();
        }
      }

      @Override
      public void keyPressed(KeyEvent e) {
      }
    });
  }*/

  
  // appears unused, removed when looking to change completion trigger [fry 140801]
  /*
  public void showSuggestionLater(final DefaultListModel defListModel, final String word) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        showSuggestion(defListModel,word);
      }

    });
  }
  */

  /**
   * Calculates location of caret and displays the suggestion popup at the location. 
   * 
   * @param defListModel
   * @param subWord
   */
  protected void showSuggestion(DefaultListModel<CompletionCandidate> defListModel,String subWord) {
    hideSuggestion();
    if (defListModel.size() == 0) {
      log("TextArea: No suggestions to show.");
      return;
    }
    int position = getCaretPosition();
    Point location = new Point();
    try {
      location.x = offsetToX(getCaretLine(), position
          - getLineStartOffset(getCaretLine()));
      location.y = lineToY(getCaretLine())
          + getPainter().getFontMetrics().getHeight() + getPainter().getFontMetrics().getDescent();
      //log("TA position: " + location);
    } catch (Exception e2) {
      e2.printStackTrace();
      return;
    }

    if (subWord.length() < 2) {
      return;
    }
    //if (suggestion == null)
    suggestion = new CompletionPanel(this, position, subWord, defListModel,
                      location,editor);
//    else
//      suggestion.updateList(defListModel, subWord, location, position);
//    
//    suggestion.setVisible(true);
    requestFocusInWindow();
//    SwingUtilities.invokeLater(new Runnable() {
//      @Override
//      public void run() {
//        requestFocusInWindow();
//      }
//    });
  }

  /**
   * Hides suggestion popup
   */
  protected void hideSuggestion() {
    if (suggestion != null) {
      suggestion.hide();
      //log("Suggestion hidden.");
      suggestion = null;
    }
  }

  // TweakMode code

  // save input listeners to stop/start text edit
	ComponentListener[] prevCompListeners;
	MouseListener[] prevMouseListeners;
	MouseMotionListener[] prevMMotionListeners;
	KeyListener[] prevKeyListeners;

	boolean interactiveMode;

	/* remove all standard interaction listeners */
	public void removeAllListeners()
	{
		ComponentListener[] componentListeners = painter
				.getComponentListeners();
		MouseListener[] mouseListeners = painter.getMouseListeners();
		MouseMotionListener[] mouseMotionListeners = painter
				.getMouseMotionListeners();
		KeyListener[] keyListeners = editor.getKeyListeners();

		for (ComponentListener cl : componentListeners)
			painter.removeComponentListener(cl);

		for (MouseListener ml : mouseListeners)
			painter.removeMouseListener(ml);

		for (MouseMotionListener mml : mouseMotionListeners)
			painter.removeMouseMotionListener(mml);

		for (KeyListener kl : keyListeners) {
			editor.removeKeyListener(kl);
		}
	}

	public void startInteractiveMode()
	{
		// ignore if we are already in interactiveMode
		if (interactiveMode)
			return;

		removeAllListeners();

		// add our private interaction listeners
		customPainter.addMouseListener(customPainter);
		customPainter.addMouseMotionListener(customPainter);
		customPainter.startInterativeMode();
		customPainter.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
		this.editable = false;
		this.caretBlinks = false;
		this.setCaretVisible(false);
		interactiveMode = true;
	}

	public void stopInteractiveMode()
	{
		// ignore if we are not in interactive mode
		if (!interactiveMode)
			return;

		removeAllListeners();
		addPrevListeners();

		customPainter.stopInteractiveMode();
		customPainter.setCursor(new Cursor(Cursor.TEXT_CURSOR));
		this.editable = true;
		this.caretBlinks = true;
		this.setCaretVisible(true);

		interactiveMode = false;
	}

	public int getHorizontalScroll()
	{
		return horizontal.getValue();
	}

	private void addPrevListeners()
	{
		// add the original text-edit listeners
		for (ComponentListener cl : prevCompListeners) {
			customPainter.addComponentListener(cl);
		}
		for (MouseListener ml : prevMouseListeners) {
			customPainter.addMouseListener(ml);
		}
		for (MouseMotionListener mml : prevMMotionListeners) {
			customPainter.addMouseMotionListener(mml);
		}
		for (KeyListener kl : prevKeyListeners) {
			editor.addKeyListener(kl);
		}
	}

	public void updateInterface(ArrayList<Handle> handles[], ArrayList<ColorControlBox> colorBoxes[])
	{
		customPainter.updateInterface(handles, colorBoxes);
	}

}
