/*
 * 06/17/2012
 *
 * ParameterizedCompletionContext.java - Manages the state of parameterized
 * completion-related UI components during code completion.
 *
 * This library is distributed under a modified BSD license.  See the included
 * LICENSE.md file for details.
 */
package org.fife.ui.autocomplete;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Document;
import javax.swing.text.Highlighter;
import javax.swing.text.JTextComponent;
import javax.swing.text.Position;
import javax.swing.text.Highlighter.Highlight;
import javax.swing.text.Highlighter.HighlightPainter;

import org.fife.ui.autocomplete.ParameterizedCompletion.Parameter;
import org.fife.ui.autocomplete.ParameterizedCompletionInsertionInfo.ReplacementCopy;
import org.fife.ui.rsyntaxtextarea.DocumentRange;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rtextarea.ChangeableHighlightPainter;


/**
 * Manages UI and state specific to parameterized completions - the parameter
 * description tool tip, the parameter completion choices list, the actual
 * highlights in the editor, etc.  This component installs new key bindings
 * when appropriate to allow the user to cycle through the parameters of the
 * completion, and optionally cycle through completion choices for those
 * parameters.
 *
 * @author Robert Futrell
 * @version 1.0
 */
class ParameterizedCompletionContext {

	/**
	 * The parent window.
	 */
	private Window parentWindow;

	/**
	 * The parent AutoCompletion instance.
	 */
	private AutoCompletion ac;

	/**
	 * The completion being described.
	 */
	private ParameterizedCompletion pc;

	/**
	 * Whether parameterized completion assistance is active.
	 */
	private boolean active;

	/**
	 * A tool tip displaying the currently edited parameter name and type.
	 */
	private ParameterizedCompletionDescriptionToolTip tip;

	/**
	 * The painter to paint borders around the variables.
	 */
	private Highlighter.HighlightPainter p;

	private Highlighter.HighlightPainter endingP;

	private Highlighter.HighlightPainter paramCopyP;

	/**
	 * The tags for the highlights around parameters.
	 */
	private List<Object> tags;

	private List<ParamCopyInfo> paramCopyInfos;

	private transient boolean ignoringDocumentEvents;

	/**
	 * Listens for events in the text component while this window is visible.
	 */
	private Listener listener;

	/**
	 * The minimum offset into the document that the caret can move to
	 * before this tool tip disappears.
	 */
	private int minPos;

	/**
	 * The maximum offset into the document that the caret can move to
	 * before this tool tip disappears.
	 */
	private Position maxPos; // Moves with text inserted.

	private Position defaultEndOffs;

	/**
	 * The currently "selected" parameter in the displayed text.
	 */
	private int lastSelectedParam;

	/**
	 * A small popup window giving likely choices for parameterized completions.
	 */
	private ParameterizedCompletionChoicesWindow paramChoicesWindow;

	/**
	 * The text before the caret for the current parameter.  If
	 * {@link #paramChoicesWindow} is non-<code>null</code>, this is used to
	 * determine what parameter choices to actually show.
	 */
	private String paramPrefix;

	private Object oldTabKey;
	private Action oldTabAction;
	private Object oldShiftTabKey;
	private Action oldShiftTabAction;
	private Object oldUpKey;
	private Action oldUpAction;
	private Object oldDownKey;
	private Action oldDownAction;
	private Object oldEnterKey;
	private Action oldEnterAction;
	private Object oldEscapeKey;
	private Action oldEscapeAction;
	private Object oldClosingKey;
	private Action oldClosingAction;

	private static final String IM_KEY_TAB = "ParamCompKey.Tab";
	private static final String IM_KEY_SHIFT_TAB = "ParamCompKey.ShiftTab";
	private static final String IM_KEY_UP = "ParamCompKey.Up";
	private static final String IM_KEY_DOWN = "ParamCompKey.Down";
	private static final String IM_KEY_ESCAPE = "ParamCompKey.Escape";
	private static final String IM_KEY_ENTER = "ParamCompKey.Enter";
	private static final String IM_KEY_CLOSING = "ParamCompKey.Closing";


