/**
 * Model for a GUI root.
 * @author Louis-Dominique Dubeau
 * @license MPL 2.0
 * @copyright Mangalam Research Center for Buddhist Languages
 */
define(["require", "exports", "./dloc", "./domtypeguards", "./domutil", "./util"], function (require, exports, dloc_1, domtypeguards_1, domutil_1, util_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    /**
     * Raised if an attribute could not be found when converting a path to a node.
     */
    class AttributeNotFound extends Error {
        constructor(message) {
            super(message);
            util_1.fixPrototype(this, AttributeNotFound);
        }
    }
    exports.AttributeNotFound = AttributeNotFound;
    /**
     * Count the number of relevant nodes in the ``_phantom_wrap``.
     *
     * @param top The top _phantom_wrap to consider.
     */
    function countInPhantomWrap(top) {
        if (!domtypeguards_1.isElement(top) || !top.classList.contains("_phantom_wrap")) {
            throw new Error("the node should be a _phantom_wrap element");
        }
        let count = 0;
        let child = top.firstChild;
        while (child !== null) {
            if (domtypeguards_1.isElement(child)) {
                if (child.classList.contains("_phantom_wrap")) {
                    count += countInPhantomWrap(child);
                }
                else if (child.classList.contains("_real")) {
                    count += 1;
                }
                else if (child.classList.contains("_phantom")) {
                    // Phantoms don't count.
                }
                else {
                    throw new Error("unexpected element in _phantom_wrap");
                }
            }
            else if (child.nodeType === Node.TEXT_NODE) {
                // Text nodes also do not count.
            }
            else {
                throw new Error("unexpected node in _phantom_wrap");
            }
            child = child.nextSibling;
        }
        return count;
    }
    function findInPhantomWrap(top, index) {
        if (!domtypeguards_1.isElement(top) || !top.classList.contains("_phantom_wrap")) {
            throw new Error("the node should be a _phantom_wrap element");
        }
        const originalIndex = index;
        let found = null;
        let child = top.firstChild;
        while (found === null && child !== null) {
            if (domtypeguards_1.isElement(child)) {
                if (child.classList.contains("_phantom_wrap")) {
                    const result = findInPhantomWrap(child, index);
                    if (result.found !== null) {
                        found = result.found;
                    }
                    index -= result.count;
                }
                else if (child.classList.contains("_real")) {
                    index -= 1;
                    if (index < 0) {
                        found = child;
                    }
                }
                else if (child.classList.contains("_phantom")) {
                    // Phantoms don't count.
                }
                else {
                    throw new Error("unexpected element in _phantom_wrap");
                }
            }
            else if (child.nodeType === Node.TEXT_NODE) {
                // Text nodes do not count.
            }
            else {
                throw new Error("unexpected node in _phantom_wrap");
            }
            child = child.nextSibling;
        }
        return {
            found: found,
            count: originalIndex - index,
        };
    }
    /**
     * This is a [[DLocRoot]] class customized for use to mark the root of the GUI
     * tree.
     */
    class GUIRoot extends dloc_1.DLocRoot {
        /**
         * Converts a node to a path suitable to be used by the
         * [["wed/dloc".DLocRoot.pathToNode]] method so long as the root used is the
         * one for the data tree corresponding to the GUI tree to which this object
         * belongs.
         *
         * @param node The node for which to construct a path.
         *
         * @returns The path.
         */
        nodeToPath(node) {
            const root = this.node;
            if (domutil_1.closestByClass(node, "_placeholder", root) !== null) {
                throw new Error("cannot provide path to node because it is a placeholder node");
            }
            if (root === node) {
                return "";
            }
            if (!root.contains(node)) {
                throw new Error("node is not a descendant of root");
            }
            const ret = [];
            while (node !== root) {
                let parent;
                if (domtypeguards_1.isElement(node) &&
                    !node.matches("._real, ._phantom_wrap, ._attribute_value")) {
                    throw new Error("only nodes of class ._real, ._phantom_wrap, and " +
                        "._attribute_value are supported");
                }
                const attrVal = domutil_1.closestByClass(node, "_attribute_value", root);
                if (attrVal !== null) {
                    const child = domutil_1.siblingByClass(attrVal, "_attribute_name");
                    if (child === null) {
                        throw new Error("no attribute name found");
                    }
                    ret.unshift(`@${child.textContent}`);
                    parent = domutil_1.closestByClass(attrVal, "_real", root);
                    if (parent === null) {
                        throw new Error("attribute is detached from real element");
                    }
                }
                else {
                    parent = node.parentNode;
                    if (parent === null) {
                        throw new Error("detached node");
                    }
                    let offset = 0;
                    const location = domutil_1.indexOf(parent.childNodes, node);
                    for (let i = 0; i < location; ++i) {
                        const child = parent.childNodes[i];
                        if (domtypeguards_1.isText(child) ||
                            (domtypeguards_1.isElement(child) && child.classList.contains("_real"))) {
                            offset++;
                        }
                        else if (domtypeguards_1.isElement(child) &&
                            child.classList.contains("_phantom_wrap")) {
                            offset += countInPhantomWrap(child);
                        }
                    }
                    // Parent could be a document if it is not an element.
                    if (!domtypeguards_1.isElement(parent) || !parent.classList.contains("_phantom_wrap")) {
                        ret.unshift(String(offset));
                    }
                }
                node = parent;
            }
            return ret.join("/");
        }
        /**
         * This function recovers a DOM node on the basis of a path previously created
         * by [["wed/dloc".DLocRoot.nodeToPath]] provided that the root from which the
         * path was obtained is on the data tree which corresponds to the GUI tree
         * that this root was created for.
         *
         * @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();
            }
            let found = null;
            for (const part of parts) {
                const match = /^(\d+)$/.exec(part);
                if (match !== null) {
                    found = null;
                    let index = parseInt(match[1]);
                    for (let i = 0; found === null && (i < parent.childNodes.length); i++) {
                        const node = parent.childNodes[i];
                        if ((domtypeguards_1.isText(node) ||
                            (domtypeguards_1.isElement(node) && node.classList.contains("_real"))) &&
                            --index < 0) {
                            found = node;
                        }
                        else if (domtypeguards_1.isElement(node) &&
                            node.classList.contains("_phantom_wrap")) {
                            const result = findInPhantomWrap(node, index);
                            if (result.found !== null) {
                                found = result.found;
                            }
                            index -= result.count;
                        }
                    }
                    if (found === null) {
                        return null;
                    }
                    parent = found;
                }
                else {
                    throw new Error("malformed path expression");
                }
            }
            if (attribute !== undefined) {
                const name = attribute.slice(1);
                if (!domtypeguards_1.isElement(parent)) {
                    throw new Error("looking for attribute on something which is not an element");
                }
                const attrs = parent.getElementsByClassName("_attribute_name");
                found = null;
                for (let aix = 0; aix < attrs.length; ++aix) {
                    const attr = attrs[aix];
                    if (attr.textContent === name) {
                        found = attr;
                        break;
                    }
                }
                if (found === null) {
                    throw new AttributeNotFound(`could not find attribute with name: ${name}`);
                }
                parent = domutil_1.siblingByClass(found, "_attribute_value");
            }
            return parent;
        }
    }
    exports.GUIRoot = GUIRoot;
});
//  LocalWords:  MPL
//# sourceMappingURL=guiroot.js.map