/**
 * This mixin enables classes to declare relationships to child elements and provides the
 * mechanics for acquiring the {@link Ext.Element elements} and storing them on an object
 * instance as properties.
 *
 * This class is used by {@link Ext.Component components} and {@link Ext.layout.container.Container container layouts} to
 * manage their child elements.
 * 
 * A typical component that uses these features might look something like this:
 * 
 *      Ext.define('Ext.ux.SomeComponent', {
 *          extend: 'Ext.Component',
 *          
 *          childEls: [
 *              'bodyEl'
 *          ],
 *          
 *          renderTpl: [
 *              '<div id="{id}-bodyEl"></div>'
 *          ],
 *          
 *          // ...
 *      });
 * 
 * The `childEls` array lists one or more relationships to child elements managed by the
 * component. The items in this array can be either of the following types:
 * 
 * - String: the id suffix and property name in one. For example, "bodyEl" in the above
 * example means a "bodyEl" property will be added to the instance with the result of
 * {@link Ext#get} given "componentId-bodyEl" where "componentId" is the component instance's
 * id.
 * - Object: with a `name` property that names the instance property for the element, and
 * one of the following additional properties:
 *      - `id`: The full id of the child element.
 *      - `itemId`: The suffix part of the id to which "componentId-" is prepended.
 *      - `select`: A selector that will be passed to {@link Ext#select}.
 *      - `selectNode`: A selector that will be passed to {@link Ext.DomQuery#selectNode}.
 * 
 * The example above could have used this instead to achieve the same result:
 *
 *      childEls: [
 *          { name: 'bodyEl', itemId: 'bodyEl' }
 *      ]
 *
 * When using `select`, the property will be an instance of {@link Ext.CompositeElement}. In
 * all other cases, the property will be an {@link Ext.Element} or `null` if not found.
 *
 * Care should be taken when using `select` or `selectNode` to find child elements. The
 * following issues should be considered:
 * 
 * - Performance: using selectors can be slower than id lookup by a factor 10x or more.
 * - Over-selecting: selectors are applied after the DOM elements for all children have
 * been rendered, so selectors can match elements from child components (including nested
 * versions of the same component) accidentally.
 * 
 * This above issues are most important when using `select` since it returns multiple
 * elements.
 *
 * **IMPORTANT** 
 * Unlike a `renderTpl` where there is a single value for an instance, `childEls` are aggregated
 * up the class hierarchy so that they are effectively inherited. In other words, if a
 * class where to derive from `Ext.ux.SomeComponent` in the example above, it could also
 * have a `childEls` property in the same way as `Ext.ux.SomeComponent`.
 * 
 *      Ext.define('Ext.ux.AnotherComponent', {
 *          extend: 'Ext.ux.SomeComponent',
 *          
 *          childEls: [
 *              // 'bodyEl' is inherited
 *              'innerEl'
 *          ],
 *          
 *          renderTpl: [
 *              '<div id="{id}-bodyEl">'
 *                  '<div id="{id}-innerEl"></div>'
 *              '</div>'
 *          ],
 *          
 *          // ...
 *      });
 * 
 * The `renderTpl` contains both child elements and unites them in the desired markup, but
 * the `childEls` only contains the new child element. The {@link #applyChildEls} method
 * takes care of looking up all `childEls` for an instance and considers `childEls`
 * properties on all the super classes and mixins.
 * 
 * @private
 */