	/**
	 * Constructor.
	 */
	ParameterizedCompletionContext(Window owner,
			AutoCompletion ac, ParameterizedCompletion pc) {

		this.parentWindow = owner;
		this.ac = ac;
		this.pc = pc;
		listener = new Listener();

		AutoCompletionStyleContext sc = AutoCompletion.getStyleContext();
		p = new OutlineHighlightPainter(sc.getParameterOutlineColor());
		endingP = new OutlineHighlightPainter(
				sc.getParameterizedCompletionCursorPositionColor());
		paramCopyP = new ChangeableHighlightPainter(sc.getParameterCopyColor());
		tags = new ArrayList<>(1); // Usually small
		paramCopyInfos = new ArrayList<>(1);

	}


	/**
	 * Activates parameter completion support.
	 *
	 * @see #deactivate()
	 */
	public void activate() {

		if (active) {
			return;
		}

		active = true;
		JTextComponent tc = ac.getTextComponent();
		lastSelectedParam = -1;

		if (pc.getShowParameterToolTip()) {
			tip = new ParameterizedCompletionDescriptionToolTip(
					parentWindow, this, ac, pc);
			try {
				int dot = tc.getCaretPosition();
				Rectangle r = tc.modelToView(dot);
				Point p = new Point(r.x, r.y);
				SwingUtilities.convertPointToScreen(p, tc);
				r.x = p.x;
				r.y = p.y;
				tip.setLocationRelativeTo(r);
				tip.setVisible(true);
			} catch (BadLocationException ble) { // Should never happen
				UIManager.getLookAndFeel().provideErrorFeedback(tc);
				ble.printStackTrace();
				tip = null;
			}
		}

		listener.install(tc);
		// First time through, we'll need to create this window.
		if (paramChoicesWindow==null) {
			paramChoicesWindow = createParamChoicesWindow();
		}
		lastSelectedParam = getCurrentParameterIndex();
		prepareParamChoicesWindow();
		paramChoicesWindow.setVisible(true);

	}


	/**
	 * Creates the completion window offering suggestions for parameters.
	 *
	 * @return The window.
	 */
	private ParameterizedCompletionChoicesWindow createParamChoicesWindow() {
		ParameterizedCompletionChoicesWindow pcw =
			new ParameterizedCompletionChoicesWindow(parentWindow,
														ac, this);
		pcw.initialize(pc);
		return pcw;
	}


	/**
	 * Hides any popup windows and terminates parameterized completion
	 * assistance.
	 *
	 * @see #activate()
	 */
	public void deactivate() {
		if (!active) {
			return;
		}
		active = false;
		listener.uninstall();
		if (tip!=null) {
			tip.setVisible(false);
		}
		if (paramChoicesWindow!=null) {
			paramChoicesWindow.setVisible(false);
		}
	}


	/**
	 * Returns the text inserted for the parameter containing the specified
	 * offset.
	 *
	 * @param offs The offset into the document.
	 * @return The text of the parameter containing the offset, or
	 *         <code>null</code> if the offset is not in a parameter.
	 */
	public String getArgumentText(int offs) {
		List<Highlight> paramHighlights = getParameterHighlights();
		if (paramHighlights==null || paramHighlights.size()==0) {
			return null;
		}
		for (Highlight h : paramHighlights) {
			if (offs>=h.getStartOffset() && offs<=h.getEndOffset()) {
				int start = h.getStartOffset() + 1;
				int len = h.getEndOffset() - start;
				JTextComponent tc = ac.getTextComponent();
				Document doc = tc.getDocument();
				try {
					return doc.getText(start, len);
				} catch (BadLocationException ble) {
					UIManager.getLookAndFeel().provideErrorFeedback(tc);
					ble.printStackTrace();
					return null;
				}
			}
		}
		return null;
	}


	/**
	 * Returns the highlight of the current parameter.
	 *
	 * @return The current parameter's highlight, or <code>null</code> if
	 *         the caret is not in a parameter's bounds.
	 * @see #getCurrentParameterStartOffset()
	 */
	private Highlight getCurrentParameterHighlight() {

		JTextComponent tc = ac.getTextComponent();
		int dot = tc.getCaretPosition();
		if (dot>0) {
			dot--; // Workaround for Java Highlight issues
		}

		List<Highlight> paramHighlights = getParameterHighlights();
		for (Highlight h : paramHighlights) {
			if (dot>=h.getStartOffset() && dot<h.getEndOffset()) {
				return h;
			}
		}

		return null;

	}


