/* ***** BEGIN LICENSE BLOCK *****
 * Distributed under the BSD license:
 *
 * Copyright (c) 2010, Ajax.org B.V.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Ajax.org B.V. nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ***** END LICENSE BLOCK ***** */

define(function (require, exports, module) {

    var RangeList = require("./range_list").RangeList;
    var Range = require("./range").Range;
    var Selection = require("./selection").Selection;
    var onMouseDown = require("./mouse/multi_select_handler").onMouseDown;
    var event = require("./lib/event");
    var lang = require("./lib/lang");
    var commands = require("./commands/multi_select_commands");
    exports.commands = commands.defaultCommands.concat(commands.multiSelectCommands);

// Todo: session.find or editor.findVolatile that returns range
    var Search = require("./search").Search;
    var search = new Search();

    function find(session, needle, dir) {
        search.$options.wrap = true;
        search.$options.needle = needle;
        search.$options.backwards = dir == -1;
        return search.find(session);
    }

// extend EditSession
    var EditSession = require("./edit_session").EditSession;
    (function () {
        this.getSelectionMarkers = function () {
            return this.$selectionMarkers;
        };
    }).call(EditSession.prototype);

// extend Selection
    (function () {
        // list of ranges in reverse addition order
        this.ranges = null;

        // automatically sorted list of ranges
        this.rangeList = null;

        /**
         * Adds a range to a selection by entering multiselect mode, if necessary.
         * @param {Range} range The new range to add
         * @param {Boolean} $blockChangeEvents Whether or not to block changing events
         * @method Selection.addRange
         **/
        this.addRange = function (range, $blockChangeEvents) {
            if (!range)
                return;

            if (!this.inMultiSelectMode && this.rangeCount === 0) {
                var oldRange = this.toOrientedRange();
                this.rangeList.add(oldRange);
                this.rangeList.add(range);
                if (this.rangeList.ranges.length != 2) {
                    this.rangeList.removeAll();
                    return $blockChangeEvents || this.fromOrientedRange(range);
                }
                this.rangeList.removeAll();
                this.rangeList.add(oldRange);
                this.$onAddRange(oldRange);
            }

            if (!range.cursor)
                range.cursor = range.end;

            var removed = this.rangeList.add(range);

            this.$onAddRange(range);

            if (removed.length)
                this.$onRemoveRange(removed);

            if (this.rangeCount > 1 && !this.inMultiSelectMode) {
                this._signal("multiSelect");
                this.inMultiSelectMode = true;
                this.session.$undoSelect = false;
                this.rangeList.attach(this.session);
            }

            return $blockChangeEvents || this.fromOrientedRange(range);
        };

        /**
         * @method Selection.toSingleRange
         **/

        this.toSingleRange = function (range) {
            range = range || this.ranges[0];
            var removed = this.rangeList.removeAll();
            if (removed.length)
                this.$onRemoveRange(removed);

            range && this.fromOrientedRange(range);
        };

        /**
         * Removes a Range containing pos (if it exists).
         * @param {Range} pos The position to remove, as a `{row, column}` object
         * @method Selection.substractPoint
         **/
        this.substractPoint = function (pos) {
            var removed = this.rangeList.substractPoint(pos);
            if (removed) {
                this.$onRemoveRange(removed);
                return removed[0];
            }
        };

        /**
         * Merges overlapping ranges ensuring consistency after changes
         * @method Selection.mergeOverlappingRanges
         **/
        this.mergeOverlappingRanges = function () {
            var removed = this.rangeList.merge();
            if (removed.length)
                this.$onRemoveRange(removed);
            else if (this.ranges[0])
                this.fromOrientedRange(this.ranges[0]);
        };

        this.$onAddRange = function (range) {
            this.rangeCount = this.rangeList.ranges.length;
            this.ranges.unshift(range);
            this._signal("addRange", {range: range});
        };

        this.$onRemoveRange = function (removed) {
            this.rangeCount = this.rangeList.ranges.length;
            if (this.rangeCount == 1 && this.inMultiSelectMode) {
                var lastRange = this.rangeList.ranges.pop();
                removed.push(lastRange);
                this.rangeCount = 0;
            }

            for (var i = removed.length; i--;) {
                var index = this.ranges.indexOf(removed[i]);
                this.ranges.splice(index, 1);
            }

            this._signal("removeRange", {ranges: removed});

            if (this.rangeCount === 0 && this.inMultiSelectMode) {
                this.inMultiSelectMode = false;
                this._signal("singleSelect");
                this.session.$undoSelect = true;
                this.rangeList.detach(this.session);
            }

            lastRange = lastRange || this.ranges[0];
            if (lastRange && !lastRange.isEqual(this.getRange()))
                this.fromOrientedRange(lastRange);
        };

        // adds multicursor support to selection
        this.$initRangeList = function () {
            if (this.rangeList)
                return;

            this.rangeList = new RangeList();
            this.ranges = [];
            this.rangeCount = 0;
        };

        /**
         * Returns a concatenation of all the ranges.
         * @returns {Array}
         * @method Selection.getAllRanges
         **/
        this.getAllRanges = function () {
            return this.rangeCount ? this.rangeList.ranges.concat() : [this.getRange()];
        };

        /**
         * Splits all the ranges into lines.
         * @method Selection.splitIntoLines
         **/

        this.splitIntoLines = function () {
            if (this.rangeCount > 1) {
                var ranges = this.rangeList.ranges;
                var lastRange = ranges[ranges.length - 1];
                var range = Range.fromPoints(ranges[0].start, lastRange.end);

                this.toSingleRange();
                this.setSelectionRange(range, lastRange.cursor == lastRange.start);
            } else {
                var range = this.getRange();
                var isBackwards = this.isBackwards();
                var startRow = range.start.row;
                var endRow = range.end.row;
                if (startRow == endRow) {
                    if (isBackwards)
                        var start = range.end, end = range.start;
                    else
                        var start = range.start, end = range.end;

                    this.addRange(Range.fromPoints(end, end));
                    this.addRange(Range.fromPoints(start, start));
                    return;
                }

                var rectSel = [];
                var r = this.getLineRange(startRow, true);
                r.start.column = range.start.column;
                rectSel.push(r);

                for (var i = startRow + 1; i < endRow; i++)
                    rectSel.push(this.getLineRange(i, true));

                r = this.getLineRange(endRow, true);
                r.end.column = range.end.column;
                rectSel.push(r);

                rectSel.forEach(this.addRange, this);
            }
        };

        /**
         * @method Selection.toggleBlockSelection
         **/
        this.toggleBlockSelection = function () {
            if (this.rangeCount > 1) {
                var ranges = this.rangeList.ranges;
                var lastRange = ranges[ranges.length - 1];
                var range = Range.fromPoints(ranges[0].start, lastRange.end);

                this.toSingleRange();
                this.setSelectionRange(range, lastRange.cursor == lastRange.start);
            } else {
                var cursor = this.session.documentToScreenPosition(this.selectionLead);
                var anchor = this.session.documentToScreenPosition(this.selectionAnchor);

                var rectSel = this.rectangularRangeBlock(cursor, anchor);
                rectSel.forEach(this.addRange, this);
            }
        };

        /**
         *
         * Gets list of ranges composing rectangular block on the screen
         *
         * @param {Cursor} screenCursor The cursor to use
         * @param {Anchor} screenAnchor The anchor to use
         * @param {Boolean} includeEmptyLines If true, this includes ranges inside the block which are empty due to clipping
         * @returns {Range}
         * @method Selection.rectangularRangeBlock
         **/
        this.rectangularRangeBlock = function (screenCursor, screenAnchor, includeEmptyLines) {
            var rectSel = [];

            var xBackwards = screenCursor.column < screenAnchor.column;
            if (xBackwards) {
                var startColumn = screenCursor.column;
                var endColumn = screenAnchor.column;
            } else {
                var startColumn = screenAnchor.column;
                var endColumn = screenCursor.column;
            }

            var yBackwards = screenCursor.row < screenAnchor.row;
            if (yBackwards) {
                var startRow = screenCursor.row;
                var endRow = screenAnchor.row;
            } else {
                var startRow = screenAnchor.row;
                var endRow = screenCursor.row;
            }

            if (startColumn < 0)
                startColumn = 0;
            if (startRow < 0)
                startRow = 0;

            if (startRow == endRow)
                includeEmptyLines = true;

            for (var row = startRow; row <= endRow; row++) {
                var range = Range.fromPoints(
                    this.session.screenToDocumentPosition(row, startColumn),
                    this.session.screenToDocumentPosition(row, endColumn)
                );
                if (range.isEmpty()) {
                    if (docEnd && isSamePoint(range.end, docEnd))
                        break;
                    var docEnd = range.end;
                }
                range.cursor = xBackwards ? range.start : range.end;
                rectSel.push(range);
            }

            if (yBackwards)
                rectSel.reverse();

            if (!includeEmptyLines) {
                var end = rectSel.length - 1;
                while (rectSel[end].isEmpty() && end > 0)
                    end--;
                if (end > 0) {
                    var start = 0;
                    while (rectSel[start].isEmpty())
                        start++;
                }
                for (var i = end; i >= start; i--) {
                    if (rectSel[i].isEmpty())
                        rectSel.splice(i, 1);
                }
            }

            return rectSel;
        };
    }).call(Selection.prototype);

// extend Editor
    var Editor = require("./editor").Editor;
    (function () {

        /**
         *
         * Updates the cursor and marker layers.
         * @method Editor.updateSelectionMarkers
         *
         **/
        this.updateSelectionMarkers = function () {
            this.renderer.updateCursor();
            this.renderer.updateBackMarkers();
        };

        /**
         * Adds the selection and cursor.
         * @param {Range} orientedRange A range containing a cursor
         * @returns {Range}
         * @method Editor.addSelectionMarker
         **/
        this.addSelectionMarker = function (orientedRange) {
            if (!orientedRange.cursor)
                orientedRange.cursor = orientedRange.end;

            var style = this.getSelectionStyle();
            orientedRange.marker = this.session.addMarker(orientedRange, "ace_selection", style);

            this.session.$selectionMarkers.push(orientedRange);
            this.session.selectionMarkerCount = this.session.$selectionMarkers.length;
            return orientedRange;
        };

        /**
         * Removes the selection marker.
         * @param {Range} range The selection range added with [[Editor.addSelectionMarker `addSelectionMarker()`]].
         * @method Editor.removeSelectionMarker
         **/
        this.removeSelectionMarker = function (range) {
            if (!range.marker)
                return;
            this.session.removeMarker(range.marker);
            var index = this.session.$selectionMarkers.indexOf(range);
            if (index != -1)
                this.session.$selectionMarkers.splice(index, 1);
            this.session.selectionMarkerCount = this.session.$selectionMarkers.length;
        };

        this.removeSelectionMarkers = function (ranges) {
            var markerList = this.session.$selectionMarkers;
            for (var i = ranges.length; i--;) {
                var range = ranges[i];
                if (!range.marker)
                    continue;
                this.session.removeMarker(range.marker);
                var index = markerList.indexOf(range);
                if (index != -1)
                    markerList.splice(index, 1);
            }
            this.session.selectionMarkerCount = markerList.length;
        };

        this.$onAddRange = function (e) {
            this.addSelectionMarker(e.range);
            this.renderer.updateCursor();
            this.renderer.updateBackMarkers();
        };

        this.$onRemoveRange = function (e) {
            this.removeSelectionMarkers(e.ranges);
            this.renderer.updateCursor();
            this.renderer.updateBackMarkers();
        };

        this.$onMultiSelect = function (e) {
            if (this.inMultiSelectMode)
                return;
            this.inMultiSelectMode = true;

            this.setStyle("ace_multiselect");
            this.keyBinding.addKeyboardHandler(commands.keyboardHandler);
            this.commands.setDefaultHandler("exec", this.$onMultiSelectExec);

            this.renderer.updateCursor();
            this.renderer.updateBackMarkers();
        };

        this.$onSingleSelect = function (e) {
            if (this.session.multiSelect.inVirtualMode)
                return;
            this.inMultiSelectMode = false;

            this.unsetStyle("ace_multiselect");
            this.keyBinding.removeKeyboardHandler(commands.keyboardHandler);

            this.commands.removeDefaultHandler("exec", this.$onMultiSelectExec);
            this.renderer.updateCursor();
            this.renderer.updateBackMarkers();
            this._emit("changeSelection");
        };

        this.$onMultiSelectExec = function (e) {
            var command = e.command;
            var editor = e.editor;
            if (!editor.multiSelect)
                return;
            if (!command.multiSelectAction) {
                var result = command.exec(editor, e.args || {});
                editor.multiSelect.addRange(editor.multiSelect.toOrientedRange());
                editor.multiSelect.mergeOverlappingRanges();
            } else if (command.multiSelectAction == "forEach") {
                result = editor.forEachSelection(command, e.args);
            } else if (command.multiSelectAction == "forEachLine") {
                result = editor.forEachSelection(command, e.args, true);
            } else if (command.multiSelectAction == "single") {
                editor.exitMultiSelectMode();
                result = command.exec(editor, e.args || {});
            } else {
                result = command.multiSelectAction(editor, e.args || {});
            }
            return result;
        };

        /**
         * Executes a command for each selection range.
         * @param {Object} cmd The command to execute
         * @param {String} args Any arguments for the command
         * @method Editor.forEachSelection
         **/
        this.forEachSelection = function (cmd, args, options) {
            if (this.inVirtualSelectionMode)
                return;
            var keepOrder = options && options.keepOrder;
            var $byLines = options == true || options && options.$byLines
            var session = this.session;
            var selection = this.selection;
            var rangeList = selection.rangeList;
            var ranges = (keepOrder ? selection : rangeList).ranges;
            var result;

            if (!ranges.length)
                return cmd.exec ? cmd.exec(this, args || {}) : cmd(this, args || {});

            var reg = selection._eventRegistry;
            selection._eventRegistry = {};

            var tmpSel = new Selection(session);
            this.inVirtualSelectionMode = true;
            for (var i = ranges.length; i--;) {
                if ($byLines) {
                    while (i > 0 && ranges[i].start.row == ranges[i - 1].end.row)
                        i--;
                }
                tmpSel.fromOrientedRange(ranges[i]);
                tmpSel.index = i;
                this.selection = session.selection = tmpSel;
                var cmdResult = cmd.exec ? cmd.exec(this, args || {}) : cmd(this, args || {});
                if (!result && cmdResult !== undefined)
                    result = cmdResult;
                tmpSel.toOrientedRange(ranges[i]);
            }
            tmpSel.detach();

            this.selection = session.selection = selection;
            this.inVirtualSelectionMode = false;
            selection._eventRegistry = reg;
            selection.mergeOverlappingRanges();

            var anim = this.renderer.$scrollAnimation;
            this.onCursorChange();
            this.onSelectionChange();
            if (anim && anim.from == anim.to)
                this.renderer.animateScrolling(anim.from);

            return result;
        };

        /**
         * Removes all the selections except the last added one.
         * @method Editor.exitMultiSelectMode
         **/
        this.exitMultiSelectMode = function () {
            if (!this.inMultiSelectMode || this.inVirtualSelectionMode)
                return;
            this.multiSelect.toSingleRange();
        };

        this.getSelectedText = function () {
            var text = "";
            if (this.inMultiSelectMode && !this.inVirtualSelectionMode) {
                var ranges = this.multiSelect.rangeList.ranges;
                var buf = [];
                for (var i = 0; i < ranges.length; i++) {
                    buf.push(this.session.getTextRange(ranges[i]));
                }
                var nl = this.session.getDocument().getNewLineCharacter();
                text = buf.join(nl);
                if (text.length == (buf.length - 1) * nl.length)
                    text = "";
            } else if (!this.selection.isEmpty()) {
                text = this.session.getTextRange(this.getSelectionRange());
            }
            return text;
        };

        this.$checkMultiselectChange = function (e, anchor) {
            if (this.inMultiSelectMode && !this.inVirtualSelectionMode) {
                var range = this.multiSelect.ranges[0];
                if (this.multiSelect.isEmpty() && anchor == this.multiSelect.anchor)
                    return;
                var pos = anchor == this.multiSelect.anchor
                    ? range.cursor == range.start ? range.end : range.start
                    : range.cursor;
                if (pos.row != anchor.row
                    || this.session.$clipPositionToDocument(pos.row, pos.column).column != anchor.column)
                    this.multiSelect.toSingleRange(this.multiSelect.toOrientedRange());
            }
        };

        /**
         * Finds and selects all the occurences of `needle`.
         * @param {String} The text to find
         * @param {Object} The search options
         * @param {Boolean} keeps
         *
         * @returns {Number} The cumulative count of all found matches
         * @method Editor.findAll
         **/
        this.findAll = function (needle, options, additive) {
            options = options || {};
            options.needle = needle || options.needle;
            if (options.needle == undefined) {
                var range = this.selection.isEmpty()
                    ? this.selection.getWordRange()
                    : this.selection.getRange();
                options.needle = this.session.getTextRange(range);
            }
            this.$search.set(options);

            var ranges = this.$search.findAll(this.session);
            if (!ranges.length)
                return 0;

            this.$blockScrolling += 1;
            var selection = this.multiSelect;

            if (!additive)
                selection.toSingleRange(ranges[0]);

            for (var i = ranges.length; i--;)
                selection.addRange(ranges[i], true);

            // keep old selection as primary if possible
            if (range && selection.rangeList.rangeAtPoint(range.start))
                selection.addRange(range, true);

            this.$blockScrolling -= 1;

            return ranges.length;
        };

        /**
         * Adds a cursor above or below the active cursor.
         *
         * @param {Number} dir The direction of lines to select: -1 for up, 1 for down
         * @param {Boolean} skip If `true`, removes the active selection range
         *
         * @method Editor.selectMoreLines
         */
        this.selectMoreLines = function (dir, skip) {
            var range = this.selection.toOrientedRange();
            var isBackwards = range.cursor == range.end;

            var screenLead = this.session.documentToScreenPosition(range.cursor);
            if (this.selection.$desiredColumn)
                screenLead.column = this.selection.$desiredColumn;

            var lead = this.session.screenToDocumentPosition(screenLead.row + dir, screenLead.column);

            if (!range.isEmpty()) {
                var screenAnchor = this.session.documentToScreenPosition(isBackwards ? range.end : range.start);
                var anchor = this.session.screenToDocumentPosition(screenAnchor.row + dir, screenAnchor.column);
            } else {
                var anchor = lead;
            }

            if (isBackwards) {
                var newRange = Range.fromPoints(lead, anchor);
                newRange.cursor = newRange.start;
            } else {
                var newRange = Range.fromPoints(anchor, lead);
                newRange.cursor = newRange.end;
            }

            newRange.desiredColumn = screenLead.column;
            if (!this.selection.inMultiSelectMode) {
                this.selection.addRange(range);
            } else {
                if (skip)
                    var toRemove = range.cursor;
            }

            this.selection.addRange(newRange);
            if (toRemove)
                this.selection.substractPoint(toRemove);
        };

        /**
         * Transposes the selected ranges.
         * @param {Number} dir The direction to rotate selections
         * @method Editor.transposeSelections
         **/
        this.transposeSelections = function (dir) {
            var session = this.session;
            var sel = session.multiSelect;
            var all = sel.ranges;

            for (var i = all.length; i--;) {
                var range = all[i];
                if (range.isEmpty()) {
                    var tmp = session.getWordRange(range.start.row, range.start.column);
                    range.start.row = tmp.start.row;
                    range.start.column = tmp.start.column;
                    range.end.row = tmp.end.row;
                    range.end.column = tmp.end.column;
                }
            }
            sel.mergeOverlappingRanges();

            var words = [];
            for (var i = all.length; i--;) {
                var range = all[i];
                words.unshift(session.getTextRange(range));
            }

            if (dir < 0)
                words.unshift(words.pop());
            else
                words.push(words.shift());

            for (var i = all.length; i--;) {
                var range = all[i];
                var tmp = range.clone();
                session.replace(range, words[i]);
                range.start.row = tmp.start.row;
                range.start.column = tmp.start.column;
            }
        };

        /**
         * Finds the next occurence of text in an active selection and adds it to the selections.
         * @param {Number} dir The direction of lines to select: -1 for up, 1 for down
         * @param {Boolean} skip If `true`, removes the active selection range
         * @method Editor.selectMore
         **/
        this.selectMore = function (dir, skip, stopAtFirst) {
            var session = this.session;
            var sel = session.multiSelect;

            var range = sel.toOrientedRange();
            if (range.isEmpty()) {
                range = session.getWordRange(range.start.row, range.start.column);
                range.cursor = dir == -1 ? range.start : range.end;
                this.multiSelect.addRange(range);
                if (stopAtFirst)
                    return;
            }
            var needle = session.getTextRange(range);

            var newRange = find(session, needle, dir);
            if (newRange) {
                newRange.cursor = dir == -1 ? newRange.start : newRange.end;
                this.$blockScrolling += 1;
                this.session.unfold(newRange);
                this.multiSelect.addRange(newRange);
                this.$blockScrolling -= 1;
                this.renderer.scrollCursorIntoView(null, 0.5);
            }
            if (skip)
                this.multiSelect.substractPoint(range.cursor);
        };

        /**
         * Aligns the cursors or selected text.
         * @method Editor.alignCursors
         **/
        this.alignCursors = function () {
            var session = this.session;
            var sel = session.multiSelect;
            var ranges = sel.ranges;
            // filter out ranges on same row
            var row = -1;
            var sameRowRanges = ranges.filter(function (r) {
                if (r.cursor.row == row)
                    return true;
                row = r.cursor.row;
            });

            if (!ranges.length || sameRowRanges.length == ranges.length - 1) {
                var range = this.selection.getRange();
                var fr = range.start.row, lr = range.end.row;
                var guessRange = fr == lr;
                if (guessRange) {
                    var max = this.session.getLength();
                    var line;
                    do {
                        line = this.session.getLine(lr);
                    } while (/[=:]/.test(line) && ++lr < max);
                    do {
                        line = this.session.getLine(fr);
                    } while (/[=:]/.test(line) && --fr > 0);

                    if (fr < 0) fr = 0;
                    if (lr >= max) lr = max - 1;
                }
                var lines = this.session.removeFullLines(fr, lr);
                lines = this.$reAlignText(lines, guessRange);
                this.session.insert({row: fr, column: 0}, lines.join("\n") + "\n");
                if (!guessRange) {
                    range.start.column = 0;
                    range.end.column = lines[lines.length - 1].length;
                }
                this.selection.setRange(range);
            } else {
                sameRowRanges.forEach(function (r) {
                    sel.substractPoint(r.cursor);
                });

                var maxCol = 0;
                var minSpace = Infinity;
                var spaceOffsets = ranges.map(function (r) {
                    var p = r.cursor;
                    var line = session.getLine(p.row);
                    var spaceOffset = line.substr(p.column).search(/\S/g);
                    if (spaceOffset == -1)
                        spaceOffset = 0;

                    if (p.column > maxCol)
                        maxCol = p.column;
                    if (spaceOffset < minSpace)
                        minSpace = spaceOffset;
                    return spaceOffset;
                });
                ranges.forEach(function (r, i) {
                    var p = r.cursor;
                    var l = maxCol - p.column;
                    var d = spaceOffsets[i] - minSpace;
                    if (l > d)
                        session.insert(p, lang.stringRepeat(" ", l - d));
                    else
                        session.remove(new Range(p.row, p.column, p.row, p.column - l + d));

                    r.start.column = r.end.column = maxCol;
                    r.start.row = r.end.row = p.row;
                    r.cursor = r.end;
                });
                sel.fromOrientedRange(ranges[0]);
                this.renderer.updateCursor();
                this.renderer.updateBackMarkers();
            }
        };

        this.$reAlignText = function (lines, forceLeft) {
            var isLeftAligned = true, isRightAligned = true;
            var startW, textW, endW;

            return lines.map(function (line) {
                var m = line.match(/(\s*)(.*?)(\s*)([=:].*)/);
                if (!m)
                    return [line];

                if (startW == null) {
                    startW = m[1].length;
                    textW = m[2].length;
                    endW = m[3].length;
                    return m;
                }

                if (startW + textW + endW != m[1].length + m[2].length + m[3].length)
                    isRightAligned = false;
                if (startW != m[1].length)
                    isLeftAligned = false;

                if (startW > m[1].length)
                    startW = m[1].length;
                if (textW < m[2].length)
                    textW = m[2].length;
                if (endW > m[3].length)
                    endW = m[3].length;

                return m;
            }).map(forceLeft ? alignLeft :
                isLeftAligned ? isRightAligned ? alignRight : alignLeft : unAlign);

            function spaces(n) {
                return lang.stringRepeat(" ", n);
            }

            function alignLeft(m) {
                return !m[2] ? m[0] : spaces(startW) + m[2]
                    + spaces(textW - m[2].length + endW)
                    + m[4].replace(/^([=:])\s+/, "$1 ");
            }

            function alignRight(m) {
                return !m[2] ? m[0] : spaces(startW + textW - m[2].length) + m[2]
                    + spaces(endW, " ")
                    + m[4].replace(/^([=:])\s+/, "$1 ");
            }

            function unAlign(m) {
                return !m[2] ? m[0] : spaces(startW) + m[2]
                    + spaces(endW)
                    + m[4].replace(/^([=:])\s+/, "$1 ");
            }
        };
    }).call(Editor.prototype);


    function isSamePoint(p1, p2) {
        return p1.row == p2.row && p1.column == p2.column;
    }

// patch
// adds multicursor support to a session
    exports.onSessionChange = function (e) {
        var session = e.session;
        if (session && !session.multiSelect) {
            session.$selectionMarkers = [];
            session.selection.$initRangeList();
            session.multiSelect = session.selection;
        }
        this.multiSelect = session && session.multiSelect;

        var oldSession = e.oldSession;
        if (oldSession) {
            oldSession.multiSelect.off("addRange", this.$onAddRange);
            oldSession.multiSelect.off("removeRange", this.$onRemoveRange);
            oldSession.multiSelect.off("multiSelect", this.$onMultiSelect);
            oldSession.multiSelect.off("singleSelect", this.$onSingleSelect);
            oldSession.multiSelect.lead.off("change", this.$checkMultiselectChange);
            oldSession.multiSelect.anchor.off("change", this.$checkMultiselectChange);
        }

        if (session) {
            session.multiSelect.on("addRange", this.$onAddRange);
            session.multiSelect.on("removeRange", this.$onRemoveRange);
            session.multiSelect.on("multiSelect", this.$onMultiSelect);
            session.multiSelect.on("singleSelect", this.$onSingleSelect);
            session.multiSelect.lead.on("change", this.$checkMultiselectChange);
            session.multiSelect.anchor.on("change", this.$checkMultiselectChange);
        }

        if (session && this.inMultiSelectMode != session.selection.inMultiSelectMode) {
            if (session.selection.inMultiSelectMode)
                this.$onMultiSelect();
            else
                this.$onSingleSelect();
        }
    };

// MultiSelect(editor)
// adds multiple selection support to the editor
// (note: should be called only once for each editor instance)
    function MultiSelect(editor) {
        if (editor.$multiselectOnSessionChange)
            return;
        editor.$onAddRange = editor.$onAddRange.bind(editor);
        editor.$onRemoveRange = editor.$onRemoveRange.bind(editor);
        editor.$onMultiSelect = editor.$onMultiSelect.bind(editor);
        editor.$onSingleSelect = editor.$onSingleSelect.bind(editor);
        editor.$multiselectOnSessionChange = exports.onSessionChange.bind(editor);
        editor.$checkMultiselectChange = editor.$checkMultiselectChange.bind(editor);

        editor.$multiselectOnSessionChange(editor);
        editor.on("changeSession", editor.$multiselectOnSessionChange);

        editor.on("mousedown", onMouseDown);
        editor.commands.addCommands(commands.defaultCommands);

        addAltCursorListeners(editor);
    }

    function addAltCursorListeners(editor) {
        var el = editor.textInput.getElement();
        var altCursor = false;
        event.addListener(el, "keydown", function (e) {
            var altDown = e.keyCode == 18 && !(e.ctrlKey || e.shiftKey || e.metaKey);
            if (editor.$blockSelectEnabled && altDown) {
                if (!altCursor) {
                    editor.renderer.setMouseCursor("crosshair");
                    altCursor = true;
                }
            } else if (altCursor) {
                reset();
            }
        });

        event.addListener(el, "keyup", reset);
        event.addListener(el, "blur", reset);
        function reset(e) {
            if (altCursor) {
                editor.renderer.setMouseCursor("");
                altCursor = false;
                // TODO disable menu poping up
                // e && e.preventDefault()
            }
        }
    }

    exports.MultiSelect = MultiSelect;


    require("./config").defineOptions(Editor.prototype, "editor", {
        enableMultiselect: {
            set: function (val) {
                MultiSelect(this);
                if (val) {
                    this.on("changeSession", this.$multiselectOnSessionChange);
                    this.on("mousedown", onMouseDown);
                } else {
                    this.off("changeSession", this.$multiselectOnSessionChange);
                    this.off("mousedown", onMouseDown);
                }
            },
            value: true
        },
        enableBlockSelect: {
            set: function (val) {
                this.$blockSelectEnabled = val;
            },
            value: true
        }
    });


});
