/**
 * Autocomplete dropdown appears when you press a trigger the editor.
 */
(function() {
    
    tinymce.create('tinymce.plugins.AutoComplete', {
        init : function(ed) {

            ed.addCommand('mceConfAutocompleteLink', function() {
                tinymce.confluence.Autocompleter.Manager.shortcutFired("[");
            });
            ed.addCommand('mceConfAutocompleteImage', function() {
                tinymce.confluence.Autocompleter.Manager.shortcutFired("!");
            });

            ed.addShortcut("ctrl+shift+k", ed.getLang("AutoComplete"), "mceConfAutocompleteLink");
            ed.addShortcut("ctrl+shift+m", ed.getLang("AutoComplete"), "mceConfAutocompleteImage");

            var addAutocompleteHandlers = function (settings) {
                if (settings["confluence.prefs.editor.disable.autocomplete"]) {
                    return;
                }

                AJS.log("Autocomplete enabled, adding keyPress listener");

                // Certain keys prompt the autocomplete, e.g. typing [ goes into "link auto-complete" mode
                ed.onKeyPress.addToTop(tinymce.confluence.Autocompleter.Manager.triggerListener);
            };

            ed.onPostRender.add(function() {
                // The DOM might not necessarily be ready on editor post render (see similar code in the contextmenu plugin)
                AJS.$(function() {
                    if (AJS.params.remoteUser) {
                        AJS.$.getJSON(tinyMCE.settings.plugin_action_base_path + "/get-wysiwyg-settings.action", {}, addAutocompleteHandlers);
                    } else {
                        // Always enabled for anonymous users
                        addAutocompleteHandlers({});
                    }
                });
            });
        },

        getInfo : function() {
            return {
                longname : 'Auto Complete',
                author : 'Atlassian',
                authorurl : 'http://www.atlassian.com',
                version : tinymce.majorVersion + "." + tinymce.minorVersion
            };
        }
    });

    // Register plugin
    tinymce.PluginManager.add('autocomplete', tinymce.plugins.AutoComplete);
})();

tinymce.confluence.Autocompleter = {};

/**
 * Settings that each Autocomplete will be initialized on, depending on the trigger character used to activate the
 * autocomplete.
 */
AJS.toInit(function ($) {

     // TODO - update dropdown.js in AUI to be more flexible and then remove this and use a vertical sprite.
    var dropdownLink = function(text) {
        return "<a href='#'><span class='icon'></span><span><em>" + text + "</em></span></a>";
    },
        // these types match those in the Java Attachment.Type enum
        embeddableAttachmentTypes = ["image","word","excel","pdf","powerpoint"];

tinymce.confluence.Autocompleter.Settings = {

    /**
     * Link settings.
     */
    "[" : {
        ch : "[",

        getHeaderText : function (value) {
            return AJS.I18n.getText("editor.autocomplete.links.header.text");
        },

        getAdditionalLinks : function (value) {
            var searchPrompt;
            if (value) {
                var message = AJS.I18n.getText("editor.autocomplete.links.dialog.search");
                searchPrompt = AJS.format(message, value);
            } else {
                searchPrompt = AJS.I18n.getText("editor.autocomplete.links.dialog.search.no.text");
            }

            return [
                {
                    className: "search-for",
                    name: searchPrompt,
                    href: "#",
                    callback : function (control) {
                        control.replaceWithSelectedSearchText();
                        AJS.Editor.LinkBrowser.open({
                            gwtOpener: AJS.Editor.LinkBrowser.openAndSearch
                        });
                    }
                },
                {
                    className: "dropdown-insert-link",
                    html: dropdownLink(AJS.I18n.getText("editor.autocomplete.links.web.link"))
                }
            ];
        },

        additionalLinkCallback : function (control) {
            control.replaceWithSelectedSearchText();
            AJS.Editor.LinkBrowser.open({
                panelId: AJS.Editor.LinkBrowser.WEBLINK_PANEL_ID
            });
        },

        /**
         * Lists the user's Recent History. The user can make a selection to insert a link to the history item.
         */
        getSuggestionUrl: function () {
            // Only get user session history for logged-in users.
            return AJS.params.remoteUser ? "/rest/prototype/1/session/history.json" : null;
        },

        makeSuggestionParams : function () {
            return {
                maxResults: 5
            };
        },

        makeSearchParams : function (val) {
            return {
                search: "name",
                query: val
            };
        }

    },

    // Image settings
    "!" : {
        ch : "!",

        getHeaderText : function (value) {
            return AJS.I18n.getText("editor.autocomplete.images.header.text");
        },

        getAdditionalLinks : function (value) {
            var linkText = AJS.I18n.getText("editor.autocomplete.images.dialog.browse");
            return [
                {
                    className: "dropdown-insert-image",
                    html: dropdownLink(linkText)
                }
            ];
        },

        additionalLinkCallback : function (control) {
            AJS.Editor.Adapter.storeCurrentSelectionState();
            AJS.Editor.insertImageDialog(function (fileName, params, contentId) {
                AJS.Editor.Adapter.restoreSelectionState();
                var imageProperties = {
                    name : fileName,
                    params : params,
                    ownerId : contentId
                };
                control.update(imageProperties, "dontProcess");
            }, function () {
                AJS.Editor.Adapter.restoreSelectionState();
                control.die();
            });
        },
        getSuggestionUrl: function () {
            var parentId = AJS.params.attachmentSourceContentId || AJS.params.contentId;
            return +parentId ? "/rest/prototype/1/content/" + parentId + "/attachments.json" : null;
        },

        // The data sent in the AJAX request when no text entered.
        makeSuggestionParams : function() {
            return {
                attachmentType: embeddableAttachmentTypes,
                "max-results": 10
            };
        },

        makeSearchParams : function (val) {
            return {
                type: "attachment",
                attachmentType: embeddableAttachmentTypes,
                search: "name",
                query: val
            };
        }
    }
};
});

