var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
define(["require", "exports", "merge-options", "./domutil", "./mode-loader", "./wed-options-validation"], function (require, exports, merge_options_1, domutil_1, mode_loader_1, wed_options_validation_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    merge_options_1 = __importDefault(merge_options_1);
    /**
     * A node for the mode tree.
     */
    class ModeNode {
        /**
         * @param mode The mode that this node holds.
         *
         * @param editor The editor for which we are holding a mode.
         *
         * @param selector The selector that determines to what this modes apply. This
         * selector must have been converted to operate in the GUI tree.
         *
         * @param submodes The submodes set for this mode.
         *
         * @param wedOptions The cleaned up wed options that pertain to the mode held
         * by this node.
         */
        constructor(mode, editor, selector, submodes, wedOptions) {
            this.mode = mode;
            this.editor = editor;
            this.selector = selector;
            this.submodes = submodes;
            this.wedOptions = wedOptions;
        }
        /**
         * Determines whether an element matched by the selector of this ``ModeNode``
         * node in the GUI tree contains a node. If it does, this means that the mode
         * that this ``ModeNode`` holds, or one of the submode, governs the node.
         *
         * @param parentScope The element from which the selector in this ``ModeNode``
         * is interpreted.
         *
         * @param node A GUI node to test.
         *
         * @returns The element that represents the top of the mode's region of
         * activity and contains ``node``. Returns ``null`` if no element contains the
         * node.
         */
        containingElement(parentScope, node) {
            if (!parentScope.contains(node)) {
                return null;
            }
            if (this.selector === "") {
                return parentScope;
            }
            const regions = parentScope.querySelectorAll(this.selector);
            for (const region of Array.from(regions)) {
                if (region.contains(node)) {
                    return region;
                }
            }
            return null;
        }
        reduceTopFirst(fn, initialValue) {
            let value = fn(initialValue, this);
            for (const submode of this.submodes) {
                value = submode.reduceTopFirst(fn, value);
            }
            return value;
        }
        eachTopFirst(fn) {
            fn(this);
            for (const submode of this.submodes) {
                submode.eachTopFirst(fn);
            }
        }
        get attributeHidingSpecs() {
            if (this._attributeHidingSpecs === undefined) {
                const attributeHiding = this.wedOptions.attributes.autohide;
                if (attributeHiding === undefined) {
                    // No attribute hiding...
                    this._attributeHidingSpecs = null;
                }
                else {
                    const method = attributeHiding.method;
                    if (method !== "selector") {
                        throw new Error(`unknown attribute hiding method: ${method}`);
                    }
                    const specs = {
                        elements: [],
                    };
                    for (const element of attributeHiding.elements) {
                        const copy = merge_options_1.default({}, element);
                        copy.selector =
                            domutil_1.toGUISelector(copy.selector, this.mode.getAbsoluteNamespaceMappings());
                        specs.elements.push(copy);
                    }
                    this._attributeHidingSpecs = specs;
                }
            }
            return this._attributeHidingSpecs;
        }
        get decorator() {
            if (this._decorator === undefined) {
                this._decorator = this.mode.makeDecorator();
            }
            return this._decorator;
        }
    }
    /**
     * A tree containing the modes configured for the current editing session.
     */
    class ModeTree {
        /**
         * @param editor The editor for which we are building this tree.
         *
         * @param option The ``mode`` option from the options passed to the wed
         * instance. This object will construct a tree from this option.
         */
        constructor(editor, option) {
            this.editor = editor;
            this.option = option;
            this.loader = new mode_loader_1.ModeLoader(editor, editor.runtime);
        }
        /**
         * Load the modes, initialize them and build the tree.
         *
         * @returns A promise that resolves to ``this`` once all the modes are loaded
         * and initialized.
         */
        init() {
            return __awaiter(this, void 0, void 0, function* () {
                const combinedErrors = [];
                this.root = yield this.makeNodes("", this.option, (path, errors) => {
                    for (const error of errors) {
                        combinedErrors.push(`mode at path ${path} has an error in its wed options: ${error}`);
                    }
                });
                if (combinedErrors.length > 0) {
                    throw new Error(`wed options are incorrect: ${combinedErrors.join("")}`);
                }
                return this;
            });
        }
        /**
         * Make the nodes of the tree. This function operates recursively: it will
         * inspect ``option`` for a ``submode`` option and will call itself to create
         * the necessary child nodes.
         *
         * @param selector The selector associated with the options passed in the 2nd
         * argument.
         *
         * @param option The mode option being processed.
         *
         * @param errorHanler The handler to call on errors in processing the wed
         * options. If this handler is called at all, then the returned value should
         * not be used. We operate this way because we want to report all errors that
         * can be reported, rather than abort early.
         *
         * @returns A promise that resolves to the created node.
         */
        makeNodes(selector, option, errorHandler) {
            return __awaiter(this, void 0, void 0, function* () {
                const submode = option.submode;
                const mode = yield this.loader.initMode(option.path, option.options);
                const submodes = (submode !== undefined) ?
                    [yield this.makeNodes(domutil_1.toGUISelector(submode.selector, mode.getAbsoluteNamespaceMappings()), submode.mode, errorHandler)] :
                    [];
                const rawOptions = mode.getWedOptions();
                const result = wed_options_validation_1.processWedOptions(rawOptions);
                let cleanedOptions;
                if (Array.isArray(result)) {
                    errorHandler(option.path, result);
                    // This is a lie.
                    cleanedOptions = rawOptions;
                }
                else {
                    cleanedOptions = result;
                }
                return new ModeNode(mode, this.editor, selector, submodes, cleanedOptions);
            });
        }
        getMode(node) {
            return this.getModeNode(node).mode;
        }
        getDecorator(node) {
            return this.getModeNode(node).decorator;
        }
        getWedOptions(node) {
            const modeNode = this.getModeNode(node);
            return modeNode.wedOptions;
        }
        getAttributeHandling(node) {
            return this.getWedOptions(node).attributes.handling;
        }
        getAttributeHidingSpecs(node) {
            return this.getModeNode(node).attributeHidingSpecs;
        }
        /**
         * Get the mode node that governs a node.
         *
         * @param The node we want to check. This must be a done in the data tree or
         * the GUI tree.
         *
         * @returns The mode that governs the node.
         */
        getModeNode(node) {
            // Handle the trivial case where there is no submode first.
            if (this.root.submodes.length === 0) {
                return this.root;
            }
            if (domutil_1.contains(this.editor.dataRoot, node)) {
                const data = this.editor.fromDataNode(node);
                if (data !== null) {
                    node = data;
                }
            }
            if (!this.editor.guiRoot.contains(node)) {
                throw new Error("did not pass a node in the GUI or data tree");
            }
            const result = this._getModeNode(this.root, this.editor.guiRoot, node);
            if (result === undefined) {
                throw new Error("cannot find a mode for the node; something is wrong");
            }
            return result;
        }
        _getModeNode(parent, parentScope, node) {
            const scope = parent.containingElement(parentScope, node);
            if (scope !== null) {
                let narrower;
                for (const submode of parent.submodes) {
                    narrower = this._getModeNode(submode, scope, node);
                    if (narrower !== undefined) {
                        return narrower;
                    }
                }
                return parent;
            }
            return undefined;
        }
        getStylesheets() {
            return Object.keys(this.root.reduceTopFirst((accumulator, node) => {
                for (const sheet of node.mode.getStylesheets()) {
                    accumulator[sheet] = true;
                }
                return accumulator;
            }, Object.create(null)));
        }
        getMaxLabelLevel() {
            return this.maxLabelLevelNode.wedOptions.label_levels.max;
        }
        getInitialLabelLevel() {
            return this.maxLabelLevelNode.wedOptions.label_levels.initial;
        }
        /**
         * The node with the maximum label visibility level. If multiple nodes have
         * the same value, the earlier node "wins", and is the one provided by this
         * property. For instance, if the root node and its submode have the same
         * number, then this property has the root node for value.
         *
         * This is a cached value, computed on first access.
         */
        get maxLabelLevelNode() {
            if (this.cachedMaxLabelNode === undefined) {
                this.cachedMaxLabelNode = this.root.reduceTopFirst((accumulator, node) => {
                    const accMax = accumulator.wedOptions.label_levels.max;
                    const nodeMax = node.wedOptions.label_levels.max;
                    return (nodeMax > accMax) ? node : accumulator;
                }, this.root);
            }
            return this.cachedMaxLabelNode;
        }
        getValidators() {
            return this.root.reduceTopFirst((accumulator, node) => {
                const validator = node.mode.getValidator();
                return validator !== undefined ?
                    accumulator.concat(validator) : accumulator;
            }, []);
        }
        /**
         * Call on each decorator to add its event handlers.
         */
        addDecoratorHandlers() {
            this.root.eachTopFirst((node) => {
                node.decorator.addHandlers();
            });
        }
        /**
         * Call on each decorator to start listening.
         */
        startListening() {
            this.root.eachTopFirst((node) => {
                node.decorator.startListening();
            });
        }
    }
    exports.ModeTree = ModeTree;
});
//  LocalWords:  MPL submodes submode combinedErrors nd preprocessed
//  LocalWords:  stylesheets
//# sourceMappingURL=mode-tree.js.map