AJS.log("autocomplete editor_plugin_src starting");
/**
 * Autocomplete dropdown appears when you press a trigger character the editor.
 */
(function() {
    if (AJS.Meta.get('remote-user') && AJS.Meta.get('confluence.prefs.editor.disable.autocomplete')) {
        return; //autocomplete enabled by default and for anon-users
    }

    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");

            ed.onPostRender.add(function() {
                // The DOM might not necessarily be ready on editor post render (see similar code in the contextmenu plugin)
                AJS.$(function() {
                    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);
                    AJS.trigger('ready.editor.autocomplete');
                });
            });

            // CONFDEV-3649 - Handle undo/redo correctly - reattach autocomplete, if needed
            ed.onUndo.add(tinymce.confluence.Autocompleter.Manager.reattach);
            ed.onRedo.add(tinymce.confluence.Autocompleter.Manager.reattach);
        },

        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);
})();

AJS.log("tinyMce-autocomplete-settings initialising");
tinymce.confluence.Autocompleter = {
    Settings : {},

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

AJS.log("tinyMce-autocomplete-util starting");
tinymce.confluence.Autocompleter.Util = (function() {

    var loadData = function (json, query, callback, field, getRestSpecificAdditionLinks) {
        var hasErrors = json.statusMessage;
        var matrix;
        if (hasErrors) {
           matrix = [[{html: json.statusMessage, className: "error"}]];
        } else {
            var restMatrix = query ? AJS.REST.makeRestMatrixFromSearchData(json) : AJS.REST.makeRestMatrixFromData(json, field);
            matrix = AJS.REST.convertFromRest(restMatrix);
        }
        // do conversion
        function restSpecificAdditionLinksCallback(value, additionalLinks) {
            if (getRestSpecificAdditionLinks && typeof getRestSpecificAdditionLinks == "function") {
                getRestSpecificAdditionLinks(matrix, value, additionalLinks)
            }
        }
        callback(matrix, query, restSpecificAdditionLinksCallback);
    };

    return {
        /**
         * Returns the HTML of a AJS.dropdown link with an icon span. The icon span is required in the dropdown if we
         * want to use a sprite background for the link icon.
         * @param text escaped text of the dropdown item
         * @param className class name to be added to the link
         * @param iconClass class name to be added on the icon span
         * @return HTML string for the dropdown link
         */
        // we should remove this once AUI dropdown supports sprite icons
        dropdownLink : function(text, className, iconClass) {
            return "<a href='#' class='" + (className || "" ) + "'><span class='icon " + (iconClass || "") + "'></span><span>" + text + "</span></a>";
        },

        getRestData : function (autoCompleteControl, getUrl, getParams, val, callback, suggestionField, getRestSpecificAdditionLinks) {
            var url = getUrl(val);
            if (url) {
                AJS.$.ajax({
                    type: "GET",
                    url: url,
                    data: getParams(autoCompleteControl, val),
                    success: function (json) {
                        loadData.call(autoCompleteControl, json, val, callback, suggestionField, getRestSpecificAdditionLinks);
                    },
                    dataType: "json",
                    global: false,
                    timeout: 5000,
                    error: function (xml, status) { // ajax error handler
                        if (status == "timeout") {
                            loadData.call(autoCompleteControl, {statusMessage: "Timeout", query: val}, val, callback, suggestionField);
                        }
                    }
                });
            } else {
                // If no url, default items may be displayed - run the callback with no data.
                callback([], val);
            }
        }

    };

})(AJS.$);
AJS.log("tinyMce-autocomplete-control starting");

(function($) {

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

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

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

/**
 * 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 = {},
        adaptor = AJS.Editor.Adapter,
        selection = ed.selection,
        rng = selection.getRng(true),
        cursorPos = rng.startOffset,
        node = rng.startContainer,
        nodeText = node.nodeValue,
        leadingChar = options.leadingChar,
        doc = ed.getDoc(),
        backWords = options.backWords || 0;

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

    // 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 = "";
    // Cursor may still not be in a Text node, in which leave the text empty.
    if (nodeText != null) {
        text = (nodeText + "").substring(0, cursorPos);
        var pnode = node.previousSibling;
        while (pnode && pnode.nodeType == 3) {
            // add the text from any previous TextNodes
            text = pnode.nodeValue + text;
            pnode = pnode.previousSibling;
        }
    }
    // Disable trigger chars at the end of the certain strings e.g. “Hi there!”.
    // The regex allows "'<( left" and left' before the [ trigger and space, zero-width space, &nbsp; and em-dash
    // before all triggers.
    if (!backWords && text && !(/(["'<\(\u201c\u2018]\[|[\ufeff\u200b\u2014\s\xa0].)$/).test(text + leadingChar)) {
        log("init", "Cursor is in wrong word location to start autocomplete, returning null.");
        return null;
    }
    var $node = AJS.$(node);
    if ($node.closest("div.code, a[href], img, pre").length) {
        log("init", "Cursor is in wrong node to start autocomplete, returning null.");
        return null;
    }

    if( Confluence.PropertyPanel && Confluence.PropertyPanel.current){
        AJS.log('Trying to destroy property panel');
        Confluence.PropertyPanel.shouldCreate = false;
        Confluence.PropertyPanel.destroy();
    }

    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.
     */
    control.getContainer = 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 = selection.getRng(true);
            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(adaptor.HIDDEN_CHAR);
    el.append($searchTextSpan);
    selection.setNode(el[0]);

    var autocompleteSpan = control.getContainer();
    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, control.getContainer()),
        searchTextNode = searchNode[0].firstChild,
        cursorPosition = searchTextNode.nodeValue.length,
        selNode = AJS.$(doc.createElement("span")).text(suggestionHtml || adaptor.HIDDEN_CHAR);
    searchNode.empty().append(selNode);
    selection.select(selNode[0], true);
    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 (ed, e) {
            if (control.onBeforeKey && !control.onBeforeKey(e, control.text())) {
                tinymce.dom.Event.cancel(e);
                log("before", "blocked by onBeforeKey");
                return false;
            }
        },
        after = function (ed, e) {
            var rng = selection.getRng(true),
                span = control.getContainer(),
                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())) {
                tinymce.dom.Event.cancel(e);
                log("after", "blocked by onAfterKey");
                return false;
            }
        },
        press = function (ed, e) {
            if (control.onKeyPress && !control.onKeyPress(e, control.text())) {
                tinymce.dom.Event.cancel(e);
                log("press", "blocked by onKeyPress");
                return false;
            }
        },
        click = function (ed, e) {
            if (control.getContainer()[0] != e.target.parentNode) {
                log("click", "Clicked outside of autocomplete, closing.");
                control.die();
            }
        };
    ed.onKeyDown.addToTop(before);
    ed.onKeyUp.addToTop(after);
    ed.onKeyPress.addToTop(press);
    ed.onClick.addToTop(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, control.getContainer());
        if (text != null) {
            span.html(text);
            return this;
        } else {
            text = AJS.escapeEntities(span.text());
            return text.replace(adaptor.HIDDEN_CHAR, "");
        }
    };

    /**
     * Replaces the autocomplete component with the given text, which may be empty.
     * If the given text IS empty, it will always be collapsed.
     * 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
     */
    var replaceWithTextAndGetRange = function(text, collapse) {
        adaptor.replaceWithTextAndGetRange(control.getContainer(), text, collapse);
        return rng;
    };

    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) {
        if (control.dying) {
            log("die", "Already dying, returning.");
            return;
        }
        control.dying = true;
        if(Confluence.PropertyPanel) {
            Confluence.PropertyPanel.shouldCreate = true;
        }
        var container = control.getContainer();
        if (container.length) {
             log("die", "Tearing down autocomplete, cleaning up autocompleter");
            // Replace autocomplete span with its current text
            var replaceText = ((notrigger || options.backWords) ? "" : control.settings.ch) + control.text();
            rng = replaceWithTextAndGetRange(replaceText, true);
        }
        ed.onKeyDown.remove(before);
        ed.onKeyUp.remove(after);
        ed.onClick.remove(click);
        ed.onKeyPress.remove(press);
        AJS.Editor.Adapter.unbindScroll("autocomplete");
        AJS.$(document).unbind("click.autocomplete-outside");
        this.onDeath && this.onDeath();
        // Click selection to force re-display of any property panels
//        $(ed.selection.getNode()).click();
    };

    AJS.Editor.Adapter.bindScroll("autocomplete", function () {
        control.die();
    });
    AJS.$(document).bind("click.autocomplete-outside", function (e) {
        if (!AJS.$(e.target).closest("#autocomplete-dropdown").length) {
            control.die();
        }
    });
    AJS.Rte.getEditor().onBeforeExecCommand.add(function(ed, cmd) {
        if (cmd == "mceConfSavePage") {
            control.die();
        }
    });
    control.update = function (data) {
        AJS.Rte.BookmarkManager.storeBookmark();
        replaceWithTextAndGetRange("", true);
        this.settings.update(this,data);
    };

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