/**
 * Custom logging function allows for more structured output. log4javascript on the horizon.
 * @param owner the "class" this logger is for
 *
 * Params accepted by the returned log function:
 *  - caller : name of the calling method
 *  - desc : the actual log body
 *  - obj : an object or string to be rendered
 */
tinymce.confluence.Autocompleter.log = function (owner) {
    return function (caller, desc, obj) {
        // Log string objects on the same line, else on the next line
        var objIsStr = (obj != null && typeof obj != "object");
        var objStr = obj != null ? (objIsStr ? (" = " + obj) : " >") : "";
        AJS.log(owner + " - " + caller + " : " + (desc || null) + objStr);
        obj && !objIsStr && AJS.log(obj);
    };
};

/**
 * Given an autocomplete ContentEntityObject's REST data construct the alias, destination and wikimarkup for a link or image.
 *
 * @param data - the content data in REST search format
 * @param isImage - if true, the markup returned will be for an ! image insertion, not a [ link
 *
    {
        "title": "Test Page",
        "type": "page",
        "id": "853651",
        "link": [{
            "rel": "self",
            "href": "http://localhost:8080/confluence/rest/prototype/1/content/853651"
        }, {
            "rel": "alternate",
            "type": "text/html",
            "href": "http://localhost:8080/confluence/display/TST/Test+Page"
        }],
        "spaceKey": "TST",
        "spaceName": "Test Space",
        "wikiLink": "[TST:Test Page]",
    }
 */
AJS.wikiLink = function (data, isImage) {
    var alias = data.title,
        destination = data.wikiLink.slice(1, -1); // remove the [ and ]

    // CONF-18940 strip off the space key and page title if linking to an attachment on the current page
    if (data.type == "attachment" && data.space && data.space.key == AJS.params.spaceKey
     && data.ownerId == AJS.params.attachmentSourceContentId) {
            destination = destination.substring(destination.indexOf("^"));
    }

    var wikiMarkup = (alias != destination ? (alias + "|") : "") + destination;
    AJS.log("tinymce.confluence.Autocompleter.Manager.makeLinkDetails =>" + wikiMarkup);

    return {
        alias : alias,
        destination : destination,
        href : AJS.REST.findLink(data.link),
        wikiMarkup : wikiMarkup
    };
};
/**
 * Selects the word at the cursor and returns the word and the left/top location of the
 * bottom-left corner of the first word.
 *
 * @param options An options map including:
 *     - leadingChar: trigger character used to launch autocomplete
 *     - dontSuggest: Don't search based on text typed in the autocomplete span
 *     - backWords: the number of words to search backwards for
 */
