define(["require", "exports", "../dloc", "../domutil", "../search"], function (require, exports, dloc_1, domutil_1, search_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.Context = search_1.Context;
    exports.Direction = search_1.Direction;
    var Edge;
    (function (Edge) {
        Edge[Edge["START"] = 0] = "START";
        Edge[Edge["END"] = 1] = "END";
    })(Edge || (Edge = {}));
    /**
     * A search-and-replace engine for editor instances. This implements the code
     * that is common to quick searches and more complex searches. This object is
     * responsible for maintaining a search position in the document, and replacing
     * hits as required.
     */
    class SearchReplace {
        /**
         * @param editor The editor for which we are searching.
         *
         * @param scroller The scroller holding the document.
         */
        constructor(editor, scroller) {
            this.editor = editor;
            this.scroller = scroller;
            this.lastMatch = null;
            this.caretManager = this.editor.caretManager;
            const sel = this.caretManager.sel;
            const scope = (sel !== undefined && !sel.collapsed) ?
                new dloc_1.DLocRange(sel.anchor, sel.focus) : undefined;
            // If we have a scope, then we had a selection and we want to use the
            // selection's anchor, which is scope.start at this point.
            const start = scope !== undefined ? scope.start : this.caretManager.caret;
            if (start === undefined) {
                throw new Error("search without a caret!");
            }
            this.search = new search_1.Search(this.caretManager, editor.guiRoot, start, scope);
        }
        /**
         * The current match. This is ``undefined`` if we have not searched yet.  It
         * is ``null`` if nothing matches.
         */
        get current() {
            return this.search.current;
        }
        /**
         * Whether we can replace the current hit. If there is no hit, then this is
         * ``false``. If the hit is somehow collapsed, this is also
         * ``false``. Otherwise, the hit must be a well-formed range.
         */
        get canReplace() {
            const current = this.search.current;
            if (current == null) {
                return false;
            }
            if (current.collapsed) {
                return false;
            }
            return domutil_1.isWellFormedRange(current.mustMakeDOMRange());
        }
        /**
         * Update the pattern to a new value. Calling this method attempts to update
         * the current hit first, and may move in the direction of the search if
         * updating the current hit is not possible. This updates [[current]].
         *
         * @param value The new pattern value.
         *
         * @param options The search options.
         */
        updatePattern(value, options) {
            this.search.pattern = value;
            this.search.direction = options.direction;
            this.search.context = options.context;
            this.search.updateCurrent();
            this.updateHighlight();
        }
        /**
         * Find the next hit in the direction of the search. This updates [[current]].
         *
         * @param options The search options.
         */
        next(options) {
            this.search.direction = options.direction;
            this.search.context = options.context;
            this.search.next();
            this.updateHighlight();
        }
        /**
         * Update the highlight marking the current hit.
         */
        updateHighlight() {
            this.clearHighlight();
            const match = this.current;
            if (match != null) {
                this.lastMatch = match;
                this.setCaretToMatch();
                const range = match.start.mustMakeDLocRange(match.end);
                const domRange = range.mustMakeDOMRange();
                this.highlight = this.caretManager.highlightRange(range);
                const scRect = this.scroller.getBoundingClientRect();
                const rect = domRange.getBoundingClientRect();
                const leftOffset = this.scroller.scrollLeft - scRect.left;
                const topOffset = this.scroller.scrollTop - scRect.top;
                this.scroller.scrollIntoView(rect.left + leftOffset, rect.top + topOffset, rect.right + leftOffset, rect.bottom + topOffset);
            }
        }
        /**
         * Clear the highlight that this object produced to mark a hit.
         */
        clearHighlight() {
            if (this.highlight !== undefined) {
                this.highlight.parentNode.removeChild(this.highlight);
                this.highlight = undefined;
            }
        }
        /**
         * Set the caret position to the latest hit we ran into.
         */
        setCaretToMatch() {
            if (this.lastMatch !== null) {
                const loc = this.getDirectionalEnd(this.lastMatch);
                this.caretManager.setCaret(loc, { focus: false });
            }
        }
        getDirectionalEnd(range) {
            return this.getDirectionalEdge(range, Edge.END);
        }
        getDirectionalStart(range) {
            return this.getDirectionalEdge(range, Edge.START);
        }
        getDirectionalEdge(range, edge) {
            let field;
            const direction = this.search.direction;
            const start = edge === Edge.START;
            switch (direction) {
                case search_1.Direction.FORWARD:
                    field = start ? "start" : "end";
                    break;
                case search_1.Direction.BACKWARDS:
                    field = start ? "end" : "start";
                    break;
                default:
                    const d = direction;
                    throw new Error(`unknown direction: ${d}`);
            }
            return range[field];
        }
        /**
         * Replace the current hit with text.
         *
         * @param value The new text.
         *
         * @throw {Error} When called if [[canReplace]] is false.
         */
        replace(value) {
            if (!this.canReplace) {
                throw new Error("tried to replace when it is not possible");
            }
            const current = this.current;
            // With the !this.canReplace test above, it is not currently possible to
            // hit this condition.
            if (current == null) {
                throw new Error("no current match");
            }
            const caret = this.getDirectionalStart(current);
            this.caretManager.setCaret(caret, { focus: false });
            // tslint:disable-next-line:no-object-literal-type-assertion
            this.editor.fireTransformation(this.editor.replaceRangeTr, {
                range: current,
                newText: value,
                caretAtEnd: false,
            });
            this.clearHighlight();
            const caretAfter = this.caretManager.caret;
            if (caretAfter === undefined) {
                throw new Error("no caret after replacement!");
            }
            // We must update the current match because the old range is no longe valid.
            this.search.current = caretAfter.mustMakeDLocRange();
        }
    }
    exports.SearchReplace = SearchReplace;
});
//# sourceMappingURL=search-replace.js.map