Ext.define('Ext.util.ElementContainer', {

    childEls: [
        // empty - this solves a couple problems:
        //  1. It ensures that all classes have a childEls (avoid null ptr)
        //  2. It prevents mixins from smashing on their own childEls (these are gathered
        //      specifically)
    ],

    constructor: function () {
        var me = this,
            childEls;

        // if we have configured childEls, we need to merge them with those from this
        // class, its bases and the set of mixins...
        if (me.hasOwnProperty('childEls')) {
            childEls = me.childEls;
            delete me.childEls;

            me.addChildEls.apply(me, childEls);
        }
    },

    destroy: function () {
        var me = this,
            childEls = me.getChildEls(),
            child, childName, i, k;

        for (i = childEls.length; i--; ) {
            childName = childEls[i];
            if (typeof childName != 'string') {
                childName = childName.name;
            }

            child = me[childName];
            if (child) {
                me[childName] = null; // better than delete since that changes the "shape"
                child.remove();
            }
        }
    },

    /**
     * Adds each argument passed to this method to the {@link Ext.AbstractComponent#cfg-childEls childEls} array.
     */
    addChildEls: function () {
        var me = this,
            args = arguments;

        if (me.hasOwnProperty('childEls')) {
            me.childEls.push.apply(me.childEls, args);
        } else {
            me.childEls = me.getChildEls().concat(Array.prototype.slice.call(args));
        }
        
        me.prune(me.childEls, false);
    },

    /**
     * Sets references to elements inside the component. 
     * @private
     */
    applyChildEls: function(el, id) {
        var me = this,
            childEls = me.getChildEls(),
            baseId, childName, i, selector, value;

        baseId = (id || me.id) + '-';
        for (i = childEls.length; i--; ) {
            childName = childEls[i];

            if (typeof childName == 'string') {
                // We don't use Ext.get because that is 3x (or more) slower on IE6-8. Since
                // we know the el's are children of our el we use getById instead:
                value = el.getById(baseId + childName);
            } else {
                if ((selector = childName.select)) {
                    value = Ext.select(selector, true, el.dom); // a CompositeElement
                } else if ((selector = childName.selectNode)) {
                    value = Ext.get(Ext.DomQuery.selectNode(selector, el.dom));
                } else {
                    // see above re:getById...
                    value = el.getById(childName.id || (baseId + childName.itemId));
                }

                childName = childName.name;
            }

            me[childName] = value;
        }
    },

    getChildEls: function () {
        var me = this,
            self;

        // If an instance has its own childEls, that is the complete set:
        if (me.hasOwnProperty('childEls')) {
            return me.childEls;
        }

        // Typically, however, the childEls is a class-level concept, so check to see if
        // we have cached the complete set on the class:
        self = me.self;
        return self.$childEls || me.getClassChildEls(self);
    },

    getClassChildEls: function (cls) {
        var me = this,
            result = cls.$childEls,
            childEls, i, length, forked, mixin, mixins, name, parts, proto, supr, superMixins;

        if (!result) {
            // We put the various childEls arrays into parts in the order of superclass,
            // new mixins and finally from cls. These parts can be null or undefined and
            // we will skip them later.

            supr = cls.superclass;
            if (supr) {
                supr = supr.self;
                parts = [supr.$childEls || me.getClassChildEls(supr)]; // super+mixins
                superMixins = supr.prototype.mixins || {};
            } else {
                parts = [];
                superMixins = {};
            }

            proto = cls.prototype;
            mixins = proto.mixins; // since we are a mixin, there will be at least us
            for (name in mixins) {
                if (mixins.hasOwnProperty(name) && !superMixins.hasOwnProperty(name)) {
                    mixin = mixins[name].self;
                    parts.push(mixin.$childEls || me.getClassChildEls(mixin));
                }
            }

            parts.push(proto.hasOwnProperty('childEls') && proto.childEls);

            for (i = 0, length = parts.length; i < length; ++i) {
                childEls = parts[i];
                if (childEls && childEls.length) {
                    if (!result) {
                        result = childEls;
                    } else {
                        if (!forked) {
                            forked = true;
                            result = result.slice(0);
                        }
                        result.push.apply(result, childEls);
                    }
                }
            }

            cls.$childEls = result = (result ? me.prune(result, !forked) : []);
        }

        return result;
    },

    prune: function (childEls, shared) {
        var index = childEls.length,
            map = {},
            name;

        while (index--) {
            name = childEls[index];
            if (typeof name != 'string') {
                name = name.name;
            }

            if (!map[name]) {
                map[name] = 1;
            } else {
                if (shared) {
                    shared = false;
                    childEls = childEls.slice(0);
                }
                Ext.Array.erase(childEls, index, 1);
            }
        }

        return childEls;
    },

    /**
     * Removes items in the childEls array based on the return value of a supplied test
     * function. The function is called with a entry in childEls and if the test function
     * return true, that entry is removed. If false, that entry is kept.
     *
     * @param {Function} testFn The test function.
     */
    removeChildEls: function (testFn) {
        var me = this,
            old = me.getChildEls(),
            keepers = (me.childEls = []),
            n, i, cel;

        for (i = 0, n = old.length; i < n; ++i) {
            cel = old[i];
            if (!testFn(cel)) {
                keepers.push(cel);
            }
        }
    }
});