tinymce.confluence.Autocompleter.Control = function(ed, options) {

    var log = tinymce.confluence.Autocompleter.log("Autocompleter.Control");

    /**
     * The Control to be returned.
     */
    var control = {},

        /**
         * This element wraps the search text and the trigger (if present).
         */
        AUTOCOMPLETE_ID = "autocomplete",

        /**
         * This element wraps the trigger character (e.g. @, [, !)
         */
        AUTOCOMPLETE_TRIGGER_ID = "autocomplete-trigger",

        /**
         * This element contains the text the user is searching for - it should always hold the cursor.
         */
        AUTOCOMPLETE_SEARCH_TEXT_ID = "autocomplete-search-text",

        // Used to ensure that a TextNode exists under the search-text span when the ranges are set.
        HIDDEN_CHAR = "\ufeff",

        adaptor = AJS.Editor.Adapter,
        rng = adaptor.getRange(),
        cursorPos = rng.startOffset,
        node = rng.startContainer,
        nodeText = node.nodeValue,
        leadingChar = options.leadingChar,
        selection = ed.selection,
        doc = ed.getDoc(),
        backWords = options.backWords || 0;

    if (AJS.$("#" + AUTOCOMPLETE_ID, doc).length) {
        alert("Autocomplete already exists, returning null.");
        log("init", "Autocomplete already exists, returning null.");
        return null;
    }
    control.backWords = backWords;

    // Cursor may be in a <p> just outside of a TextNode, check for child node at startOffset
    if (nodeText == null && rng.collapsed && cursorPos && node.childNodes[cursorPos - 1]) {
        node = node.childNodes[cursorPos - 1];  // to the LEFT of the cursor
        nodeText = node.nodeValue;
        cursorPos = (nodeText && nodeText.length) || 0;
    }
    var text = (nodeText + "").substring(0, cursorPos),
        pnode = node.previousSibling;
    while (pnode && pnode.nodeType == 3) {
        // add the text from any previous TextNodes
        text = pnode.nodeValue + text;
        pnode = pnode.previousSibling;
    }
    // Disable leading chars at the end of the words e.g. “hi there!” - allowed after &nbsp; (\u00a0)
    if (!backWords && text && !(/["'<\ufeff\u201c\u2018\u2014\(\s\xa0]$/).test(text)) {
        log("init", "Cursor is in wrong location to start autocomplete, returning null.");
        return null;
    }
    if (AJS.$(node).closest("div.code").length) {
        log("init", "Cursor is inside code macro, returning null.");
        return null;
    }
    if (!leadingChar && nodeText == null) {
        log("init", "No text available for suggestion, range is", rng);

        // TODO - handle this (and weird TextNodes)
        nodeText = "";
    }

    // TODO - not this. See http://stackoverflow.com/questions/273789/is-there-a-version-of-javascripts-string-indexof-that-allows-for-regular-expre
    function regexLastIndexOf(str, regex, startpos) {
        !regex.global && (regex = new RegExp(regex.source, "g" + "i".slice(0, regex.ignoreCase) + "m".slice(0, regex.multiLine)));
        if (startpos == null) {
            startpos = str.length;
        } else if (startpos < 0) {
            startpos = 0;
        }
        var stringToWorkWith = str.substring(0, startpos + 1),
            lastIndexOf = -1,
            nextStop = 0;
        while ((result = regex.exec(stringToWorkWith)) != null) {
            lastIndexOf = result.index;
            regex.lastIndex = ++nextStop;
        }
        return lastIndexOf;
    }

    /**
     * Returns a jQuery-wrapped reference to the autocomplete container.
     */
    function getAutocompleteContainer() {
        return control.getSpan();
    }
    control.getSpan = function () {
        return AJS.$("#" + AUTOCOMPLETE_ID, doc);
    };

    /**
     * Starting at the given endpoint, search backward through text nodes until the requested number of words are
     * found.
     * @param node
     * @param offset
     * @param backWords
     */
    function findRangeStart(node, offset, backWords) {
        var nodeText, pNode;

        for (var i = 0; i < backWords; i++) {
            nodeText = node.nodeValue.substring(0, offset);
            offset = regexLastIndexOf(nodeText, (/\s+/), offset);
            while (offset == -1) {
                pNode = node.previousSibling;
                if (pNode && pNode.nodeType == 3) {
                    node = pNode;
                    nodeText = pNode.nodeValue;
                    if (nodeText) {
                        offset = regexLastIndexOf(nodeText, (/\s+/), nodeText.length);
                    }
                } else {
                    i = backWords;  // no point looking further
                    break;
                }
            }
        }

        return {
            node: node,
            offset: offset + 1
        };
    }

    var suggestionHtml = "";
    if (rng.collapsed && backWords && nodeText) {
        var rangeStart = findRangeStart(node, cursorPos, backWords);

        // Select from the cursor back to the start of the first word
        if (tinymce.isIE && backWords == 1) {
            var range = selection.getRng();
            range.moveStart("character", rangeStart.offset - cursorPos);
            range.select();
        } else {
            range = adaptor.createRange();
            range.setStart(rangeStart.node, rangeStart.offset);
            range.setEnd(node, cursorPos);
            selection.setRng(range);
        }
    }
    // Use the existing selection as the search term
    // TODO - html format is failing due to our preProcess on serializer. Fix that.
    suggestionHtml = selection.getContent({format : 'text'});
    log("init", "suggestionHtml", suggestionHtml);

    var el = AJS("span").attr("id", AUTOCOMPLETE_ID);
    if (leadingChar) {
        el.append(AJS("span").attr("id", AUTOCOMPLETE_TRIGGER_ID).text(leadingChar));
    }
    
    var $searchTextSpan = AJS("span").attr("id", AUTOCOMPLETE_SEARCH_TEXT_ID);
    $searchTextSpan.text(HIDDEN_CHAR);
    el.append($searchTextSpan);
    selection.setNode(el[0]);

    // TODO - this == el?
    var autocompleteSpan = getAutocompleteContainer();
    control.previousSearchText = "";
    control.settings = tinymce.confluence.Autocompleter.Settings[leadingChar || "["];  // default to link

    // Put the cursor inside the new span, at the end.
    var searchNode = AJS.$("#" + AUTOCOMPLETE_SEARCH_TEXT_ID, getAutocompleteContainer()),
        searchTextNode = searchNode[0].firstChild,
        cursorPosition = searchTextNode.nodeValue.length,
        selNode = AJS.$(doc.createElement("span")).text(suggestionHtml || HIDDEN_CHAR);
    searchNode.empty().append(selNode);
    selection.select(selNode[0]);
    selection.collapse();

    var position = tinymce.DOM.getPos(autocompleteSpan[0]),
        height = autocompleteSpan.height();
    log("init", "position", position);
    log("init", "pixel offset", autocompleteSpan.offset());


    // Events
    var before = function (e) {
            if (control.onBeforeKey && !control.onBeforeKey(e, control.text())) {
                e.stopPropagation();
                e.preventDefault();
                log("after", "blocked by onBeforeKey");
                return false;
            }
        },
        after = function (e) {
            var rng = adaptor.getRange(),
                span = getAutocompleteContainer(),
                node = rng.startContainer,
                parent = node.parentNode;
            node.nodeType == 3 && (parent = parent.parentNode);
            var grandpa = parent.parentNode,
                outsideSearchSpan = parent != span[0] && grandpa != span[0];
            if (e.keyCode == 27 || outsideSearchSpan) {
                log("after", "dying because of: " + outsideSearchSpan ? "outside search span" : "escape pressed");
                control.die();
            } else if (control.onAfterKey && !control.onAfterKey(e, control.text())) {
                e.stopPropagation();
                e.preventDefault();
                log("after", "blocked by onAfterKey");
                return false;
            }
        },
        press = function (e) {
            if (control.onKeyPress && !control.onKeyPress(e, control.text())) {
                e.stopPropagation();
                e.preventDefault();
                log("after", "blocked by onKeyPress");
                return false;
            }
        },
        click = function (e) {
            if (getAutocompleteContainer()[0] != e.target.parentNode) {
                log("click", "Clicked outside of autocomplete, closing.");
                control.die();
            }
        };
    AJS.$(doc).keydown(before).keyup(after).keypress(press).click(click);


    // For Recent History and certain other searches, ignore the selected text for searching.
    control.word = "";
    if (!options.keepAlias) {
        control.word = suggestionHtml;
    } else {
        log("init", "No suggestion based on previous or selected text");
    }

    control.left = position.x;
    control.top = position.y + height;

    control.text = function (text) {
        var span = AJS.$("#" + AUTOCOMPLETE_SEARCH_TEXT_ID, getAutocompleteContainer());
        if (text != null) {
            span.html(text);
            return this;
        } else {
            text = AJS.escapeEntities(span.text());
            var str = text.replace(HIDDEN_CHAR, "");
//            log("control.text", "str=" + str + ", length=" + str.length);
            return str;
        }
    };

    /**
     * Replaces the autocomplete component with the given text, which may be empty.
     * If the collapse parameter is true, the range will be collapsed at the end of the text.
     * @param text  string to replace autocomplete with
     * @param collapse if true, collapse range to end of text, else select text
     */
    // Make a range to place the cursor at the position where the autocomplete was
    var replaceWithTextAndGetRange = function(text, collapse) {
        var container = getAutocompleteContainer(), rng;
        if (tinymce.isIE) {
            container.before(text);

            rng = ed.getDoc().body.createTextRange();
            rng.moveToElementText(container[0]);
            if (collapse) {
                rng.collapse(false);
            }
            !collapse && rng.moveStart("character", -text.length) && rng.moveEnd("character", -text.length - 1);
            rng.select();
            container.remove();
        } else {
            var parent = container[0].parentNode,
                adapter = AJS.Editor.Adapter,
                cursorPosition = adapter.getChildIndex(parent, container[0]),
                offset = collapse ? 1 : 0; // +!!collapse

            rng = adapter.createRange();
            rng.setStart(parent, cursorPosition + offset);
            rng.setEnd(parent, cursorPosition + 1);

            container.before(text || HIDDEN_CHAR).remove();
            selection.setRng(rng);
        }
        control.die();
        return rng;
    };

    // Used with Link Dialog
    control.replaceWithSelectedSearchText = function () {
        // Get the autocomplete search text and select the entire autocomplete
        var replaceText = control.text();
        log("replaceWithSelectedSearchText", replaceText);
        replaceWithTextAndGetRange(replaceText, false);
        return replaceText;
    };

    control.die = function (notrigger) {
        log("die", "Started");
        if (control.dying) {
            log("die", "Already dying, returning.");
            return;
        } else {
            control.dying = true;
        }

        var container = getAutocompleteContainer();
        if (container.length) {
             log("die", "Tearing down autocomplete, cleaning up autocompleter");

            // Replace autocomplete span with its current text
            var suggestionParent = container.parent()[0],
                adapter = AJS.Editor.Adapter,
                rng = adapter.createRange(),
                cursorPosition = adapter.getChildIndex(suggestionParent, container[0]) + 1,  // AFTER the span
                replaceText = ((notrigger || options.backWords) ? "" : control.settings.ch) + control.text();

                rng = replaceWithTextAndGetRange(replaceText, true);
        } else {
            log("die", "No container to tear down");
        }
        AJS.$(doc).unbind("keydown", before).unbind("keyup", after).unbind("click", click).unbind("keypress", press);
        this.onDeath && this.onDeath();
    };

    /**
    * Pushes the cursor to the correct location.
    */
    // insertionPoint is either a W3C or IE range
    var cleanupAfterInsertion = function (insertionPoint) {
        control.die();
    };

    control.update = function (data, dontProcess) {
        var isImage = (this.settings.ch == "!"),
            linkDetails = dontProcess ? data : AJS.wikiLink(data.restObj, isImage),
            insertionPoint = replaceWithTextAndGetRange("", true),
            insertedNode,
            name = data.restObj && data.restObj.title || data.name;     // data.name may have been HTML-escaped

        if (isImage) {
            if (dontProcess || data.restObj.niceType == "Image") {
                var params = linkDetails.params || {};
                var dest = linkDetails.destination && linkDetails.destination.replace(/^\^/, "") || "";      // leading ^ is not needed for images attached to the current page

                insertedNode = tinyMCE.confImage.Ok(
                        name,
                        params.thumbnail,
                        params.border,
                        params.align ? params.align :   "",
                        null,
                        data.ownerId || data.restObj.ownerId,
                        dest
                );
            } else {
                // Other embeddable content, such as a viewfile macro variant
                AJS.Editor.Adapter.storeCurrentSelectionState();
                var macroName;
                switch (data.restObj.niceType) {
                    case 'PDF Document':            macroName = "viewpdf"; break;
                    case 'Word Document':           macroName = "viewdoc"; break;
                    case 'Excel Spreadsheet':       macroName = "viewxls"; break;
                    case 'PowerPoint Presentation': macroName = "viewppt"; break;
                }
                var spacePage = linkDetails.destination.substring(0, linkDetails.destination.indexOf("^"));
                var macroParams = {
                    page: spacePage,
                    name: name
                };
                AJS.MacroBrowser.Macros.viewdoc.beforeParamsRetrieved(macroParams);  // tweak for macro expected format
                var paramsArr = [];
                for (var key in macroParams) {
                    macroParams[key] && paramsArr.push(key + "=" + macroParams[key]);
                }
                var macroMarkup = "{" + macroName + ":" + paramsArr.join("|") + "}";
                tinymce.confluence.macrobrowser.insertMacroAtSelectionFromMarkup(macroMarkup, cleanupAfterInsertion);
                return;
            }

            // The span is not replaced by confImage.Ok and is still after the image. Select it (to deselect the image)
            // and remove the span.
        } else {
            insertedNode = AJS.Editor.Adapter.insertLink(linkDetails);
        }
        cleanupAfterInsertion(insertionPoint);
    };

    control.removeSpan = function () {
        getAutocompleteContainer().remove();
    };
    return control;
};

tinymce.confluence.Autocompleter.Manager = (function ($) {

    var log = tinymce.confluence.Autocompleter.log("Autocompleter.Manager");

    /**
     * There will only be one control active at a time so a reference to it can be shared across methods.
     */
    var control;

    /**
    *  The quicksearch component that does most of the work.
    */
    var qs;

    /**
     * Creates the param object that will be sent with the quicksearch AJAX request.
     */
    var makeParams = function (val) {
        if (!val) {
            return control.settings.makeSuggestionParams();
        }

        // User typed something, return REST search query params
        return control.settings.makeSearchParams(val);
    };

    /**
     * Creates the URL base (no params) that will be used for the quicksearch AJAX request.
     */
    var getPath = function (c) {

        if (!c.value()) {
            return control.settings.getSuggestionUrl();
        }

        // User typed something, return a REST search query path
        return "/rest/prototype/1/search.json";
    };

    /**
     * Converts an object in REST format into a matrix containing the REST data.
     *
     * @async - called from an AJAX callback method
     * @param restObj object in Confluence REST format
     * @param query the query value searched for
     */
    var makeRestMatrixFromData = function (restObj, query) {
        var restMatrix = [];
        if (!query) {
            var resultArr;
            if (control.settings.ch == "!") {
                resultArr = restObj.attachment;
            } else {
                resultArr = restObj.content;   // "history" has array called "content"
            }
            if (resultArr && resultArr.length)
                restMatrix.push(resultArr);

        } else if (restObj.group) {
            // "search" REST has group object with result array, but it is not the same order as the existing
            // quick nav, so sort into content|attachment|userinfo|spacedesc
            var set = {
                content: [],
                attachment: [],
                userinfo: [],
                spacedesc: []
            };
            for (var i = 0, ii = restObj.group.length;i < ii;i++) {
                var group = restObj.group[i];
                set[group.name] && set[group.name].push(group.result);
            }
            restMatrix = restMatrix.concat(set.content, set.attachment, set.userinfo, set.spacedesc);
        } else {
            log("makeRestMatrixFromData", "WARNING: Unknown rest object", restObj);
        }
        return restMatrix;
    };

    /**
     * Takes the matrix of objects in dropdown format and adds any required items.
     */
    var addDropdownData = function (matrix, c) {

        // Add a header row with a description.
        var value = c.value();
        var newMatrix = [[{
            className: "menu-header",
            name: control.settings.getHeaderText(value),
            href: "#"
        }]];

        // Add the result array, and any additional links
        newMatrix = newMatrix.concat(matrix);
        newMatrix.push(control.settings.getAdditionalLinks(value));

        return newMatrix;
    };


        /**
         * Called when the user hits a key combination at the end of some text to autocomplete.
         * If there is no text at the cursor, the user's Recent History is displayed instead.
         *
         * options include:
         *  - leadingChar - determines the type of autocomplete, e.g. [ , !
         *  - backWords - the number of words to search backwards for
         */
    var startAutoComplete = function (options) {
            log("startAutoComplete", "Started");
            control = tinymce.confluence.Autocompleter.Control(AJS.Editor.Adapter.getEditor(), options);
            if (!control) {
                return false;
            }
            var selectionHandler = function (e, selection) {
                e.preventDefault();
                var result = AJS.$.data(AJS.$("a > span", selection)[0], "properties");
                if (result && typeof result.callback == "function") {
                    result.callback(control);
                } else if (selection.find("span.icon").length) {
                    // TODO - update dropdown.js in AUI to add $.data to li's created with both html and href
                    // For now, check for the icon span added by dropdownLink in autocomplete settings
                    control.settings.additionalLinkCallback(control);
                } else if (result.className != "menu-header") {
                    log("selectionHandler", "Inserting link from dropdown selection");
                    control.update(result);
                }
            };
            var theSpan = control.getSpan(),
                winWidth = AJS.$(window).width();
            qs = AJS.quickSearch({
                path : getPath,
                makeParams : makeParams,
                addDropdownData : addDropdownData,
                makeRestMatrixFromData : makeRestMatrixFromData,
                onShow : function (dropdown) {
                    log("onShow", "Post-processing the dropdown");
                    AJS.$("a.menu-header").closest("ol").addClass("top-menu-item");
                    AJS.$(".aui-shadow").hide();
                    AJS.$("#autocomplete-dropdown ol:empty").hide();
                    var iframe = AJS.$("#wysiwygTextarea_ifr")[0];
                    iframe.shim && iframe.shim.hide();
                    AJS.$("a.menu-header").unbind().click(function (e) {
                        e.preventDefault();
                        control.die();
                    });
                },

                // Add spaces and tooltips in the same way as the content search.
                dropdownPostprocess : AJS.quicksearch.dropdownPostprocess,

                dropdownPlacement : function (dd) {
                    var parent = AJS.$("#autocomplete-dropdown");
                    if (!parent.length) {
                        parent = AJS("div").addClass("aui-dd-parent quick-nav-drop-down").attr("id", "autocomplete-dropdown").appendTo("body");
                    }
                    var offset = AJS.Editor.Adapter.offset(theSpan),
                        overlap = parent.width() + offset.left - winWidth + 10,
                        gapForArrowY = 10,
                        gapForArrowX = 0,
                        top = offset.top + control.getSpan().height() + gapForArrowY,
                        left = offset.left - (overlap > 0 ? overlap : 0) - gapForArrowX;
                    parent.append(dd).css({
                        position: "absolute",
                        top: top,
                        left: left
                    });
                    if (window.Raphael) {
                        if (qs.raphaelArrow) {
                            qs.raphaelArrow.canvas.style.left = offset.left + 4 + "px";
                            qs.raphaelArrow.canvas.style.top = top - 5 + "px";
                        } else {
                            var r = Raphael(offset.left + 4, top - 5, 12, 6);
                            r.path("M0.001,6.001l6.001-6.001,6.001,6.001").attr({
                                fill: "#f0f0f0",
                                stroke: "#bbb"
                            });
                            r.canvas.style.zIndex = 3000;
                            qs.raphaelArrow = r;
                        }
                    }
                },  // just put it at the end of the body, it will be positioned with onShow
                onDeath : function () {
                    AJS.$("#autocomplete-dropdown").remove();
                    qs.raphaelArrow && qs.raphaelArrow.remove && qs.raphaelArrow.remove();
                },
                ajsDropDownOptions: {
                    selectionHandler: selectionHandler,
                    className : "autocomplete"
                }
            });
            qs.focus = function () {
                // qsinput.focus();
                log("focus", "Resetting RTE cursor after autocomplete finished");
            };

            var jumpover = function (current, cdd, dir) {
                current.links[cdd.focused] && current.links[cdd.focused].getElementsByTagName("a")[0].className == "menu-header" && (cdd.focused += dir);
            }, up = -1, down = 1;

            control.onBeforeKey = function (e, text) {
                // TODO - find a way to reuse the existing dropdown.js code.
                if (e.keyCode == 40 || e.keyCode == 38 || e.keyCode == 13) {
                    var current = qs.dd.current,
                        cdd = current.$[0],
                        focus = (isNaN(cdd.focused) ? -1 : cdd.focused);
                    if (focus == -1 && e.keyCode == 13) {  // user hit enter when nothing selected
                        reset();
                        return true;
                    }
                    current.cleanFocus();
                    cdd.focused = focus;
                    if (e.keyCode == 40) {
                        cdd.focused++;
                        jumpover(current, cdd, down);
                    } else if (e.keyCode == 38) {
                        cdd.focused--;
                        jumpover(current, cdd, up);
                    } else {
                        selectionHandler.call(current, e, AJS.$(current.links[cdd.focused]));
                        return false;
                    }
                    if (cdd.focused < 0) {
                        cdd.focused = current.links.length - 1;
                        jumpover(current, cdd, up);
                    }
                    if (cdd.focused > current.links.length - 1) {
                        cdd.focused = 0;
                        jumpover(current, cdd, down);
                    }
                    if (current.links[cdd.focused]) {
                        AJS.$(current.links[cdd.focused]).addClass("active");
                    }
                    if (e.keyCode == 13) {
                        tinymce.dom.Event.cancel(e);
                    }
                    return false;
                }
                if (e.keyCode == 27 || (e.keyCode == 8 && !text)) {
                    // User has key-downed backspace but text is *already* blank - close autocomplete.
                    log("control.onBeforeKey", "killing control and returning false");
                    control.die(e.keyCode == 8);
                    return false;
                }

                return true;
            };
            // Blocker for browser default actions for up and down keys
            control.onKeyPress = function (e, text) {
                if (e.keyCode == 40 || e.keyCode == 38 || e.keyCode == 13) {
                    tinymce.dom.Event.cancel(e);
                    log("control.onKeyPress", "returning false");
                    return false;
                }
                return true;
            };
            var twoLetters = /\S{2,}/;
            control.onAfterKey = function (e, text) {
                if (e.keyCode == 221 && control && control.settings.ch == "[") {
                    // User has key-downed backspace but text is *already* blank - close autocomplete.
                    log("control.onAfterKey", "killing control and returning false");
                    control.die();
                    return false;
                }
                log("onAfterKey", "Change qs to “" + text + "”");
                // User deleted back to zero characters - should display default suggestions again.
                var forceUpdate = (e.keyCode == 8 && !text);
                (forceUpdate || twoLetters.test(text)) && qs.change(text, forceUpdate);
                return true;
            };
            control.onDeath = function () {
                log("onDeath", "control onDeath called");
                if (qs) {
                    qs.die();
                    qs.closing = true;
                }
                AJS.Editor.Adapter.onHideEditor = onHideEditor;
            };


            var onHideEditor = AJS.Editor.Adapter.onHideEditor;
            AJS.Editor.Adapter.onHideEditor = function () {
                onHideEditor();
                reset();
            };
            // Start the dropdown with no text entered, to display the default suggestions.
            qs.change(control.word, "force");
            return true;
        };

    var reset = function () {
        control.die();
        control = null;
    };

    return {

        getQuickSearch: function() {
            return qs;
        },

        // HACK - keyPress used so we can capture composite keystrokes like Sh-2 == @
        triggerListener: function(ed, e) {
            // TODO - broken on Enter after fixing for Backspace
            var returnValue = true,
                ch = AJS.$.browser.msie ? e.keyCode : e.which;

            if (qs) {
                // We need this listener because the control's keypress listener may have been unbound by the
                // control being taken down on enter *keydown*. 
                if (ch == 13) { // enter
                    tinymce.dom.Event.cancel(e);
                    returnValue = false;
                }
            }
            qs && qs.closing && (qs = null);
            if (!returnValue) {
                return false;
            }

            var character = String.fromCharCode(ch);   // charCode back to '@'
            if (!qs && character in tinymce.confluence.Autocompleter.Settings) {
                log("triggerListener", "Auto-complete initiated: trigger is ", character);

                // Add the suggestion span and kill the event - we'll add the letter manually
                startAutoComplete({
                    leadingChar: character
                }) && tinymce.dom.Event.cancel(e);
            }

            return returnValue;
        },

        /**
         * Called when a Ctrl-Sh-K or Ctrl-Sh-M shortcut is fired, selects the previous word.
         *
         * Multiple shortcuts will select more previous words to narrow the search.
         */
        shortcutFired: function(leadingChar) {
            var backWords = 1;
            qs && qs.closing && (qs = null);
            if (qs) {
                backWords = control.backWords + 1;
                log("shortcutFired", "autocomplete active, increasing word selection to: " + backWords);
                // the shortcut itself will be closing the previous autocomplete
                reset();
            }
            return startAutoComplete({
                leadingChar: leadingChar,
                backWords: backWords
            });
        }
    };
})(AJS.$);


