/**
 * This class is intended to be extended or created via the
 * {@link Ext.container.Container#layout layout} configuration property.
 * See {@link Ext.container.Container#layout} for additional details.
 */
Ext.define('Ext.layout.container.Container', {
    extend: 'Ext.layout.Layout',
    alternateClassName: 'Ext.layout.ContainerLayout',
    alias: 'layout.container',

    mixins: [
        'Ext.util.ElementContainer'
    ],

    requires: [
        'Ext.XTemplate'
    ],

    type: 'container',

    /**
     * @cfg {String} itemCls
     * An optional extra CSS class that will be added to the container. This can be useful for
     * adding customized styles to the container or any of its children using standard CSS
     * rules. See {@link Ext.Component}.{@link Ext.Component#componentCls componentCls} also.
     */

    /**
     * @private
     * Called by an owning Panel before the Panel begins its collapse process.
     * Most layouts will not need to override the default Ext.emptyFn implementation.
     */
    beginCollapse: Ext.emptyFn,

    /**
     * @private
     * Called by an owning Panel before the Panel begins its expand process.
     * Most layouts will not need to override the default Ext.emptyFn implementation.
     */
    beginExpand: Ext.emptyFn,

    /**
     * An object which contains boolean properties specifying which properties are to be
     * animated upon flush of child Component ContextItems. For example, Accordion would
     * have:
     *
     *      {
     *          y: true,
     *          height: true
     *      }
     *
     * @private
     */
    animatePolicy: null,

    /**
     * @private
     * tracks the number of child items that do not use "liquid" CSS layout
     */
    activeItemCount: 0,

    renderTpl: [
        '{%this.renderBody(out,values)%}'
    ],

    usesContainerHeight: true,
    usesContainerWidth: true,
    usesHeight: true,
    usesWidth: true,

    constructor: function() {
        this.callParent(arguments);
        this.mixins.elementCt.constructor.call(this);
    },

    destroy: function() {
        this.mixins.elementCt.destroy.call(this);
        this.callParent();
    },

    /**
     * In addition to work done by our base classes, containers benefit from some extra
     * cached data. The following properties are added to the ownerContext:
     *
     *  - visibleItems: the result of {@link #getVisibleItems}
     *  - childItems: the ContextItem[] for each visible item
     *  - targetContext: the ContextItem for the {@link #getTarget} element
     */
    beginLayout: function(ownerContext) {
        this.callParent(arguments);

        ownerContext.targetContext = ownerContext.paddingContext =
            ownerContext.getEl('getTarget', this);

        this.cacheChildItems(ownerContext);
    },

    beginLayoutCycle: function(ownerContext, firstCycle) {
        var me = this;

        me.callParent(arguments);

        if (firstCycle) {
            if (me.usesContainerHeight) {
                ++ownerContext.consumersContainerHeight;
            }

            if (me.usesContainerWidth) {
                ++ownerContext.consumersContainerWidth;
            }
        }
    },

    cacheChildItems: function(ownerContext) {
        var me = this,
            context, childItems, items, length, i;

        // if we neither read nor set the size of our items, we can skip creation of
        // the childItems array
        if (me.needsItemSize || me.setsItemSize) {
            context = ownerContext.context;
            childItems = ownerContext.childItems = [];
            items = ownerContext.visibleItems = me.getVisibleItems();
            length = items.length;

            for (i = 0; i < length; ++i) {
                childItems.push(context.getCmp(items[i]));
            }
        }
    },

    cacheElements: function() {
        var owner = this.owner;

        this.attachChildEls(owner.el, owner); // from ElementContainer mixin
    },

    calculate: function(ownerContext) {
        var props = ownerContext.props,
            el = ownerContext.el;

        if (ownerContext.widthModel.shrinkWrap && isNaN(props.width)) {
            ownerContext.setContentWidth(el.getWidth());
        }

        if (ownerContext.heightModel.shrinkWrap && isNaN(props.height)) {
            ownerContext.setContentHeight(el.getHeight());
        }
    },

    /**
     * Adds layout's itemCls and owning Container's itemCls
     * @protected
     */
    configureItem: function(item) {
        var me = this,
            itemCls = me.itemCls,
            ownerItemCls = me.owner.itemCls,
            needsCopy,
            addClasses;

        // Effectively callParent but without the function overhead
        item.ownerLayout = me;

        if (itemCls) {
            // itemCls can be a single class or an array
            if (typeof itemCls === 'string') {
                addClasses = [itemCls];
            }
            else {
                addClasses = itemCls;
                needsCopy = !!addClasses;
            }
        }

        if (ownerItemCls) {
            // Add some extra logic so we don't clone the array unnecessarily
            if (needsCopy) {
                addClasses = Ext.Array.clone(addClasses);
            }

            addClasses = Ext.Array.push(addClasses || [], ownerItemCls);
        }

        if (addClasses) {
            item.addCls(addClasses);
        }
    },

    doRenderBody: function(out, renderData) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!

        this.renderItems(out, renderData);
        this.renderContent(out, renderData);
    },

    doRenderContainer: function(out, renderData) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!

        var me = renderData.$comp.layout,
            tpl = me.getRenderTpl(),
            data = me.getRenderData();

        tpl.applyOut(data, out);
    },

    doRenderItems: function(out, renderData) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!

        var me = renderData.$layout,
            tree = me.getRenderTree();

        if (tree) {
            Ext.DomHelper.generateMarkup(tree, out);
        }
    },

    doRenderTabGuard: function(out, renderData, position) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!

        var cmp = renderData.$comp,
            tabGuardTpl;

        // Due to framing, we will be called in two different ways: in the frameTpl or in
        // the renderTpl. The frameTpl version enters via doRenderFramingTabGuard which
        // sets "$skipTabGuards" on the renderTpl's renderData.
        //
        if (cmp.tabGuard && !renderData.$skipTabGuards) {
            tabGuardTpl = cmp.lookupTpl('tabGuardTpl');

            if (tabGuardTpl) {
                renderData.tabGuard = position;
                renderData.tabGuardEl = cmp.tabGuardElements[position];

                cmp.addChildEl(renderData.tabGuardEl);
                tabGuardTpl.applyOut(renderData, out);

                delete renderData.tabGuard;
                delete renderData.tabGuardEl;
            }
        }
    },

    finishRender: function() {
        var me = this,
            target, items;

        me.callParent();

        me.cacheElements();

        target = me.getRenderTarget();
        items = me.getLayoutItems();

        me.finishRenderItems(target, items);
    },

    /**
     * @private
     * Called for every layout in the layout context after all the layouts have been finally flushed
     */
    notifyOwner: function() {
        //<debug>
        if (!this._hasTargetWarning && this.targetCls && !this.getTarget().hasCls(this.targetCls)) {
            this._hasTargetWarning = true;
            Ext.log.warn('targetCls is missing. This may mean that getTargetEl() ' +
                         'is being overridden but not applyTargetCls(). ' + this.owner.id);
        }
        //</debug>

        this.owner.afterLayout(this);
    },

    /**
     * Returns the container size (that of the target). Only the fixed-sized dimensions can
     * be returned because the shrinkWrap dimensions are based on the contentWidth/Height
     * as determined by the container layout.
     *
     * @param {Ext.layout.ContextItem} ownerContext The owner's context item.
     * @param {Boolean} [inDom=false] True if the container size must be in the DOM.
     * @return {Object} The size
     * @return {Number} return.width The width
     * @return {Number} return.height The height
     * @protected
     */
    getContainerSize: function(ownerContext, inDom) {
        // Subtle But Important:
        //
        // We don't want to call getProp/hasProp et.al. unless we in fact need that value
        // for our results! If we call it and don't need it, the layout manager will think
        // we depend on it and will schedule us again should it change.

        var targetContext = ownerContext.targetContext,
            frameInfo = targetContext.getFrameInfo(),
            padding = ownerContext.paddingContext.getPaddingInfo(),
            got = 0,
            needed = 0,
            gotWidth, gotHeight, width, height;

        // In an shrinkWrap width/height case, we must not ask for any of these dimensions
        // because they will be determined by contentWidth/Height which is calculated by
        // this layout...

        // Fit/Card layouts are able to set just the width of children, allowing child's
        // resulting height to autosize the Container.
        // See examples/tabs/tabs.html for an example of this.

        if (!ownerContext.widthModel.shrinkWrap) {
            ++needed;
            width = inDom ? targetContext.getDomProp('width') : targetContext.getProp('width');
            gotWidth = (typeof width === 'number');

            if (gotWidth) {
                ++got;
                width -= frameInfo.width + padding.width;

                if (width < 0) {
                    width = 0;
                }
            }
        }

        if (!ownerContext.heightModel.shrinkWrap) {
            ++needed;
            height = inDom ? targetContext.getDomProp('height') : targetContext.getProp('height');
            gotHeight = (typeof height === 'number');

            if (gotHeight) {
                ++got;
                height -= frameInfo.height + padding.height;

                if (height < 0) {
                    height = 0;
                }
            }
        }

        return {
            width: width,
            height: height,
            needed: needed,
            got: got,
            gotAll: got === needed,
            gotWidth: gotWidth,
            gotHeight: gotHeight
        };
    },

    // This method is used to offset the DOM position when checking
    // whether the element is a certain child of the target. This is
    // required in cases where the extra elements prepended to the target
    // before any of the items. An example of this is when using labelAlign: 'top'
    // on a field. The label appears first in the DOM before any child items are
    // created, so when we check the position we need to add an extra offset.
    // Containers that create an innerCt are exempt because this new element
    // preserves the order
    getPositionOffset: function(position) {
        var offset;

        if (!this.createsInnerCt) {
            offset = this.owner.itemNodeOffset;

            if (offset) {
                position += offset;
            }
        }

        return position;
    },

    /**
     * Returns an array of child components either for a render phase (Performed in the beforeLayout
     * method of the layout's base class), or the layout phase (onLayout).
     * @return {Ext.Component[]} of child components
     */
    getLayoutItems: function() {
        var owner = this.owner,
            items = owner && owner.items;

        return (items && items.items) || [];
    },

    getRenderData: function() {
        var comp = this.owner;

        return {
            $comp: comp,
            $layout: this,
            ownerId: comp.id
        };
    },

    /**
     * @protected
     * Returns all items that are rendered
     * @return {Array} All matching items
     */
    getRenderedItems: function() {
        var me = this,
            target = me.getRenderTarget(),
            items = me.getLayoutItems(),
            ln = items.length,
            renderedItems = [],
            i, pos, item;

        for (i = 0, pos = 0; i < ln; i++, pos++) {
            item = items[i];

            if (item.rendered) {
                if (item.ignoreDomPosition) {
                    --pos;
                }
                else if (!this.isValidParent(item, target, pos)) {
                    continue;
                }

                renderedItems.push(item);
            }
        }

        return renderedItems;
    },

    /**
     * Returns the element into which rendering must take place. Defaults to the owner Container's
     * target element.
     *
     * May be overridden in layout managers which implement an inner element.
     *
     * @return {Ext.dom.Element}
     */
    getRenderTarget: function() {
        return this.owner.getTargetEl();
    },

    /**
     * Returns the element into which extra functional DOM elements can be inserted.
     * Defaults to the owner Component's encapsulating element.
     *
     * May be overridden in Component layout managers which implement a
     * {@link #getRenderTarget component render target} which must only contain child components.
     * @return {Ext.dom.Element}
     */
    getElementTarget: function() {
        return this.getRenderTarget();
    },

    getRenderTpl: function() {
        var me = this,
            renderTpl = Ext.XTemplate.getTpl(this, 'renderTpl');

        // Make sure all standard callout methods for the owner component are placed on the
        // XTemplate instance (but only once please):
        if (!renderTpl.renderContent) {
            me.owner.setupRenderTpl(renderTpl);
        }

        return renderTpl;
    },

    getRenderTree: function() {
        var result,
            items = this.owner.items,
            itemsGen,
            renderCfgs = {};

        do {
            itemsGen = items.generation;
            result = this.getItemsRenderTree(this.getLayoutItems(), renderCfgs);
        } while (items.generation !== itemsGen);

        return result;
    },

    renderChildren: function() {
        var me = this,
            ownerItems = me.owner.items,
            target = me.getRenderTarget(),
            itemsGen, items;

        // During the render phase, new items may be added. Specifically, a panel will
        // create a placeholder component during render if required, so we need to catch
        // it here so we can render it.
        do {
            itemsGen = ownerItems.generation;
            items = me.getLayoutItems();
            me.renderItems(items, target);
        } while (ownerItems.generation !== itemsGen);
    },

    getScrollbarsNeeded: function(width, height, contentWidth, contentHeight) {
        var scrollbarSize = Ext.scrollbar.size(),
            hasWidth = typeof width === 'number',
            hasHeight = typeof height === 'number',
            needHorz = 0,
            needVert = 0;

        // No space-consuming scrollbars.
        if (!scrollbarSize.width) {
            return 0;
        }

        if (hasHeight && height < contentHeight) {
            needVert = 2;
            width -= scrollbarSize.width;
        }

        if (hasWidth && width < contentWidth) {
            needHorz = 1;

            if (!needVert && hasHeight) {
                height -= scrollbarSize.height;

                if (height < contentHeight) {
                    needVert = 2;
                }
            }
        }

        return needVert + needHorz;
    },

    /**
     * Returns the owner component's resize element.
     * @return {Ext.dom.Element}
     */
    getTarget: function() {
        return this.owner.getTargetEl();
    },

    /**
     * @protected
     * Returns all items that are both rendered and visible
     * @return {Array} All matching items
     */
    getVisibleItems: function() {
        var target = this.getRenderTarget(),
            items = this.getLayoutItems(),
            ln = items.length,
            visibleItems = [],
            i, pos, item;

        for (i = 0, pos = 0; i < ln; i++, pos++) {
            item = items[i];

            if (item.rendered && item.hidden !== true && !item.floated) {
                if (item.ignoreDomPosition) {
                    --pos;
                }
                else if (!this.isValidParent(item, target, pos)) {
                    continue;
                }

                visibleItems.push(item);
            }
        }

        return visibleItems;
    },

    getMoveAfterIndex: function(after) {
        return this.owner.items.indexOf(after) + 1;
    },

    moveItemBefore: function(item, before) {
        var owner = this.owner,
            items = owner.items,
            index = items.indexOf(item),
            toIndex;

        if (item === before) {
            return item;
        }

        if (before) {
            toIndex = items.indexOf(before);

            if (index > -1 && index < toIndex) {
                --toIndex;
            }
        }
        else {
            toIndex = items.length;
        }

        return owner.insert(toIndex, item);
    },

    setupRenderTpl: function(renderTpl) {
        renderTpl.renderBody = this.doRenderBody;
        renderTpl.renderContainer = this.doRenderContainer;
        renderTpl.renderItems = this.doRenderItems;
        renderTpl.renderTabGuard = this.doRenderTabGuard;
    },

    getContentTarget: function() {
        return this.owner.getDefaultContentTarget();
    },

    onAdd: function(item) {
        if (!item.liquidLayout) {
            ++this.activeItemCount;
        }

        this.callParent([item]);
    },

    onRemove: function(item, isDestroying) {
        if (!item.liquidLayout) {
            --this.activeItemCount;
        }

        this.callParent([item, isDestroying]);
    }
});