/**
 * @class Ext.layout.component.field.Field
 * @extends Ext.layout.component.Component
 * Layout class for components with {@link Ext.form.Labelable field labeling}, handling the sizing and alignment of
 * the form control, label, and error message treatment.
 * @private
 */
Ext.define('Ext.layout.component.field.Field', {

    /* Begin Definitions */

    alias: ['layout.field'],

    extend: 'Ext.layout.component.Component',

    uses: ['Ext.tip.QuickTip', 'Ext.util.TextMetrics'],

    /* End Definitions */

    type: 'field',

    beforeLayout: function(width, height) {
        var me = this;
        return me.callParent(arguments) || (!me.owner.preventMark && me.activeError !== me.owner.getActiveError());
    },

    onLayout: function(width, height) {
        var me = this,
            owner = me.owner,
            labelStrategy = me.getLabelStrategy(),
            errorStrategy = me.getErrorStrategy(),
            isDefined = Ext.isDefined,
            isNumber = Ext.isNumber,
            lastSize, autoWidth, autoHeight, info, undef;

        lastSize = me.lastComponentSize || {};
        if (!isDefined(width)) {
            width = lastSize.width;
            if (width < 0) { //first pass lastComponentSize.width is -Infinity
                width = undef;
            }
        }
        if (!isDefined(height)) {
            height = lastSize.height;
            if (height < 0) { //first pass lastComponentSize.height is -Infinity
                height = undef;
            }
        }
        autoWidth = !isNumber(width);
        autoHeight = !isNumber(height);

        info = {
            autoWidth: autoWidth,
            autoHeight: autoHeight,
            width: autoWidth ? owner.getBodyNaturalWidth() : width, //always give a pixel width
            height: height,
            setOuterWidth: false, //whether the outer el width should be set to the calculated width

            // insets for the bodyEl from each side of the component layout area
            insets: {
                top: 0,
                right: 0,
                bottom: 0,
                left: 0
            }
        };

        // NOTE the order of calculating insets and setting styles here is very important; we must first
        // calculate and set horizontal layout alone, as the horizontal sizing of elements can have an impact
        // on the vertical sizes due to wrapping, then calculate and set the vertical layout.

        // perform preparation on the label and error (setting css classes, qtips, etc.)
        labelStrategy.prepare(owner, info);
        errorStrategy.prepare(owner, info);

        // calculate the horizontal insets for the label and error
        labelStrategy.adjustHorizInsets(owner, info);
        errorStrategy.adjustHorizInsets(owner, info);

        // set horizontal styles for label and error based on the current insets
        labelStrategy.layoutHoriz(owner, info);
        errorStrategy.layoutHoriz(owner, info);

        // calculate the vertical insets for the label and error
        labelStrategy.adjustVertInsets(owner, info);
        errorStrategy.adjustVertInsets(owner, info);

        // set vertical styles for label and error based on the current insets
        labelStrategy.layoutVert(owner, info);
        errorStrategy.layoutVert(owner, info);

        // perform sizing of the elements based on the final dimensions and insets
        if (autoWidth && autoHeight) {
            // Don't use setTargetSize if auto-sized, so the calculated size is not reused next time
            me.setElementSize(owner.el, (info.setOuterWidth ? info.width : undef), info.height);
        } else {
            me.setTargetSize((!autoWidth || info.setOuterWidth ? info.width : undef), info.height);
        }
        me.sizeBody(info);

        me.activeError = owner.getActiveError();
    },


    /**
     * Perform sizing and alignment of the bodyEl (and children) to match the calculated insets.
     */
    sizeBody: function(info) {
        var me = this,
            owner = me.owner,
            insets = info.insets,
            totalWidth = info.width,
            totalHeight = info.height,
            width = Ext.isNumber(totalWidth) ? totalWidth - insets.left - insets.right : totalWidth,
            height = Ext.isNumber(totalHeight) ? totalHeight - insets.top - insets.bottom : totalHeight;

        // size the bodyEl
        me.setElementSize(owner.bodyEl, width, height);

        // size the bodyEl's inner contents if necessary
        me.sizeBodyContents(width, height);
    },

    /**
     * Size the contents of the field body, given the full dimensions of the bodyEl. Does nothing by
     * default, subclasses can override to handle their specific contents.
     * @param {Number} width The bodyEl width
     * @param {Number} height The bodyEl height
     */
    sizeBodyContents: Ext.emptyFn,


    /**
     * Return the set of strategy functions from the {@link #labelStrategies labelStrategies collection}
     * that is appropriate for the field's {@link Ext.form.field.Field#labelAlign labelAlign} config.
     */
    getLabelStrategy: function() {
        var me = this,
            strategies = me.labelStrategies,
            labelAlign = me.owner.labelAlign;
        return strategies[labelAlign] || strategies.base;
    },

    /**
     * Return the set of strategy functions from the {@link #errorStrategies errorStrategies collection}
     * that is appropriate for the field's {@link Ext.form.field.Field#msgTarget msgTarget} config.
     */
    getErrorStrategy: function() {
        var me = this,
            owner = me.owner,
            strategies = me.errorStrategies,
            msgTarget = owner.msgTarget;
        return !owner.preventMark && Ext.isString(msgTarget) ?
                (strategies[msgTarget] || strategies.elementId) :
                strategies.none;
    },



    /**
     * Collection of named strategies for laying out and adjusting labels to accommodate error messages.
     * An appropriate one will be chosen based on the owner field's {@link Ext.form.field.Field#labelAlign} config.
     */
    labelStrategies: (function() {
        var applyIf = Ext.applyIf,
            emptyFn = Ext.emptyFn,
            base = {
                prepare: function(owner, info) {
                    var cls = owner.labelCls + '-' + owner.labelAlign,
                        labelEl = owner.labelEl;
                    if (labelEl && !labelEl.hasCls(cls)) {
                        labelEl.addCls(cls);
                    }
                },
                adjustHorizInsets: emptyFn,
                adjustVertInsets: emptyFn,
                layoutHoriz: emptyFn,
                layoutVert: emptyFn
            },
            left = applyIf({
                prepare: function(owner, info) {
                    base.prepare(owner, info);
                    // If auto width, add the label width to the body's natural width.
                    if (info.autoWidth) {
                        info.width += (!owner.labelEl ? 0 : owner.labelWidth + owner.labelPad);
                    }
                    // Must set outer width to prevent field from wrapping below floated label
                    info.setOuterWidth = true;
                },
                adjustHorizInsets: function(owner, info) {
                    if (owner.labelEl) {
                        info.insets.left += owner.labelWidth + owner.labelPad;
                    }
                },
                layoutHoriz: function(owner, info) {
                    // For content-box browsers we can't rely on Labelable.js#getLabelableRenderData
                    // setting the width style because it needs to account for the final calculated
                    // padding/border styles for the label. So we set the width programmatically here to
                    // normalize content-box sizing, while letting border-box browsers use the original
                    // width style.
                    var labelEl = owner.labelEl;
                    if (labelEl && !owner.isLabelSized && !Ext.isBorderBox) {
                        labelEl.setWidth(owner.labelWidth);
                        owner.isLabelSized = true;
                    }
                }
            }, base);


        return {
            base: base,

            /**
             * Label displayed above the bodyEl
             */
            top: applyIf({
                adjustVertInsets: function(owner, info) {
                    var labelEl = owner.labelEl;
                    if (labelEl) {
                        info.insets.top += Ext.util.TextMetrics.measure(labelEl, owner.fieldLabel, info.width).height +
                                           labelEl.getFrameWidth('tb') + owner.labelPad;
                    }
                }
            }, base),

            /**
             * Label displayed to the left of the bodyEl
             */
            left: left,

            /**
             * Same as left, only difference is text-align in CSS
             */
            right: left
        };
    })(),



    /**
     * Collection of named strategies for laying out and adjusting insets to accommodate error messages.
     * An appropriate one will be chosen based on the owner field's {@link Ext.form.field.Field#msgTarget} config.
     */
    errorStrategies: (function() {
        function setDisplayed(el, displayed) {
            var wasDisplayed = el.getStyle('display') !== 'none';
            if (displayed !== wasDisplayed) {
                el.setDisplayed(displayed);
            }
        }

        function setStyle(el, name, value) {
            if (el.getStyle(name) !== value) {
                el.setStyle(name, value);
            }
        }

        var applyIf = Ext.applyIf,
            emptyFn = Ext.emptyFn,
            base = {
                prepare: function(owner) {
                    setDisplayed(owner.errorEl, false);
                },
                adjustHorizInsets: emptyFn,
                adjustVertInsets: emptyFn,
                layoutHoriz: emptyFn,
                layoutVert: emptyFn
            };

        return {
            none: base,

            /**
             * Error displayed as icon (with QuickTip on hover) to right of the bodyEl
             */
            side: applyIf({
                prepare: function(owner) {
                    var errorEl = owner.errorEl;
                    errorEl.addCls(Ext.baseCSSPrefix + 'form-invalid-icon');
                    Ext.layout.component.field.Field.initTip();
                    errorEl.dom.setAttribute('data-errorqtip', owner.getActiveError() || '');
                    setDisplayed(errorEl, owner.hasActiveError());
                },
                adjustHorizInsets: function(owner, info) {
                    if (owner.autoFitErrors && owner.hasActiveError()) {
                        info.insets.right += owner.errorEl.getWidth();
                    }
                },
                layoutHoriz: function(owner, info) {
                    if (owner.hasActiveError()) {
                        setStyle(owner.errorEl, 'left', info.width - info.insets.right + 'px');
                    }
                },
                layoutVert: function(owner, info) {
                    if (owner.hasActiveError()) {
                        setStyle(owner.errorEl, 'top', info.insets.top + 'px');
                    }
                }
            }, base),

            /**
             * Error message displayed underneath the bodyEl
             */
            under: applyIf({
                prepare: function(owner) {
                    var errorEl = owner.errorEl,
                        cls = Ext.baseCSSPrefix + 'form-invalid-under';
                    if (!errorEl.hasCls(cls)) {
                        errorEl.addCls(cls);
                    }
                    setDisplayed(errorEl, owner.hasActiveError());
                },
                adjustVertInsets: function(owner, info) {
                    if (owner.autoFitErrors) {
                        info.insets.bottom += owner.errorEl.getHeight();
                    }
                },
                layoutHoriz: function(owner, info) {
                    var errorEl = owner.errorEl,
                        insets = info.insets;

                    setStyle(errorEl, 'width', info.width - insets.right - insets.left + 'px');
                    setStyle(errorEl, 'marginLeft', insets.left + 'px');
                }
            }, base),

            /**
             * Error displayed as QuickTip on hover of the field container
             */
            qtip: applyIf({
                prepare: function(owner) {
                    setDisplayed(owner.errorEl, false);
                    Ext.layout.component.field.Field.initTip();
                    owner.getActionEl().dom.setAttribute('data-errorqtip', owner.getActiveError() || '');
                }
            }, base),

            /**
             * Error displayed as title tip on hover of the field container
             */
            title: applyIf({
                prepare: function(owner) {
                    setDisplayed(owner.errorEl, false);
                    owner.el.dom.title = owner.getActiveError() || '';
                }
            }, base),

            /**
             * Error message displayed as content of an element with a given id elsewhere in the app
             */
            elementId: applyIf({
                prepare: function(owner) {
                    setDisplayed(owner.errorEl, false);
                    var targetEl = Ext.fly(owner.msgTarget);
                    if (targetEl) {
                        targetEl.dom.innerHTML = owner.getActiveError() || '';
                        targetEl.setDisplayed(owner.hasActiveError());
                    }
                }
            }, base)
        };
    })(),

    statics: {
        /**
         * Use a custom QuickTip instance separate from the main QuickTips singleton, so that we
         * can give it a custom frame style. Responds to errorqtip rather than the qtip property.
         */
        initTip: function() {
            var tip = this.tip;
            if (!tip) {
                tip = this.tip = Ext.create('Ext.tip.QuickTip', {
                    baseCls: Ext.baseCSSPrefix + 'form-invalid-tip',
                    renderTo: Ext.getBody()
                });
                tip.tagConfig = Ext.apply({}, {attribute: 'errorqtip'}, tip.tagConfig);
            }
        },

        /**
         * Destroy the error tip instance.
         */
        destroyTip: function() {
            var tip = this.tip;
            if (tip) {
                tip.destroy();
                delete this.tip;
            }
        }
    }

});