/**
 * Box is a superclass for the two box layouts:
 *
 * * {@link Ext.layout.HBox hbox}
 * * {@link Ext.layout.VBox vbox}
 *
 * Box itself is never used directly, but its subclasses provide flexible arrangement of
 * child components inside a {@link Ext.Container Container}.
 *
 * ## Horizontal Box
 *
 * HBox allows you to easily lay out child components horizontally. It can size items based
 * on a fixed width or a fraction of the total width available, enabling you to achieve
 * flexible layouts that expand or contract to fill the space available.
 *
 * See the {@link Ext.layout.HBox HBox layout docs} for more information on using hboxes.
 *
 * ## Vertical Box
 *
 * VBox allows you to easily lay out child components vertically. It can size items based
 * on a fixed height or a fraction of the total height available, enabling you to achieve
 * flexible layouts that expand or contract to fill the space available.
 *
 * See the {@link Ext.layout.VBox VBox layout docs} for more information on using vboxes.
 */
Ext.define('Ext.layout.Box', {
    extend: 'Ext.layout.Auto',
    alias: 'layout.box',
 
    isBox: true,
 
    config: {
        orient: 'horizontal',
 
        /**
         * @cfg {String} align
         * Controls how the child items of the container are aligned. Acceptable
         * configuration values for this property are:
         *
         * - ** start ** : child items are packed together at left side of container
         * - ** center ** : child items are packed together at mid-width of container
         * - ** end ** : child items are packed together at right side of container
         * - **stretch** : child items are stretched vertically to fill the height of the
         *  container
         *
         * @accessor
         */
        align: 'stretch',
 
        /**
         * @cfg {Boolean} constrainAlign
         * Limits the size of {@link #align aligned} components to the size of the container.
         *
         * In order for this option to work in Safari, the container must have
         * {@link Ext.Container#autoSize autoSize} set to `false`.
         */
        constrainAlign: false,
 
        /**
         * @cfg {String} pack
         * Controls how the child items of the container are packed together. Acceptable
         * configuration values for this property are:
         *
         * - ** start ** : child items are packed together at left side of container
         * - ** center ** : child items are packed together at mid-width of container
         * - ** end ** : child items are packed together at right side of container
         * - ** space-between ** : child items are distributed evenly with the first
         * item at the start and the last item at the end
         * - ** space-around ** : child items are distributed evenly with equal space
         * around them
         * - ** justify ** : behaves the same as `space-between` for backward compatibility.
         *
         * @accessor
         */
        pack: 'start',
 
        /**
         * @cfg {Boolean} vertical
         * `true` to layout items vertically, otherwise horizontally.
         *
         * @since 6.2.0
         */
        vertical: false,
 
        /**
         * @cfg {Boolean} reverse
         * `true` to reverse the natural layout direction.
         * - When vertical, items are laid out bottom to top.
         * - When horizontal (assuming LTR), items are laid out right to left.
         *
         * @since 6.5.0
         */
        reverse: false,
 
        // @cmd-auto-dependency { defaultType: "Ext.layout.overflow.Scroller" }
        /**
         * @cfg {Object/String} overflow Configuration for this layout's overflow. Example:
         *
         *     Ext.create('Ext.Container', {
         *         layout: {
         *             type: 'hbox',
         *             overflow: 'scroller'
         *         }
         *     });
         *
         * @accessor
         * @since 6.5.1
         */
        overflow: null,
 
        /**
         * @cfg {true/false/'nowrap'/'wrap'/'wrap-reverse} [wrap=false]
         * `true` to wrap items onto multiple lines when the container overflows.
         * Can also be a string value for CSS `flex-wrap`.
         *
         * @since 6.5.1
         */
        wrap: null
    },
 
    cls: Ext.baseCSSPrefix + 'layout-box',
    baseItemCls: Ext.baseCSSPrefix + 'layout-box-item',
    constrainAlignCls: Ext.baseCSSPrefix + 'constrain-align',
    flexedCls: Ext.baseCSSPrefix + 'flexed',
 
    wrapClsMap: {
        true: Ext.baseCSSPrefix + 'wrap',
        'wrap': Ext.baseCSSPrefix + 'wrap',
        'wrap-reverse': Ext.baseCSSPrefix + 'wrap-reverse'
    },
 
    //<debug>
    boxRe: /^(?:box|hbox|vbox)$/,
    //</debug>
 
    orientMap: {
        horizontal: {
            sizeProp: 'width',
            containerCls: [
                Ext.baseCSSPrefix + 'layout-hbox',
                Ext.baseCSSPrefix + 'horizontal'
            ],
            itemCls: Ext.baseCSSPrefix + 'layout-hbox-item'
        },
        vertical: {
            sizeProp: 'height',
            containerCls: [
                Ext.baseCSSPrefix + 'layout-vbox',
                Ext.baseCSSPrefix + 'vertical'
            ],
            itemCls: Ext.baseCSSPrefix + 'layout-vbox-item'
        }
    },
 
    constructor: function(config) {
        var me = this;
 
        me.callParent([config]);
        me.positionSortFn = me.positionSortFn.bind(me);
    },
 
    setConfig: function(name, value, options) {
        var config = name,
            type;
 
        // We override setConfig to accept config objects of {type:'box'},  {type:'hbox'}
        // and {type:'vbox'} and adjust the "vertical" config properly.
 
        if (name) {
            if (typeof name === 'string') {
                config = {};
                config[name] = value;
            }
            else {
                Ext.apply({}, name);
                options = value;
            }
 
            type = config.type;
            delete config.type;
 
            //<debug>
            if (type && !this.boxRe.test(type)) {
                Ext.raise('Cannot change layout from ' + this.$className + ' to "' + type + '"');
            }
            //</debug>
 
            if (config.vertical == null) {
                if (type === 'vbox') {
                    config.vertical = true;
                }
                else if (type === 'hbox') {
                    config.vertical = false;
                }
            }
 
            this.callParent([ config, options ]);
        }
 
        return this;
    },
 
    destroy: function() {
        Ext.destroy(this.getOverflow());
        this.positionSortFn = null;
        this.callParent();
    },
 
    updateContainer: function(container, oldContainer) {
        var listener = {
            flexchange: 'onItemFlexChange',
            scope: this,
            delegate: '> component'
        };
 
        this.callParent([container, oldContainer]);
 
        if (container) {
            container.on(listener);
        }
 
        if (oldContainer) {
            oldContainer.un(listener);
        }
    },
 
    updateVertical: function(vertical) {
        this.setOrient(vertical ? 'vertical' : 'horizontal');
    },
 
    //<debug>
    applyOrient: function(orient) {
        if (orient !== 'horizontal' && orient !== 'vertical') {
            Ext.log.error("Invalid box orient of: '" + orient +
                "', must be either 'horizontal' or 'vertical'");
        }
 
        return orient;
    },
    //</debug>
 
    updateOrient: function(orient, oldOrient) {
        var me = this,
            container = me.getContainer(),
            overflow = me.getOverflow(),
            renderTarget = container.getRenderTarget(),
            innerItems = container.innerItems,
            len = innerItems.length,
            map = me.orientMap,
            newMap = map[orient],
            oldMap = map[oldOrient],
            vertical = orient === 'vertical',
            i, itemCls, item;
 
        me.sizePropertyName = newMap.sizeProp;
 
        if (oldOrient) {
            renderTarget.removeCls(oldMap.containerCls);
 
            for (= 0; i < len; ++i) {
                innerItems[i].removeCls(oldMap.itemCls);
            }
        }
 
        renderTarget.addCls(newMap.containerCls);
 
        me.itemCls = itemCls = [me.baseItemCls, newMap.itemCls];
 
        for (= 0; i < len; ++i) {
            item = innerItems[i];
            item.addCls(itemCls);
        }
 
        me.setVertical(vertical);
 
        me.positionFn = vertical ? 'getTop' : 'getLeft';
 
        if (overflow) {
            overflow.setVertical(vertical);
        }
    },
 
    updateConstrainAlign: function(constrainAlign) {
        this.getContainer().getRenderTarget().toggleCls(this.constrainAlignCls,
                                                        constrainAlign);
    },
 
    onItemInnerStateChange: function(item, isInner) {
        var me = this,
            flex;
 
        me.callParent(arguments);
 
        if (isInner) {
            flex = item.getFlex();
 
            if (flex) {
                me.setItemFlex(item, flex);
            }
        }
        else {
            me.setItemFlex(item, null);
        }
    },
 
    onItemFlexChange: function(item, flex) {
        if (item.isInnerItem()) {
            this.setItemFlex(item, flex);
        }
    },
 
    /**
     * Sets the flex of an item in this box layout.
     * @param {Ext.Component} item The item of this layout which you want to update the
     * flex of.
     * @param {Object} flex The flex to set on this method
     */
    setItemFlex: function(item, flex) {
        var el = item.el,
            type = typeof flex,
            isNumber = (type === 'number'),
            isString = (type === 'string'),
            parts, grow;
 
        if (!flex || isNumber || isString) {
            if (isNumber) {
                grow = flex;
                flex = flex + ' ' + flex;
            }
            else if (isString) {
                parts = Ext.String.splitWords(flex);
                grow = parts[0];
 
                if (parts.length === 1) {
                    flex = grow + ' ' + grow;
                }
            }
 
            el.setStyle('flex', flex);
        }
        else {
            grow = flex.grow;
 
            el.setStyle({
                flexGrow: grow,
                flexShrink: flex.shrink,
                flexBasis: flex.basis
            });
        }
 
        item.toggleCls(this.flexedCls, !!grow);
    },
 
    convertPosition: function(position) {
        var positionMap = this.positionMap;
 
        if (positionMap.hasOwnProperty(position)) {
            return positionMap[position];
        }
 
        return position;
    },
 
    applyAlign: function(align) {
        return this.convertPosition(align);
    },
 
    updateAlign: function(align, oldAlign) {
        this.getContainer().getRenderTarget().swapCls(align, oldAlign, true,
                                                      Ext.baseCSSPrefix + 'align');
    },
 
    applyPack: function(pack) {
        return this.convertPosition(pack);
    },
 
    updatePack: function(pack, oldPack) {
        this.getContainer().getRenderTarget().swapCls(pack, oldPack, true,
                                                      Ext.baseCSSPrefix + 'pack');
    },
 
    updateReverse: function(reverse) {
        this.getContainer().getRenderTarget().toggleCls(Ext.baseCSSPrefix + 'reverse',
                                                        reverse);
    },
 
    /**
     * Scrolls the specified item into view.
     *
     * @param {Ext.Widget} [item] The item to be scrolled into view
     *
     * @param {Object} [options] An object containing options to modify the operation.
     *
     * @param {Ext.Widget} [options.item] The item to be scrolled into view
     * @param {Boolean} [options.animation] Pass `true` to animate the row into view.
     * @param {Number} [options.offset] Offset to scroll to from the last fully visible
     * items on either side.
     * Positive numbers will scroll to the bottom or right side.
     * Negative numbers will scroll to the top or left side.
     * @param {'min'/'max'} [options.scroll='min'] A value of 'min' will scroll the item
     * to the nearest edge that will make it visible.
     * A value of 'max' will scroll the item to the furthest edge that will make it visible
     */
    ensureVisible: function(item, options) {
        var me = this,
            container, scrollable, scrollerTarget, vertical, targetInfo,
            itemInfo, oversized, scroll, delta, deltaX, deltaY, translatable;
 
        if (!item.isWidget) {
            options = item;
            item = options.item;
        }
 
        if (options && !isNaN(options.offset)) {
            item = this.getItemByOffset(options.offset);
        }
 
        // return if item doesn't exist
        if (!item) {
            return;
        }
 
        container = this.getContainer();
        scrollable = container.getScrollable();
        scrollerTarget = scrollable.getElement();
        vertical = me.getVertical();
        targetInfo = me.getItemInfo(scrollerTarget);
        itemInfo = me.getItemInfo(item);
        oversized = itemInfo.size > targetInfo.size;
        scroll = (options && options.scroll) || 'min';
 
        translatable = scrollable.translatable;
 
        // prevent js error if scrollable is empty
        if (me._currentEnsureVisibleItem === item && (translatable && translatable.isAnimating)) {
            return;
        }
 
        if (scroll === 'min') {
            if ((!oversized && (itemInfo.start < targetInfo.start)) ||
                    (oversized && (itemInfo.start > targetInfo.start))) {
                delta = itemInfo.start - targetInfo.start;
            }
            else if ((!oversized && (itemInfo.end > targetInfo.end)) ||
                    (oversized && itemInfo.end < targetInfo.end)) {
                delta = itemInfo.end - targetInfo.end;
            }
            else if (oversized && itemInfo.start < targetInfo.start &&
                    itemInfo.end > targetInfo.end) {
                delta = itemInfo.start - targetInfo.start;
            }
        }
        else {
            // Move to Previous page
            if (itemInfo.start < targetInfo.start) {
                delta = itemInfo.end - targetInfo.end;
            }
            else { // Move to Next page
                delta = itemInfo.start - targetInfo.start;
            }
        }
 
        if (delta) {
            deltaX = !vertical ? delta : null;
            deltaY = vertical ? delta : null;
 
            me._currentEnsureVisibleItem = item;
            scrollable.scrollBy(deltaX, deltaY, options.animation);
        }
    },
 
    /**
     * Determine which item is forward/backward inside the layout by offset
     * @param indexOffset offset to be used to determine which item to scroll to
     * an offset of -1 will find the first item off the top/left side
     * where an offset of 2 will find the second item off the bottom/right side
     *
     * @private
     */
    getItemByOffset: function(indexOffset) {
        var me = this,
            container = this.getContainer(),
            scrollerTarget = container.getScrollable().getElement(),
            targetInfo = me.getItemInfo(scrollerTarget),
            items = container.getInnerItems(),
            len = items.length,
            minFrontDistance = -Infinity,
            minEndDistance = -Infinity,
            startIndex = 0,
            endIndex = len - 1,
            index, i, itemFrontDistance, itemEndDistance,
            item, itemInfo;
 
        if (!indexOffset) {
            return;
        }
 
        items.sort(me.positionSortFn);
 
        for (= 0; i < len; i++) {
            item = items[i];
            itemInfo = me.getItemInfo(item);
            itemFrontDistance = itemInfo.start - targetInfo.start;
            itemEndDistance = targetInfo.end - itemInfo.end;
 
            if ((itemFrontDistance > minFrontDistance) && (itemFrontDistance < 0) &&
                    itemEndDistance > 0) {
                minFrontDistance = itemFrontDistance;
                startIndex = i;
            }
 
            if ((itemEndDistance > minEndDistance) && (itemEndDistance < 0) &&
                    itemFrontDistance > 0) {
                minEndDistance = itemEndDistance;
                endIndex = i;
                break;
            }
        }
 
        if (indexOffset > 0) {
            indexOffset--;
            index = endIndex += indexOffset;
 
            if (endIndex >= len) {
                index = len - 1;
            }
        }
        else {
            indexOffset++;
            index = startIndex += indexOffset;
 
            if (startIndex < 0) {
                index = 0;
            }
        }
 
        return items[index];
    },
 
    getItemInfo: function(item) {
        var me = this,
            vertical = me.getVertical(),
            el = item.el;
 
        return {
            start: el[vertical ? 'getTop' : 'getLeft'](),
            end: el[vertical ? 'getBottom' : 'getRight'](),
            size: el[vertical ? 'getHeight' : 'getWidth']()
        };
    },
 
    createOverflow: function(config) {
        return Ext.apply({
            owner: this,
            vertical: this.getVertical()
        }, config);
    },
 
    applyOverflow: function(config, existing) {
        return Ext.Factory.layoutOverflow.update(existing, config, this, 'createOverflow');
    },
 
    updateWrap: function(wrap, oldWrap) {
        var me = this,
            el = me.getContainer().getRenderTarget(),
            map = me.wrapClsMap,
            cls;
 
        if (oldWrap) {
            cls = map[oldWrap];
 
            if (cls) {
                el.removeCls(cls);
            }
        }
 
        if (wrap) {
            cls = map[wrap];
 
            if (cls) {
                el.addCls(cls);
            }
        }
    },
 
    privates: {
        positionSortFn: function(a, b) {
            var fn = this.positionFn;
 
            a = a.el[fn]();
            b = b.el[fn]();
 
            if (< b) {
                return -1;
            }
            else if (< a) {
                return 1;
            }
 
            return 0;
        }
    }
});