/**
 * Single checkbox field. Can be used as a direct replacement for traditional checkbox fields.
 * Also serves as a parent class for {@link Ext.form.field.Radio radio buttons}.
 *
 * ## Labeling
 *
 * In addition to the {@link Ext.form.Labelable standard field labeling options}, checkboxes
 * may be given an optional {@link #boxLabel} which will be displayed immediately after checkbox.
 * Also see {@link Ext.form.CheckboxGroup} for a convenient method of grouping related checkboxes.
 *
 * # Values
 *
 * The main value of a checkbox is a boolean, indicating whether or not the checkbox is checked.
 * The following values will check the checkbox:
 *
 * - `true`
 * - `'true'`
 * - `'1'`
 * - `'on'`
 *
 * Any other value will un-check the checkbox.
 *
 * In addition to the main boolean value, you may also specify a separate {@link #inputValue}.
 * This will be sent as the parameter value when the form is
 * {@link Ext.form.Basic#submit submitted}. You will want to set this value if you have multiple
 * checkboxes with the same {@link #name}. If not specified, the value `on` will be used.
 *
 * ## Example usage
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         bodyPadding: 10,
 *         width: 300,
 *         title: 'Pizza Order',
 *         items: [
 *             {
 *                 xtype: 'fieldcontainer',
 *                 fieldLabel: 'Toppings',
 *                 defaultType: 'checkboxfield',
 *                 items: [
 *                     {
 *                         boxLabel  : 'Anchovies',
 *                         name      : 'topping',
 *                         inputValue: '1',
 *                         id        : 'checkbox1'
 *                     }, {
 *                         boxLabel  : 'Artichoke Hearts',
 *                         name      : 'topping',
 *                         inputValue: '2',
 *                         checked   : true,
 *                         id        : 'checkbox2'
 *                     }, {
 *                         boxLabel  : 'Bacon',
 *                         name      : 'topping',
 *                         inputValue: '3',
 *                         id        : 'checkbox3'
 *                     }
 *                 ]
 *             }
 *         ],
 *         bbar: [
 *             {
 *                 text: 'Select Bacon',
 *                 handler: function() {
 *                     Ext.getCmp('checkbox3').setValue(true);
 *                 }
 *             },
 *             '-',
 *             {
 *                 text: 'Select All',
 *                 handler: function() {
 *                     Ext.getCmp('checkbox1').setValue(true);
 *                     Ext.getCmp('checkbox2').setValue(true);
 *                     Ext.getCmp('checkbox3').setValue(true);
 *                 }
 *             },
 *             {
 *                 text: 'Deselect All',
 *                 handler: function() {
 *                     Ext.getCmp('checkbox1').setValue(false);
 *                     Ext.getCmp('checkbox2').setValue(false);
 *                     Ext.getCmp('checkbox3').setValue(false);
 *                 }
 *             }
 *         ],
 *         renderTo: Ext.getBody()
 *     });
 */
