var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
define(["require", "exports", "rangy", "./dloc", "./domtypeguards", "./domutil"], function (require, exports, rangy_1, dloc_1, domtypeguards_1, domutil_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    rangy_1 = __importDefault(rangy_1);
    /** The direction of searches. */
    var Direction;
    (function (Direction) {
        Direction[Direction["FORWARD"] = 0] = "FORWARD";
        Direction[Direction["BACKWARDS"] = 1] = "BACKWARDS";
    })(Direction = exports.Direction || (exports.Direction = {}));
    /** The context for searches. */
    var Context;
    (function (Context) {
        /** Everywhere in a document, including non-editable graphical elements. */
        Context[Context["EVERYWHERE"] = 0] = "EVERYWHERE";
        /** Only element text. */
        Context[Context["TEXT"] = 1] = "TEXT";
        /** Only attribute values. */
        Context[Context["ATTRIBUTE_VALUES"] = 2] = "ATTRIBUTE_VALUES";
    })(Context = exports.Context || (exports.Context = {}));
    function unknownDirection(d) {
        throw new Error(`unknown direction: ${d}`);
    }
    function directionToRangyDirection(direction) {
        // There does not seem to be a way currently to declare this map in a way
        // that will enforce that all directions have a value. :-/
        // tslint:disable-next-line:no-object-literal-type-assertion
        const ret = {
            [Direction.FORWARD]: "forward",
            [Direction.BACKWARDS]: "backward",
        }[direction];
        if (ret === undefined) {
            // We have to cast to never since we're not using the switch exhaustion.
            return unknownDirection(direction);
        }
        return ret;
    }
    function nodeInScope(doc, node, scope) {
        const vrange = doc.createRange();
        vrange.selectNode(node);
        // The range that encompasses the node, must be completely within scope.
        return (vrange.compareBoundaryPoints(Range.START_TO_START, scope) >= 0) &&
            (vrange.compareBoundaryPoints(Range.END_TO_END, scope) <= 0);
    }
    /**
     * This is a utility class that holds a position among a list of elements
     * (representing attributes, in our usage).
     */
    class AttributeValueCursor {
        /**
         * @param values The values to iterate over.
         *
         * @param direction The direction to iterate over.
         */
        constructor(values, direction) {
            this.values = values;
            this.direction = direction;
            this.resetToStart();
        }
        /**
         * @param value The index to reset this iterator to.
         */
        reset(value) {
            this.current = value;
        }
        /**
         * @param value Reset to the start of [[values]]. This will be position 0 for
         * an iterator moving forward. Or the end of [[values]] for an iterator moving
         * backwards.
         */
        resetToStart() {
            switch (this.direction) {
                case Direction.FORWARD:
                    this.current = 0;
                    break;
                case Direction.BACKWARDS:
                    this.current = this.values.length - 1;
                    break;
                default:
                    return unknownDirection(this.direction);
            }
        }
        /**
         * @returns ``true`` if we have not reached the end of the array.
         */
        get hasNext() {
            switch (this.direction) {
                case Direction.FORWARD:
                    return this.current < this.values.length;
                case Direction.BACKWARDS:
                    return this.current >= 0;
                default:
                    return unknownDirection(this.direction);
            }
        }
        /**
         * This is the next element in iteration order. Moves the iterator in the
         * direction of travel.
         */
        get next() {
            const ret = this.values[this.current];
            this.inc();
            return ret;
        }
        /**
         * Moves the iterator in the direction of travel.
         */
        inc() {
            switch (this.direction) {
                case Direction.FORWARD:
                    this.current++;
                    break;
                case Direction.BACKWARDS:
                    this.current--;
                    break;
                default:
                    return unknownDirection(this.direction);
            }
        }
    }
    /**
     * This models a search on the GUI tree. Performing searches directly on the
     * data tree is theoretically possible but fraught with problems. For instance,
     * some data may not be visible to users and so the search in the data tree
     * would have to constantly refer to the GUI tree to determine whether a hit
     * should be shown. Additionally, the order of the data shown in the GUI tree
     * may differ from the order in the data tree.
     */
    class Search {
        constructor(caretManager, guiRoot, start, scope) {
            this.caretManager = caretManager;
            this.guiRoot = guiRoot;
            this.start = start;
            this._pattern = "";
            /** The direction in which the search moves. */
            this.direction = Direction.FORWARD;
            /** The context for the search. */
            this.context = Context.EVERYWHERE;
            this.root = dloc_1.getRoot(guiRoot);
            this.setScope(scope);
            const realScope = this.scope;
            if (realScope.start.compare(start) > 0 ||
                realScope.end.compare(start) < 0) {
                throw new Error("the scope does not contain the start position");
            }
            this.start = start;
        }
        set pattern(value) {
            this._pattern = value;
        }
        get pattern() {
            return this._pattern;
        }
        /**
         * Set the search scope. No result will be returned outside the scope. Setting
         * the scope to ``undefined`` means "search the whole document".
         */
        setScope(range) {
            if (range === undefined) {
                this._scope = undefined;
                return;
            }
            if (!range.isValid()) {
                throw new Error("passed an invalid range");
            }
            const { start, end } = range;
            if (start.root !== this.root.node) {
                throw new Error("the range does not use the search's root");
            }
            // Since the start and end of a range must share the same root, we don't
            // have to test the end of the range.
            this._scope = start.compare(end) > 0 ?
                // Start is after end, flip them.
                new dloc_1.DLocRange(end, start) :
                // Regular order
                new dloc_1.DLocRange(start, end);
        }
        get scope() {
            if (this._scope === undefined) {
                this._scope = new dloc_1.DLocRange(dloc_1.DLoc.mustMakeDLoc(this.root, this.guiRoot, 0), dloc_1.DLoc.mustMakeDLoc(this.root, this.guiRoot, this.guiRoot.childNodes.length));
            }
            return this._scope;
        }
        updateCurrent() {
            this._next(true);
        }
        next() {
            this._next(false);
        }
        _next(includeCurrent) {
            let ret = null;
            if (this.pattern !== "") {
                let rollPosition;
                let start;
                switch (this.direction) {
                    case Direction.FORWARD: {
                        start = this.getForwardSearchStart(includeCurrent);
                        rollPosition = this.scope.start;
                        break;
                    }
                    case Direction.BACKWARDS: {
                        start = this.getBackwardSearchStart(includeCurrent);
                        rollPosition = this.scope.end;
                        break;
                    }
                    default:
                        return unknownDirection(this.direction);
                }
                const hit = this.find(start, this.direction);
                if (hit !== null) {
                    ret = new dloc_1.DLocRange(dloc_1.DLoc.mustMakeDLoc(this.root, hit.startContainer, hit.startOffset), dloc_1.DLoc.mustMakeDLoc(this.root, hit.endContainer, hit.endOffset));
                }
                else {
                    // If we did not get a hit, we roll over on the next search.
                    this.start = rollPosition;
                }
            }
            this.current = ret;
        }
        find(start, direction) {
            if (this.context === Context.ATTRIBUTE_VALUES) {
                return this.findAttributeValue(start, direction);
            }
            return this.findText(start, directionToRangyDirection(direction));
        }
        findText(start, direction) {
            // tslint:disable-next-line:no-any
            const config = rangy_1.default.config;
            const range = new rangy_1.default.WrappedRange(start.makeRange());
            if (this.context === Context.TEXT) {
                config.customIsCollapsedNode = (node) => {
                    return domtypeguards_1.isElement(node) && node.closest("._phantom") !== null;
                };
            }
            let found = range.findText(this.pattern, {
                withinRange: this.scope.mustMakeDOMRange(),
                direction,
            });
            // There is a bug in Rangy that makes it so that it may sometimes return
            // hits outside the scope. Test for it.
            if (found) {
                const hitStart = dloc_1.DLoc.mustMakeDLoc(this.guiRoot, range.startContainer, range.startOffset);
                if (!this.scope.contains(hitStart)) {
                    found = false;
                }
            }
            config.customIsCollapsedNode = undefined;
            return found ? range.nativeRange : null;
        }
        // tslint:disable-next-line:max-func-body-length
        findAttributeValue(start, direction) {
            // Implement our own logic instead of relying on rangy. We can just move
            // from attribute value to attribute value and checks the values.
            const guiRoot = this.guiRoot;
            const allValues = Array.from(guiRoot.getElementsByClassName("_attribute_value"));
            const caretManager = this.caretManager;
            const valueCursor = new AttributeValueCursor(allValues, direction);
            const attrValue = domutil_1.closestByClass(start.node, "_attribute_value", guiRoot);
            const doc = guiRoot.ownerDocument;
            const scope = this.scope.mustMakeDOMRange();
            if (attrValue === null) {
                // We need to find the next attribute.
                let found;
                while (valueCursor.hasNext) {
                    const value = valueCursor.next;
                    if (nodeInScope(doc, value, scope) &&
                        // tslint:disable-next-line:no-bitwise
                        ((value.compareDocumentPosition(start.node) &
                            Node.DOCUMENT_POSITION_PRECEDING) !== 0)) {
                        found = value;
                        break;
                    }
                }
                if (found === undefined) {
                    return null;
                }
                start = start.make(found, 0);
            }
            else {
                const index = allValues.indexOf(attrValue);
                if (index === -1) {
                    throw new Error("internal error: cannot find value in array!");
                }
                valueCursor.reset(index);
                valueCursor.inc();
            }
            let dataLoc = caretManager.toDataLocation(start);
            // tslint:disable-next-line:no-constant-condition
            while (true) {
                // Going into the data tree simplifies some of the work here.
                const node = dataLoc.node;
                let index;
                switch (direction) {
                    case Direction.FORWARD:
                        index = node.value.indexOf(this.pattern, dataLoc.offset);
                        break;
                    case Direction.BACKWARDS:
                        // For a backward search, the hit is not allowed to cross the start
                        // position. (This, by the way, is the same way Emacs operates.)
                        const startOffset = dataLoc.offset - this.pattern.length;
                        index = (startOffset < 0) ?
                            -1 : node.value.lastIndexOf(this.pattern, startOffset);
                        break;
                    default:
                        return unknownDirection(direction);
                }
                if (index !== -1) {
                    const rangeStart = caretManager.mustFromDataLocation(dataLoc.makeWithOffset(index));
                    const rangeEnd = caretManager.mustFromDataLocation(dataLoc.makeWithOffset(index + this.pattern.length));
                    if (this.scope.contains(rangeStart) && this.scope.contains(rangeEnd)) {
                        return new dloc_1.DLocRange(rangeStart, rangeEnd).mustMakeDOMRange();
                    }
                }
                // We did not find a good hit, so continue searching other values.
                let next = null;
                while (next === null && valueCursor.hasNext) {
                    next = valueCursor.next;
                    if (!nodeInScope(doc, next, scope)) {
                        next = null;
                    }
                }
                if (next === null) {
                    return null;
                }
                let dataNext = caretManager.toDataLocation(next, 0);
                switch (direction) {
                    case Direction.FORWARD:
                        break;
                    case Direction.BACKWARDS:
                        dataNext =
                            dataNext.makeWithOffset(dataNext.node.value.length);
                        break;
                    default:
                        return unknownDirection(direction);
                }
                dataLoc = dataNext;
            }
        }
        getForwardSearchStart(includeCurrent) {
            if (this.current != null) {
                return includeCurrent ? this.current.start : this.current.end;
            }
            return this.start;
        }
        getBackwardSearchStart(includeCurrent) {
            let ret;
            if (this.current != null) {
                ret = includeCurrent ? this.prevEnd : this.current.start;
            }
            if (ret === undefined) {
                ret = this.start;
            }
            this.prevEnd = ret;
            return ret;
        }
    }
    exports.Search = Search;
});
//# sourceMappingURL=search.js.map