# Source: ui/substitution.reel/substitution.js

    /**
     * @module "montage/ui/substitution.reel"
     */
    var Slot = require("../slot.reel").Slot,
        Promise = require("../../core/promise").Promise,
        logger = require("../../core/logger").logger("substitution");

    /**
     * The substitution is a structural component and it should be used when there
     * are different types of content (e.g.: different panels) at the same time but
     * only one of them is shown at a time.
     *
     * A possible use case for the substitution is the implementation of a Tab
     * component content pane.
     *
     * The different types of content of a substitution are configured by declaring
     * them in the template as the DOM content of the substitution.
     * Each type of content is given to the substitution as a template argument.
     * Template arguments are declared by assigning the attribute `data-arg` to an
     * element that is the immediate child of the substitution.
     *
     * ##### Example - Declaring the substitution content
     * ```html
     * <div data-montage-id="substitution">
     *     <div data-arg="info" data-montage-id="infoPanel"></div>
     *     <div data-arg="contacts" data-montage-id="contactsPanel"></div>
     *     <div data-arg="review" data-montage-id="reviewPanel"></div>
     * </div>
     * ```
     * `info`, `contacts` and `review` are the types of content declared and
     * available as substitution content.
     * The type of content displayed by the substitution is defined by the
     * [switchValue]{@link Substitution#switchValue} property. The available values
     * of this property in this example are: `info`, `contacts` and `review`.
     *
     * ##### Example - Other substitution template configurations
     * ```html
     * <div data-montage-id="substitution">
     *     <div data-arg="info">
     *         Name: John Doe
     *     </div>
     *     <div data-arg="contacts">
     *         E-mail: ...
     *         Mobile: ...
     *     </div>
     *     <div data-arg="review">
     *         ...
     *     </div>
     * </div>
     * ```
     *
     * @class Substitution
     * @classdesc A structural component that reveals one of its template arguments
     * at a time.
     * @extends Slot
     */
    exports.Substitution = Slot.specialize( /** @lends Substitution.prototype # */ {

        hasTemplate: {
            enumerable: false,
            value: false
        },

        constructor: {
            value: function Substitution() {
                this._switchElements = Object.create(null);
                this._switchComponents = Object.create(null);
                this._switchComponentTreeLoaded = Object.create(null);
            }
        },

        _allChildComponents: {
            value: null
        },

        deserializedFromTemplate: {
            value: function () {
                this._allChildComponents = this.childComponents.slice(0);

                if (this.switchValue) {
                    this._loadSwitchComponentTree(this.switchValue);
                }
            }
        },

        _switchElements: {
            value: null
        },
        _switchComponentTreeLoaded: {
            value: null
        },

        /**
         * This method is used to dynamically add content to the substitution. This
         * is usually done by declaring the content in the template as the DOM
         * content of the substitution. However, in more advanced usages of the
         * substitution, this information might not be available at writing time.
         *
         * Throws when the `element` given has a parent node.
         *
         * @function
         * @param {string} key The key that identifies the content given, similar to
         *                 `data-arg` when declaring the content in the template.
         * @param {Node} element The element that will be shown when the `key` is
         *               the selected [switchValue]{@link Substitution#switchValue}.
         *               This element needs to be detached from the DOM and cannot
         *               have a parent node.
         */
        addSwitchElement: {
            value: function (key, element) {
                if (element.parentNode) {
                    throw new Error("Can't handle elements inside the DOM.");
                }

                this._switchElements[key] = element;
                this._switchComponents[key] = element.component;
                this._findFringeComponents(element, this._allChildComponents);
            }
        },

        _findFringeComponents: {
            value: function (element, components) {
                var nodes;

                components = components || [];

                if (element.component) {
                    components.push(element.component);
                } else {
                    nodes = element.children;
                    for (var i = 0, node; (node = nodes[i]); i++) {
                        this._findFringeComponents(node, components);
                    }
                }

                return components;
            }
        },

        _drawnSwitchValue: {
            value: null
        },

        _switchValue: {
            value: null
        },

        /**
         * The switch value selects which content the substitution should show.
         * The possible values are the ones defined as template arguments of the
         * substitution.
         *
         * @type {string}
         */
        switchValue: {
            get: function () {
                return this._switchValue;
            },
            set: function (value) {

                if (this._switchValue === value || this._isSwitchingContent) {
                    return;
                }

                this._switchValue = value;

                // switchElements is only ready after the first draw
                // At first draw the substitution automatically draws what is in
                // the switchValue so we defer any content loading until the first
                // draw.
                if (!this._firstDraw && !this.isDeserializing) {
                    this._loadContent(value);
                }
            }
        },

        enterDocument: {
            value: function (firstTime) {
                var argumentNames;

                Slot.prototype.enterDocument.apply(this, arguments);

                if (firstTime) {
                    argumentNames = this.getDomArgumentNames();
                    for (var i = 0, name; (name = argumentNames[i]); i++) {
                        this._switchElements[name] = this.extractDomArgument(name);
                        this._switchComponents[name] = this._switchElements[name].component;
                    }

                    this._loadContent(this.switchValue);
                    // TODO: Force the component to update its DOM now because the
                    // updateComponentDom already happened for this draw cycle.
                    // In the future the DrawManager will handle adding and
                    // removing nodes from the DOM at any time before draw().
                    this._updateComponentDom();
                }
            }
        },

        _loadContent: {
            value: function (value) {
                var typeOfValue = typeof value;
                this.content = this._switchElements[value] || null;

                if ((typeOfValue === "string" || typeOfValue === "number") && !this._switchComponentTreeLoaded[value]) {
                    this._loadSwitchComponentTree(value);
                }
            }
        },

        contentDidChange: {
            value: function (newContent, oldContent) {
                Slot.prototype.contentDidChange.call(this, newContent, oldContent);

                if (this._drawnSwitchValue) {
                    if (this._switchComponents[this._drawnSwitchValue]) {
                        this._switchElements[this._drawnSwitchValue] = this._switchComponents[this._drawnSwitchValue].element;
                    }
                }
                this._drawnSwitchValue = this._switchValue;
            }
        },

        _loadSwitchComponentTree: {
            value: function (value) {
                var self = this,
                    childComponents = this._allChildComponents,
                    element = this._switchElements[value],
                    substitutionElement = this.element,
                    canDrawGate = this.canDrawGate,
                    component,
                    currentElement,
                    promises = [];

                if (!element) {
                    element = this._getSubstitutionDomArgument(value);
                }

                for (var i = 0; i < childComponents.length; i++) {
                    component = childComponents[i];
                    currentElement = component.element;

                    // Search the DOM tree up until we find the switch element or
                    // the substitution element
                    while (currentElement !== element &&
                           currentElement !== substitutionElement &&
                           currentElement.parentNode) {
                        currentElement = currentElement.parentNode;
                    }
                    // If we found the switch element before finding the
                    // substitution element it means this component is inside the
                    // selected switch value.
                    if (currentElement === element) {
                        promises.push(component.loadComponentTree());
                    }
                }

                if (promises.length > 0) {
                    canDrawGate.setField(value + "ComponentTreeLoaded", false);

                    Promise.all(promises).then(function () {
                        self._switchComponentTreeLoaded[value] = true;
                        canDrawGate.setField(value + "ComponentTreeLoaded", true);
                        self._canDraw = true;
                        self.needsDraw = true;
                    });
                } else {
                    this._switchComponentTreeLoaded[value] = true;
                    this.needsDraw = true;
                }
            }
        },

       /**
        * This function is used to get the dom arguments before the first draw,
        * _domArguments are only available at the first draw.
        * We need it before so we can start loading the component tree as soon as
        * possible without having to wait for the first draw.
        * @private
        */
        _getSubstitutionDomArgument: {
            value: function (name) {
                var candidates,
                    node,
                    element,
                    elementId,
                    serialization,
                    labels,
                    template = this._ownerDocumentPart.template;

                element = this.element;
                candidates = element.querySelectorAll("*[" + this.DOM_ARG_ATTRIBUTE + "='" + name + "']");

                // Make sure that the argument we find is indeed part of element and
                // not an argument from an inner component.
                nextCandidate:
                for (var i = 0, candidate; (candidate = candidates[i]); i++) {
                    node = candidate;
                    while ((node = node.parentNode) !== element) {
                        elementId = template.getElementId(node);

                        // Check if this node is an element of a component.
                        // TODO: Make this operation faster
                        if (elementId) {
                            serialization = template.getSerialization();
                            labels = serialization.getSerializationLabelsWithElements(
                                elementId);

                            if (labels.length > 0) {
                                // This candidate is inside another component so
                                // skip it.
                                continue nextCandidate;
                            }
                        }
                    }
                    return candidate;
                }
            }
        },

        /**
         * By default the substitution doesn't expand the entire component tree of
         * all its content, only of the content that needs to be shown.
         * This is an optimization to avoid loading all the content at page load
         * time.
         *
         * However, if for some reason it is desirable to load the entire content
         * at page load time this property can be set to `true`.
         *
         * @type {boolean}
         * @default false
         */
        shouldLoadComponentTree: {
            value: false
        },

        transition: {
            value: null
        }
    });