	private int getCurrentParameterIndex() {

		JTextComponent tc = ac.getTextComponent();
		int dot = tc.getCaretPosition();
		if (dot>0) {
			dot--; // Workaround for Java Highlight issues
		}

		List<Highlight> paramHighlights = getParameterHighlights();
		for (int i=0; i<paramHighlights.size(); i++) {
			Highlight h = paramHighlights.get(i);
			if (dot>=h.getStartOffset() && dot<h.getEndOffset()) {
				return i;
			}
		}

		return -1;

	}


	/**
	 * Returns the starting offset of the current parameter.
	 *
	 * @return The current parameter's starting offset, or <code>-1</code> if
	 *         the caret is not in a parameter's bounds.
	 * @see #getCurrentParameterHighlight()
	 */
	private int getCurrentParameterStartOffset() {
		Highlight h = getCurrentParameterHighlight();
		return h!=null ? h.getStartOffset()+1 : -1;
	}


	/**
	 * Returns the highlight from a list that comes "first" in a list.  Even
	 * though most parameter highlights are ordered, sometimes they aren't
	 * (e.g. the "cursor" parameter in a template completion is always last,
	 * even though it can be anywhere in the template).
	 *
	 * @param highlights The list of highlights.  Assumed to be non-empty.
	 * @return The highlight that comes first in the document.
	 * @see #getLastHighlight(List)
	 */
	private static int getFirstHighlight(List<Highlight> highlights) {
		int first = -1;
		Highlight firstH = null;
		for (int i=0; i<highlights.size(); i++) {
			Highlight h = highlights.get(i);
			if (firstH==null || h.getStartOffset()<firstH.getStartOffset()) {
				firstH = h;
				first = i;
			}
		}
		return first;
	}


	/**
	 * Returns the highlight from a list that comes "last" in that list.  Even
	 * though most parameter highlights are ordered, sometimes they aren't
	 * (e.g. the "cursor" parameter in a template completion is always last,
	 * even though it can be anywhere in the template).
	 *
	 * @param highlights The list of highlights.  Assumed to be non-empty.
	 * @return The highlight that comes last in the document.
	 * @see #getFirstHighlight(List)
	 */
	private static int getLastHighlight(List<Highlight> highlights) {
		int last = -1;
		Highlight lastH = null;
		for (int i=highlights.size()-1; i>=0; i--) {
			Highlight h = highlights.get(i);
			if (lastH==null || h.getStartOffset()>lastH.getStartOffset()) {
				lastH = h;
				last = i;
			}
		}
		return last;
	}


	public List<Highlight> getParameterHighlights() {
		List<Highlight> paramHighlights = new ArrayList<>(2);
		JTextComponent tc = ac.getTextComponent();
		Highlight[] highlights = tc.getHighlighter().getHighlights();
		for (Highlight highlight : highlights) {
			HighlightPainter painter = highlight.getPainter();
			if (painter == p || painter == endingP) {
				paramHighlights.add(highlight);
			}
		}
		return paramHighlights;
	}


	/**
	 * Inserts the choice selected in the parameter choices window.
	 *
	 * @return Whether the choice was inserted.  This will be <code>false</code>
	 *         if the window is not visible, or no choice is selected.
	 */
	boolean insertSelectedChoice() {
		if (paramChoicesWindow!=null && paramChoicesWindow.isVisible()) {
			String choice = paramChoicesWindow.getSelectedChoice();
			if (choice!=null) {
				JTextComponent tc = ac.getTextComponent();
				Highlight h = getCurrentParameterHighlight();
				if (h!=null) {
					// "+1" is a workaround for Java Highlight issues.
					tc.setSelectionStart(h.getStartOffset()+1);
					tc.setSelectionEnd(h.getEndOffset());
					tc.replaceSelection(choice);
					moveToNextParam();
				}
				else {
					UIManager.getLookAndFeel().provideErrorFeedback(tc);
				}
				return true;
			}
		}
		return false;
	}


