var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
define(["require", "exports", "jquery", "./domtypeguards", "./domutil"], function (require, exports, jquery_1, domtypeguards_1, domutil_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    jquery_1 = __importDefault(jquery_1);
    /**
     * A class for objects that are used to mark DOM nodes as roots for the purpose
     * of using DLoc objects.
     */
    class DLocRoot {
        /**
         * @param el The element to which this object is associated.
         */
        constructor(node) {
            this.node = node;
            if (jquery_1.default.data(node, "wed-dloc-root") != null) {
                throw new Error("node already marked as root");
            }
            jquery_1.default.data(node, "wed-dloc-root", this);
        }
        /**
         * Converts a node to a path. A path is a string representation of the
         * location of a node relative to the root.
         *
         * @param node The node for which to construct a path.
         *
         * @returns The path.
         */
        nodeToPath(node) {
            if (node == null) {
                throw new Error("invalid node parameter");
            }
            const root = this.node;
            if (root === node) {
                return "";
            }
            if (!domutil_1.contains(root, node)) {
                throw new Error("node is not a descendant of root");
            }
            const ret = [];
            while (node !== root) {
                let parent;
                if (domtypeguards_1.isAttr(node)) {
                    parent = node.ownerElement;
                    ret.unshift(`@${node.name}`);
                }
                else {
                    let offset = 0;
                    parent = node.parentNode;
                    let offsetNode = node.previousSibling;
                    while (offsetNode !== null) {
                        const t = offsetNode.nodeType;
                        if ((t === Node.TEXT_NODE) || (t === Node.ELEMENT_NODE)) {
                            offset++;
                        }
                        offsetNode = offsetNode.previousSibling;
                    }
                    ret.unshift(String(offset));
                }
                // We checked whether the node is contained by root so we should never run
                // into a null parent.
                node = parent;
            }
            return ret.join("/");
        }
        /**
         * This function recovers a DOM node on the basis of a path previously created
         * by [[nodeToPath]].
         *
         * @param path The path to interpret.
         *
         * @returns The node corresponding to the path, or ``null`` if no such node
         * exists.
         *
         * @throws {Error} If given a malformed ``path``.
         */
        pathToNode(path) {
            const root = this.node;
            if (path === "") {
                return root;
            }
            const parts = path.split(/\//);
            let parent = root;
            let attribute;
            // Set aside the last part if it is an attribute.
            if (parts[parts.length - 1][0] === "@") {
                attribute = parts.pop();
            }
            for (const part of parts) {
                if (/^(\d+)$/.test(part)) {
                    let index = parseInt(part);
                    let found = null;
                    let node = parent.firstChild;
                    while (node !== null && found === null) {
                        const t = node.nodeType;
                        if ((t === Node.TEXT_NODE || (t === Node.ELEMENT_NODE)) &&
                            --index < 0) {
                            found = node;
                        }
                        node = node.nextSibling;
                    }
                    if (found === null) {
                        return null;
                    }
                    parent = found;
                }
                else {
                    throw new Error("malformed path expression");
                }
            }
            if (attribute === undefined) {
                return parent;
            }
            if (!domtypeguards_1.isElement(parent)) {
                throw new Error(`parent must be an element since we are looking for an \
attribute`);
            }
            return parent.getAttributeNode(attribute.slice(1));
        }
    }
    exports.DLocRoot = DLocRoot;
    function getTestLength(node) {
        let testLength;
        if (domtypeguards_1.isAttr(node)) {
            testLength = node.value.length;
        }
        else {
            switch (node.nodeType) {
                case Node.TEXT_NODE:
                    testLength = node.data.length;
                    break;
                case Node.DOCUMENT_NODE:
                case Node.ELEMENT_NODE:
                    testLength = node.childNodes.length;
                    break;
                default:
                    throw new Error("unexpected node type");
            }
        }
        return testLength;
    }
    /**
     * ``DLoc`` objects model locations in a DOM tree. Although the current
     * implementation does not enforce this, **these objects are to be treated as
     * immutable**. These objects have ``node`` and ``offset`` properties that are
     * to be interpreted in the same way DOM locations usually are: the ``node`` is
     * the location of a DOM ``Node`` in a DOM tree (or an attribute), and
     * ``offset`` is a location in that node. ``DLoc`` objects are said to have a
     * ``root`` relative to which they are positioned.
     *
     * A DLoc object can point to an offset inside an ``Element``, inside a ``Text``
     * node or inside an ``Attr``.
     *
     * Use [[makeDLoc]] to make ``DLoc`` objects. Calling this constructor directly
     * is not legal.
     *
     */
    class DLoc {
        /**
         * @param root The root of the DOM tree to which this DLoc applies.
         *
         * @param node The node of the location.
         *
         * @param offset The offset of the location.
         */
        constructor(root, node, offset) {
            this.root = root;
            this.node = node;
            this.offset = offset;
        }
        /**
         * This is the node to which this location points. For locations pointing to
         * attributes and text nodes, that's the same as [[node]]. For locations
         * pointing to an element, that's the child to which the ``node, offset`` pair
         * points. Since this pair may point after the last child of an element, the
         * child obtained may be ``undefined``.
         */
        get pointedNode() {
            if (domtypeguards_1.isElement(this.node)) {
                return this.node.childNodes[this.offset];
            }
            return this.node;
        }
        /**
         * Creates a copy of the location.
         */
        clone() {
            return new DLoc(this.root, this.node, this.offset);
        }
        static makeDLoc(root, node, offset, normalize) {
            if (node instanceof Array) {
                normalize = offset;
                [node, offset] = node;
            }
            if (normalize === undefined) {
                normalize = false;
            }
            if (node == null) {
                return undefined;
            }
            if (offset === undefined) {
                const parent = node.parentNode;
                if (parent === null) {
                    throw new Error("trying to get parent of a detached node");
                }
                offset = domutil_1.indexOf(parent.childNodes, node);
                node = parent;
            }
            else {
                if (typeof offset !== "number") {
                    throw new Error("offset is not a number, somehow");
                }
                if (offset < 0) {
                    if (normalize) {
                        offset = 0;
                    }
                    else {
                        throw new Error("negative offsets are not allowed");
                    }
                }
            }
            if (root instanceof DLocRoot) {
                root = root.node;
            }
            else if (jquery_1.default.data(root, "wed-dloc-root") == null) {
                throw new Error("root has not been marked as a root");
            }
            if (!domutil_1.contains(root, node)) {
                throw new Error("node not in root");
            }
            const testLength = getTestLength(node);
            if (offset > testLength) {
                if (normalize) {
                    offset = testLength;
                }
                else {
                    throw new Error("offset greater than allowable value");
                }
            }
            return new DLoc(root, node, offset);
        }
        // @ts-ignore
        static mustMakeDLoc(root, node, 
        // @ts-ignore
        offset, 
        // @ts-ignore
        normalize) {
            let nodeToCheck = node;
            if (nodeToCheck instanceof Array) {
                nodeToCheck = nodeToCheck[0];
            }
            if (nodeToCheck == null) {
                throw new Error("called mustMakeDLoc with an absent node");
            }
            // tslint:disable-next-line:no-any
            return this.makeDLoc.apply(this, arguments);
        }
        make(node, offset) {
            if (node instanceof Array) {
                return DLoc.mustMakeDLoc(this.root, node);
            }
            if (offset !== undefined && typeof offset !== "number") {
                throw new Error("if the 1st argument is a node, the 2nd must be a number or undefined");
            }
            return DLoc.mustMakeDLoc(this.root, node, offset);
        }
        /**
         * Make a new location with the same node as the current location but with a
         * new offset.
         *
         * @param offset The offset of the new location.
         *
         * @returns The new location.
         */
        makeWithOffset(offset) {
            if (offset === this.offset) {
                return this;
            }
            return this.make(this.node, offset);
        }
        /**
         * Make a new location. Let's define "current node" as the node of the current
         * location. The new location points to the current node. (The offset of the
         * current location is effectively ignored.) That is, the new location has for
         * node the parent node of the current node, and for offset the offset of the
         * current node in its parent.
         *
         * @returns The location in the parent, as described above.
         *
         * @throws {Error} If the current node has no parent.
         */
        getLocationInParent() {
            const node = this.node;
            const parent = node.parentNode;
            if (parent === null) {
                throw new Error("trying to get parent of a detached node");
            }
            return this.make(parent, domutil_1.indexOf(parent.childNodes, node));
        }
        /**
         * Same as [[getLocationInParent]] except that the location points *after* the
         * current node.
         *
         * @returns The location in the parent, as described above.
         *
         * @throws {Error} If the current node has no parent.
         */
        getLocationAfterInParent() {
            const node = this.node;
            const parent = node.parentNode;
            if (parent === null) {
                throw new Error("trying to get parent of a detached node");
            }
            return this.make(parent, domutil_1.indexOf(parent.childNodes, node) + 1);
        }
        /**
         * Converts the location to an array. This array contains only the node and
         * offset of the location. The root is not included because this method is of
         * use to pass data to functions that work with raw DOM information. These
         * functions do not typically expect a root.
         *
         * @returns The node and offset pair.
         */
        toArray() {
            return [this.node, this.offset];
        }
        makeRange(other) {
            if (domtypeguards_1.isAttr(this.node)) {
                throw new Error("cannot make range from attribute node");
            }
            if (!this.isValid()) {
                return undefined;
            }
            if (other === undefined) {
                const range = this.node.ownerDocument.createRange();
                range.setStart(this.node, this.offset);
                return range;
            }
            if (domtypeguards_1.isAttr(other.node)) {
                throw new Error("cannot make range from attribute node");
            }
            if (!other.isValid()) {
                return undefined;
            }
            return domutil_1.rangeFromPoints(this.node, this.offset, other.node, other.offset);
        }
        /**
         * Make a range from this location. If ``other`` is not specified, the range
         * starts and ends with this location. If ``other`` is specified, the range
         * goes from this location to the ``other`` location.
         *
         * @param other The other location to use.
         *
         * @returns The range.
         */
        makeDLocRange(other) {
            if (!this.isValid()) {
                return undefined;
            }
            if (other === undefined) {
                // tslint:disable-next-line:no-use-before-declare
                return new DLocRange(this, this);
            }
            if (!other.isValid()) {
                return undefined;
            }
            // tslint:disable-next-line:no-use-before-declare
            return new DLocRange(this, other);
        }
        /**
         * Like [[makeDLocRange]] but throws if it cannot make a range, rather than
         * return ``undefined``.
         */
        mustMakeDLocRange(other) {
            const ret = other !== undefined ?
                this.makeDLocRange(other) : this.makeDLocRange();
            if (ret === undefined) {
                throw new Error("cannot make a range");
            }
            return ret;
        }
        /**
         * Verifies whether the ``DLoc`` object points to a valid location. The
         * location is valid if its ``node`` is a child of its ``root`` and if its
         * ``offset`` points inside the range of children of its ``node``.
         *
         * @returns {boolean} Whether the object is valid.
         */
        isValid() {
            const node = this.node;
            // We do not check that offset is greater than 0 as this would be
            // done while constructing the object.
            return this.root.contains(domtypeguards_1.isAttr(node) ? node.ownerElement : node) &&
                this.offset <= getTestLength(node);
        }
        /**
         * Creates a new ``DLoc`` object with an offset that is valid. It does this by
         * "normalizing" the offset, i.e. by setting the offset to its maximum
         * possible value.
         *
         * @returns The normalized location. This will be ``this``, if it so happens
         * that ``this`` is already valid.
         */
        normalizeOffset() {
            const node = this.node;
            const testLength = getTestLength(node);
            if (this.offset > testLength) {
                return this.make(node, testLength);
            }
            return this;
        }
        /**
         * @returns Whether ``this`` and ``other`` are equal. They are equal if they
         * are the same object or if they point to the same location.
         */
        equals(other) {
            if (other == null) {
                return false;
            }
            return this === other ||
                (this.node === other.node) &&
                    (this.offset === other.offset);
        }
        /**
         * Compare two locations. Note that for attribute ordering, this class
         * arbitrarily decides that the order of two attributes on the same element is
         * the same as the order of their ``name`` fields as if they were sorted in an
         * array with ``Array.prototype.sort()``. This differs from how
         * ``Node.compareDocumentPosition`` determines the order of attributes. We
         * want something stable, which is not implementation dependent. In all other
         * cases, the nodes are compared in the same way
         * ``Node.compareDocumentPosition`` does.
         *
         * @param other The other location to compare this one with.
         *
         * @returns ``0`` if the locations are the same. ``-1`` if this location comes
         * first. ``1`` if the other location comes first.
         *
         * @throws {Error} If the nodes are disconnected.
         */
        compare(other) {
            if (this.equals(other)) {
                return 0;
            }
            let { node: thisNode, offset: thisOffset } = this;
            let { node: otherNode, offset: otherOffset } = other;
            // We need to handle attributes specially, because
            // ``compareDocumentPosition`` does not work reliably with attribute nodes.
            if (domtypeguards_1.isAttr(thisNode)) {
                if (domtypeguards_1.isAttr(otherNode)) {
                    // We do not want an implementation-specific order when we compare
                    // attributes. So we perform our own test.
                    if (thisNode.ownerElement === otherNode.ownerElement) {
                        // It is not clear what the default comparison function is, so create
                        // a temporary array and sort.
                        const names = [thisNode.name, otherNode.name].sort();
                        // 0 is not a possible value here because it is not possible for
                        // thisNode.name to equal otherNode.name.
                        return names[0] === thisNode.name ? -1 : 1;
                    }
                }
                const owner = thisNode.ownerElement;
                if (owner === other.pointedNode) {
                    // This location points into an attribute that belongs to the node
                    // that other points to. So this is later than other.
                    return 1;
                }
                // If we get here we'll rely on ``compareDocumentPosition`` but using the
                // position of the element that has the attribute.
                thisNode = owner.parentNode;
                thisOffset = domutil_1.indexOf(thisNode.childNodes, owner);
            }
            if (domtypeguards_1.isAttr(otherNode)) {
                const owner = otherNode.ownerElement;
                if (owner === this.pointedNode) {
                    // The other location points into an attribute that belongs to the node
                    // that this location points to. So this is earlier than other.
                    return -1;
                }
                // If we get here we'll rely on ``compareDocumentPosition`` but using the
                // position of the element that has the attribute.
                otherNode = owner.parentNode;
                otherOffset = domutil_1.indexOf(otherNode.childNodes, owner);
            }
            return domutil_1.comparePositions(thisNode, thisOffset, otherNode, otherOffset);
        }
    }
    exports.DLoc = DLoc;
    /**
     * Finds the root under which a node resides. Note that in cases where an
     * undefined result is useless, you should use [[getRoot]] instead.
     *
     * @param node The node whose root we want.
     *
     * @returns The root object, or ``undefined`` if the root can't be found.
     */
    function findRoot(node) {
        while (node != null) {
            if (domtypeguards_1.isElement(node) || domtypeguards_1.isDocument(node)) {
                const root = jquery_1.default.data(node, "wed-dloc-root");
                if (root != null) {
                    return root;
                }
            }
            node = node.parentNode;
        }
        return undefined;
    }
    exports.findRoot = findRoot;
    /**
     * Gets the root under which a node resides.
     *
     * @param node The node whose root we want.
     *
     * @returns The root node.
     *
     * @throws {Error} If the root cannot be found.
     */
    function getRoot(node) {
        const ret = findRoot(node);
        if (ret == null) {
            throw new Error("no root found");
        }
        return ret;
    }
    exports.getRoot = getRoot;
    /**
     * Represents a range spanning locations indicated by two [[DLoc]] objects.
     * Though this is not enforced at the VM level, objects of this class are to be
     * considered immutable.
     */
    class DLocRange {
        /**
         * @param start The start of the range.
         * @param end The end of the range.
         */
        constructor(start, end) {
            this.start = start;
            this.end = end;
            if (start.root !== end.root) {
                throw new Error("the start and end must be in the same document");
            }
        }
        /** Whether this range is collapsed. */
        get collapsed() {
            return this.start.equals(this.end);
        }
        /**
         * Make a DOM range.
         *
         * @returns The range. Or ``undefined`` if either the start or end are not
         * pointing to valid positions.
         *
         * @throws {Error} If trying to make a range from an attribute node. DOM
         * ranges can only point into elements or text nodes.
         */
        makeDOMRange() {
            if (domtypeguards_1.isAttr(this.start.node)) {
                throw new Error("cannot make range from attribute node");
            }
            if (!this.start.isValid()) {
                return undefined;
            }
            if (domtypeguards_1.isAttr(this.end.node)) {
                throw new Error("cannot make range from attribute node");
            }
            if (!this.end.isValid()) {
                return undefined;
            }
            return domutil_1.rangeFromPoints(this.start.node, this.start.offset, this.end.node, this.end.offset).range;
        }
        /**
         * Same as [[makeDOMRange]] but throws instead of returning ``undefined``.
         */
        mustMakeDOMRange() {
            const ret = this.makeDOMRange();
            if (ret === undefined) {
                throw new Error("cannot make a range");
            }
            return ret;
        }
        /**
         * @returns Whether ``this`` and ``other`` are equal. They are equal if they
         * are the same object or if they have equal start and ends.
         */
        equals(other) {
            if (other == null) {
                return false;
            }
            return this === other ||
                (this.start.equals(other.start) && this.end.equals(other.end));
        }
        /**
         * @returns Whether the two endpoints of the range are valid.
         */
        isValid() {
            return this.start.isValid() && this.end.isValid();
        }
        /**
         * @param loc The location to test.
         *
         * @returns Whether a location is within the range.
         */
        contains(loc) {
            const startTest = this.start.compare(loc);
            const endTest = this.end.compare(loc);
            // Reversed ranges are valid. So one end must be lower or equal to loc, and
            // the other end must be greater or equal to loc. The following test ensures
            // this. (If both are -1, then the result is > 0, and if both are 1, then
            // then result > 0.)
            return startTest * endTest <= 0;
        }
    }
    exports.DLocRange = DLocRange;
});
//  LocalWords:  makeDLoc DLoc domutil jquery MPL dloc mustMakeDLoc nd thisNode
//  LocalWords:  otherNode compareDocumentPosition makeDOMRange
//# sourceMappingURL=dloc.js.map