YUI.add('aui-ace-autocomplete-base', function (A, NAME) {

/**
 * The ACE Editor Component
 *
 * @module aui-ace-editor
 * @submodule aui-ace-autocomplete-base
 */

var Lang = A.Lang,
    AArray = A.Array,
    Do = A.Do,
    ADOM = A.DOM,

    FILL_MODE_INSERT = 1,
    FILL_MODE_OVERWRITE = 0,

    Base = function() {};

/**
 * A base class for AutoCompleteBase.
 *
 * @class A.AceEditor.AutoCompleteBase
 * @param {Object} config Object literal specifying widget configuration
 * properties.
 * @constructor
 */
Base.prototype = {

    /**
     * Construction logic executed during AutoCompleteBase instantiation.
     * Lifecycle.
     *
     * @method initializer
     * @protected
     */
    initializer: function() {
        var instance = this,
            processor;

        instance._editorCommands = [];

        A.after(instance._bindUIACBase, instance, 'renderUI');

        processor = instance.get('processor');

        if (processor && !processor.get('host')) {
            processor.set('host', instance);
        }

        instance._onResultsErrorFn = A.bind('_onResultsError', instance);
        instance._onResultsSuccessFn = A.bind('_onResultsSuccess', instance);
    },

    /**
     * Inserts the provided suggestion as a string to the editor. The added text
     * can overwrite the match or to be inserted depending on the `fillMode`
     * attribute.
     *
     * @method _addSuggestion
     * @param {String} content
     * @protected
     * @return {Do.Halt} Instance of Do.Halt to stop function execution
     */
    _addSuggestion: function(content) {
        var instance = this,
            cursorPosition,
            data,
            editor,
            matchParams,
            overwriteRange,
            Range,
            startColumn,
            startRow;

        instance._lockEditor = true;

        editor = instance._getEditor();

        data = instance.get('processor').getSuggestion(instance._matchParams.match, content);

        if (instance.get('fillMode') === Base.FILL_MODE_OVERWRITE) {
            matchParams = instance._matchParams;

            startRow = matchParams.row;

            startColumn = matchParams.column - matchParams.match.content.length;

            cursorPosition = editor.getCursorPosition();

            Range = ace.require('ace/range').Range;

            overwriteRange = new Range(startRow, startColumn, cursorPosition.row, cursorPosition.column);

            editor.getSession().replace(overwriteRange, data);
        }
        else {
            editor.insert(data);
        }

        editor.focus();

        instance._lockEditor = false;

        instance.fire('addSuggestion', data);

        return new Do.Halt(null);
    },

    /**
     * Binds editor events.
     *
     * @method _bindUIACBase
     * @protected
     */
    _bindUIACBase: function() {
        var instance = this,
            editor;

        instance.publish(
            'cursorChange', {
                defaultFn: instance._defaultCursorChangeFn
            }
        );

        editor = instance._getEditor();

        instance._onChangeFn = A.bind('_onEditorChange', instance);

        editor.on('change', instance._onChangeFn);

        editor.commands.addCommand({
            name: 'showAutoComplete',
            bindKey: A.merge(
                instance.get('showListKey'), {
                    sender: 'editor|cli'
                }
            ),
            exec: function() {
                var cursorPosition = editor.getCursorPosition();

                instance._processAutoComplete(cursorPosition.row, cursorPosition.column);
            }
        });

        instance._onEditorChangeCursorFn = A.bind('_onEditorChangeCursor', instance);

        editor.getSelection().on('changeCursor', instance._onEditorChangeCursorFn);

        instance.on('destroy', instance._destroyUIACBase, instance);
    },

    /**
     * Checks if the cursor is out of the row/column on the latest match. If so,
     * fires an `cursorOut` event.
     *
     * @method _defaultCursorChangeFn
     * @param {CustomEvent} event The fired event
     * @protected
     */
    _defaultCursorChangeFn: function() {
        var instance = this,
            column,
            cursorPosition,
            editor,
            matchParams,
            row;

        editor = instance._getEditor();

        cursorPosition = editor.getCursorPosition();

        row = cursorPosition.row;
        column = cursorPosition.column;

        matchParams = instance._matchParams;

        if (row !== matchParams.row || column < matchParams.match.start) {
            instance.fire('cursorOut');
        }
    },

    /**
     * Removes the listeners to editor commands.
     *
     * @method _destroyUIACBase
     * @protected
     */
    _destroyUIACBase: function() {
        var instance = this,
            editor;

        editor = instance._getEditor();

        editor.commands.removeCommand('showAutoComplete');

        editor.removeListener('change', instance._onChangeFn);

        editor.getSelection().removeListener('changeCursor', instance._onEditorChangeCursorFn);

        instance._removeAutoCompleteCommands();
    },

    /**
     * Returns the editor instance.
     *
     * @method _getEditor
     * @protected
     * @return {Object} Editor instance
     */
    _getEditor: function() {
        var instance = this;

        return instance.get('host').getEditor();
    },

    /**
     * Filters and sorts the found suggestions using the existing chain of
     * `filters` and `sorters`.
     *
     * @method _filterResults
     * @param {String} content
     * @param {Array} results
     * @protected
     * @return {Array} The filtered results
     */
    _filterResults: function(content, results) {
        var instance = this,
            filters,
            i,
            length,
            sorters;

        filters = instance.get('filters');

        for (i = 0, length = filters.length; i < length; ++i) {
            results = filters[i].call(instance, content, results.concat());

            if (!results.length) {
                break;
            }
        }

        sorters = instance.get('sorters');

        for (i = 0, length = sorters.length; i < length; ++i) {
            results = sorters[i].call(instance, content, results.concat());

            if (!results.length) {
                break;
            }
        }

        return results;
    },

    /**
     * Checks for new line or tab character and adds a suggestion to the editor
     * if so.
     *
     * @method _handleEnter
     * @param {String} text
     * @protected
     * @return {Do.Halt} If text is new line or tab character, returns an
     *     instance of Do.Halt to stop function execution
     */
    _handleEnter: function(text) {
        var instance = this,
            selectedEntry;

        if (text === '\n' || text === '\t') {
            selectedEntry = instance._getSelectedEntry();

            return instance._addSuggestion(selectedEntry);
        }
    },

    /**
     * Handles editor change event. If editor is not locked and data action is
     * insert or remove text, process auto complete.
     *
     * @method _onEditorChange
     * @param {CustomEvent} event The fired event
     * @protected
     */
    _onEditorChange: function(event) {
        var instance = this,
            column,
            dataAction,
            endRow,
            startRow;

        dataAction = event.action;

        if (!instance._lockEditor && (dataAction === 'insert' || dataAction === 'remove')) {
            column = event.start.column;
            endRow = event.end.row;
            startRow = event.start.row;

            if (dataAction === 'insert' && startRow === endRow) {
                instance._processAutoComplete(startRow, column + 1);
            }

            instance.fire(
                dataAction, {
                    column: column,
                    endRow: endRow,
                    startRow: startRow
                }
            );
        }
    },

    /**
     * Fires cursor change event providing the current position as event
     * payload.
     *
     * @method _onEditorChangeCursor
     * @param {CustomEvent} event The fired event
     * @protected
     */
    _onEditorChangeCursor: function() {
        var instance = this;

        instance.fire('cursorChange', instance._getEditor().getCursorPosition());
    },

    /**
     * Fires an `resultsError` event containing the error.
     *
     * @method _onResultsError
     * @param error
     * @protected
     */
    _onResultsError: function(error) {
        var instance = this;

        instance.fire('resultsError', error);
    },

    /**
     * Updates `results` attribute with the provided results.
     *
     * @method _onResultsSuccess
     * @param {Array} results
     * @protected
     */
    _onResultsSuccess: function(results) {
        var instance = this;

        instance.set('results', results);
    },

    /**
     * Overwrites the following editor commands:
     * onTextInput,
     * golinedown
     * golineup
     * gotoend
     * gotolineend
     * gotolinestart
     * gotopagedown
     * gotopageup
     * gotostart
     *
     * @method _overwriteCommands
     * @protected
     */
    _overwriteCommands: function() {
        var instance = this,
            commands,
            editor;

        editor = instance._getEditor();

        commands = editor.commands.commands;

        instance._editorCommands.push(
            Do.before(instance._handleEnter, editor, 'onTextInput', instance),
            Do.before(instance._handleKey, commands.golinedown, 'exec', instance, 40),
            Do.before(instance._handleKey, commands.golineup, 'exec', instance, 38),
            Do.before(instance._handleKey, commands.gotoend, 'exec', instance, 35),
            Do.before(instance._handleKey, commands.gotolineend, 'exec', instance, 35),
            Do.before(instance._handleKey, commands.gotolinestart, 'exec', instance, 36),
            Do.before(instance._handleKey, commands.gotopagedown, 'exec', instance, 34),
            Do.before(instance._handleKey, commands.gotopageup, 'exec', instance, 33),
            Do.before(instance._handleKey, commands.gotostart, 'exec', instance, 36)
        );
    },

    /**
     * Checks for phrase match.
     *
     * @method _phraseMatch
     * @param {String} content The content to be checked for phrase match
     * @param {Array} results The results to be filtered
     * @param {Boolean} caseSensitive Should the check be case sensitive or not
     * @protected
     * @return {Array} The filtered results
     */
    _phraseMatch: function(content, results, caseSensitive) {
        if (!content) {
            return results;
        }

        return AArray.filter(
            results,
            function(item) {
                var result = true;

                if (item === content) {
                    result = false;
                }
                else {
                    if (!caseSensitive) {
                        item = item.toLowerCase();

                        content = content.toLowerCase();
                    }

                    if (item.indexOf(content) === -1) {
                        result = false;
                    }
                }

                return result;
            }
        );
    },

    /**
     * Invokes the loaded content processor and checks for match. If found,
     * provides the match together with information about current row and column
     * and invokes processor's `getResults` function in order to retrieve
     * results. At the end, fires and `match` event with the following
     * properties: column - the current column coords - the page coordinates of
     * the match line - the current line match - the current match row - the
     * current row
     *
     * @method _processAutoComplete
     * @param {Number} row The row on which match happened
     * @param {Number} column The column on which match happened
     * @protected
     */
    _processAutoComplete: function(row, column) {
        var instance = this,
            col,
            coords,
            editor,
            line,
            match,
            processor;

        col = column;

        editor = instance._getEditor();

        line = editor.getSession().getLine(row);

        line = line.substring(0, column);

        processor = instance.get('processor');

        match = processor.getMatch(line);

        if (Lang.isObject(match)) {
            coords = editor.renderer.textToScreenCoordinates(row, column);

            coords.pageX += ADOM.docScrollX();
            coords.pageY += ADOM.docScrollY();

            instance._matchParams = {
                column: column,
                match: match,
                row: row
            };

            processor.getResults(match, instance._onResultsSuccessFn, instance._onResultsErrorFn);
        }

        instance.fire(
            'match', {
                column: column,
                coords: coords,
                line: line,
                match: match,
                row: row
            }
        );
    },

    /**
     * Detaches the previously attached editor commands.
     *
     * @method _removeAutoCompleteCommands
     * @protected
     */
    _removeAutoCompleteCommands: function() {
        var instance = this;

        (new A.EventHandle(instance._editorCommands)).detach();

        instance._editorCommands.length = 0;
    },

    /**
     * Sorts the results in ascending order, taking in consideration the length
     * of the content.
     *
     * @method _sortAscLength
     * @param {String} content The text content
     * @param {Array} results The results to be filtered
     * @param {Boolean} caseSensitive Should we filter these results
     *     alphabetically
     * @protected
     * @return {Array} The sorted results
     */
    _sortAscLength: function(content, results, caseSensitive) {
        return results.sort(
            function(item1, item2) {
                var index1,
                    index2,
                    result;

                result = 0;

                if (!caseSensitive) {
                    item1 = item1.toLowerCase();

                    item2 = item2.toLowerCase();
                }

                index1 = item1.indexOf(content);

                index2 = item2.indexOf(content);

                if (index1 === 0 && index2 === 0) {
                    result = item1.localeCompare(item2);
                }
                else if (index1 === 0) {
                    result = -1;
                }
                else if (index2 === 0) {
                    result = 1;
                }
                else {
                    result = item1.localeCompare(item2);
                }

                return result;
            }
        );
    },

    /**
     * Validates the value of `fillMode` attribute.
     *
     * @method _validateFillMode
     * @param value
     * @protected
     * @return {Boolean} True if mode is 'overwrite' - value '0' or 'insert' -
     *     value '1'
     */
    _validateFillMode: function(value) {
        return (value === Base.FILL_MODE_OVERWRITE || value === Base.FILL_MODE_INSERT);
    }
};

/**
 * Exposes a constant for insert fill mode. See `fillMode` for more information.
 *
 * @property FILL_MODE_INSERT
 * @static
 */
Base.FILL_MODE_INSERT = FILL_MODE_INSERT;

/**
 * Exposes a constant for overwrite fill mode. See `fillMode` for more
 * information.
 *
 * @property FILL_MODE_OVERWRITE
 * @static
 */
Base.FILL_MODE_OVERWRITE = FILL_MODE_OVERWRITE;

/**
 * Static property which provides a string to identify the class.
 *
 * @property NAME
 * @type String
 * @static
 */
Base.NAME = 'ace-autocomplete-base';

/**
 * Static property which provides a string to identify the namespace.
 *
 * @property NS
 * @type String
 * @static
 */
Base.NS = 'ace-autocomplete-base';

/**
 * Static property used to define the default attribute
 * configuration for AutoCompleteBase.
 *
 * @property ATTRS
 * @type Object
 * @static
 */
Base.ATTRS = {

    /**
     * The mode in which the AutoComplete should operate. Can be one of these:
     * INSERT - value '0' or OVERWRITE - value '1'. In case of INSERT mode, when
     * Editor adds a suggestion, it will be added next to the matched
     * expression. In case of OVERWRITE mode, the suggestion will overwrite the
     * matched expression.
     *
     * @attribute fillMode
     * @default 1 - OVERWRITE mode
     * @type Number
     */
    fillMode: {
        validator: '_validateFillMode',
        value: Base.FILL_MODE_OVERWRITE
    },

    /**
     * Provides an array of filter functions which will filter the results. By
     * default there is one function which provides phrase match filtering.
     *
     * @attribute filters
     * @default Array with one function which provides phrase match filtering
     * @type Array
     */
    filters: {
        valueFn: function() {
            var instance = this;

            return [
                instance._phraseMatch
                ];
        }
    },

    /**
     * The default processor which will be used to process matches.
     *
     * @attribute processor
     * @type Object | Function
     */
    processor: {
        validator: function(value) {
            return Lang.isObject(value) || Lang.isFunction(value);
        }
    },

    /**
     * The keyboard combination which should be used to show the list with found
     * results.
     *
     * @attribute showListKey
     * @default 'Alt-Space' for Mac, 'Ctrl-Space' for PC
     * @type Object
     */
    showListKey: {
        validator: Lang.isObject,
        value: {
            mac: 'Alt-Space',
            win: 'Ctrl-Space'
        }
    },

    /**
     * Provides an array of sorter functions which will sort the results. By
     * default there is one function which sorts the results in ascending order.
     *
     * @attribute sorters
     * @default Array with one function which sorts results in ascending order
     * @type Array
     */
    sorters: {
        valueFn: function() {
            var instance = this;

            return [
                instance._sortAscLength
                ];
        }
    }
};

A.AceEditor.AutoCompleteBase = Base;

}, '3.1.0-deprecated.95', {"requires": ["aui-ace-editor"]});