	/**
	 * Installs key bindings on the text component that facilitate the user
	 * editing this completion's parameters.
	 *
	 * @see #uninstallKeyBindings()
	 */
	private void installKeyBindings() {

		if (AutoCompletion.getDebug()) {
			System.out.println("CompletionContext: Installing keybindings");
		}

		JTextComponent tc = ac.getTextComponent();
		InputMap im = tc.getInputMap();
		ActionMap am = tc.getActionMap();

		KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
		oldTabKey = im.get(ks);
		im.put(ks, IM_KEY_TAB);
		oldTabAction = am.get(IM_KEY_TAB);
		am.put(IM_KEY_TAB, new NextParamAction());

		ks = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_MASK);
		oldShiftTabKey = im.get(ks);
		im.put(ks, IM_KEY_SHIFT_TAB);
		oldShiftTabAction = am.get(IM_KEY_SHIFT_TAB);
		am.put(IM_KEY_SHIFT_TAB, new PrevParamAction());

		ks = KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0);
		oldUpKey = im.get(ks);
		im.put(ks, IM_KEY_UP);
		oldUpAction = am.get(IM_KEY_UP);
		am.put(IM_KEY_UP, new NextChoiceAction(-1, oldUpAction));

		ks = KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0);
		oldDownKey = im.get(ks);
		im.put(ks, IM_KEY_DOWN);
		oldDownAction = am.get(IM_KEY_DOWN);
		am.put(IM_KEY_DOWN, new NextChoiceAction(1, oldDownAction));

		ks = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
		oldEnterKey = im.get(ks);
		im.put(ks, IM_KEY_ENTER);
		oldEnterAction = am.get(IM_KEY_ENTER);
		am.put(IM_KEY_ENTER, new GotoEndAction());

		ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
		oldEscapeKey = im.get(ks);
		im.put(ks, IM_KEY_ESCAPE);
		oldEscapeAction = am.get(IM_KEY_ESCAPE);
		am.put(IM_KEY_ESCAPE, new HideAction());

		char end = pc.getProvider().getParameterListEnd();
		if (end > 0) {
			ks = KeyStroke.getKeyStroke(end);
			oldClosingKey = im.get(ks);
			im.put(ks, IM_KEY_CLOSING);
			oldClosingAction = am.get(IM_KEY_CLOSING);
			am.put(IM_KEY_CLOSING, new ClosingAction());
		}
	}


	/**
	 * Moves to and selects the next parameter.
	 *
	 * @see #moveToPreviousParam()
	 */
	private void moveToNextParam() {

		JTextComponent tc = ac.getTextComponent();
		int dot = tc.getCaretPosition();
		int tagCount = tags.size();
		if (tagCount==0) {
			tc.setCaretPosition(maxPos.getOffset());
			deactivate();
		}

		Highlight currentNext = null;
		int pos = -1;
		List<Highlight> highlights = getParameterHighlights();
		for (int i=0; i<highlights.size(); i++) {
			Highlight hl = highlights.get(i);
			// Check "< dot", not "<= dot" as OutlineHighlightPainter paints
			// starting at one char AFTER the highlight starts, to work around
			// Java issue.  Thanks to Matthew Adereth!
			if (currentNext==null || currentNext.getStartOffset()</*=*/dot ||
					(hl.getStartOffset()>dot &&
					hl.getStartOffset()<=currentNext.getStartOffset())) {
				currentNext = hl;
				pos = i;
			}
		}

		// No params after caret - go to first one
		if (currentNext.getStartOffset()+1<=dot) {
			int nextIndex = getFirstHighlight(highlights);
			currentNext = highlights.get(nextIndex);
			pos = 0;
		}

		// "+1" is a workaround for Java Highlight issues.
		tc.setSelectionStart(currentNext.getStartOffset()+1);
		tc.setSelectionEnd(currentNext.getEndOffset());
		updateToolTipText(pos);

	}


	/**
	 * Moves to and selects the previous parameter.
	 *
	 * @see #moveToNextParam()
	 */
	private void moveToPreviousParam() {

		JTextComponent tc = ac.getTextComponent();

		int tagCount = tags.size();
		if (tagCount==0) { // Should never happen
			tc.setCaretPosition(maxPos.getOffset());
			deactivate();
		}

		int dot = tc.getCaretPosition();
		int selStart = tc.getSelectionStart()-1; // Workaround for Java Highlight issues.
		Highlight currentPrev = null;
		int pos = 0;
		List<Highlight> highlights = getParameterHighlights();

		for (int i=0; i<highlights.size(); i++) {
			Highlight h = highlights.get(i);
			if (currentPrev==null || currentPrev.getStartOffset()>=dot ||
					(h.getStartOffset()<selStart &&
					(h.getStartOffset()>currentPrev.getStartOffset() ||
							pos==lastSelectedParam))) {
				currentPrev = h;
				pos = i;
			}
		}

		// Loop back from param 0 to last param.
		int firstIndex = getFirstHighlight(highlights);
		//if (pos==0 && lastSelectedParam==0 && highlights.size()>1) {
		if (pos==firstIndex && lastSelectedParam==firstIndex && highlights.size()>1) {
			pos = getLastHighlight(highlights);
			currentPrev = highlights.get(pos);
			// "+1" is a workaround for Java Highlight issues.
			tc.setSelectionStart(currentPrev.getStartOffset()+1);
			tc.setSelectionEnd(currentPrev.getEndOffset());
			updateToolTipText(pos);
		}
		else if (currentPrev!=null && dot>currentPrev.getStartOffset()) {
			// "+1" is a workaround for Java Highlight issues.
			tc.setSelectionStart(currentPrev.getStartOffset()+1);
			tc.setSelectionEnd(currentPrev.getEndOffset());
			updateToolTipText(pos);
		}
		else {
			tc.setCaretPosition(maxPos.getOffset());
			deactivate();
		}

	}


	private void possiblyUpdateParamCopies(Document doc) {

		int index = getCurrentParameterIndex();
		// FunctionCompletions add an extra param at end of inserted text
		if (index>-1 && index<pc.getParamCount()) {

			// Typing in an "end parameter" => stop parameter assistance.
			Parameter param = pc.getParam(index);
			if (param.isEndParam()) {
				deactivate();
				return;
			}

			// Get the current value of the current parameter.
			List<Highlight> paramHighlights = getParameterHighlights();
			Highlight h = paramHighlights.get(index);
			int start = h.getStartOffset() + 1; // param offsets are offset (!) by 1
			int len = h.getEndOffset() - start;
			String replacement = null;
			try {
				replacement = doc.getText(start, len);
			} catch (BadLocationException ble) {
				ble.printStackTrace(); // Never happens
			}

			// Replace any param copies tracking this parameter with the
			// value of this parameter.
			for (ParamCopyInfo pci : paramCopyInfos) {
				if (pci.paramName.equals(param.getName())) {
					pci.h = replaceHighlightedText(doc, pci.h, replacement);
				}
			}

		}

		else { // Probably the "end parameter" for FunctionCompletions.
			deactivate();
		}

	}


	/**
	 * Updates the optional window listing likely completion choices.
	 */
	private void prepareParamChoicesWindow() {

		// If this window was set to null, the user pressed Escape to hide it
		if (paramChoicesWindow!=null) {

			int offs = getCurrentParameterStartOffset();
			if (offs==-1) {
				paramChoicesWindow.setVisible(false);
				return;
			}

			JTextComponent tc = ac.getTextComponent();
			try {
				Rectangle r = tc.modelToView(offs);
				Point p = new Point(r.x, r.y);
				SwingUtilities.convertPointToScreen(p, tc);
				r.x = p.x;
				r.y = p.y;
				paramChoicesWindow.setLocationRelativeTo(r);
			} catch (BadLocationException ble) { // Should never happen
				UIManager.getLookAndFeel().provideErrorFeedback(tc);
				ble.printStackTrace();
			}

			// Toggles visibility, if necessary.
			paramChoicesWindow.setParameter(lastSelectedParam, paramPrefix);

		}

	}


	/**
	 * Removes the bounding boxes around parameters.
	 */
	private void removeParameterHighlights() {
		JTextComponent tc = ac.getTextComponent();
		Highlighter h = tc.getHighlighter();
		for (Object tag : tags) {
			h.removeHighlight(tag);
		}
		tags.clear();
		for (ParamCopyInfo pci : paramCopyInfos) {
			h.removeHighlight(pci.h);
		}
		paramCopyInfos.clear();
	}


	/**
	 * Replaces highlighted text with new text.  Takes special care so that
	 * the highlight stays just around the newly-highlighted text, since
	 * Swing's <code>Highlight</code> classes are funny about insertions at
	 * their start offsets.
	 *
	 * @param doc The document.
	 * @param h The highlight whose text to change.
	 * @param replacement The new text to be in the highlight.
	 * @return The replacement highlight for <code>h</code>.
	 */
	private Highlight replaceHighlightedText(Document doc, Highlight h,
									String replacement) {
		try {

			int start = h.getStartOffset();
			int len = h.getEndOffset() - start;
			Highlighter highlighter = ac.getTextComponent().getHighlighter();
			highlighter.removeHighlight(h);

			if (doc instanceof AbstractDocument) {
				((AbstractDocument)doc).replace(start, len, replacement, null);
			}
			else {
				doc.remove(start, len);
				doc.insertString(start, replacement, null);
			}

			int newEnd = start + replacement.length();
			h = (Highlight)highlighter.addHighlight(start, newEnd, paramCopyP);
			return h;

		} catch (BadLocationException ble) {
			ble.printStackTrace(); // Never happens
		}

		return null;

	}


	/**
	 * Removes the key bindings we installed.
	 *
	 * @see #installKeyBindings()
	 */
	private void uninstallKeyBindings() {

		if (AutoCompletion.getDebug()) {
			System.out.println("CompletionContext Uninstalling keybindings");
		}

		JTextComponent tc = ac.getTextComponent();
		InputMap im = tc.getInputMap();
		ActionMap am = tc.getActionMap();

		KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
		im.put(ks, oldTabKey);
		am.put(IM_KEY_TAB, oldTabAction);

		ks = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_MASK);
		im.put(ks, oldShiftTabKey);
		am.put(IM_KEY_SHIFT_TAB, oldShiftTabAction);

		ks = KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0);
		im.put(ks, oldUpKey);
		am.put(IM_KEY_UP, oldUpAction);

		ks = KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0);
		im.put(ks, oldDownKey);
		am.put(IM_KEY_DOWN, oldDownAction);

		ks = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
		im.put(ks, oldEnterKey);
		am.put(IM_KEY_ENTER, oldEnterAction);

		ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
		im.put(ks, oldEscapeKey);
		am.put(IM_KEY_ESCAPE, oldEscapeAction);

		char end = pc.getProvider().getParameterListEnd();
		if (end > 0) {
			ks = KeyStroke.getKeyStroke(end);
			im.put(ks, oldClosingKey);
			am.put(IM_KEY_CLOSING, oldClosingAction);
		}
	}


	/**
	 * Updates the text in the tool tip to have the current parameter
	 * displayed in bold.  The "current parameter" is determined from the
	 * current caret position.
	 *
	 * @return The "prefix" of text in the caret's parameter before the caret.
	 */
	private String updateToolTipText() {

		JTextComponent tc = ac.getTextComponent();
		int dot = tc.getSelectionStart();
		int mark = tc.getSelectionEnd();
		int index = -1;
		String paramPrefix = null;

		List<Highlight> paramHighlights = getParameterHighlights();
		for (int i=0; i<paramHighlights.size(); i++) {
			Highlight h = paramHighlights.get(i);
			// "+1" because of param hack - see OutlineHighlightPainter
			int start = h.getStartOffset()+1;
			if (dot>=start && dot<=h.getEndOffset()) {
				try {
					// All text selected => offer all suggestions, otherwise
					// use prefix before selection
					if (dot!=start || mark!=h.getEndOffset()) {
						paramPrefix = tc.getText(start, dot-start);
					}
				} catch (BadLocationException ble) {
					ble.printStackTrace();
				}
				index = i;
				break;
			}
		}

		updateToolTipText(index);
		return paramPrefix;

	}


	private void updateToolTipText(int selectedParam) {
		if (selectedParam!=lastSelectedParam) {
			if (tip!=null) {
				tip.updateText(selectedParam);
			}
			this.lastSelectedParam = selectedParam;
		}
	}


	/**
	 * Updates the <code>LookAndFeel</code> of all popup windows this context
	 * manages.
	 */
	public void updateUI() {
		if (tip!=null) {
			tip.updateUI();
		}
		if (paramChoicesWindow!=null) {
			paramChoicesWindow.updateUI();
		}
	}


	/**
	 * Called when the user presses Enter while entering parameters.
	 */
	private class GotoEndAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {

			// If the param choices window is visible and something is chosen,
			// replace the parameter with it and move to the next one.
			if (paramChoicesWindow!=null && paramChoicesWindow.isVisible()) {
				if (insertSelectedChoice()) {
					return;
				}
			}

			// Otherwise, just move to the end.
			deactivate();
			JTextComponent tc = ac.getTextComponent();
			int dot = tc.getCaretPosition();
			if (dot!=defaultEndOffs.getOffset()) {
				tc.setCaretPosition(defaultEndOffs.getOffset());
			}
			else {
				// oldEnterAction isn't what we're looking for (wrong key)
				Action a = getDefaultEnterAction(tc);
				if (a!=null) {
					a.actionPerformed(e);
				}
				else {
					tc.replaceSelection("\n");
				}
			}

		}

		private Action getDefaultEnterAction(JTextComponent tc) {
			ActionMap am = tc.getActionMap();
			return am.get(DefaultEditorKit.insertBreakAction);
		}

	}


	/**
	 * Called when the user types the character marking the closing of the
	 * parameter list, such as '<code>)</code>'.
	 */
	private class ClosingAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {

			JTextComponent tc = ac.getTextComponent();
			int dot = tc.getCaretPosition();
			char end = pc.getProvider().getParameterListEnd();

			// Are they at or past the end of the parameters?
			if (dot>=maxPos.getOffset()-2) { // ">=" for overwrite mode

				// Try to decide if we're closing a paren that is a part
				// of the (last) arg being typed.
				String text = getArgumentText(dot);
				if (text!=null) {
					char start = pc.getProvider().getParameterListStart();
					int startCount = getCount(text, start);
					int endCount = getCount(text, end);
					if (startCount>endCount) { // Just closing a paren
						tc.replaceSelection(Character.toString(end));
						return;
					}
				}
				//tc.setCaretPosition(maxPos.getOffset());
				tc.setCaretPosition(Math.min(tc.getCaretPosition()+1,
						tc.getDocument().getLength()));

				deactivate();

			}

			// If not (in the middle of parameters), just insert the paren.
			else {
				tc.replaceSelection(Character.toString(end));
			}

		}

		public int getCount(String text, char ch) {
			int count = 0;
			int old = 0;
			int pos;
			while ((pos=text.indexOf(ch, old))>-1) {
				count++;
				old = pos + 1;
			}

			return count;
		}

	}


	/**
	 * Action performed when the user hits the escape key.
	 */
	private class HideAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			// On first escape press, if the param choices window is visible,
			// just remove it, but keep ability to tab through params.  If
			// param choices window isn't visible, or second escape press,
			// exit tabbing through params entirely.
			if (paramChoicesWindow!=null && paramChoicesWindow.isVisible()) {
				paramChoicesWindow.setVisible(false);
				paramChoicesWindow = null;
			}
			else {
				deactivate();
			}
		}

	}


	/**
	 * Listens for various events in the text component while this tool tip
	 * is visible.
	 */
	private class Listener implements FocusListener, CaretListener,
							DocumentListener {

		private boolean markOccurrencesEnabled;

		/**
		 * Called when the text component's caret moves.
		 *
		 * @param e The event.
		 */
		@Override
		public void caretUpdate(CaretEvent e) {
			if (maxPos==null) { // Sanity check
				deactivate();
				return;
			}
			int dot = e.getDot();
			if (dot<minPos || dot>maxPos.getOffset()) {
				deactivate();
				return;
			}
			paramPrefix = updateToolTipText();
			if (active) {
				prepareParamChoicesWindow();
			}
		}


		@Override
		public void changedUpdate(DocumentEvent e) {
		}


		/**
		 * Called when the text component gains focus.
		 *
		 * @param e The event.
		 */
		@Override
		public void focusGained(FocusEvent e) {
			// Do nothing
		}


		/**
		 * Called when the text component loses focus.
		 *
		 * @param e The event.
		 */
		@Override
		public void focusLost(FocusEvent e) {
			deactivate();
		}


		private void handleDocumentEvent(final DocumentEvent e) {
			if (!ignoringDocumentEvents) {
				ignoringDocumentEvents = true;
				SwingUtilities.invokeLater(() -> {
					possiblyUpdateParamCopies(e.getDocument());
					ignoringDocumentEvents = false;
				});
			}
		}


		@Override
		public void insertUpdate(DocumentEvent e) {
			handleDocumentEvent(e);
		}


		/**
		 * Installs this listener onto a text component.
		 *
		 * @param tc The text component to install onto.
		 * @see #uninstall()
		 */
		public void install(JTextComponent tc) {

			boolean replaceTabs = false;
			if (tc instanceof RSyntaxTextArea) {
				RSyntaxTextArea textArea = (RSyntaxTextArea)tc;
				markOccurrencesEnabled = textArea.getMarkOccurrences();
				textArea.setMarkOccurrences(false);
				replaceTabs = textArea.getTabsEmulated();
			}

			Highlighter h = tc.getHighlighter();

			try {

				// Insert the parameter text
				ParameterizedCompletionInsertionInfo info =
					pc.getInsertionInfo(tc, replaceTabs);
				tc.replaceSelection(info.getTextToInsert());

				// Add highlights around the parameters.
				final int replacementCount = info.getReplacementCount();
				for (int i=0; i<replacementCount; i++) {
					DocumentRange dr = info.getReplacementLocation(i);
					HighlightPainter painter = i<replacementCount-1 ? p : endingP;
					// "-1" is a workaround for Java Highlight issues.
					tags.add(h.addHighlight(
							dr.getStartOffset()-1, dr.getEndOffset(), painter));
				}
				for (int i=0; i<info.getReplacementCopyCount(); i++) {
					ReplacementCopy rc = info.getReplacementCopy(i);
					paramCopyInfos.add(new ParamCopyInfo(rc.getId(),
						(Highlight)h.addHighlight(rc.getStart(), rc.getEnd(),
								paramCopyP)));
				}

				// Go back and start at the first parameter.
				tc.setCaretPosition(info.getSelectionStart());
				if (info.hasSelection()) {
					tc.moveCaretPosition(info.getSelectionEnd());
				}

				minPos = info.getMinOffset();
				maxPos = info.getMaxOffset();
				try {
					Document doc = tc.getDocument();
					if (maxPos.getOffset()==0) {
						// Positions at offset 0 don't track document changes,
						// so we must manually do this here.  This is not a
						// common occurrence.
						maxPos = doc.createPosition(
								info.getTextToInsert().length());
					}
					defaultEndOffs = doc.createPosition(
							info.getDefaultEndOffs());
				} catch (BadLocationException ble) {
					ble.printStackTrace(); // Never happens
				}

				// Listen for document events AFTER we insert
				tc.getDocument().addDocumentListener(this);

			} catch (BadLocationException ble) {
				ble.printStackTrace(); // Never happens
			}

			// Add listeners to the text component, AFTER text insertion.
			tc.addCaretListener(this);
			tc.addFocusListener(this);
			installKeyBindings();

		}


		@Override
		public void removeUpdate(DocumentEvent e) {
			handleDocumentEvent(e);
		}


		/**
		 * Uninstalls this listener from the current text component.
		 */
		public void uninstall() {

			JTextComponent tc = ac.getTextComponent();
			tc.removeCaretListener(this);
			tc.removeFocusListener(this);
			tc.getDocument().removeDocumentListener(this);
			uninstallKeyBindings();

			if (markOccurrencesEnabled) {
				((RSyntaxTextArea)tc).setMarkOccurrences(markOccurrencesEnabled);
			}

			// Remove WeakReferences in javax.swing.text.
			maxPos = null;
			minPos = -1;
			removeParameterHighlights();

		}


	}


	/**
	 * Action performed when the user presses the up or down arrow keys and
	 * the parameter completion choices popup is visible.
	 */
	private class NextChoiceAction extends AbstractAction {

		private Action oldAction;
		private int amount;

		NextChoiceAction(int amount, Action oldAction) {
			this.amount = amount;
			this.oldAction = oldAction;
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			if (paramChoicesWindow!=null && paramChoicesWindow.isVisible()) {
				paramChoicesWindow.incSelection(amount);
			}
			else if (oldAction!=null) {
				oldAction.actionPerformed(e);
			}
			else {
				deactivate();
			}
		}

	}


	/**
	 * Action performed when the user hits the tab key.
	 */
	private class NextParamAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			moveToNextParam();
		}

	}


	/**
	 * Information about a parameter.
	 */
	private static class ParamCopyInfo {

		private String paramName;
		private Highlight h;

		ParamCopyInfo(String paramName, Highlight h) {
			this.paramName = paramName;
			this.h = h;
		}

	}


	/**
	 * Action performed when the user hits shift+tab.
	 */
	private class PrevParamAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			moveToPreviousParam();
		}

	}


}