Ext.define('Ext.form.field.Checkbox', {
    extend: 'Ext.form.field.Base',
    alias: ['widget.checkboxfield', 'widget.checkbox'],
    alternateClassName: 'Ext.form.Checkbox',
    requires: ['Ext.XTemplate', 'Ext.form.CheckboxManager' ],
 
    /**
     * @cfg {Boolean/String/Number} modelValue
     * The value to use for {@link #getModelData} when checked.
     *
     * @since 6.2.1
     */
    modelValue: true,
 
    /**
     * @cfg {Boolean/String/Number} modelValueUnchecked
     * The value to use for {@link #getModelData} when unchecked.
     *
     * @since 6.2.1
     */
    modelValueUnchecked: false,
 
    // inputEl should always retain the same size, never stretch
    stretchInputElFixed: false,
 
    /**
     * @property {Ext.dom.Element} boxLabelEl
     * A reference to the label element created for the {@link #boxLabel}. Only present
     * if the component has been rendered and has a boxLabel configured.
     */
 
    /**
     * @cfg childEls
     * @inheritdoc
     */
    childEls: [
        'boxLabelEl',
        'innerWrapEl',
        'displayEl'
    ],
 
    /* eslint-disable indent, max-len */
    // note: {id} here is really {inputId}, but {cmpId} is available
    /**
     * @cfg fieldSubTpl
     * @inheritdoc
     */
    fieldSubTpl: [
        '<div id="{cmpId}-innerWrapEl" data-ref="innerWrapEl" role="presentation"',
            ' class="{wrapInnerCls}">',
            '<tpl if="labelAlignedBefore">',
                '{beforeBoxLabelTpl}',
                '<label id="{cmpId}-boxLabelEl" data-ref="boxLabelEl" {boxLabelAttrTpl} class="{boxLabelCls} ',
                        '{boxLabelCls}-{ui} {boxLabelCls}-{boxLabelAlign} {noBoxLabelCls} {childElCls}" for="{id}">',
                    '{beforeBoxLabelTextTpl}',
                    '{boxLabel}',
                    '{afterBoxLabelTextTpl}',
                '</label>',
                '{afterBoxLabelTpl}',
            '</tpl>',
            '<span id="{cmpId}-displayEl" data-ref="displayEl" role="presentation" class="{fieldCls} {typeCls} ',
                '{typeCls}-{ui} {inputCls} {inputCls}-{ui} {fixCls} {childElCls} {afterLabelCls}">',
                '<input type="{inputType}" id="{id}" name="{inputName}" data-ref="inputEl" {inputAttrTpl}',
                    '<tpl if="tabIdx != null"> tabindex="{tabIdx}"</tpl>',
                    '<tpl if="disabled"> disabled="disabled"</tpl>',
                    '<tpl if="checked"> checked="checked"</tpl>',
                    '<tpl if="fieldStyle"> style="{fieldStyle}"</tpl>',
                    ' class="{checkboxCls}" autocomplete="off" hidefocus="true" ',
                    '<tpl foreach="ariaElAttributes"> {$}="{.}"</tpl>',
                    '<tpl foreach="inputElAriaAttributes"> {$}="{.}"</tpl>',
                    '/>',
            '</span>',
            '<tpl if="!labelAlignedBefore">',
                '{beforeBoxLabelTpl}',
                '<label id="{cmpId}-boxLabelEl" data-ref="boxLabelEl" {boxLabelAttrTpl} class="{boxLabelCls} ',
                        '{boxLabelCls}-{ui} {boxLabelCls}-{boxLabelAlign} {noBoxLabelCls} {childElCls}" for="{id}">',
                    '{beforeBoxLabelTextTpl}',
                    '{boxLabel}',
                    '{afterBoxLabelTextTpl}',
                '</label>',
                '{afterBoxLabelTpl}',
            '</tpl>',
        '</div>',
        {
            disableFormats: true,
            compiled: true
        }
    ],
    /* eslint-enable indent, max-len */
 
    /**
     * @cfg publishes
     * @inheritdoc
     */
    publishes: {
        checked: 1
    },
 
    subTplInsertions: [
        /**
         * @cfg {String/Array/Ext.XTemplate} beforeBoxLabelTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * before the box label element. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'beforeBoxLabelTpl',
 
        /**
         * @cfg {String/Array/Ext.XTemplate} afterBoxLabelTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * after the box label element. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'afterBoxLabelTpl',
 
        /**
         * @cfg {String/Array/Ext.XTemplate} beforeBoxLabelTextTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * before the box label text. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'beforeBoxLabelTextTpl',
 
        /**
         * @cfg {String/Array/Ext.XTemplate} afterBoxLabelTextTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * after the box label text. If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'afterBoxLabelTextTpl',
 
        /**
         * @cfg {String/Array/Ext.XTemplate} boxLabelAttrTpl
         * An optional string or `XTemplate` configuration to insert in the field markup
         * inside the box label element (as attributes). If an `XTemplate` is used, the component's
         * {@link Ext.form.field.Base#getSubTplData subTpl data} serves as the context.
         */
        'boxLabelAttrTpl',
 
        'inputAttrTpl'
    ],
 
    /**
     * @property {Boolean} isCheckbox
     * `true` in this class to identify an object as an instantiated Checkbox, or subclass thereof.
     */
    isCheckbox: true,
 
    /**
     * @cfg {String} focusCls
     * The CSS class to use when the checkbox receives focus
     */
    focusCls: 'form-checkbox-focus',
 
    /**
     * @cfg {String} [fieldCls='x-form-field']
     * The default CSS class for the checkbox
     */
 
    /**
     * @private
     */
    fieldBodyCls: Ext.baseCSSPrefix + 'form-cb-wrap',
 
    /**
     * @cfg {Boolean} checked
     * true if the checkbox should render initially checked
     */
    checked: false,
 
    /**
     * @cfg {String} checkedCls
     * The CSS class(es) added to the component's main element when it is in the checked state.
     * You can add your own class (checkedCls='myClass x-form-cb-checked') or replace the default 
     * class altogether (checkedCls='myClass').
     */
    checkedCls: Ext.baseCSSPrefix + 'form-cb-checked',
 
    /**
     * @cfg {String} boxLabel
     * An optional text label that will appear next to the checkbox. Whether it appears before
     * or after the checkbox is determined by the {@link #boxLabelAlign} config.
     */
 
    /**
     * @cfg {String} boxLabelCls
     * The CSS class to be applied to the {@link #boxLabel} element
     */
    boxLabelCls: Ext.baseCSSPrefix + 'form-cb-label',
 
    /**
     * @cfg {String} boxLabelAlign
     * The position relative to the checkbox where the {@link #boxLabel} should appear.
     * Recognized values are 'before' and 'after'.
     */
    boxLabelAlign: 'after',
 
    afterLabelCls: Ext.baseCSSPrefix + 'form-cb-after',
 
    wrapInnerCls: Ext.baseCSSPrefix + 'form-cb-wrap-inner',
 
    noBoxLabelCls: Ext.baseCSSPrefix + 'form-cb-no-box-label',
 
    /**
     * @cfg {String/Boolean} inputValue
     * The value that should go into the generated input element's value attribute and
     * should be used as the parameter value when submitting as part of a form.
     */
    inputValue: 'on',
 
    /**
     * @cfg {String} uncheckedValue
     * If configured, this will be submitted as the checkbox's value during form submit
     * if the checkbox is unchecked. By default this is undefined, which results in
     * nothing being submitted for the checkbox field when the form is submitted
     * (the default behavior of HTML checkboxes).
     */
 
    /**
     * @cfg {Function/String} [handler=undefined]
     * A function called when the {@link #checked} value changes (can be used instead of handling
     * the {@link #change change event}).
     * @cfg {Ext.form.field.Checkbox} handler.checkbox The Checkbox being toggled.
     * @cfg {Boolean} handler.checked The new checked state of the checkbox.
     * @controllable
     */
 
    /**
     * @cfg {Object} scope
     * An object to use as the scope ('this' reference) of the {@link #handler} function.
     *
     * Defaults to this Checkbox.
     */
 
    /**
     * @private
     */
    checkChangeEvents: [],
 
    // See IE8 override
    changeEventName: 'change',
 
    /**
     * @cfg inputType
     * @inheritdoc
     */
    inputType: 'checkbox',
 
    /**
     * @cfg isTextInput
     * @inheritdoc
     */
    isTextInput: false,
 
    /**
     * @property ariaRole
     * @inheritdoc
     */
    ariaRole: 'native',
 
    /**
     * @private
     */
    onRe: /^on$/i,
 
    // the form-cb css class is for styling shared between checkbox and subclasses (radio)
    inputCls: Ext.baseCSSPrefix + 'form-cb',
    _checkboxCls: Ext.baseCSSPrefix + 'form-cb-input',
 
    initComponent: function() {
        var me = this,
            value = me.value;
 
        if (value !== undefined) {
            me.checked = me.isChecked(value, me.inputValue);
        }
 
        me.callParent();
 
        me.getManager().add(me);
    },
 
    // Checkboxes and Radio buttons may have their names managed by their respective group.
    // This happens in CheckboxGroup.onAdd() so we skip default name assignment here.
    initDefaultName: Ext.emptyFn,
 
    initValue: function() {
        var me = this,
            checked = !!me.checked;
 
        /**
         * @property {Object} originalValue
         * The original value of the field as configured in the {@link #checked} configuration,
         * or as loaded by the last form load operation if the form's
         * {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} setting is `true`.
         */
        me.originalValue = me.initialValue = me.lastValue = checked;
 
        // Set the initial checked state
        me.setValue(checked);
    },
 
    getElConfig: function() {
        var me = this;
 
        // Add the checked class if this begins checked
        if (me.isChecked(me.rawValue, me.inputValue)) {
            me.addCls(me.checkedCls);
        }
 
        if (!me.fieldLabel) {
            me.skipLabelForAttribute = true;
        }
 
        return me.callParent();
    },
 
    getModelData: function() {
        var me = this,
            o = me.callParent(arguments);
 
        if (o) {
            o[me.getName()] = me.checked ? me.modelValue : me.modelValueUnchecked;
        }
 
        return o;
    },
 
    getSubTplData: function(fieldData) {
        var me = this,
            boxLabel = me.boxLabel,
            boxLabelAlign = me.boxLabelAlign,
            labelAlignedBefore = boxLabelAlign === 'before',
            data, inputElAttr;
 
        data = Ext.apply(me.callParent([fieldData]), {
            inputType: me.inputType,
            checkboxCls: me._checkboxCls,
            disabled: me.readOnly || me.disabled,
            checked: !!me.checked,
            wrapInnerCls: me.wrapInnerCls,
            boxLabel: boxLabel,
            boxLabelCls: me.boxLabelCls,
            boxLabelAlign: boxLabelAlign,
            labelAlignedBefore: labelAlignedBefore,
            afterLabelCls: labelAlignedBefore ? me.afterLabelCls : '',
            noBoxLabelCls: !boxLabel ? me.noBoxLabelCls : '',
 
            // We need to have name attribute on the <input> element
            // even if it wasn't specified in component config;
            // some browsers (Chrome, Safari) will treat missing name
            // as empty, grouping all radio buttons with empty name
            // together. This causes funky but unwanted effects
            // with regards to keyboard navigation.
            inputName: me.name || me.id
        });
 
        inputElAttr = data.inputElAriaAttributes;
 
        if (inputElAttr) {
            // aria-readonly is not valid for Checkboxes and Radio buttons
            delete inputElAttr['aria-readonly'];
        }
 
        return data;
    },
 
    initEvents: function() {
        var me = this;
 
        me.callParent();
 
        me.inputEl.on(me.changeEventName, me.onChangeEvent, me, { delegated: false });
 
        // In all IE versions it is possible to focus ANY element by clicking
        // regardless of tabIndex attribute. In this case, clicking on boxLabelEl
        // will end up focusing its parent bodyEl before focusing and activating
        // the associated input element. Dark wizardry in Focus publisher fails
        // to propagate the second focusin event so we have to accommodate here
        // by not allowing bodyEl to focus.
        if (Ext.isIE) {
            me.bodyEl.on('mousedown', me.onBodyElMousedown, me);
        }
 
        // Conversely in Safari and Firefox on Mac clicking either box label or input
        // itself will result in input activation, value change, and immediate blur
        // to the document body. We place more faith in consistency over platform
        // specific quirks so have to force inputEl focus here and prevent blurring.
        // Oh Sanity Where Art Thou. :/
        else if (Ext.isMac && (Ext.isGecko || Ext.isSafari)) {
            me.boxLabelEl.on('mousedown', me.onBoxLabelOrInputMousedown, me);
            me.inputEl.on('mousedown', me.onBoxLabelOrInputMousedown, me);
        }
    },
 
    /**
     * Sets the {@link #boxLabel} for this checkbox.
     * @param {String} boxLabel The new label
     */
    setBoxLabel: function(boxLabel) {
        var me = this;
 
        me.boxLabel = boxLabel;
 
        if (me.rendered) {
            me.boxLabelEl.setHtml(boxLabel);
            me.boxLabelEl[boxLabel ? 'removeCls' : 'addCls'](me.noBoxLabelCls);
            me.updateLayout();
        }
    },
 
    /**
     * @private
     * Handle mousedown events on bodyEl. See explanations in initEvents().
     */
    onBodyElMousedown: function(e) {
        if (e.target !== this.inputEl.dom) {
            e.preventDefault();
        }
    },
 
    /**
     * @private
     * Handle mousedown events on boxLabelEl and inputEl.
     * See explanations in initEvents().
     */
    onBoxLabelOrInputMousedown: function(e) {
        this.inputEl.focus();
        e.preventDefault();
    },
 
    /**
     * @private
     * Handle the change event from the DOM.
     */
    onChangeEvent: function(e) {
        this.updateValueFromDom();
    },
 
    /**
     * @private
     */
    updateValueFromDom: function() {
        var me = this,
            inputEl = me.inputEl && me.inputEl.dom;
 
        if (inputEl) {
            me.checked = me.rawValue = me.value = inputEl.checked;
 
            me.checkChange();
        }
    },
 
    /**
     * @private
     */
    updateCheckedCls: function(checked) {
        var me = this;
 
        checked = checked != null ? checked : me.getValue();
 
        me[checked ? 'addCls' : 'removeCls'](me.checkedCls);
    },
 
    /**
     * Returns the checked state of the checkbox.
     * @return {Boolean} True if checked, else false
     */
    getRawValue: function() {
        var inputEl = this.inputEl && this.inputEl.dom;
 
        return inputEl ? inputEl.checked : this.checked;
    },
 
    /**
     * Returns the checked state of the checkbox.
     * @return {Boolean} True if checked, else false
     */
    getValue: function() {
        var inputEl = this.inputEl && this.inputEl.dom;
 
        return inputEl ? inputEl.checked : this.checked;
    },
 
    /**
     * Returns the submit value for the checkbox which can be used when submitting forms.
     * @return {String} If checked the {@link #inputValue} is returned; otherwise the
     * {@link #uncheckedValue} (or null if the latter is not configured).
     */
    getSubmitValue: function() {
        var unchecked = this.uncheckedValue,
            uncheckedVal = Ext.isDefined(unchecked) ? unchecked : null;
 
        return this.getValue() ? this.inputValue : uncheckedVal;
    },
 
    isChecked: function(rawValue, inputValue) {
        var ret = false;
 
        if (rawValue === true || rawValue === 'true') {
            ret = true;
        }
        else {
            if (inputValue !== 'on' && (inputValue || inputValue === 0) &&
                (Ext.isString(rawValue) || Ext.isNumber(rawValue))) {
                ret = rawValue == inputValue; // eslint-disable-line eqeqeq
            }
            else {
                ret = rawValue === '1' || rawValue === 1 || this.onRe.test(rawValue);
            }
        }
 
        return ret;
    },
 
    /**
     * Sets the checked state of the checkbox.
     *
     * @param {Boolean/String/Number} value The following values will check the checkbox:
     * - `true, 'true'.
     * - '1', 1, or 'on'`, when there is no {@link #inputValue}.
     * - Value that matches the {@link #inputValue}.
     * Any other value will un-check the checkbox.
     * @return {Boolean} the new checked state of the checkbox
     */
    setRawValue: function(value) {
        var me = this,
            inputEl = me.inputEl && me.inputEl.dom,
            checked = me.isChecked(value, me.inputValue);
 
        if (inputEl) {
            // Setting checked property will fire unwanted propertychange event in IE8.
            me.duringSetRawValue = true;
            inputEl.checked = checked;
            me.duringSetRawValue = false;
 
            me.updateCheckedCls(checked);
        }
 
        me.checked = me.rawValue = checked;
 
        if (!me.duringSetValue) {
            me.lastValue = checked;
        }
 
        return checked;
    },
 
    /**
     * Sets the checked state of the checkbox, and invokes change detection.
     * @param {Array/Boolean/String} checked The following values will check the checkbox:
     * `true, 'true', '1', or 'on'`, as well as a String that matches the {@link #inputValue}.
     * Any other value will  un-check the checkbox.
     *
     * You may also pass an array of string values. If an array of strings is passed, all checkboxes
     * in the group with a matched name will be checked.  The checkbox will be unchecked
     * if a corresponding value is not found in the array.
     * @return {Ext.form.field.Checkbox} this
     */
    setValue: function(checked) {
        var me = this,
            boxes, i, len, box;
 
        // If an array of strings is passed, find all checkboxes in the group with the same name
        // as this one and check all those whose inputValue is in the array, un-checking all the 
        // others. This is to facilitate setting values from Ext.form.Basic#setValues, 
        // but is not publicly documented as we don't want users depending on this 
        // behavior.
        if (Ext.isArray(checked)) {
            boxes = me.getManager().getByName(me.name, me.getFormId()).items;
            len = boxes.length;
 
            for (= 0; i < len; ++i) {
                box = boxes[i];
                box.setValue(Ext.Array.contains(checked, box.inputValue));
            }
        }
        else {
            // The callParent() call ends up trigger setRawValue, we only want to modify
            // the lastValue when setRawValue being called independently.
            me.duringSetValue = true;
            me.callParent(arguments);
            delete me.duringSetValue;
        }
 
        return me;
    },
 
    /**
     * @method valueToRaw
     * @private
     */
    valueToRaw: Ext.identityFn,
 
    /**
     * @private
     * Called when the checkbox's checked state changes. Invokes the {@link #handler} callback
     * function if specified.
     */
    onChange: function(newVal, oldVal) {
        var me = this,
            handler = me.handler;
 
        me.updateCheckedCls(newVal);
 
        if (handler) {
            Ext.callback(handler, me.scope, [me, newVal], 0, me);
        }
 
        me.callParent(arguments);
 
        if (me.reference && me.publishState) {
            me.publishState('checked', newVal);
        }
    },
 
    /**
     * @private
     */
    resetOriginalValue: function(fromBoxInGroup) {
        var me = this,
            boxes, box, len, i;
 
        // If we're resetting the value of a field in a group, also reset the others.
        if (!fromBoxInGroup) {
            boxes = me.getManager().getByName(me.name, me.getFormId()).items;
            len = boxes.length;
 
            for (= 0; i < len; ++i) {
                box = boxes[i];
 
                if (box !== me) {
                    boxes[i].resetOriginalValue(true);
                }
            }
        }
 
        me.callParent();
    },
 
    doDestroy: function() {
        this.getManager().removeAtKey(this.id);
 
        this.callParent();
    },
 
    getManager: function() {
        return Ext.form.CheckboxManager;
    },
 
    onEnable: function() {
        var me = this,
            inputEl = me.inputEl && me.inputEl.dom;
 
        me.callParent();
 
        if (inputEl) {
            // Can still be disabled if the field is readOnly
            inputEl.disabled = me.readOnly;
        }
    },
 
    setReadOnly: function(readOnly) {
        var me = this,
            inputEl = me.inputEl && me.inputEl.dom;
 
        if (inputEl) {
            // Set the button to disabled when readonly
            inputEl.disabled = !!readOnly || me.disabled;
        }
 
        me.callParent(arguments);
    },
 
    getFormId: function() {
        var me = this,
            form;
 
        if (!me.formId) {
            form = me.up('form');
 
            if (form) {
                me.formId = form.id;
            }
        }
 
        return me.formId;
    },
 
    getFocusClsEl: function() {
        return this.displayEl;
    }
});