/*
 * 06/17/2012
 *
 * ParameritizedCompletionContext.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
 * RSyntaxTextArea.License.txt file for details.
 */
package com.fr.design.gui.autocomplete;

import com.fr.design.gui.autocomplete.ParameterizedCompletion.Parameter;
import com.fr.design.gui.autocomplete.ParameterizedCompletionInsertionInfo.ReplacementCopy;
import com.fr.design.gui.syntax.ui.rsyntaxtextarea.DocumentRange;
import com.fr.design.gui.syntax.ui.rsyntaxtextarea.RSyntaxTextArea;
import com.fr.design.gui.syntax.ui.rtextarea.ChangeableHighlightPainter;
import com.fr.general.FRLogger;

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.*;
import javax.swing.text.Highlighter.Highlight;
import javax.swing.text.Highlighter.HighlightPainter;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;


/**
 * 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.
     */
    public 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<Object>(1); // Usually small
        paramCopyInfos = new ArrayList<ParamCopyInfo>(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);

                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);

                    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 final 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 final 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<Highlight>(2);
        JTextComponent tc = ac.getTextComponent();
        Highlight[] highlights = tc.getHighlighter().getHighlights();
        for (int i = 0; i < highlights.length; i++) {
            HighlightPainter painter = highlights[i].getPainter();
            if (painter == p || painter == endingP) {
                paramHighlights.add(highlights[i]);
            }
        }
        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.isDebug()) {
            FRLogger.getLogger().debug("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();
        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);
            if (needUpdate(currentNext, hl, dot)) {
                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);

    }

    private boolean needUpdate(Highlight currentNext, Highlight hl, int dot) {
        // Check "< dot", not "<= dot" as OutlineHighlightPainter paints
        // starting at one char AFTER the highlight starts, to work around
        // Java issue.  Thanks to Matthew Adereth!
        return currentNext == null || currentNext.getStartOffset() </*=*/dot ||
                (hl.getStartOffset() > dot &&
                        hl.getStartOffset() <= currentNext.getStartOffset());
    }


    /**
     * 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 (pos == lastSelectedParam || needUpdate(currentPrev, dot, h, selStart)) {
                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 boolean needUpdate(Highlight currentPrev, int dot, Highlight h, int selStart) {
        return currentPrev == null
                || currentPrev.getStartOffset() >= dot
                || (currentPrev.getStartOffset() < h.getStartOffset() && h.getStartOffset() < selStart);
    }


    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) {
                // 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);

            }

            // 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 (int i = 0; i < tags.size(); i++) {
            h.removeHighlight(tags.get(i));
        }
        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) {
            // Never happens
        }

        return null;

    }


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

        if (AutoCompletion.isDebug()) {
            FRLogger.getLogger().debug("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();
        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) {

                }
                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 {

        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 {

        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 = 0;
            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 {

        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.
         */
        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();
            }
        }


        public void changedUpdate(DocumentEvent e) {
        }


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


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


        private void handleDocumentEvent(final DocumentEvent e) {
            if (!ignoringDocumentEvents) {
                ignoringDocumentEvents = true;
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        possiblyUpdateParamCopies(e.getDocument());
                        ignoringDocumentEvents = false;
                    }
                });
            }
        }


        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 {
                    defaultEndOffs = tc.getDocument().createPosition(info.getDefaultEndOffs());
                } catch (BadLocationException ble) {
                    // Never happens
                }
                // Listen for document events AFTER we insert
                tc.getDocument().addDocumentListener(this);
            } catch (BadLocationException ble) {
                // Never happens
            }
            // Add listeners to the text component, AFTER text insertion.
            tc.addCaretListener(this);
            tc.addFocusListener(this);
            installKeyBindings();
        }

        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;

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

        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 {

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

    }


    private static class ParamCopyInfo {

        private String paramName;
        private Highlight h;

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

    }


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

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

    }


}