/*
 * Copyright (c) 2018-present, Vitaly Tomilov
 *
 * See the LICENSE file at the top-level directory of this distribution
 * for licensing information.
 *
 * Removal or modification of this copyright notice is prohibited.
 */

(function () {
    'use strict';

    /**
     * Registered app controllers.
     *
     * It is a controller name-to-function map for the app's local controllers.
     *
     * @type {Object.<JSName, function>}
     */
    var ctrlRegistered = {};

    /**
     * Live global controllers (initialized, but not destroyed), with each property -
     * controller name set to an array of controller objects.
     *
     * @type {Object.<CtrlName, EController[]>}
     */
    var ctrlGlobal = {};

    /**
     * Live local controllers (initialized, but not destroyed), with each property -
     * controller name set to an array of controller objects.
     *
     * @type {Object.<CtrlName, EController[]>}
     */
    var ctrlLocal = {};

    /**
     * Global name-to-function cache for all controllers.
     *
     * @type {Object.<CtrlName, function>}
     */
    var ctrlCache = {};

    /**
     * All controlled elements currently in the DOM.
     *
     * @type {ControlledElement[]}
     */
    var elements = [];

    /**
     * Library's root object.
     *
     * @type {ERoot}
     */
    var root = new ERoot();

    /**
     * Alternative root name, if it was specified.
     *
     * @type {string}
     */
    var altRootName;

    /**
     * Helps observing when elements are removed.
     *
     * @type {DestroyObserver}
     */
    var observer = new DestroyObserver();

    /**
     * Indicates when executing a controller constructor.
     *
     * @type {boolean}
     */
    var constructing = false;

    /**
     * Validates controller name, optionally trimmed.
     *
     * @param {string|CtrlName} cn
     * Controller name.
     *
     * @param {boolean} [t=false]
     * Trims the name before validation.
     *
     * @returns {CtrlName|undefined}
     * Valid controller name, or nothing.
     */
    function validateControllerName(cn, t) {
        if (typeof cn === 'string') {
            cn = t ? trim(cn) : cn;
            var m = cn.match(/^[a-z$_][$\w]*(\.[a-z$_][$\w]*)*$/i);
            if (m && m[0] === cn) {
                return cn;
            }
        }
    }

    /**
     * Validates parameter as a valid DOM Element object.
     *
     * @param {external:HTMLElement} e
     * Element object to be validated.
     */
    function validateElement(e) {
        if (!e || typeof e.innerHTML !== 'string') {
            throw new TypeError('Parameter ' + jStr(e) + ' does not represent a valid DOM element.');
        }
    }

    /**
     * Validates a string to be a proper JavaScript open name.
     *
     * @param {string} name
     *
     * @returns {boolean}
     */
    function validJsVariable(name) {
        var m = name.match(/[a-z$_][a-z$_0-9]*/i);
        return m && m[0] === name;
    }

    /**
     * Validates an entity being registered.
     *
     * @param {JSName} name
     * Name of the entity.
     *
     * @param {function} cb
     * Callback function.
     *
     * @param {string} entity
     * Entity name.
     *
     * @returns {JSName}
     * Validated entity name.
     */
    function validateEntity(name, cb, entity) {
        var n = typeof name === 'string' ? trim(name) : '';
        if (!validJsVariable(n)) {
            throw new TypeError('Invalid ' + entity + ' name ' + jStr(name) + ' specified.');
        }
        if (typeof cb !== 'function') {
            throw new TypeError('Initialization function for ' + entity + ' ' + jStr(name) + ' is missing.');
        }
        return n;
    }

    /**
     * Wraps a value to be friendly within error messages.
     *
     * @param {*} value
     * @returns {string}
     */
    function jStr(value) {
        var t = typeof value;
        if (t === 'undefined' || t === 'boolean' || t === 'number' || value === null) {
            return '<' + value + '>';
        }
        if (t === 'function') {
            return '<' + value.toString() + '>';
        }
        return JSON.stringify(value);
    }

    /**
     * Retrieves full start tag from a DOM element.
     *
     * @param {HTMLElement} element
     *
     * @returns {string}
     * Full start tag string.
     */
    function startTag(element) {
        var h = element.outerHTML;
        return h.substring(0, h.indexOf('>') + 1);
    }

    /**
     * Searches for all elements that match selectors, and optionally - within a parent node.
     *
     * @param {string} selectors
     * Standard selectors.
     *
     * @param {HTMLElement} [node]
     * Parent node to search for children.
     *
     * @returns {HTMLElement[] | ControlledElement[]}
     */
    function findAll(selectors, node) {
        var f = (node || document).querySelectorAll(selectors);
        var l = f.length, arr = new Array(l);
        while (l--) {
            arr[l] = f[l];
        }
        return arr;
    }

    /**
     * Gets the primary attribute's value, if the attribute exists,
     * or else it gets the secondary attribute's value.
     *
     * @param {HTMLElement} e
     * Element to get the value from.
     *
     * @param {string} primary
     * Primary attribute name.
     *
     * @param {string} secondary
     * Secondary attribute name.
     *
     * @returns {string}
     * The attribute's value.
     */
    function getAttribute(e, primary, secondary) {
        if (e.hasAttribute(primary)) {
            return e.getAttribute(primary);
        }
        return e.getAttribute(secondary);
    }

    /**
     * Trims a string, by removing all trailing spaces, tabs and line breaks.
     *
     * @param {string|JSName} txt
     * Input string.
     *
     * @returns {string}
     * The resulting string.
     *
     */
    function trim(txt) {
        return txt.replace(/^[\s]*|[\s]*$/g, '');
    }

    /**
     * Creates a read-only enumerable property on an object.
     *
     * @param {object} target
     * Target object.
     *
     * @param {string|JSName|CtrlName} prop
     * Property name.
     *
     * @param {*} value
     * Property value.
     */
    function readOnlyProp(target, prop, value) {
        Object.defineProperty(target, prop, {value: value, enumerable: true});
    }

    /**
     * Binding Status.
     *
     * It helps with smart asynchronous bindings management, to help executing only
     * the necessary minimum of bindings, based on the current flow of requests.
     *
     * @type {{nodes: Array, cb: Array, waiting: boolean, glob: boolean}}
     */
    var bs = {
        nodes: [], // local elements
        cb: [], // all callbacks
        waiting: false, // timer is currently waiting
        glob: false // global async is being processed
    };

    /**
     * General binding processor.
     *
     * It implements the logic of bindings reduced to the absolute minimum DOM usage.
     *
     * @param {external:HTMLElement} [node]
     * Element to start processing from.
     *
     * @param {boolean|function} process
     * Determines how to process the binding:
     * - _any falsy value (default):_ the binding will be done asynchronously;
     * - _a function:_ binding is asynchronous, calling the function when finished;
     * - _any truthy value, except a function type:_ forces synchronous binding.
     */
    function processBinding(node, process) {
        var cb = typeof process === 'function' && process;
        if (process && !cb) {
            // synchronous binding;
            if (bs.waiting) {
                // timer is now waiting
                if (node) {
                    // Cancel asynchronous binding on the same node:
                    var idx = bs.nodes.indexOf(node);
                    if (idx >= 0) {
                        bs.nodes.splice(idx, 1);
                    }
                } else {
                    // A global synchronous binding cancels everything for
                    // asynchronous processing, except callback notifications:
                    bs.waiting = false;
                    bs.glob = false;
                    bs.nodes.length = 0;
                }
            }
            bindAll(node);
        } else {
            // asynchronous binding;
            if (node) {
                // local binding, append the request, if unique:
                if (bs.nodes.indexOf(node) === -1) {
                    bs.nodes.push(node);
                }
            } else {
                // global binding, cancels local bindings:
                bs.glob = true;
                bs.nodes.length = 0;
            }
            if (cb) {
                // callback notifications:
                bs.cb.push(cb);
            }
            if (bs.waiting) {
                return;
            }
            bs.waiting = true;
            setTimeout(function () {
                var nodes = bs.nodes.slice();
                var cbs = bs.cb.slice();
                bs.nodes.length = 0;
                bs.cb.length = 0;
                if (bs.waiting) {
                    bs.waiting = false;
                    if (bs.glob) {
                        bs.glob = false;
                        bindAll(null);
                    } else {
                        nodes.forEach(bindAll);
                    }
                }
                for (var i = 0; i < cbs.length; i++) {
                    cbs[i]();
                }
            });
        }
    }

    /**
     * Registers the live controller in the global list, so it can be found globally.
     *
     * @param {CtrlName|string} name
     * Controller name.
     *
     * @param {EController} c
     * Controller object.
     *
     * @param {boolean} [local=false]
     * Local-controller flag.
     */
    function addLiveCtrl(name, c, local) {
        var target = local ? ctrlLocal : ctrlGlobal;
        target[name] = target[name] || [];
        target[name].push(c);
    }

    /**
     * @class ControllerClass
     * @private
     * @readonly
     * @property {string|null} $ccn
     * Controller Class Name, if it is a valid controller class, or `null` otherwise.
     * @property {string} name
     * @property {ControlledElement} node
     */

    /**
     * Helper for creating a controller in a safe way.
     *
     * @param {string|CtrlName} name
     * Controller name.
     *
     * @param {HTMLElement|ControlledElement} e
     * Element associated with the controller.
     *
     * @param {function|ControllerClass} f
     * Controller's construction function or class.
     *
     * @returns {EController}
     * Created controller.
     */
    function createController(name, e, f) {
        validateClass(f);
        constructing = true;
        var c;
        var cc = {name: name, node: e}; // controller context
        try {
            if (f.$ccn) {
                var Cls = f; // it is a controller class
                c = new Cls(cc);
                if (c.name !== name || c.node !== e) {
                    throw new Error('Controller class "' + f.$ccn + '" passed invalid parameters to "EController" constructor.');
                }
            } else {
                c = new EController(cc);
                f.call(c, c);
            }
        } catch (err) {
            constructing = false;
            throw err;
        } finally {
            constructing = false;
        }
        return c;
    }

    /**
     * Validates and prepares a function / class for instantiation.
     *
     * @param {function|ControllerClass} func
     * Function or class to be validated.
     */
    function validateClass(func) {
        if (func.$ccn === undefined) {
            var m = Function.prototype.toString.call(func).match(/^class\s+([a-zA-Z$_][a-zA-Z$_0-9]*)/);
            var name = m && m[1];
            if (name && !(func.prototype instanceof EController)) {
                throw new Error('Invalid controller class "' + name + '", as it does not derive from "EController".');
            }
            Object.defineProperty(func, '$ccn', {value: name});
        }
    }

    /**
     * Binds to controllers all elements that are not yet bound,
     * within the specified parent element, or globally.
     *
     * @param {HTMLElement} [node]
     * Top-level node element to start searching from. When not specified,
     * the search is done for the entire document.
     */
    function bindAll(node) {
        var allCtrl = [], els = [];
        findAll('[data-e-bind],[e-bind]', node)
            .forEach(function (e) {
                if (!e.controllers) {
                    var namesMap = {}, eCtrl;
                    getAttribute(e, 'data-e-bind', 'e-bind')
                        .split(',')
                        .forEach(function (name) {
                            name = trim(name);
                            if (name) {
                                if (!validateControllerName(name)) {
                                    throw new Error('Invalid controller name ' + jStr(name) + ': ' + startTag(e));
                                }
                                if (name in namesMap) {
                                    throw new Error('Duplicate controller name ' + jStr(name) + ' not allowed: ' + startTag(e));
                                }
                                namesMap[name] = true;
                                var f = getCtrlFunc(name, e);
                                var c = createController(name, e, f);
                                eCtrl = eCtrl || {};
                                readOnlyProp(eCtrl, name, c);
                                allCtrl.push(c);
                                addLiveCtrl(name, c);
                            }
                        });
                    if (eCtrl) {
                        readOnlyProp(e, 'controllers', eCtrl);
                        elements.push(e);
                        els.push(e);
                    }
                }
            });
        els.forEach(observer.watch);
        eventNotify(allCtrl, 'onInit');
        eventNotify(allCtrl, 'onReady');
    }

    /**
     * Abstract event parameter-less notification for controllers.
     *
     * @param {Array<EController|ERoot>} arr
     * List of controllers.
     *
     * @param {string} event
     * Event name.
     */
    function eventNotify(arr, event) {
        for (var i = 0; i < arr.length; i++) {
            var a = arr[i];
            if (typeof a[event] === 'function') {
                a[event]();
            }
        }
    }

    /**
     * @constructor
     * @private
     * @description
     * Helps watching node elements removal from DOM, in order to provide {@link EController.event:onDestroy onDestroy}
     * notification for all corresponding controllers.
     *
     * For IE9/10 that do not support `MutationObserver`, it executes a manual check once a second.
     */
    function DestroyObserver() {
        var mo, timer;

        // MutationObserver does not exist in JEST:
        // istanbul ignore else
        if (typeof MutationObserver === 'undefined') {
            // istanbul ignore else
            if (typeof window !== 'undefined' && window) {
                // We do not create any timer when inside Node.js
                timer = setInterval(manualCheck, 1000); // This is a work-around for IE9 and IE10
            }
        } else {
            mo = new MutationObserver(mutantCB);
        }

        /**
         * @method DestroyObserver#stop
         * @description
         * To be used only from tests, it helps fully reset the library.
         */
        this.stop = function () {
            // istanbul ignore else
            if (timer) {
                clearInterval(timer);
                timer = null;
            }
            // MutationObserver does not exist in JEST:
            // istanbul ignore next
            if (mo) {
                mo.disconnect();
                mo = null;
            }
        };

        /**
         * @method DestroyObserver#watch
         * @description
         * Initiates watching the element.
         *
         * @param {HTMLElement} e
         * Element to be watched.
         */
        this.watch = function (e) {
            // MutationObserver does not exist in JEST:
            // istanbul ignore if
            if (mo) {
                mo.observe(e, {childList: true});
            }
        };

        // MutationObserver does not exist in JEST:
        // istanbul ignore next
        function mutantCB(mutations) {
            mutations.forEach(function (m) {
                for (var i = 0; i < m.removedNodes.length; i++) {
                    var e = m.removedNodes[i];
                    if (e.controllers) {
                        var idx = elements.indexOf(e);
                        if (idx >= 0) {
                            elements.splice(idx, 1);
                            destroyNotify(e);
                        }
                        removeControllers(e);
                    }
                }
            });
        }

        /**
         * Removes all controllers from ctrlGlobal, as per the element.
         *
         * @param {ControlledElement} e
         */
        function removeControllers(e) {
            for (var a in e.controllers) {
                remove(a, ctrlGlobal);
                remove(a, ctrlLocal);
            }

            function remove(name, dest) {
                var c = dest[name];
                if (c) {
                    var i = dest[name].indexOf(e.controllers[name]);
                    dest[name].splice(i, 1);
                    if (!dest[name].length) {
                        delete dest[name];
                    }
                }
            }
        }


        /**
         * Manual check for controlled elements that have been deleted from DOM.
         */
        function manualCheck() {
            var i = elements.length;
            if (i) {
                var ce = findAll('[data-e-bind],[e-bind]'); // all controlled elements;
                while (i--) {
                    var e = elements[i];
                    if (ce.indexOf(e) === -1) {
                        elements.splice(i, 1);
                        destroyNotify(e);
                        removeControllers(e);
                    }
                }
            }
        }

        /**
         * Sends onDestroy notification into all controllers of an element.
         *
         * @param {ControlledElement} e
         */
        function destroyNotify(e) {
            var c = [];
            for (var i in e.controllers) {
                c.push(e.controllers[i]);
            }
            eventNotify(c, 'onDestroy');
        }

    }

    /**
     * @interface ControlledElement
     * @extends external:HTMLElement
     * @description
     * Represents a standard DOM element, extended with read-only property `controllers`.
     *
     * This type is provided by the library automatically, after binding elements to controllers.
     *
     * @see
     * {@link ERoot#bind ERoot.bind},
     * {@link EController#bind EController.bind}
     */

    /**
     * @member {Object.<CtrlName, EController>} ControlledElement#controllers
     * @readonly
     * @description
     * An object - map, with each property (controller name) of type {@link EController}.
     * And each property in the object is read-only.
     *
     * This property is available only after the controller has been initialized.
     *
     * For example, if you have a binding like this:
     *
     * ```html
     * <div e-bind="homeCtrl, view.main"></div>
     * ```
     *
     * Then you can access those controllers like this:
     *
     * ```js
     * app.addController('homeCtrl', function(ctrl) {
     *
     *     // During the controller's construction,
     *     // this.node.controllers is undefined.
     *
     *     this.onInit = function() {
     *         var ctrl1 = this.node.controllers.homeCtrl;
     *         // ctrl1 = ctrl = this
     *
     *         var ctrl2 = this.node.controllers['view.main'];
     *     };
     * });
     * ```
     */

    /**
     * Searches for a controller, based on its full name.
     * For that it uses the cache of names, plus modules.
     *
     * @param {string|CtrlName} name
     * Controller name to be resolved.
     *
     * @param {HTMLElement} [e]
     * Element available as the context.
     *
     * @param {boolean} [noError=false]
     * Tells it not to throw on errors, and rather return null.
     *
     * @returns {function|class|null}
     * Either a valid controller or throws an error. But if `noError` is true,
     * and no controller found, it returns `null`.
     *
     */
    function getCtrlFunc(name, e, noError) {
        if (name in ctrlCache) {
            return ctrlCache[name]; // use the cache
        }
        if (name.indexOf('.') === -1) {
            // it is an in-app controller;
            var f = ctrlRegistered[name]; // the function
            if (f) {
                ctrlCache[name] = f; // updating cache
                return f;
            }
        } else {
            // the controller is from a module
            var names = name.split('.');
            var mod = names[0];
            if (!(mod in root.modules)) {
                if (noError) {
                    return null;
                }
                throw new Error('Module ' + jStr(mod) + ' not found' + (e ? ': ' + startTag(e) : '.'));
            }
            var obj = root.modules[mod];
            for (var i = 1; i < names.length; i++) {
                var n = names[i];
                if (n in obj) {
                    obj = obj[n];
                } else {
                    obj = null;
                    break;
                }
            }
            if (typeof obj === 'function') {
                ctrlCache[name] = obj;
                return obj;
            }
        }
        if (noError) {
            return null;
        }
        throw new Error('Controller ' + jStr(name) + ' not found' + (e ? ': ' + startTag(e) : '.'));
    }

    /**
     * Parses a controller name, while allowing for trailing spaces.
     *
     * @param {CtrlName} cn
     * Controller name.
     *
     * @returns {CtrlName}
     * Validated controller name (without trailing spaces).
     */
    function parseControllerName(cn) {
        var name = validateControllerName(cn, true);
        if (!name) {
            throw new TypeError('Invalid controller name ' + jStr(cn) + ' specified.');
        }
        return name;
    }

    /**
     * @interface ERoot
     * @description
     * Root interface of the library, available via global variable `excellent`.
     *
     * You can make this interface also available via an alias name that can be set via
     * attribute `e-root` or `data-e-root` on the `HTML` element:
     *
     * ```html
     * <HTML e-root="app">
     * ```
     *
     * @see
     * {@link ERoot#analyze analyze},
     * {@link ERoot#addController addController},
     * {@link ERoot#addAlias addAlias},
     * {@link ERoot#addModule addModule},
     * {@link ERoot#addService addService},
     * {@link ERoot#attach attach},
     * {@link ERoot#bind bind},
     * {@link ERoot#bindFor bindFor},
     * {@link ERoot#find find},
     * {@link ERoot#findOne findOne},
     * {@link ERoot#getCtrlFunc getCtrlFunc},
     * {@link ERoot#reset reset},
     * {@link ERoot#modules modules},
     * {@link ERoot#services services},
     * {@link ERoot#version version},
     * {@link ERoot.event:onReady onReady}
     */
    function ERoot() {

        /**
         * @member ERoot#version
         * @type {string}
         * @readonly
         * @description
         * Library version, automatically injected during the build/compression process, and so available
         * only with the compressed version of the library. But you should be using `excellent.min.js` always,
         * because the library is distributed with the source maps.
         */
        readOnlyProp(this, 'version', '<version>');

        /**
         * @member ERoot#services
         * @type {Object.<JSName, {}>}
         * @readonly
         * @description
         * Namespace of all services, registered with method {@link ERoot#addService addService}.
         *
         * @see {@link ERoot#addService addService}
         */
        readOnlyProp(this, 'services', {});

        /**
         * @member ERoot#modules
         * @type {Object.<JSName, {}>}
         * @readonly
         * @description
         * Namespace of all modules, registered with method {@link ERoot#addModule addModule}.
         *
         * @see {@link ERoot#addModule addModule}
         */
        readOnlyProp(this, 'modules', {});

        /**
         * @member ERoot#EController
         * @type {class}
         * @readonly
         * @private
         * @description
         * Exposing class EController, just for compatibility with TypeScript's require usage.
         */
        readOnlyProp(this, 'EController', EController);

        /**
         * @method ERoot#addController
         * @description
         * Registers a new application-level controller.
         *
         * A controller is either a function or ES6 class that implements it, paired with a unique name by which it is represented.
         *
         * Typical implementation for any reusable controller is to be done inside a module, which registers itself
         * (and all its controllers automatically) with {@link ERoot#addModule addModule}. It is only when you
         * need some application-specific controllers that you would create them on the application level, and then
         * you need to use this method, in order to register them.
         *
         * If controller with such name already exists, then the method will do the following:
         *
         *  - throw an error, if the controller is different from the original
         *  - nothing (and return `false`), if the controller passed in is the same
         *
         * _**TIP:** Reusable controllers should always reside inside modules._
         *
         * And if the purpose of your controller is only to extend and/or configure other controller(s), then method
         * {@link ERoot#addAlias addAlias} offers a simpler syntax for adding such controllers.
         *
         * @param {JSName} name
         * Controller name. Trailing spaces are ignored.
         *
         * @param {function|class} func
         * Either a function or ES6 class that implements the controller:
         * - a function is called with controller's scope/instance as a single parameter, and as `this` context,
         * to initialize the controller as required.
         * - for an ES6 class, a new instance is created.
         *
         * @returns {boolean}
         * Indication of whether the controller was added:
         * - `true` - a new controller has been added
         * - `false` - a controller with the same name and implementation was added previously
         *
         * @see
         * {@link ERoot#addAlias addAlias},
         * {@link ERoot#addModule addModule},
         * {@link ERoot#addService addService},
         * {@link EController.event:onInit EController.onInit},
         * {@link EController.event:onReady EController.onReady},
         * {@link EController.event:onDestroy EController.onDestroy}
         *
         * @example
         *
         * // ES5 syntax:
         * //
         * app.addController('ctrlName', function(ctrl) {
         *     // this = ctrl
         *
         *     // Initializing your controller here:
         *     //
         *     // - setting up public properties and methods
         *     // - setting up event handlers, as needed
         *     // - changing DOM, if needed
         *
         *     // Creating all event handlers, as needed:
         *
         *     this.onInit = function() {
         *         // - can do ctrl.extend(...) here, to extend functionality
         *         // - can find controllers created through explicit binding
         *     };
         *
         *     this.onReady = function() {
         *         // can find all controllers here, including the ones
         *         // created implicitly (through extension)
         *     };
         *
         *     this.onDestroy = function() {
         *         // any clean-up, if needed
         *     };
         *
         *     // Creating public properties + methods for
         *     // communication with other controllers:
         *
         *     this.someProp = 123;
         *
         *     this.someMethod = function() {
         *         // do something
         *     };
         * });
         *
         * @example
         *
         * // ES6 class syntax:
         * //
         * class MyController extends EController {
         *     // If you want to use a constructor in your controller class,
         *     // you must pass Controller Context parameter into the parent,
         *     // or else there will be a construction-related error thrown:
         *     constructor(cc) {
         *         super(cc); // pass Controller Context into the parent class
         *
         *         // here you can access and modify DOM
         *     }
         *
         *     onInit() {
         *         // - can do ctrl.extend(...) here, to extend functionality
         *         // - can find controllers created through explicit binding
         *     }
         *
         *     onReady() {
         *         // can find all controllers here, including the ones
         *         // created implicitly (through extension)
         *     }
         *
         *     onDestroy() {
         *         // any clean-up, if needed
         *     }
         * }
         *
         * app.addController('ctrlName', MyController);
         */
        this.addController = function (name, func) {
            name = validateEntity(name, func, 'controller');
            validateClass(func);
            if (name in ctrlRegistered) {
                // controller name has been registered previously
                if (ctrlRegistered[name] === func) {
                    // it is the same controller, so we can just ignore it;
                    return false;
                }
                throw new Error('Controller with name ' + jStr(name) + ' already exists.');
            }
            ctrlRegistered[name] = func;
            return true;
        };

        /**
         * @method ERoot#addAlias
         * @description
         * Creates a simplified controller as a configurable alias.
         *
         * Any controller that extends other controllers, using method {@link EController#extend EController.extend},
         * is effectively an alias. And this method simplifies creation of such controllers, suitable when you only want
         * a new alias for extended controller(s), and optionally configured.
         *
         * This method is particularity useful during integration into an app, creating simpler aliases and configuring
         * their extended controllers at the same time. You may decide to have a few such controllers, as a means of
         * simplifying the app setup, i.e. instead of searching for a controller to change its configuration, you can
         * resort to using a pre-configured alias, even if it is for a single controller:
         *
         * ```js
         * app.addAlias('errorBoard', 'someModule.panels.centered', function(c) {
         *    // here we presume that controller 'someModule.panels.centered'
         *    // implements method setConfig(configObject):
         *    c.setConfig({bgColor: 'red', color: 'white'});
         * });
         * ```
         *
         * In cases when all you want is to create an alias for a single controller name, inside an app or a module,
         * method {@link ERoot#getCtrlFunc getCtrlFunc} may be more appropriate for this.
         *
         * @param {JSName} name
         * New controller/alias name. Trailing spaces are ignored.
         *
         * @param {CtrlName|CtrlName[]} ctrlNames
         * Either a single controller name, or an array of names, for which the new alias is created.
         *
         * Trailing spaces are ignored.
         *
         * @param {function} [cb]
         * Optional callback for re-configuring controllers.
         *
         * It takes a dynamic list of parameters, matching the specified controllers.
         *
         * Calling context `this` is set to the created alias controller.
         *
         * @see
         * {@link ERoot#addController addController},
         * {@link ERoot#getCtrlFunc getCtrlFunc}
         *
         * @example
         *
         * // Create a new controller aliasName as an alias for ['controller1', 'controller2'],
         * // so instead of e-bind="controller1, controller2" we can use e-bind="aliasName":
         *
         * app.addAlias('aliasName', ['controller1', 'controller2']);
         *
         * @example
         *
         * // Create a new controller-alias, and re-configure it at the same time:
         *
         * app.addAlias('aliasName', ['controller1', 'controller2'], function(c1, c2) {
         *     // this = controller 'aliasName' object
         *     // c1 = controller 'controller1' object
         *     // c2 = controller 'controller2' object
         *
         *     this.node.className = 'myClass';
         *
         *     c1.someMethod(123);
         *     c2.someMethod('hello!');
         * });
         *
         */
        this.addAlias = function (name, ctrlNames, cb) {
            root.addController(name, function () {
                this.onInit = function () {
                    var c = this.extend(ctrlNames);
                    if (typeof cb === 'function') {
                        cb.apply(this, Array.isArray(c) && c || [c]);
                    }
                };
            });
        };

        /**
         * @method ERoot#addService
         * @description
         * Adds and initializes a new service, which is simply an isolated namespace that contains
         * generic reusable code, to be shared across components and/or the application.
         *
         * If the service with such name already exists, the method will do nothing, and return `false`.
         *
         * Each added service becomes globally available via the {@link ERoot#services services} namespace.
         *
         * @param {JSName} name
         * Service name. Trailing spaces are ignored.
         *
         * @param {function} func
         * Service initialization function, to be called with the service's scope as a single parameter,
         * and as `this` context, to initialize the service as required.
         *
         * @see
         * {@link ERoot#addController addController},
         * {@link ERoot#addModule addModule}
         *
         * @returns {boolean}
         * Indication of whether the service was added:
         * - `true` - the service has been successfully added
         * - `false` - ignoring the service, as it was added previously
         *
         * @example
         *
         * app.addService('serviceName', function(scope) {
         *     // this = scope
         *
         *     // Implement the service API on the scope here:
         *
         *     this.getMessage = function() {
         *         // implement the method here
         *     };
         * });
         */
        this.addService = function (name, func) {
            name = validateEntity(name, func, 'service');
            if (name in root.services) {
                return false;
            }
            var scope = {};
            func.call(scope, scope);
            readOnlyProp(root.services, name, scope);
            return true;
        };

        /**
         * @method ERoot#addModule
         * @description
         * Adds and initializes a new module, which is effectively an isolated namespace of controllers.
         *
         * If the module with such name already exists, the method will do nothing, and return `false`.
         *
         * Unlike application-level controllers, which register themselves by calling {@link ERoot#addController addController},
         * all controllers inside a module are available automatically, just as the module is added.
         *
         * Each added module is listed within the {@link ERoot#modules modules} namespace.
         *
         * @param {JSName} name
         * Module name. Trailing spaces are ignored.
         *
         * @param {function} func
         * Module initialization function, to be called with the module's scope as a single parameter,
         * and as `this` context, to initialize the module as required.
         *
         * @returns {boolean}
         * Indication of whether the module was added:
         * - `true` - the module has been successfully added
         * - `false` - ignoring the module, as it was added previously
         *
         * @see
         * {@link ERoot#addController addController},
         * {@link ERoot#addService addService}
         *
         * @example
         *
         * app.addModule('moduleName', function(scope) {
         *     // this = scope
         *
         *     // Creating functions-controllers on the scope:
         *
         *     this.ctrl1 = function() {
         *         // controller implementation here;
         *     };
         *
         *     // Can use sub-spaces of any depth:
         *     this.effects = {
         *         fadeIn: function() {
         *             // implement fadeIn controller here;
         *         },
         *         fadeOut: function() {
         *             // implement fadeOut controller here;
         *         };
         *     };
         * });
         *
         * @example
         *
         * // Modules can also use ES6 classes as controllers:
         *
         * class MyController extends EController {
         *     onInit() {
         *         this.node.innerHTML = 'Hello!';
         *     }
         * }
         *
         * app.addModule('moduleName', function(scope) {
         *     // this = scope
         *
         *     this.ctrl1 = MyController;
         * });
         */
        this.addModule = function (name, func) {
            name = validateEntity(name, func, 'module');
            if (name in root.modules) {
                return false;
            }
            var scope = {};
            func.call(scope, scope);
            readOnlyProp(root.modules, name, scope);
            return true;
        };

        /**
         * @method ERoot#attach
         * @description
         * Manually attaches/binds and initializes controller(s) to one specific DOM element, bypassing the automatic
         * controller binding.
         *
         * This method is to simplify the binding when elements are available only as DOM objects, and not as HTML.
         *
         * Most practical use cases for this method are that of an integration process into an application.
         * If however, you decide to call it from inside a controller, please note that while it will work during
         * and after {@link EController.event:onInit onInit} event, it will throw an error, if called during the
         * controller's construction, because it will cause nested binding execution, which this library does not support.
         *
         * The method's primary use is as an integration tool, and a replacement for automatic binding, not an addition.
         * However, if you try to combine it, note that while attaching to an element previously bound automatically,
         * it will work correctly, as an extension (just like method {@link EController#extend EController.extend}),
         * but automatic binding will not work on elements with manually attached controllers, due to the conflict
         * of controllers initialization in this case.
         *
         * Similar to method {@link EController#extend EController.extend}, it creates and returns a new controller(s),
         * according to the parameters. And if you specify a controller name that's already bound to the element,
         * that controller is returned instead, to be reused, because only a single controller type can be bound to any
         * given element.
         *
         * The method sets/updates attribute `data-e-bind` / `e-bind` according to the new bindings.
         *
         * @param {external:HTMLElement} e
         * Either a new DOM element or a {@link ControlledElement}, to bind with the specified controller(s).
         *
         * @param {CtrlName|CtrlName[]} names
         * Either a single controller name, or an array of names. Trailing spaces are ignored.
         *
         * @returns {EController|EController[]}
         * - if you pass in a single controller name, it returns a single created controller.
         * - if you pass in an array of names, it returns an array of created controllers.
         *
         * The returned controller(s) have already finished processing event {@link EController.event:onReady EController.onReady}.
         *
         * @see
         * {@link ERoot#bind bind},
         * {@link ERoot#bindFor bindFor},
         * {@link EController#bind EController.bind}
         *
         * @example
         *
         * // This is how you can integrate a controller into an existing app,
         * // without using element-to-controller explicit bindings:
         *
         * var e = document.getElementById('someId'); // find a DOM element
         * var c = app.attach(e, 'myController'); // attach a controller to it
         * c.someMethod(data); // data = parametrization data for the controller
         *
         * @example
         *
         * // This example is only to show how ERoot.attach relates to EController.extend,
         * // but not how it is to be used, as using ERoot.attach like this is pointless.
         *
         * app.addController('ctrlName', function(ctrl) {
         *     this.onInit = function() {
         *
         *         // Specifically in this context, the result of
         *         // calling the following two lines is identical:
         *
         *         var a = ctrl.extend(['ctrl1', 'ctrl2']);
         *
         *         var b = app.attach(ctrl.node, ['ctrl1', 'ctrl2']);
         *     };
         * });
         */
        this.attach = function (e, names) {
            validateElement(e);
            if (constructing) {
                throw new Error('Cannot invoke ERoot.attach from a controller constructor.');
            }
            var ctrl = e.controllers || {};
            var created = [], attrNames = [];

            function ext(n) {
                var cn = validateControllerName(n, true);
                if (!cn) {
                    throw new TypeError('Invalid controller name ' + jStr(n) + ' specified.');
                }
                var c = ctrl[cn];
                if (!c) {
                    var f = getCtrlFunc(cn);
                    c = createController(cn, e, f);
                    readOnlyProp(ctrl, cn, c);
                    addLiveCtrl(cn, c);
                    created.push(c);
                    attrNames.push(cn);
                }
                return c;
            }

            var result = Array.isArray(names) ? names.map(ext) : ext(names);

            if (!e.controllers) {
                readOnlyProp(e, 'controllers', ctrl);
                elements.push(e);
                observer.watch(e);
            }

            // Need to set the attribute, if missing, or else EController.find
            // won't see it; and worse - event onDestroy won't work in IE9/10
            var attrValue = getAttribute(e, 'data-e-bind', 'e-bind');
            if (attrValue) {
                var oldAttr = attrValue.split(',').map(trim);
                attrNames.forEach(function (a) {
                    if (oldAttr.indexOf(a) === -1) {
                        oldAttr.push(a);
                    }
                });
                attrNames = oldAttr;
            }
            attrValue = attrNames.join(', ');
            var an = e.hasAttribute('e-bind') ? 'e-bind' : 'data-e-bind';
            e.setAttribute(an, attrValue);

            eventNotify(created, 'onInit');
            eventNotify(created, 'onReady');
            return result;
        };

        /**
         * @method ERoot#reset
         * @description
         * Performs instant hard reset of the entire library state, including the root interface object.
         *
         * It is only to help with some automatic tests that may require fresh state of the library.
         *
         * **NOTE:** If an alternative root name was set with `e-root` / `data-e-root`, its value is reset,
         * but the name itself is not removed from the global scope, because the library can pick it up
         * only once, on the initial run.
         *
         * @see
         * {@link ERoot#analyze analyze}
         */
        this.reset = function () {
            ctrlRegistered = {};
            ctrlGlobal = {};
            ctrlLocal = {};
            ctrlCache = {};
            elements.length = 0;
            observer.stop();
            observer = new DestroyObserver();
            constructing = false;
            root = new ERoot();
            window.excellent = root;
            if (altRootName) {
                window[altRootName] = root;
            }
        };

        /**
         * @method ERoot#bind
         * @description
         * Searches for all elements in the document not yet bound, and binds them to controllers.
         *
         * It will search for all elements in the document that contain attribute `data-e-bind` / `e-bind`,
         * but without controllers yet, create and initialize controllers, as specified by the attribute,
         * which is expected to contain valid names (comma-separated) of existing controllers.
         *
         * Normally, a controller creates new controlled elements within its children, and then uses
         * {@link EController#bind EController.bind} method. It is only when you create a new controlled
         * element that's not a child element, then you would use this global binding. For a random-element
         * binding see {@link ERoot#bindFor bindFor}.
         *
         * Note that when integrating your controllers into an application, if you are dealing with DOM objects rather than HTML,
         * then you can alternatively make use of method {@link ERoot#attach attach}, to inject and initialize controllers
         * for one specific DOM element.
         *
         * You should try to avoid use of synchronous bindings, if possible. The binding engine implements the logic
         * of minimizing the number of checks against the DOM, but it works/scales best when requests are asynchronous.
         *
         * @param {boolean|function} [process=false]
         * Determines how to process the binding:
         * - _any falsy value (default):_ the binding will be done asynchronously;
         * - _a function:_ binding is asynchronous, calling the function when finished;
         * - _any truthy value, except a function type:_ forces synchronous binding.
         *
         * @see
         * {@link ERoot#bindFor bindFor},
         * {@link ERoot#attach attach},
         * {@link EController#bind EController.bind}
         */
        this.bind = function (process) {
            processBinding(null, process);
        };

        /**
         * @method ERoot#bindFor
         * @description
         * Searches and initializes new bindings inside the specified element.
         *
         * Typically, you would trigger local bindings via {@link EController#bind EController.bind}. But when you know the element
         * that contains new bindings, and do not want to create a controller for it, or use the global {@link ERoot#bind bind},
         * you can use this method instead.
         *
         * @param {external:HTMLElement} e
         * DOM element with new bindings among its children - elements with attribute `data-e-bind` / `e-bind` set to valid names
         * (comma-separated) of existing controllers.
         *
         * @param {boolean|function} [process=false]
         * Determines how to process the binding:
         * - _any falsy value (default):_ the binding will be done asynchronously;
         * - _a function:_ binding is asynchronous, calling the function when finished;
         * - _any truthy value, except a function type:_ forces synchronous binding.
         *
         * @see
         * {@link ERoot#bind bind},
         * {@link EController#bind EController.bind}
         *
         * @example
         *
         * var e = document.getElementById('someId');
         * e.innerHTML = '<div e-bind="ctrl1, ctrl2"></div>';
         * app.bindFor(e); // bind child elements to controllers
         */
        this.bindFor = function (e, process) {
            validateElement(e);
            processBinding(e, process);
        };

        /**
         * @method ERoot#find
         * @description
         * Searches for all initialized controllers in the entire application, based on the controller name,
         * including the extended controllers.
         *
         * The search is based solely on the internal map of controllers, without involving DOM, and provides instant results.
         * Because of this, it will always significantly outperform method {@link EController#find EController.find},
         * even though the latter searches only among child elements, but it uses DOM.
         *
         * It will find explicitly created controllers, if called during or after event {@link EController.event:onInit EController.onInit},
         * and implicitly created controllers (extended via method {@link EController#extend EController.extend}), if called during or after event
         * {@link EController.event:onReady EController.onReady}. And it will find everything, if called during or after global event
         * {@link ERoot.event:onReady ERoot.onReady}.
         *
         * @param {CtrlName} name
         * Controller name to search by. Trailing spaces are ignored.
         *
         * @returns {EController[]}
         * List of found initialized controllers.
         *
         * @see
         * {@link ERoot#findOne findOne},
         * {@link EController#find EController.find},
         * {@link EController#findOne EController.findOne}
         */
        this.find = function (name) {
            var cn = parseControllerName(name);
            if (cn in ctrlGlobal) {
                return ctrlGlobal[cn].slice();
            }
            return [];
        };

        /**
         * @method ERoot#findOne
         * @description
         * Implements a safe-check search for a single initialized controller, in the entire application, based on the controller name.
         *
         * The method will throw an error, if multiple or no controllers found.
         *
         * It will find explicitly created controllers, if called during or after event {@link EController.event:onInit EController.onInit},
         * and implicitly created controllers (extended via method {@link EController#extend EController.extend}), if called during or after event
         * {@link EController.event:onReady EController.onReady}. And it will find everything, if called during or after global event
         * {@link ERoot.event:onReady ERoot.onReady}.
         *
         * @param {CtrlName} name
         * Controller name to search by. Trailing spaces are ignored.
         *
         * @returns {EController}
         * A single controller with the matching name.
         *
         * @see
         * {@link ERoot#find find},
         * {@link EController#find EController.find},
         * {@link EController#findOne EController.findOne}
         */
        this.findOne = function (name) {
            var a = this.find(name);
            if (a.length !== 1) {
                throw new Error('Expected a single controller from findOne(' + jStr(name) + '), but found ' + a.length + '.');
            }
            return a[0];
        };

        /**
         * @method ERoot#getCtrlFunc
         * @description
         * Resolves a full controller name into the corresponding controller (function/class).
         *
         * This lets you create controllers on the app level or inside modules that directly alias an existing controller,
         * without extending it (methods {@link EController#extend EController.extend} and {@link ERoot#addAlias ERoot.addAlias}).
         *
         * ```js
         * // Example of declaring a controller inside a module,
         * // as an alias for a controller from another module:
         *
         * scope.print = e.getCtrlFunc('printModule.default.showUI');
         *
         * // e = excellent root object, scope = module's scope object
         * ```
         *
         * @param {CtrlName} name
         * Full controller name. Trailing spaces are ignored.
         *
         * @param {boolean} [noError=false]
         * By default, the method throws an error whenever it fails to resolve the specified name into a valid controller.
         * Passing in `noError = true` forces it to return `null` when the module or controller are not found.
         * This however will not suppress errors related to passing in an invalid controller name.
         *
         * Example of where you might want to use it - provide an alternative controller when the desired one could not be
         * resolved for some reasons, like when inclusion of a certain module into the app is optional.
         * This way you can also check whether the containing module is included or not.
         *
         * @returns {function|class|null}
         * Initialization controller (function/class).
         *
         * It can return `null` only when the function fails because the module or controller were not found,
         * and `noError` was passed in as a truthy value.
         *
         * @see
         * {@link ERoot#addAlias addAlias}
         *
         * @example
         *
         * // Adding a controller-alias, just to shorten another controller's name:
         *
         * app.addController('shortName', app.getCtrlFunc('module1.very.long.name'));
         *
         */
        this.getCtrlFunc = function (name, noError) {
            return getCtrlFunc(parseControllerName(name), null, noError);
        };

        /**
         * @method ERoot#analyze
         * @description
         * Pulls together and returns a snapshot of the current state of the library, as {@link EStatistics} object.
         *
         * This method is to help with debugging your application, and for automatic tests.
         *
         * @returns {EStatistics}
         * Statistics Data.
         *
         * @see
         * {@link ERoot#reset reset}
         */
        this.analyze = function () {
            var res = {
                bindings: {
                    locals: bs.nodes.length,
                    callbacks: bs.cb.length,
                    waiting: bs.waiting,
                    global: bs.glob
                },
                controllers: {
                    global: {},
                    local: {},
                    registered: Object.keys(ctrlRegistered),
                    total: 0
                },
                elements: elements.slice(),
                modules: root.modules,
                services: root.services
            };
            for (var g in ctrlGlobal) {
                res.controllers.total += ctrlGlobal[g].length;
                res.controllers.global[g] = ctrlGlobal[g].slice();
            }
            for (var l in ctrlLocal) {
                res.controllers.total += ctrlLocal[l].length;
                res.controllers.local[l] = ctrlLocal[l].slice();
            }
            return res;
        };
    }

    /**
     * @interface EStatistics
     * @description
     * Statistics / Diagnostics data, as returned by method {@link ERoot#analyze ERoot.analyze}.
     *
     * @property bindings
     * Element-to-controller binding status.
     *
     * @property {number} bindings.locals
     * Number of pending child-binding requests, from {@link EController#bind EController.bind}.
     *
     * @property {number} bindings.callbacks
     * Number of pending recipients awaiting a notification when the requested binding is finished.
     *
     * @property {boolean} bindings.waiting
     * Binding engine has triggered a timer for the next asynchronous update, and is now waiting for it.
     *
     * @property {boolean} bindings.global
     * A global asynchronous request is being processed.
     *
     * @property controllers
     * Details about controllers.
     *
     * @property {Object.<CtrlName, EController[]>} controllers.global
     * All global live controllers, visible to search methods {@link ERoot#find ERoot.find} and {@link ERoot#findOne ERoot.findOne}.
     *
     * @property {Object.<CtrlName, EController[]>} controllers.local
     * All local live controllers (created via {@link EController#extend EController.extend} with `local` = `true`),
     * and thus not visible to search methods {@link ERoot#find ERoot.find} and {@link ERoot#findOne ERoot.findOne}.
     *
     * @property {JSName[]} controllers.registered
     * Names of all registered controllers.
     *
     * @property {number} controllers.total
     * Total number of all live controllers.
     *
     * @property {ControlledElement[]} elements
     * List of all controlled elements currently in the DOM.
     *
     * @property {Object.<JSName, {}>} modules
     * All registered and initialized modules.
     *
     * @property {Object.<JSName, {}>} services
     * All registered and initialized services.
     *
     * */

    /**
     * @event ERoot.onReady
     * @type {function}
     * @description
     * Called only once, after initializing controllers in the app for the first time.
     *
     * It represents the state of the application when it is ready to find all controllers
     * and communicate with them. This includes controllers created through extension, i.e.
     * all controllers have finished processing {@link EController.event:onReady onReady}
     * event at this point.
     *
     * @see
     * {@link EController.event:onInit EController.onInit},
     * {@link EController.event:onReady EController.onReady}
     *
     * @example
     *
     * app.onReady = function() {
     *   // All explicit and extended controllers now can be located;
     *
     *   // Let's find our main app controller, and ask it to do something:
     *   app.findOne('appCtrl').doSomething();
     * };
     */

    /**
     * @class EController
     * @hideconstructor
     * @description
     * Controller interface, attached to each {@link ControlledElement} in the DOM.
     *
     * It is created automatically, during element-to-controller binding, or when {@link ERoot#attach attaching} to an element.
     *
     * @param {} cc
     * Controller Context.
     *
     * @param {CtrlName} cc.name
     * Controller name.
     *
     * @param {ControlledElement} cc.node
     * Controller's node element.
     *
     * @see
     * {@link EController#name name},
     * {@link EController#node node},
     * {@link EController#bind bind},
     * {@link EController#depends depends},
     * {@link EController#extend extend},
     * {@link EController#find find},
     * {@link EController#findOne findOne},
     * {@link EController.event:onInit onInit},
     * {@link EController.event:onReady onReady},
     * {@link EController.event:onDestroy onDestroy}
     */
    function EController(cc) {

        cc = cc || {};

        /**
         * @member EController#name
         * @type {CtrlName}
         * @readonly
         * @description
         * Full name of the controller, i.e. the name from which the controller was instantiated.
         */
        readOnlyProp(this, 'name', cc.name);

        /**
         * @member EController#node
         * @type {ControlledElement}
         * @readonly
         * @description
         * The DOM element/node this controller is bound to.
         *
         * Every controller is bound to a DOM element in the document, either through binding or direct attachment.
         * And at the core of every component is direct communication with the element it is bound to.
         */
        readOnlyProp(this, 'node', cc.node);
    }

    /**
     * @event EController.onInit
     * @type {function}
     * @description
     * Initialization event handler.
     *
     * It represents the state of the controller when it is ready to do any of the following:
     *  - find explicitly bound (through `e-bind` attribute) controllers and communicate with them
     *  - extend the element with other controllers (via method {@link EController#extend extend})
     *
     * Note that at this point you cannot locate or communicate with outside controllers being extended
     * (via method {@link EController#extend extend}). For that you need to use {@link EController.event:onReady onReady} event.
     *
     * If a controller doesn't extend or communicate with other controllers, then it does not need to handle this event.
     *
     * @see
     * {@link EController.event:onReady onReady},
     * {@link EController.event:onDestroy onDestroy},
     * {@link ERoot.event:onReady ERoot.onReady}
     *
     * @example
     *
     * app.addController('ctrlName', function(ctrl) {
     *    ctrl.onInit = function() {
     *        // this = ctrl
     *
     *        // - you can use ctrl.extend here, to extend it
     *        // - you can find explicitly created controllers
     *    };
     * });
     */

    /**
     * @event EController.onReady
     * @type {function}
     * @description
     * Post-initialization event (happens after event {@link EController.event:onInit onInit}).
     *
     * At this point you can find and communicate with controllers created implicitly, through extension (via method {@link EController#extend extend}).
     *
     * Controllers can only be extended during {@link EController.event:onInit onInit}, and they are initialized right after. If you need to find and communicate
     * with such extended controllers, it is only possible during or after this event.
     *
     * @see
     * {@link EController.event:onInit onInit},
     * {@link EController.event:onDestroy onDestroy},
     * {@link ERoot.event:onReady ERoot.onReady}
     *
     * @example
     *
     * app.addController('ctrlName', function(ctrl) {
     *    ctrl.onReady = function() {
     *        // this = ctrl
     *
     *        // you can find and communicate with all controllers here
     *    };
     * });
     */

    /**
     * @event EController.onDestroy
     * @type {function}
     * @description
     * De-initialization event handler.
     *
     * It signals the controller that its element has been removed from DOM, and it is time to release any pre-allocated resources, if necessary.
     *
     * In any modern browser, the event is triggered immediately, courtesy of {@link external:MutationObserver MutationObserver},
     * while in older browsers (IE9 and IE10), it falls back on a manual background check that runs every second. You can make it instant
     * under IE9/10 also, by adding one of the `MutationObserver` polyfills to the app.
     *
     * @see
     * {@link ERoot.event:onReady ERoot.onReady}
     *
     * @example
     *
     * app.addController('ctrlName', function(ctrl) {
     *    ctrl.onDestroy = function() {
     *        // this = ctrl
     *
     *        // Release any resources here, if necessary
     *    };
     * });
     */

    /**
     * @method EController#bind
     * @description
     * Signals the framework that the element's inner content has been modified to contain new child controlled elements,
     * and that it is time to bind those with the corresponding controllers.
     *
     * It will search for all child elements that contain attribute `data-e-bind` / `e-bind`, but without controllers yet,
     * create and initialize controllers, as specified by the attribute, which is expected to contain valid names
     * (comma-separated) of existing controllers.
     *
     * This method requires that the calling controller has been initialized.
     *
     * Note that when integrating your controllers into an application, if you are dealing with DOM objects rather than HTML,
     * then you can alternatively make use of method {@link ERoot#attach ERoot.attach}, to inject and initialize controllers
     * for one specific DOM element.
     *
     * You should try to avoid use of synchronous bindings, if possible. The binding engine implements the logic of minimizing
     * the number of checks against the DOM, but it works/scales best when requests are asynchronous.
     *
     * @param {boolean|function} [process=false]
     * Determines how to process the binding:
     * - _any falsy value (default):_ the binding will be done asynchronously;
     * - _a function:_ binding is asynchronous, calling the function when finished;
     * - _any truthy value, except a function type:_ forces synchronous binding.
     *
     * @see
     * {@link ERoot#bind ERoot.bind},
     * {@link ERoot#bindFor ERoot.bindFor},
     * {@link ERoot#attach ERoot.attach}
     *
     * @example
     *
     * app.addController('appCtrl', function(ctrl) {
     *
     *     // You can also change content here, but
     *     // the binding only possible during onInit;
     *
     *     ctrl.onInit = function() {
     *         // Injecting a new controlled element:
     *         ctrl.node.innerHTML = '<div e-bind="someCtrl"></div>';
     *
     *         // Asynchronously bind all child controlled elements:
     *         ctrl.bind(function() {
     *             // Binding has finished, we can now find the controller:
     *             var c = ctrl.findOne('someCtrl');
     *             // and communicate with it:
     *             c.someMethod();
     *         });
     *
     *         // The following binding would produce the same result:
     *         // app.bindFor(this.node, cb);
     *     };
     * });
     */
    EController.prototype.bind = function (process) {
        this.verifyInit('bind');
        processBinding(this.node, process);
    };

    /**
     * @method EController#extend
     * @description
     * Extends other controller(s) with new functionality, thus providing functional inheritance.
     *
     * It creates and returns a new controller(s), according to the parameters. But if you specify a controller
     * name that's already bound to the element, that controller is returned instead, to be reused, because
     * only a single controller type can be bound to any given element.
     *
     * This method can only be called during event {@link EController.event:onInit onInit}.
     *
     * Note that while the calling controller has immediate access to extended controllers, as they are returned
     * by the method, other/global controllers can communicate with them only during or after event
     * {@link EController.event:onReady onReady}.
     *
     * @param {CtrlName|CtrlName[]} names
     * Either a single controller name, or an array of names. Trailing spaces are ignored.
     *
     * @param {boolean} [local=false]
     * When `true`, newly created controllers are not registered in the global map,
     * so global search methods {@link ERoot#find ERoot.find} and {@link ERoot#findOne ERoot.findOne}
     * would not find them. This offers a level of encapsulation / privacy, in case access to such
     * extended controllers by an outside controller is undesirable.
     *
     * Note that if the element already has the controller, it is reused, and flag `local` is then ignored.
     *
     * @returns {EController|EController[]}
     * - if you pass in a single controller name, it returns a single controller.
     * - if you pass in an array of names, it returns an array of controllers.
     *
     * @see
     * {@link EController#depends depends}
     *
     * @example
     *
     * app.addController('myController', function(ctrl) {
     *     // Optional early dependency diagnostics:
     *     ctrl.depends(['dragDrop', 'removable']);
     *
     *     ctrl.onInit = function() {
     *         var c = ctrl.extend(['dragDrop', 'removable']);
     *
     *         // c[0] = ctrl.node.controllers.dragDrop
     *         // c[1] = ctrl.node.controllers.removable
     *     };
     * });
     */
    EController.prototype.extend = function (names, local) {
        var ctrl = this.verifyInit('extend');
        var created = [];

        function ext(n) {
            var cn = validateControllerName(n, true);
            if (!cn) {
                throw new TypeError('Invalid controller name ' + jStr(n) + ' specified.');
            }
            var c = ctrl[cn];
            if (!c) {
                var f = getCtrlFunc(cn);
                c = createController(cn, this.node, f);
                readOnlyProp(ctrl, cn, c);
                addLiveCtrl(cn, c, local);
                created.push(c);
            }
            return c;
        }

        var result = Array.isArray(names) ? names.map(ext, this) : ext.call(this, names);
        eventNotify(created, 'onInit');
        eventNotify(created, 'onReady');
        return result;
    };

    /**
     * @method EController#depends
     * @description
     * Verifies that each controller in the list of dependencies exists, or else throws an error.
     * It is normally used during the controller's construction.
     *
     * This optional level of verification is mainly useful when using dynamically injected controllers
     * (not just the ones through method {@link EController#extend extend}), as it is best to verify such
     * dependencies before trying to use them.
     *
     * Note however, that this method may not see controllers registered dynamically.
     *
     * @param {CtrlName[]} names
     * List of controller names. It should include all controller names that are due to be extended
     * (via method {@link EController#extend extend}), plus the ones that can be requested dynamically.
     *
     * Trailing spaces are ignored.
     *
     * @see {@link EController#extend extend}
     *
     * @example
     *
     * app.addController('ctrlName', function(ctrl) {
     *
     *     // Make sure every controller on the list is available,
     *     // or else throw a detailed dependency error:
     *     ctrl.depends(['appCtrl', 'mod.ctrl1']);
     *
     * });
     */
    EController.prototype.depends = function (names) {
        if (!Array.isArray(names)) {
            throw new TypeError('Invalid list of controller names.');
        }
        names.forEach(function (n) {
            var cn = validateControllerName(n, true);
            if (!cn) {
                throw new TypeError('Invalid controller name ' + jStr(n) + ' specified.');
            }
            if (!getCtrlFunc(cn, null, true)) {
                throw new Error('Controller ' + jStr(this.name) + ' depends on ' + jStr(cn) + ', which was not found.');
            }
        }, this);
    };

    /**
     * @method EController#findOne
     * @description
     * Implements a safe-check search for a single initialized child controller by a given controller name.
     *
     * The method will throw an error, if multiple or no controllers found.
     *
     * It will find explicitly created controllers, if called during or after event {@link EController.event:onInit onInit},
     * and implicitly created controllers (extended via method {@link EController#extend extend}), if called during or after event
     * {@link EController.event:onReady onReady}.
     *
     * @param {CtrlName} name
     * Controller name to search by. Trailing spaces are ignored.
     *
     * @returns {EController}
     * A single child controller with the matching name.
     *
     * @see
     * {@link EController#find find},
     * {@link ERoot#find ERoot.find},
     * {@link ERoot#findOne ERoot.findOne}
     */
    EController.prototype.findOne = function (name) {
        var a = this.find(name);
        if (a.length !== 1) {
            throw new Error('Expected a single controller from ' + jStr(this.name) + '.findOne(' + jStr(name) + '), but found ' + a.length + '.');
        }
        return a[0];
    };

    /**
     * @method EController#find
     * @description
     * Searches for all initialized child controllers, by a given controller name, including the extended controllers.
     *
     * This method searches through DOM, as it needs to iterate over child elements. And because of that, even though
     * it searches just through a sub-set of elements, it is always slower than the global {@link ERoot#find ERoot.find} method.
     *
     * It will find explicitly created controllers, if called during or after event {@link EController.event:onInit onInit},
     * and implicitly created controllers (extended via method {@link EController#extend extend}), if called during or after event
     * {@link EController.event:onReady onReady}.
     *
     * @param {CtrlName} name
     * Controller name to search by. Trailing spaces are ignored.
     *
     * @returns {EController[]}
     * List of initialized child controllers.
     *
     * @see
     * {@link EController#findOne findOne},
     * {@link ERoot#findOne ERoot.findOne},
     * {@link ERoot#find ERoot.find}
     */
    EController.prototype.find = function (name) {
        var cn = parseControllerName(name);
        return findAll('[data-e-bind],[e-bind]', this.node)
            .filter(function (e) {
                return e.controllers && e.controllers[cn];
            })
            .map(function (e) {
                return e.controllers[cn];
            });
    };

    /**
     * @method EController#verifyInit
     * @private
     * @description
     * Verifies that this controller has been initialized, or else throws an error.
     *
     * This method is for internal use.
     *
     * @param {string} method
     * Name of the method that requires verification.
     *
     * @returns {EController[]}
     * Controllers linked to the element.
     */
    EController.prototype.verifyInit = function (method) {
        var c = this.node.controllers;
        if (!c) {
            throw new Error('Method "' + method + '" cannot be used before initialization.');
        }
        return c;
    };

    /**
     * Global initialization.
     *
     * It excludes else statements from test coverage, because only
     * under JEST we have 'module' and 'window' at the same time.
     */
    (function () {
        /* istanbul ignore else */
        if (typeof module === 'object' && module && typeof module.exports === 'object') {
            module.exports = root; // UMD support
        }
        /* istanbul ignore else */
        if (typeof window !== 'undefined' && window) {
            window.excellent = root; // default root name
            window.EController = EController;
            var e = findAll('[data-e-root],[e-root]');
            if (e.length) {
                if (e.length > 1) {
                    throw new Error('Multiple root elements are not allowed.');
                }
                var name = getAttribute(e[0], 'data-e-root', 'e-root');
                if (!validJsVariable(name)) {
                    // The name must adhere to JavaScript open-name syntax!
                    throw new Error('Invalid ' + jStr(name) + ' root name specified: ' + startTag(e[0]));
                }
                altRootName = name;
                window[name] = root; // adding alternative root name
            }
            document.addEventListener('DOMContentLoaded', function () {
                processBinding(null, true); // binding all elements synchronously
                eventNotify([root], 'onReady');
            });
        }
    })();

})();

/**
 * @typedef JSName
 * @type {string}
 * @description
 * It is a string that complies with the open-name syntax for JavaScript variables:
 * - It must contain 1 or more symbols
 * - It is treated as case-sensitive
 * - Allowed symbols are: `a-z`, `A-Z`, `0-9`, `$` and `_`
 * - It cannot start with a digit (`0-9`)
 */

/**
 * @typedef CtrlName
 * @type {string}
 * @description
 * It is a standard JavaScript nested-name string, made up by 1 or more {@link JSName} strings, joined by a dot:
 *
 * ```js
 * module_1.$name2._ctrl3
 * ```
 *
 * - It cannot start or end with a dot
 * - It cannot have any spaces in between
 * - It is treated as case-sensitive
 *
 * The string represents a full controller name, depending on how many {@link JSName} entries it contains:
 *
 * - When it contains a single {@link JSName}, it always refers to an app-level controller, as added with
 *   {@link ERoot#addController ERoot.addController}
 * - When it contains more than one {@link JSName}, then the first one is a module name, as added with
 *   {@link ERoot#addModule ERoot.addModule}, followed by either a simple or nested controller name in that module.
 */

/**
 * @external HTMLElement
 * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
 */

/**
 * @external MutationObserver
 * @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
 */