/**
 * Finds and removes an orphan autocomplete control. Contents of control are
 * replaced with the text content of the control and the text is then selected in the editor.
 *
 * @return {
 *   leadingChar: trigger character used to launch autocomplete
 *   content: search text contained in control
 * }
 */
tinymce.confluence.Autocompleter.Control.removeOrphanedControl = function() {
    var doc = $(AJS.Rte.getEditor().getDoc()),
        placeholder = doc.find('#' + AUTOCOMPLETE_ID),
        leadingChar,
        content;
    if(!placeholder.length) {
        return null;
    }
    leadingChar = doc.find('#' + AUTOCOMPLETE_TRIGGER_ID).text();
    content = doc.find('#' + AUTOCOMPLETE_SEARCH_TEXT_ID).text();

    Confluence.Editor.Adapter.replaceWithTextAndGetRange(placeholder, content, false);

    return {
        leadingChar: leadingChar,
        content: content
    };
};

})(AJS.$);
AJS.log("tinyMce-autocomplete-manager starting");
tinymce.confluence.Autocompleter.Manager = (function ($) {

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

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

    /**
    *  The input driven dropdown component that does most of the work.
    */
    var idd;

    /**
     * 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
     *  - selectFirstItem   true/false, whether or not the first item in the drop down should be selected
     *  - dropDownDelay     the delay in milliseconds before the dropdown will be opened. Used to delay AJAX
     *                      requests. Defaults to 200ms.
     */
    var startAutoComplete = function (options) {
            log("startAutoComplete", "Started");
            autoCompleteControl = tinymce.confluence.Autocompleter.Control(AJS.Rte.getEditor(), options);
            if (!autoCompleteControl) {
                return false;
            }
            var selectionHandler = function (e, selection) {
                e.preventDefault();
                var result = AJS.$.data(selection[0], "properties");
                if (result && typeof result.callback == "function") {
                    result.callback(autoCompleteControl);
                } else if (result.className != "menu-header") {
                    log("selectionHandler", "Inserting link from dropdown selection");
                    autoCompleteControl.update(result);
                }

                autoCompleteControl.die();
            };
            var moveHandler = function (selection, dir) {
                var current = AJS.dropDown.current;
                if (selection && selection.find("a").is(".menu-header")) {
                    dir == "up" ? current.moveUp(): current.moveDown();
                }
            };

            var winWidth = AJS.$(window).width(),
                preferredPosition,
                raphaelArrowForPosition;

            var POSITION_ABOVE = 1,
                POSITION_BELOW = 2;

            var testLastPos;

            var getPreferredPosition = function(spaceAvailable, heightRequired) {
                if(spaceAvailable.below >= heightRequired) {
                    return POSITION_BELOW;
                } else if(spaceAvailable.above >= heightRequired) {
                    return POSITION_ABOVE;
                }
                // Not enough space - so use biggest space
                return spaceAvailable.below > spaceAvailable.above ? POSITION_BELOW : POSITION_ABOVE;
            };

            var dropDownStillFits = function(spaceAvailable, heightRequired) {
                if(preferredPosition == POSITION_ABOVE) {
                    return spaceAvailable.above >= heightRequired;
                }
                return spaceAvailable.below >= heightRequired;
            };

            var renderTip = function(ddTop, ddLeft, ddHeight, showShadow) {
                var fill,
                    path,
                    tipTop,
                    tipLeft;

                if (window.Raphael) {
                    if(idd.raphaelArrow && raphaelArrowForPosition != preferredPosition) {
                        // need different type of tip - remove existing one
                        idd.raphaelArrow.remove && idd.raphaelArrow.remove();
                        idd.raphaelArrow = null;
                    }
                    if(preferredPosition == POSITION_ABOVE) {
                        fill = "#ffffff";
                        path = "M0.001-1.000l6.001,6.001,6.001-6.001";
                        tipTop = ddTop + ddHeight - 1; // Overlap
                    } else {
                        fill = "#f0f0f0";
                        path = "M0.001,6.001l6.001-6.001,6.001,6.001";
                        tipTop = ddTop - 5; // Overlap
                    }
                    tipLeft = ddLeft + 4;

                    if (idd.raphaelArrow) {
                        idd.raphaelArrow.canvas.style.left = tipLeft + "px";
                        idd.raphaelArrow.canvas.style.top = tipTop + "px";
                    } else {
                        var r = Raphael(tipLeft, tipTop, 16, 10),
                            tipPath = r.path(path);
                        tipPath.attr({
                            fill: fill,
                            stroke: "#bbb"
                        });
                        r.canvas.style.zIndex = 3000;
                        idd.raphaelArrow = r;
                        raphaelArrowForPosition = preferredPosition;
                        if(showShadow && $.support.opacity) {
                            // tip shadows are awful without opacity (i.e. in IE).
                            tipPath.clone().translate(4,4).attr({fill: "#A0A0A0", stroke: "#A0A0A0", opacity:".5", blur:"1"}).toBack();
                        }
                    }
                }
            };


            idd = AJS.inputDrivenDropdown({
                onShow : function (dd) {
                    log("onShow", "Post-processing the dropdown");
                    var iframe = Confluence.Editor.Adapter.getEditorFrame();
                    iframe && iframe.shim && iframe.shim.hide();

                    dd.find("a.menu-header").unbind().click(function (e) {
                        e.preventDefault();
                        autoCompleteControl.die();
                    });
                    this.reset();

                    if (autoCompleteControl.settings.selectFirstItem && dd.find("a:not('.menu-header')").length) {
                        AJS.dropDown.current.moveDown();
                    }
                },
                dropdownPlacement : function (dd) {
                    var parent = $("#autocomplete-dropdown"),
                        anchor = autoCompleteControl.getContainer(),
                        spaceAvailable,
                        top,
                        left,
                        offset,
                        overlap,
                        gapForArrowY = 10,
                        gapForArrowX = 0,
                        ddHeight,
                        heightRequired,
                        showTipShadow;

                    if (!parent.length) {
                        parent = AJS("div").addClass("aui-dd-parent quick-nav-drop-down").attr("id", "autocomplete-dropdown").appendTo("body");
                    }

                    spaceAvailable = AJS.Position.spaceAboveBelow(Confluence.Editor.Adapter.getEditorFrame(), anchor);

                    // Ensure correct styling so height calcs are correct
                    dd.find("ol:has(a.menu-header)").addClass("top-menu-item");
                    dd.find("ol:empty").hide();
                    // Height of dd changes after adding to parent (due to css), so do this first
                    parent.append(dd);

                    offset = Confluence.Editor.Adapter.offset(anchor);

                    overlap = parent.width() + offset.left - winWidth + 10;
                    left = offset.left - (overlap > 0 ? overlap : 0) - gapForArrowX;
                    ddHeight = dd.outerHeight(true);
                    heightRequired = ddHeight + gapForArrowY;

                    if(preferredPosition) {
                        // Favour current position unless it doesn't fit
                        // we don't want the drop down to keep changing relative position.
                        if(!dropDownStillFits(spaceAvailable, heightRequired)) {
                            preferredPosition = null;
                        }
                    }

                    if(!preferredPosition) {
                        preferredPosition = getPreferredPosition(spaceAvailable, heightRequired);
                    }

                    if(preferredPosition == POSITION_ABOVE) {
                        top = offset.top - ddHeight - gapForArrowY;
                        showTipShadow = true;
                    } else {
                        top = offset.top + anchor.height() + gapForArrowY;
                        showTipShadow = false;
                    }

                    parent.css({
                        position: "absolute",
                        top: top,
                        left: left
                    });

                    //position it to the actual autocomplete element (makes it look pretty on long lines)
                    renderTip(top, offset.left, ddHeight, showTipShadow);

                },
                onDeath : function () {
                    $("#autocomplete-dropdown").remove();
                    idd.raphaelArrow && idd.raphaelArrow.remove && idd.raphaelArrow.remove();
                },
                ajsDropDownOptions: {
                    selectionHandler: selectionHandler,
                    moveHandler: moveHandler,
                    className : "autocomplete " + autoCompleteControl.settings.dropDownClassName
                },
                getDataAndRunCallback: function(val) {
                    autoCompleteControl && autoCompleteControl.settings.getDataAndRunCallback &&
                    autoCompleteControl.settings.getDataAndRunCallback(autoCompleteControl, val,
                        function(matrix, query, restSpecificAdditionLinksCallback) {
                            matrix.unshift([
                                {
                                    className: "menu-header dropdown-prevent-highlight",
                                    href: "#",
                                    name: autoCompleteControl.settings.getHeaderText(autoCompleteControl, val)
                                }
                            ]);
                            matrix.push(autoCompleteControl.settings.getAdditionalLinks(autoCompleteControl, val, restSpecificAdditionLinksCallback));

                            // If the idd control is still active update it with the new data.
                            idd && idd.show(matrix, query, [query]);
                        }
                    );
                },
                dropDownDelay: autoCompleteControl.settings.dropDownDelay
            });

            autoCompleteControl.onBeforeKey = function (e, text) {
                if (e.keyCode == 40 || e.keyCode == 38 || e.keyCode == 13) {
                    tinymce.dom.Event.cancel(e);
                    return false;
                }
                if (e.keyCode == 27 || e.keyCode == 9 || (e.keyCode == 8 && !text)) {
                    // User has key-downed backspace but text is *already* blank - close autocomplete.
                    log("autoCompleteControl.onBeforeKey", "killing autoCompleteControl and returning false");
                    autoCompleteControl.die(e.keyCode == 8);
                    return false;
                }

                return true;
            };
            // Blocker for browser default actions for up and down keys
            autoCompleteControl.onKeyPress = function (e, text) {
                var ch = AJS.$.browser.msie ? e.keyCode : e.which,
                    character = String.fromCharCode(ch);   // charCode back to '@'
                if (e.keyCode == 13) {
                    tinymce.dom.Event.cancel(e);
                    return false;
                }
                var endCharIndex = AJS.indexOf(autoCompleteControl.settings.endChars,character);
                if (endCharIndex != -1) {
                    log("autoCompleteControl.onKeyPress", "caught autocomplete-closing char " + character + ", closing");
                    autoCompleteControl.die();
                }
                return true;
            };
            var twoLetters = /\S{2,}/;
            autoCompleteControl.onAfterKey = function (e, text) {
                if (e.keyCode == 40 || e.keyCode == 38 || e.keyCode == 13) {
                    var current = AJS.dropDown.current;
                    if (!current) {
                        log("autoCompleteControl.onBeforeKey", "key caught before dropdown ready, ignoring");
                        return false;
                    }
                    if (current.getFocusIndex() == -1 && e.keyCode == 13) {  // user hit enter when nothing selected
                        reset();
                        return true;
                    }
                    // AJS-609 - the AUI dropdown code expects a jQuery event
                    if (!e.which)
                        e.which = e.keyCode;

                    return current.moveFocus(e);
                }

                // User deleted back to zero characters - should display default suggestions again.
                var forceUpdate = (e.keyCode == 8 && !text);
                if (forceUpdate || twoLetters.test(text)) {
                    log("onAfterKey", "Changed search string to '" + text + "'");
                    idd.change(text, forceUpdate);
                }
                return true;
            };
            autoCompleteControl.onDeath = function () {
                log("onDeath", "autoCompleteControl onDeath called");
                if (idd) {
                    idd.remove();
                    idd.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.
            idd.change(autoCompleteControl.word, "force");
            return true;
        };

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

    var reattach = function() {
        var orphan = tinymce.confluence.Autocompleter.Control.removeOrphanedControl();
        orphan && tinymce.confluence.Autocompleter.Manager.shortcutFired(orphan.leadingChar);
    };

    return {

        getInputDrivenDropdown: function() {
            return idd;
        },

        // keyPress used so we can capture composite keystrokes like Sh-2 == @
        triggerListener: function(ed, e) {
            var ch = AJS.$.browser.msie ? e.keyCode : e.which;

            idd && idd.closing && (idd = null);

            var character = String.fromCharCode(ch);   // charCode back to '@'
            if (!idd && 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 true;
        },

        /**
         * 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;
            idd && idd.closing && (idd = null);
            if (idd) {
                backWords = autoCompleteControl.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
            });
        },

        /**
         * Attempt to reattach to an autocomplete span if there is no active control
         * (e.g. on an undo/redo opperation).
         */
        reattach: reattach
    };
})(AJS.$);


/**
 * Settings that each Autocomplete will be initialized on, depending on the trigger character used to activate the
 * autocomplete.
 */
(function () {
    AJS.log("tinyMce-autocomplete-settings-links initialising");
    var autoComplete = tinymce.confluence.Autocompleter,

    getUrl = function(val) {
        if (val) {
            return Confluence.getContextPath() + "/rest/prototype/1/search.json";
        } else if (AJS.Meta.get('remote-user')) {
            return Confluence.getContextPath() + "/rest/prototype/1/session/history.json";
        }

        return null;
    },

    getParams = function(autoCompleteControl, val) {
        var params = {
            "max-results": autoCompleteControl.maxResults
        };
        if (val) {
            params.query = Confluence.unescapeEntities(val);
            params.search = "name";
            params.preferredSpaceKey = AJS.Meta.get('space-key');
        }
        return params;  
    };

    // Link settings.
    tinymce.confluence.Autocompleter.Settings["["] = {

        ch : "[",
        endChars : ["]"],

        dropDownClassName: "autocomplete-links",
        selectFirstItem: true,

        getHeaderText : function (autoCompleteControl, value) {
            return "Link suggestions";
        },

        getAdditionalLinks : function (autoCompleteControl, value, restSpecificAdditionLinksCallback) {
            var searchPrompt;
            if (value) {
                var message = "Search for &lsquo;{0}&rsquo;";
                searchPrompt = AJS.format(message, value);
            } else {
                searchPrompt = "Search";
            }
            var LinkBrowser = Confluence.Editor.LinkBrowser;
            var additionalLinks = [
                {
                    className: "search-for",
                    name: searchPrompt,
                    href: "#",
                    callback : function (autoCompleteControl) {
                        autoCompleteControl.replaceWithSelectedSearchText();
                        var lb = LinkBrowser.open({
                            panelKey: LinkBrowser.SEARCH_PANEL
                        });
                        lb.doSearch(lb.getLocationPresenter().getLinkContent());
                    }
                },
                {
                    className: "dropdown-insert-link",
                    html: autoComplete.Util.dropdownLink("Insert Web Link", "dropdown-prevent-highlight", "editor-icon"),
                    callback: function (autoCompleteControl) {
                        autoCompleteControl.replaceWithSelectedSearchText();
                        LinkBrowser.open({
                            panelKey: LinkBrowser.WEBLINK_PANEL
                        });
                    }
                }
            ];

            restSpecificAdditionLinksCallback && restSpecificAdditionLinksCallback(value, additionalLinks);

            return additionalLinks;
        },

        getDataAndRunCallback : function(autoCompleteControl, val, callback) {
            function getRestSpecificAdditionLinks(matrix, value, additionalLinks) {
                function doesPageAlreadyExist() {
                    var pages = matrix[1];
                    var firstEntry = pages[0].restObj;

                    if (firstEntry.type == "page") {
                        return firstEntry.space.key == AJS.Meta.get('space-key') && firstEntry.title.toLowerCase() == value.toLowerCase();
                    }

                    return false;
                }

                if (value) {
                    if (matrix.length < 2 || !doesPageAlreadyExist()) {
                        additionalLinks.push({
                            className: "insert-create-page-link",
                            html: autoComplete.Util.dropdownLink("Insert Link to Create Page", "dropdown-prevent-highlight", "editor-icon"),
                            callback: function (autoCompleteControl) {
                                var title = Confluence.unescapeEntities(value);
                                var link = Confluence.Link.createLinkToNewPage(title, AJS.Meta.get('space-key'));
                                autoCompleteControl.update(link);
                            }
                        });
                    }
                }
            }
            tinymce.confluence.Autocompleter.Util.getRestData(autoCompleteControl, getUrl, getParams, val, callback, "content", getRestSpecificAdditionLinks);
        },

        update : function(autoCompleteControl, link) {
            if (link.restObj) {
                link = Confluence.Link.fromREST(link.restObj);
            }
            link.insert();
        }
    }

})();


/**
 * Settings that each Autocomplete will be initialized on, depending on the trigger character used to activate the
 * autocomplete.
 */
(function ($) {
    AJS.log("tinyMce-autocomplete-settings-media initialising");

    // these types match those in the Java Attachment.Type enum
    var embeddableAttachmentTypes = ["image","word","excel","pdf","powerpoint","multimedia"],

    getUrl = function(val) {
        var parentId = AJS.params.attachmentSourceContentId || AJS.Meta.get('content-id');
        if (val) {
            return Confluence.getContextPath() + "/rest/prototype/1/search.json";
        } else if (+parentId) {
            return Confluence.getContextPath() + "/rest/prototype/1/content/" + parentId + "/attachments.json";
        }

        return null;
    },

    getParams = function(autoCompleteControl, val)
    {
        return val ?
        {
            type: "attachment",
            attachmentType: embeddableAttachmentTypes,
            search: "name",
            "max-results": autoCompleteControl.maxResults,
            query: val
        } : {
            attachmentType: embeddableAttachmentTypes,
            "max-results": autoCompleteControl.maxResults
        };
    };

    // Media settings
    tinymce.confluence.Autocompleter.Settings["!"] = {
        ch : "!",
        endChars : ["!"],

        dropDownClassName: "autocomplete-media",
        selectFirstItem: true,

        getHeaderText : function (autoCompleteControl, value) {
            return "Media suggestions";
        },

        getAdditionalLinks : function (autoCompleteControl, value) {
            return [
                {
                    className: "dropdown-insert-image",
                    html: tinymce.confluence.Autocompleter.Util.dropdownLink(
                            "Open Image Browser", "dropdown-prevent-highlight", "editor-icon"),
                    callback: function(autoCompleteControl) {
                        autoCompleteControl.replaceWithSelectedSearchText();
                        Confluence.Editor.defaultInsertImageDialog();
                    }
                },
                {
                    className: "dropdown-insert-macro",
                    html: tinymce.confluence.Autocompleter.Util.dropdownLink(
                            "Insert Other Media", "dropdown-prevent-highlight", "editor-icon"),
                    callback: function(autoCompleteControl) {
                        autoCompleteControl.replaceWithSelectedSearchText();
                        tinymce.confluence.macrobrowser.macroBrowserToolbarButtonClicked({
                            selectedCategory: "media"
                        });
                    }
                }
            ];
        },

        getDataAndRunCallback : function(autoCompleteControl, val, callback) {
            tinymce.confluence.Autocompleter.Util.getRestData(autoCompleteControl, getUrl, getParams, val, callback, "attachment");
        },

        update : function(autoCompleteControl, data) {
            var linkDetails = AJS.REST.wikiLink(data.restObj),
                name = data.restObj && data.restObj.title || data.name;

            if (data.restObj.niceType == "Image") {
                // leading ^ is not needed for images attached to the current page
                var destination = linkDetails.destination && linkDetails.destination.replace(/^\^/, "");
                var propertyMap = $.extend({
                    filename: name,
                    contentId: data.ownerId || data.restObj.ownerId
                }, linkDetails.params);
                tinymce.confluence.ImageUtils.insertFromProperties(propertyMap);

            } else {
                // Other embeddable content, such as a viewfile macro variant
                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;
                    case 'Multimedia':              macroName = "multimedia"; break;
                }
                var spacePage = linkDetails.destination.substring(0, linkDetails.destination.indexOf("^"));
                var macroParams = {
                    page: spacePage,
                    name: name
                };
                AJS.MacroBrowser.getMacroJsOverride("viewdoc").beforeParamsRetrieved(macroParams);  // tweak for macro expected format
                // The Office Connector JS should really strip the page param if it is empty, but
                // it doesn't and making a new OC release just for that one line change is too painful to contemplate.
                if (!macroParams.page) {
                    delete macroParams.page;
                }
                var macroRenderRequest = {
                    contentId: AJS.Meta.get('content-id') || "0",
                    macro: {
                        name: macroName,
                        params: macroParams,
                        defaultParameterValue: "",
                        body : ""
                    }
                };
                tinymce.confluence.MacroUtils.insertMacro(macroRenderRequest, autoCompleteControl.die );
            }
        }

    };
})(AJS.$);

/**
 * Settings that each Autocomplete will be initialized on, depending on the trigger character used to activate the
 * autocomplete.
 */
(function ($) {
    AJS.log("tinyMce-autocomplete-settings-macro initialising");
    var dropdownSelectionMade = function(autoCompleteControl, options) {
        var macroMetadata = options.presetMacroMetadata;

        autoCompleteControl.replaceWithSelectedSearchText();

        if (!macroMetadata) {
            tinymce.confluence.macrobrowser.macroBrowserToolbarButtonClicked(options);
        } else {
            // only open the macro browser if there are required parameters
            if (AJS.MacroBrowser.hasRequiredParameters(macroMetadata)) {
                tinymce.confluence.macrobrowser.macroBrowserToolbarButtonClicked(options);
            } else {
                AJS.Rte.BookmarkManager.storeBookmark();

                tinymce.confluence.MacroUtils.insertMacro({
                    contentId: AJS.Meta.get('content-id') || "0",
                    macro: {
                        name: macroMetadata.macroName,
                        body : ""
                    }
                });
            }
        }
    };

    var makeMacroDropdownItem = function (summary) {
            if (summary.hidden && !AJS.MacroBrowser.isHiddenMacroShown(summary)) {
                return null;    // macros like {viewfile} hidden from the browser should be hidden from the dropdown
            }

            var item = {
                className: "autocomplete-macro-" + summary.macroName,
                callback: function(autoCompleteControl) {
                    dropdownSelectionMade(autoCompleteControl, {
                        ignoreEditorSelection: true,       // the selected text will be the search term, ignore it
                        presetMacroMetadata: summary
                    });
                }
            };

            if (summary.icon) {
                item.name = summary.title;
                item.href = "#";
                item.icon = (summary.icon.relative ? AJS.params.staticResourceUrlPrefix : "") + summary.icon.location;
            } else {
                item.html = tinymce.confluence.Autocompleter.Util.dropdownLink(summary.title);
            }

            return item;
        };

    // Link settings.
    tinymce.confluence.Autocompleter.Settings["{"] = {

        ch : "{",
        endChars : ["}", ":", "{"],
        dropDownClassName: "autocomplete-macros",
        dropDownDelay: 0, // No delay needed because there is no AJAX request involved
        selectFirstItem: true,

        getHeaderText : function (autoCompleteControl, value) {
            return "Macro suggestions";
        },

        getAdditionalLinks : function (autoCompleteControl, value) {
            return [
                {
                    className: "dropdown-insert-macro",
                    html: tinymce.confluence.Autocompleter.Util.dropdownLink(
                            "Open Macro Browser", "dropdown-prevent-highlight", "editor-icon"),
                    callback: function(autoCompleteControl) {
                        var searchText = autoCompleteControl.text();
                        dropdownSelectionMade(autoCompleteControl, { searchText: searchText });
                    }
                }
            ];
        },

        getDataAndRunCallback : function(autoCompleteControl, val, callback) {
            var dropdownItems = [];
            if (!val) {
                $("#macro-insert-list li").each(function() {
                    var macroMetadata = AJS.MacroBrowser.getMacroMetadata($(this).attr("data-macro-name"));
                    if (macroMetadata) {
                        var dropdownItem = makeMacroDropdownItem(macroMetadata);
                        dropdownItem && dropdownItems.push(dropdownItem);
                    }
                });
            } else {
                var summaries = AJS.MacroBrowser.searchSummaries(val, { keywordsField: "keywordsNoDesc" }),
                    itemCount;

                for (var i = 0, ii = summaries.length; i < ii; i++) {
                    var dropdownItem = makeMacroDropdownItem(summaries[i]);
                    if (dropdownItem && dropdownItems.push(dropdownItem) == autoCompleteControl.maxResults)
                       break;
                }
            }
            callback([dropdownItems], val);
        },

        update : function (autoCompleteControl, data) {
            throw new Error("All items in the Macro Autocomplete dropdown must have a callback function");
        }
    }
})(AJS.$);


