/*
 * 12/21/2008
 *
 * AutoCompletion.java - Handles auto-completion for a text component.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package org.fife.ui.autocomplete;

import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import javax.swing.*;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;


/**
 * Adds auto-completion to a text component.  Provides a popup window with a
 * list of auto-complete choices on a given keystroke, such as Crtrl+Space.<p>
 *
 * Depending on the {@link CompletionProvider} installed, the following
 * auto-completion features may be enabled:
 * 
 * <ul>
 *    <li>An auto-complete choices list made visible via e.g. Ctrl+Space</li>
 *    <li>A "description" window displayed alongside the choices list that
 *        provides documentation on the currently selected completion choice
 *        (as seen in Eclipse and NetBeans).</li>
 *    <li>Parameter assistance.  If this is enabled, if the user enters a
 *        "parameterized" completion, such as a method or a function, then
 *        they will receive a tool tip describing the arguments they have to
 *        enter to the completion.  Also, the arguments can be navigated via
 *        tab and shift+tab (a la Eclipse and NetBeans).</li>
 * </ul>
 *
 * @author Robert Futrell
 * @version 1.0
 */
/*
 * This class handles intercepting window and hierarchy events from the text
 * component, so the popup window is only visible when it should be visible.
 * It also handles communication between the CompletionProvider and the actual
 * popup Window.
 */
public class AutoCompletion {

	/**
	 * The text component we're providing completion for.
	 */
	private JTextComponent textComponent;

	/**
	 * The parent window of {@link #textComponent}.
	 */
	private Window parentWindow;

	/**
	 * The popup window containing completion choices.
	 */
	private AutoCompletePopupWindow popupWindow;

	/**
	 * The preferred size of the completion choices window.  This field exists
	 * because the user will likely set the preferred size of the window
	 * before it is actually created.
	 */
	private Dimension preferredChoicesWindowSize;

	/**
	 * The preferred size of the optional description window.  This field
	 * only exists because the user may (and usually will) set the size of
	 * the description window before it exists (it must be parented to a
	 * Window).
	 */
	private Dimension preferredDescWindowSize;

	/**
	 * Manages any parameterized completions that are inserted.
	 */
	private ParameterizedCompletionContext pcc;

	/**
	 * Provides the completion options relevant to the current caret position.
	 */
	private CompletionProvider provider;

	/**
	 * The renderer to use for the completion choices.  If this is
	 * <code>null</code>, then a default renderer is used.
	 */
	private ListCellRenderer renderer;

	/**
	 * The handler to use when an external URL is clicked in the help
	 * documentation.
	 */
	private ExternalURLHandler externalURLHandler;

	/**
	 * An optional redirector that converts URL's to some other location before
	 * being handed over to <code>externalURLHandler</code>.
	 */
	private static LinkRedirector linkRedirector;

	/**
	 * Whether the description window should be displayed along with the
	 * completion choice window.
	 */
	private boolean showDescWindow;

	/**
	 * Whether auto-complete is enabled.
	 */
	private boolean autoCompleteEnabled;

	/**
	 * Whether the auto-activation of auto-complete (after a delay, after the
	 * user types an appropriate character) is enabled.
	 */
	private boolean autoActivationEnabled;

	/**
	 * Whether or not, when there is only a single auto-complete option
	 * that matches the text at the current text position, that text should
	 * be auto-inserted, instead of the completion window displaying.
	 */
	private boolean autoCompleteSingleChoices;

	/**
	 * Whether parameter assistance is enabled.
	 */
	private boolean parameterAssistanceEnabled;

	/**
	 * A renderer used for {@link Completion}s in the optional parameter
	 * choices popup window (displayed when a {@link ParameterizedCompletion}
	 * is code-completed).  If this isn't set, a default renderer is used.
	 */
	private ListCellRenderer paramChoicesRenderer;

	/**
	 * The keystroke that triggers the completion window.
	 */
	private KeyStroke trigger;

	/**
	 * The previous key in the text component's <code>InputMap</code> for the
	 * trigger key.
	 */
	private Object oldTriggerKey;

	/**
	 * The action previously assigned to {@link #trigger}, so we can reset it
	 * if the user disables auto-completion.
	 */
	private Action oldTriggerAction;

	/**
	 * The previous key in the text component's <code>InputMap</code> for the
	 * parameter completion trigger key.
	 */
	private Object oldParenKey;

	/**
	 * The action previously assigned to the parameter completion key, so we
	 * can reset it when we uninstall.
	 */
	private Action oldParenAction;

	/**
	 * Listens for events in the parent window that affect the visibility of
	 * the popup windows.
	 */
	private ParentWindowListener parentWindowListener;

	/**
	 * Listens for events from the text component that affect the visibility
	 * of the popup windows.
	 */
	private TextComponentListener textComponentListener;

	/**
	 * Listens for events in the text component that cause the popup windows
	 * to automatically activate.
	 */
	private AutoActivationListener autoActivationListener;

	/**
	 * Listens for LAF changes so the auto-complete windows automatically
	 * update themselves accordingly.
	 */
	private LookAndFeelChangeListener lafListener;

	/**
	 * The key used in the input map for the AutoComplete action.
	 */
	private static final String PARAM_TRIGGER_KEY	= "AutoComplete";

	/**
	 * Key used in the input map for the parameter completion action.
	 */
	private static final String PARAM_COMPLETE_KEY = "AutoCompletion.FunctionStart";

	/**
	 * Stores how to render auto-completion-specific highlights in text
	 * components.
	 */
	private static final AutoCompletionStyleContext styleContext =
			new AutoCompletionStyleContext();

	/**
	 * Whether debug messages should be printed to stdout as AutoCompletion
	 * runs.
	 */
	private static final boolean DEBUG = initDebug();


	/**
	 * Constructor.
	 *
	 * @param provider The completion provider.  This cannot be
	 *        <code>null</code>.
	 */
	public AutoCompletion(CompletionProvider provider) {

		setChoicesWindowSize(350, 200);
		setDescriptionWindowSize(350, 250);

		setCompletionProvider(provider);
		setTriggerKey(getDefaultTriggerKey());
		setAutoCompleteEnabled(true);
		setAutoCompleteSingleChoices(true);
		setAutoActivationEnabled(false);
		setShowDescWindow(false);
		parentWindowListener = new ParentWindowListener();
		textComponentListener = new TextComponentListener();
		autoActivationListener = new AutoActivationListener();
		lafListener = new LookAndFeelChangeListener();

	}


	/**
	 * Displays the popup window.  Hosting applications can call this method
	 * to programmatically begin an auto-completion operation.
	 */
	public void doCompletion() {
		refreshPopupWindow();
	}


	/**
	 * Returns the delay between when the user types a character and when the
	 * code completion popup should automatically appear (if applicable).
	 *
	 * @return The delay, in milliseconds.
	 * @see #setAutoActivationDelay(int)
	 */
	public int getAutoActivationDelay() {
		return autoActivationListener.timer.getDelay();
	}


	/**
	 * Returns whether, if a single auto-complete choice is available, it
	 * should be automatically inserted, without displaying the popup menu.
	 *
	 * @return Whether to auto-complete single choices.
	 * @see #setAutoCompleteSingleChoices(boolean)
	 */
	public boolean getAutoCompleteSingleChoices() {
		return autoCompleteSingleChoices;
	}


	/**
	 * Returns the completion provider.
	 *
	 * @return The completion provider.
	 */
	public CompletionProvider getCompletionProvider() {
		return provider;
	}


	/**
	 * Returns whether debug is enabled for AutoCompletion.
	 *
	 * @return Whether debug is enabled.
	 */
	static boolean getDebug() {
		return DEBUG;
	}


	/**
	 * Returns the default auto-complete "trigger key" for this OS.  For
	 * Windows, for example, it is Ctrl+Space.
	 *
	 * @return The default auto-complete trigger key.
	 */
	public static KeyStroke getDefaultTriggerKey() {
		// Default to CTRL, even on Mac, since Ctrl+Space activates Spotlight
		int mask = InputEvent.CTRL_MASK;
		return KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, mask);
	}


	/**
	 * Returns the handler to use when an external URL is clicked in the
	 * description window.
	 *
	 * @return The handler.
	 * @see #setExternalURLHandler(ExternalURLHandler)
	 * @see #getLinkRedirector()
	 */
	public ExternalURLHandler getExternalURLHandler() {
		return externalURLHandler;
	}


	int getLineOfCaret() {
		Document doc = textComponent.getDocument();
		Element root = doc.getDefaultRootElement();
		return root.getElementIndex(textComponent.getCaretPosition());
	}


	/**
	 * Returns the link redirector, if any.
	 *
	 * @return The link redirector, or <code>null</code> if none.
	 * @see #setLinkRedirector(LinkRedirector)
	 */
	public static LinkRedirector getLinkRedirector() {
		return linkRedirector;
	}


	/**
	 * Returns the default list cell renderer used when a completion provider
	 * does not supply its own.
	 *
	 * @return The default list cell renderer.
	 * @see #setListCellRenderer(ListCellRenderer)
	 */
	public ListCellRenderer getListCellRenderer() {
		return renderer;
	}


	/**
	 * Returns the renderer to use for {@link Completion}s in the optional
	 * parameter choices popup window (displayed when a
	 * {@link ParameterizedCompletion} is code-completed).  If this returns
	 * <code>null</code>, a default renderer is used.
	 *
	 * @return The renderer to use.
	 * @see #setParamChoicesRenderer(ListCellRenderer)
	 * @see #isParameterAssistanceEnabled()
	 */
	public ListCellRenderer getParamChoicesRenderer() {
		return paramChoicesRenderer;
	}


	/**
	 * Returns the text to replace with in the document.  This is a
	 * "last-chance" hook for subclasses to make special modifications to the
	 * completion text inserted.  The default implementation simply returns
	 * <tt>c.getReplacementText()</tt>.  You usually will not need to modify
	 * this method.
	 *
	 * @param c The completion being inserted.
	 * @param doc The document being modified.
	 * @param start The start of the text being replaced.
	 * @param len The length of the text being replaced.
	 * @return The text to replace with.
	 */
	protected String getReplacementText(Completion c, Document doc, int start,
										int len) {
		return c.getReplacementText();
	}


	/**
	 * Returns whether the "description window" should be shown alongside
	 * the completion window.
	 *
	 * @return Whether the description window should be shown.
	 * @see #setShowDescWindow(boolean)
	 */
	public boolean getShowDescWindow() {
		return showDescWindow;
	}


	/**
	 * Returns the style context describing how auto-completion related
	 * highlights in the editor are rendered.
	 *
	 * @return The style context.
	 */
	public static AutoCompletionStyleContext getStyleContext() {
		return styleContext;
	}


	/**
	 * Returns the text component for which auto-completion is enabled.
	 *
	 * @return The text component, or <code>null</code> if this
	 *         {@link AutoCompletion} is not installed on any text component.
	 * @see #install(JTextComponent)
	 */
	public JTextComponent getTextComponent() {
		return textComponent;
	}


	/**
	 * Returns the orientation of the text component we're installed to.
	 *
	 * @return The orientation of the text component, or <code>null</code> if
	 *         we are not installed on one.
	 */
	ComponentOrientation getTextComponentOrientation() {
		return textComponent==null ? null :
									textComponent.getComponentOrientation();
	}


	/**
	 * Returns the "trigger key" used for auto-complete.
	 *
	 * @return The trigger key.
	 * @see #setTriggerKey(KeyStroke)
	 */
	public KeyStroke getTriggerKey() {
		return trigger;
	}


	/**
	 * Hides any child windows being displayed by the auto-completion system.
	 *
	 * @return Whether any windows were visible.
	 */
	public boolean hideChildWindows() {
		//return hidePopupWindow() || hideToolTipWindow();
		boolean res = hidePopupWindow();
		res |= hideParameterCompletionPopups();
		return res;
	}


	/**
	 * Hides and disposes of any parameter completion-related popups.
	 *
	 * @return Whether any such windows were visible (and thus hidden).
	 */
	private boolean hideParameterCompletionPopups() {
		if (pcc!=null) {
			pcc.deactivate();
			pcc = null;
			return true;
		}
		return false;
	}


	/**
	 * Hides the popup window, if it is visible.
	 *
	 * @return Whether the popup window was visible.
	 */
	private boolean hidePopupWindow() {
		if (popupWindow!=null) {
			if (popupWindow.isVisible()) {
				popupWindow.setVisible(false);
				return true;
			}
		}
		return false;
	}


	/**
	 * Determines whether debug should be enabled for the AutoCompletion
	 * library.  This method checks a system property, but takes care of
	 * {@link SecurityException}s in case we're in an applet or WebStart.
	 *
	 * @return Whether debug should be enabled.
	 */
	private static final boolean initDebug() {
		boolean debug = false;
		try {
			debug = Boolean.getBoolean("AutoCompletion.debug");
		} catch (SecurityException se) { // We're in an applet or WebStart.
			debug = false;
		}
		return debug;
	}


	/**
	 * Inserts a completion.  Any time a code completion event occurs, the
	 * actual text insertion happens through this method.
	 *
	 * @param c A completion to insert.  This cannot be <code>null</code>.
	 */
	protected final void insertCompletion(Completion c) {
		insertCompletion(c, false);
	}


	/**
	 * Inserts a completion.  Any time a code completion event occurs, the
	 * actual text insertion happens through this method.
	 *
	 * @param c A completion to insert.  This cannot be <code>null</code>.
	 * @param typedParamListStartChar Whether the parameterized completion
	 *        start character was typed (typically <code>'('</code>).
	 */
	protected void insertCompletion(Completion c,
			boolean typedParamListStartChar) {

		JTextComponent textComp = getTextComponent();
		String alreadyEntered = c.getAlreadyEntered(textComp);
		hidePopupWindow();
		Caret caret = textComp.getCaret();

		int dot = caret.getDot();
		int len = alreadyEntered.length();
		int start = dot-len;
		String replacement = getReplacementText(c, textComp.getDocument(),
												start, len);

		caret.setDot(start);
		caret.moveDot(dot);
		textComp.replaceSelection(replacement);

		if (isParameterAssistanceEnabled() &&
				(c instanceof ParameterizedCompletion)) {
			ParameterizedCompletion pc = (ParameterizedCompletion)c;
			startParameterizedCompletionAssistance(pc, typedParamListStartChar);
		}

	}


	/**
	 * Installs this auto-completion on a text component.  If this
	 * {@link AutoCompletion} is already installed on another text component,
	 * it is uninstalled first.
	 *
	 * @param c The text component.
	 * @see #uninstall()
	 */
	public void install(JTextComponent c) {

		if (textComponent!=null) {
			uninstall();
		}

		this.textComponent = c;
		installTriggerKey(getTriggerKey());

		// Install the function completion key, if there is one.
		// NOTE: We cannot do this if the start char is ' ' (e.g. just a space
		// between the function name and parameters) because it overrides
		// RSTA's special space action.  It seems KeyStorke.getKeyStroke(' ')
		// hoses ctrl+space, shift+space, etc., even though I think it
		// shouldn't...
		char start = provider.getParameterListStart();
		if (start!=0 && start!=' ') {
			InputMap im = c.getInputMap();
			ActionMap am = c.getActionMap();
			KeyStroke ks = KeyStroke.getKeyStroke(start);
			oldParenKey = im.get(ks);
			im.put(ks, PARAM_COMPLETE_KEY);
			oldParenAction = am.get(PARAM_COMPLETE_KEY);
			am.put(PARAM_COMPLETE_KEY,
							new ParameterizedCompletionStartAction(start));
		}

		textComponentListener.addTo(this.textComponent);
		// In case textComponent is already in a window...
		textComponentListener.hierarchyChanged(null);

		if (isAutoActivationEnabled()) {
			autoActivationListener.addTo(this.textComponent);
		}

		UIManager.addPropertyChangeListener(lafListener);
		updateUI(); // In case there have been changes since we uninstalled

	}


	/**
	 * Installs a "trigger key" action onto the current text component.
	 *
	 * @param ks The keystroke that should trigger the action.
	 * @see #uninstallTriggerKey() 
	 */
	private void installTriggerKey(KeyStroke ks) {
		InputMap im = textComponent.getInputMap();
		oldTriggerKey = im.get(ks);
		im.put(ks, PARAM_TRIGGER_KEY);
		ActionMap am = textComponent.getActionMap();
		oldTriggerAction = am.get(PARAM_TRIGGER_KEY);
		am.put(PARAM_TRIGGER_KEY, new AutoCompleteAction());
	}


	/**
	 * Returns whether auto-activation is enabled (that is, whether the
	 * completion popup will automatically appear after a delay when the user
	 * types an appropriate character).  Note that this parameter will be
	 * ignored if auto-completion is disabled.
	 *
	 * @return Whether auto-activation is enabled.
	 * @see #setAutoActivationEnabled(boolean)
	 * @see #getAutoActivationDelay()
	 * @see #isAutoCompleteEnabled()
	 */
	public boolean isAutoActivationEnabled() {
		return autoActivationEnabled;
	}


	/**
	 * Returns whether auto-completion is enabled.
	 *
	 * @return Whether auto-completion is enabled.
	 * @see #setAutoCompleteEnabled(boolean)
	 */
	public boolean isAutoCompleteEnabled() {
		return autoCompleteEnabled;
	}


	/**
	 * Returns whether parameter assistance is enabled.
	 *
	 * @return Whether parameter assistance is enabled.
	 * @see #setParameterAssistanceEnabled(boolean)
	 */
	public boolean isParameterAssistanceEnabled() {
		return parameterAssistanceEnabled;
	}


	/**
	 * Returns whether the popup window is visible.
	 *
	 * @return Whether the popup window is visible.
	 */
	private boolean isPopupVisible() {
		return popupWindow!=null && popupWindow.isVisible();
	}


	/**
	 * Refreshes the popup window.  First, this method gets the possible
	 * completions for the current caret position.  If there are none, and the
	 * popup is visible, it is hidden.  If there are some completions and the
	 * popup is hidden, it is made visible and made to display the completions.
	 * If there are some completions and the popup is visible, its list is
	 * updated to the current set of completions.
	 *
	 * @return The current line number of the caret.
	 */
	protected int refreshPopupWindow() {

		// A return value of null => don't suggest completions
		String text = provider.getAlreadyEnteredText(textComponent);
		if (text==null && !isPopupVisible()) {
			return getLineOfCaret();
		}

		// If the popup is currently visible, and they type a space (or any
		// character that resets the completion list to "all completions"),
		// the popup window should be hidden instead of being reset to show
		// everything.
		int textLen = text==null ? 0 : text.length();
		if (textLen==0) {
			if (isPopupVisible()) {
				hidePopupWindow();
				return getLineOfCaret();
			}
		}

		final List<Completion> completions = provider.
										getCompletions(textComponent);
		int count = completions.size();

		if (count>1 || (count==1 && (isPopupVisible() || textLen==0)) ||
				(count==1 && !getAutoCompleteSingleChoices())) {

			if (popupWindow==null) {
				popupWindow = new AutoCompletePopupWindow(parentWindow, this);
				// Completion is usually done for code, which is always done
				// LTR, so make completion stuff RTL only if text component is
				// also RTL.
				popupWindow.applyComponentOrientation(
											getTextComponentOrientation());
				if (renderer!=null) {
					popupWindow.setListCellRenderer(renderer);
				}
				if (preferredChoicesWindowSize!=null) {
					popupWindow.setSize(preferredChoicesWindowSize);
				}
				if (preferredDescWindowSize!=null) {
					popupWindow.setDescriptionWindowSize(
												preferredDescWindowSize);
				}
			}

			popupWindow.setCompletions(completions);

			if (!popupWindow.isVisible()) {
				Rectangle r = null;
				try {
					r = textComponent.modelToView(textComponent.
														getCaretPosition());
				} catch (BadLocationException ble) {
					ble.printStackTrace();
					return -1;
				}
				Point p = new Point(r.x, r.y);
				SwingUtilities.convertPointToScreen(p, textComponent);
				r.x = p.x;
				r.y = p.y;
				popupWindow.setLocationRelativeTo(r);
				popupWindow.setVisible(true);
			}

		}

		else if (count==1) { // !isPopupVisible && autoCompleteSingleChoices
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					insertCompletion(completions.get(0));
				}
			});
		}

		else {
			hidePopupWindow();
		}

		return getLineOfCaret();

	}


	/**
	 * Sets the delay between when the user types a character and when the
	 * code completion popup should automatically appear (if applicable).
	 *
	 * @param ms The delay, in milliseconds.  This should be greater than zero.
	 * @see #getAutoActivationDelay()
	 */
	public void setAutoActivationDelay(int ms) {
		ms = Math.max(0, ms);
		autoActivationListener.timer.stop();
		autoActivationListener.timer.setInitialDelay(ms);
	}


	/**
	 * Toggles whether auto-activation is enabled.  Note that auto-activation
	 * also depends on auto-completion itself being enabled.
	 *
	 * @param enabled Whether auto-activation is enabled.
	 * @see #isAutoActivationEnabled()
	 * @see #setAutoActivationDelay(int)
	 */
	public void setAutoActivationEnabled(boolean enabled) {
		if (enabled!=autoActivationEnabled) {
			autoActivationEnabled = enabled;
			if (textComponent!=null) {
				if (autoActivationEnabled) {
					autoActivationListener.addTo(textComponent);
				}
				else {
					autoActivationListener.removeFrom(textComponent);
				}
			}
		}
	}


	/**
	 * Sets whether auto-completion is enabled.
	 *
	 * @param enabled Whether auto-completion is enabled.
	 * @see #isAutoCompleteEnabled()
	 */
	public void setAutoCompleteEnabled(boolean enabled) {
		if (enabled!=autoCompleteEnabled) {
			autoCompleteEnabled = enabled;
			hidePopupWindow();
		}
	}


	/**
	 * Sets whether, if a single auto-complete choice is available, it should
	 * be automatically inserted, without displaying the popup menu.
	 *
	 * @param autoComplete Whether to auto-complete single choices.
	 * @see #getAutoCompleteSingleChoices()
	 */
	public void setAutoCompleteSingleChoices(boolean autoComplete) {
		autoCompleteSingleChoices = autoComplete;
	}


	/**
	 * Sets the completion provider being used.
	 *
	 * @param provider The new completion provider.  This cannot be
	 *        <code>null</code>.
	 * @throws IllegalArgumentException If <code>provider</code> is
	 *         <code>null</code>.
	 */
	public void setCompletionProvider(CompletionProvider provider) {
		if (provider==null) {
			throw new IllegalArgumentException("provider cannot be null");
		}
		this.provider = provider;
		hidePopupWindow(); // In case new choices should be displayed.
	}


	/**
	 * Sets the size of the completion choices window.
	 *
	 * @param w The new width.
	 * @param h The new height.
	 * @see #setDescriptionWindowSize(int, int)
	 */
	public void setChoicesWindowSize(int w, int h) {
		preferredChoicesWindowSize = new Dimension(w, h);
		if (popupWindow!=null) {
			popupWindow.setSize(preferredChoicesWindowSize);
		}
	}


	/**
	 * Sets the size of the description window.
	 *
	 * @param w The new width.
	 * @param h The new height.
	 * @see #setChoicesWindowSize(int, int)
	 */
	public void setDescriptionWindowSize(int w, int h) {
		preferredDescWindowSize = new Dimension(w, h);
		if (popupWindow!=null) {
			popupWindow.setDescriptionWindowSize(preferredDescWindowSize);
		}
	}


	/**
	 * Sets the handler to use when an external URL is clicked in the
	 * description window.  This handler can perform some action, such as
	 * open the URL in a web browser.  The default implementation will open
	 * the URL in a browser, but only if running in Java 6.  If you want
	 * browser support for Java 5 and below, or otherwise want to respond to
	 * hyperlink clicks, you will have to install your own handler to do so.
	 *
	 * @param handler The new handler.
	 * @see #getExternalURLHandler()
	 */
	public void setExternalURLHandler(ExternalURLHandler handler) {
		this.externalURLHandler = handler;
	}


	/**
	 * Sets the redirector for external URL's found in code completion
	 * documentation.  When a non-local link in completion popups is clicked,
	 * this redirector is given the chance to modify the URL fetched and
	 * displayed.
	 *
	 * @param linkRedirector The link redirector, or <code>null</code> for
	 *        none.
	 * @see #getLinkRedirector()
	 */
	public static void setLinkRedirector(LinkRedirector linkRedirector) {
		AutoCompletion.linkRedirector = linkRedirector;
	}


	/**
	 * Sets the default list cell renderer to use when a completion provider
	 * does not supply its own.
	 *
	 * @param renderer The renderer to use.  If this is <code>null</code>,
	 *        a default renderer is used.
	 * @see #getListCellRenderer()
	 */
	public void setListCellRenderer(ListCellRenderer renderer) {
		this.renderer = renderer;
		if (popupWindow!=null) {
			popupWindow.setListCellRenderer(renderer);
			hidePopupWindow();
		}
	}


	/**
	 * Sets the renderer to use for {@link Completion}s in the optional
	 * parameter choices popup window (displayed when a
	 * {@link ParameterizedCompletion} is code-completed).  If this isn't set,
	 * a default renderer is used.
	 *
	 * @param r The renderer to use.
	 * @see #getParamChoicesRenderer()
	 * @see #setParameterAssistanceEnabled(boolean)
	 */
	public void setParamChoicesRenderer(ListCellRenderer r) {
		paramChoicesRenderer = r;
	}


	/**
	 * Sets whether parameter assistance is enabled.  If parameter assistance
	 * is enabled, and a "parameterized" completion (such as a function or
	 * method) is inserted, the user will get "assistance" in inserting the
	 * parameters in the form of a popup window with documentation and easy
	 * tabbing through the arguments (as seen in Eclipse and NetBeans).
	 *
	 * @param enabled Whether parameter assistance should be enabled.
	 * @see #isParameterAssistanceEnabled()
	 */
	public void setParameterAssistanceEnabled(boolean enabled) {
		parameterAssistanceEnabled = enabled;
	}


	/**
	 * Sets whether the "description window" should be shown beside the
	 * completion window.
	 *
	 * @param show Whether to show the description window.
	 * @see #getShowDescWindow()
	 */
	public void setShowDescWindow(boolean show) {
		hidePopupWindow(); // Needed to force it to take effect
		showDescWindow = show;
	}


	/**
	 * Sets the keystroke that should be used to trigger the auto-complete
	 * popup window.
	 *
	 * @param ks The keystroke.
	 * @throws IllegalArgumentException If <code>ks</code> is <code>null</code>.
	 * @see #getTriggerKey()
	 */
	public void setTriggerKey(KeyStroke ks) {
		if (ks==null) {
			throw new IllegalArgumentException("trigger key cannot be null");
		}
		if (!ks.equals(trigger)) {
			if (textComponent!=null) {
				// Put old trigger action back.
				uninstallTriggerKey();
				// Grab current action for new trigger and replace it.
				installTriggerKey(ks);
			}
			trigger = ks;
		}
	}


	/**
	 * Displays a "tool tip" detailing the inputs to the function just entered.
	 *
	 * @param pc The completion.
	 * @param typedParamListStartChar Whether the parameterized completion list
	 *        starting character was typed.
	 */
	private void startParameterizedCompletionAssistance(
				ParameterizedCompletion pc, boolean typedParamListStartChar) {

		// Get rid of the previous tool tip window, if there is one.
		hideParameterCompletionPopups();

		// Don't bother with a tool tip if there are no parameters, but if
		// they typed e.g. the opening '(', make them overtype the ')'.
		if (pc.getParamCount()==0 && !(pc instanceof TemplateCompletion)) {
			CompletionProvider p = pc.getProvider();
			char end = p.getParameterListEnd(); // Might be '\0'
			String text = end=='\0' ? "" : Character.toString(end);
			if (typedParamListStartChar) {
				String template = "${}" + text + "${cursor}";
				textComponent.replaceSelection(Character.toString(p.getParameterListStart()));
				TemplateCompletion tc = new TemplateCompletion(p, null, null,  template);
				pc = tc;
			}
			else {
				text = p.getParameterListStart() + text;
				textComponent.replaceSelection(text);
				return;
			}
		}

		pcc = new ParameterizedCompletionContext(parentWindow, this, pc);
		pcc.activate();

	}


	/**
	 * Uninstalls this auto-completion from its text component.  If it is not
	 * installed on any text component, nothing happens.
	 *
	 * @see #install(JTextComponent)
	 */
	public void uninstall() {

		if (textComponent!=null) {

			hidePopupWindow(); // Unregisters listeners, actions, etc.

			uninstallTriggerKey();

			// Uninstall the function completion key.
			char start = provider.getParameterListStart();
			if (start!=0) {
				KeyStroke ks = KeyStroke.getKeyStroke(start);
				InputMap im = textComponent.getInputMap();
				im.put(ks, oldParenKey);
				ActionMap am = textComponent.getActionMap();
				am.put(PARAM_COMPLETE_KEY, oldParenAction);
			}

			textComponentListener.removeFrom(textComponent);
			if (parentWindow!=null) {
				parentWindowListener.removeFrom(parentWindow);
			}

			if (isAutoActivationEnabled()) {
				autoActivationListener.removeFrom(textComponent);
			}

			UIManager.removePropertyChangeListener(lafListener);

			textComponent = null;
			popupWindow = null;

		}

	}


	/**
	 * Replaces the "trigger key" action with the one that was there
	 * before auto-completion was installed.
	 *
	 * @see #installTriggerKey(KeyStroke)
	 */
	private void uninstallTriggerKey() {
		InputMap im = textComponent.getInputMap();
		im.put(trigger, oldTriggerKey);
		ActionMap am = textComponent.getActionMap();
		am.put(PARAM_TRIGGER_KEY, oldTriggerAction);
	}


	/**
	 * Updates the LookAndFeel of the popup window.  Applications can call
	 * this method as appropriate if they support changing the LookAndFeel
	 * at runtime.
	 */
	private void updateUI() {
		if (popupWindow!=null) {
			popupWindow.updateUI();
		}
		if (pcc!=null) {
			pcc.updateUI();
		}
		// Will practically always be a JComponent (a JLabel)
		if (paramChoicesRenderer instanceof JComponent) {
			((JComponent)paramChoicesRenderer).updateUI();
		}
	}


	/**
	 * Listens for events in the text component to auto-activate the code
	 * completion popup.
	 */
	private class AutoActivationListener extends FocusAdapter
				implements DocumentListener, CaretListener, ActionListener {

		private Timer timer;
		private boolean justInserted;

		public AutoActivationListener() {
			timer = new Timer(200, this);
			timer.setRepeats(false);
		}

		public void actionPerformed(ActionEvent e) {
			doCompletion();
		}

		public void addTo(JTextComponent tc) {
			tc.addFocusListener(this);
			tc.getDocument().addDocumentListener(this);
			tc.addCaretListener(this);
		}

		public void caretUpdate(CaretEvent e) {
			if (justInserted) {
				justInserted = false;
			}
			else {
				timer.stop();
			}
		}

		public void changedUpdate(DocumentEvent e) {
			 // Ignore
		}

		@Override
		public void focusLost(FocusEvent e) {
			timer.stop();
			//hideChildWindows(); Other listener will do this
		}

		public void insertUpdate(DocumentEvent e) {
			justInserted = false;
			if (isAutoCompleteEnabled() && isAutoActivationEnabled() &&
					e.getLength()==1) {
				if (provider.isAutoActivateOkay(textComponent)) {
					timer.restart();
					justInserted = true;
				}
				else {
					timer.stop();
				}
			}
			else {
				timer.stop();
			}
		}

		public void removeFrom(JTextComponent tc) {
			tc.removeFocusListener(this);
			tc.getDocument().removeDocumentListener(this);
			tc.removeCaretListener(this);
			timer.stop();
			justInserted = false;
		}

		public void removeUpdate(DocumentEvent e) {
			timer.stop();
		}

	}


	/**
	 * The <code>Action</code> that displays the popup window if
	 * auto-completion is enabled.
	 */
	private class AutoCompleteAction extends AbstractAction {

		public void actionPerformed(ActionEvent e) {
			if (isAutoCompleteEnabled()) {
				refreshPopupWindow();
			}
			else if (oldTriggerAction!=null) {
				oldTriggerAction.actionPerformed(e);
			}
		}

	}


	/**
	 * Listens for LookAndFeel changes and updates the various popup windows
	 * involved in auto-completion accordingly.
	 */
	private class LookAndFeelChangeListener implements PropertyChangeListener {

		public void propertyChange(PropertyChangeEvent e) {
			String name = e.getPropertyName();
			if ("lookAndFeel".equals(name)) {
				updateUI();
			}
		}

	}


	/**
	 * Action that starts a parameterized completion, e.g. after '(' is
	 * typed.
	 */
	private class ParameterizedCompletionStartAction extends AbstractAction {

		private String start;

		public ParameterizedCompletionStartAction(char ch) {
			this.start = Character.toString(ch);
		}

		public void actionPerformed(ActionEvent e) {

			// Prevents keystrokes from messing up
			boolean wasVisible = hidePopupWindow();

			// Only proceed if they were selecting a completion
			if (!wasVisible || !isParameterAssistanceEnabled()) {
				textComponent.replaceSelection(start);
				return;
			}

			Completion c = popupWindow.getSelection();
			if (c instanceof ParameterizedCompletion) { // Should always be true
				// Fixes capitalization of the entered text.
				insertCompletion(c, true);
			}

		}

	}


	/**
	 * Listens for events in the parent window of the text component with
	 * auto-completion enabled.
	 */
	private class ParentWindowListener extends ComponentAdapter
									implements WindowFocusListener {

		public void addTo(Window w) {
			w.addComponentListener(this);
			w.addWindowFocusListener(this);
		}

		@Override
		public void componentHidden(ComponentEvent e) {
			hideChildWindows();
		}

		@Override
		public void componentMoved(ComponentEvent e) {
			hideChildWindows();
		}

		@Override
		public void componentResized(ComponentEvent e) {
			hideChildWindows();
		}

		public void removeFrom(Window w) {
			w.removeComponentListener(this);
			w.removeWindowFocusListener(this);
		}

		public void windowGainedFocus(WindowEvent e) {
		}

		public void windowLostFocus(WindowEvent e) {
			hideChildWindows();
		}

	}


	/**
	 * Listens for events from the text component we're installed on.
	 */
	private class TextComponentListener extends FocusAdapter
				implements HierarchyListener {

		void addTo(JTextComponent tc) {
			tc.addFocusListener(this);
			tc.addHierarchyListener(this);
		}

		/**
		 * Hide the auto-completion windows when the text component loses
		 * focus.
		 */
		@Override
		public void focusLost(FocusEvent e) {
			hideChildWindows();
		}

		/**
		 * Called when the component hierarchy for our text component changes.
		 * When the text component is added to a new {@link Window}, this
		 * method registers listeners on that <code>Window</code>.
		 *
		 * @param e The event.
		 */
		public void hierarchyChanged(HierarchyEvent e) {

			// NOTE: e many be null as we call this method at other times.
			//System.out.println("Hierarchy changed! " + e);

			Window oldParentWindow = parentWindow;
			parentWindow = SwingUtilities.getWindowAncestor(textComponent);
			if (parentWindow!=oldParentWindow) {
				if (oldParentWindow!=null) {
					parentWindowListener.removeFrom(oldParentWindow);
				}
				if (parentWindow!=null) {
					parentWindowListener.addTo(parentWindow);
				}
			}

		}

		public void removeFrom(JTextComponent tc) {
			tc.removeFocusListener(this);
			tc.removeHierarchyListener(this);
		}

	}


}