/**
 * Input masks provide a way for developers to define rules that govern user input. This ensures
 * that data is submitted in an expected format and with the appropriate character set.
 *
 * Frequent uses of input masks include:
 *
 * + Zip or postal codes
 * + Times
 * + Dates
 * + Telephone numbers
 *
 * ## Character Sets
 *
 * Input mask characters can be defined by representations of the desired set.  For instance,
 * if you only want to allow numbers, you can use 0 or 9.  Here is the list of default
 * representations:
 *
 * + '*': '[A-Za-z0-9]' // any case letter A-Z, any integer
 * + 'a': '[a-z]'       // any lower case letter a-z
 * + 'A': '[A-Z]'       // any upper case letter A-Z
 * + '0': '[0-9]'       // any integer
 * + '9': '[0-9]'        // any integer
 *
 * So, to create a telephone input mask, you could use:
 *
 * + (000) 000-0000
 *
 * or
 *
 * + (999) 999-9999
 *
 * ## Telephone input mask
 *
 *     @example toolkit=modern
 *     Ext.create({
 *         fullscreen: true,
 *         xtype: 'formpanel',
 *
 *         items: [{
 *             xtype: 'textfield',
 *             label: 'Phone Number',
 *             placeholder: '(xxx) xxx-xxxx',
 *             inputMask: '(999) 999-9999'
 *         }]
 *     });
 */
Ext.define('Ext.field.InputMask', function(InputMask) { return { // eslint-disable-line brace-style
    requires: [
        'Ext.util.LRU'
    ],
 
    cachedConfig: {
        blank: '_',
 
        characters: {
            '*': '[A-Za-z0-9]',
            'a': '[a-z]',
            'A': '[A-Z]',
            '0': '[0-9]',
            '9': '[0-9]'
        },
 
        ignoreCase: true
    },
 
    config: {
        /**
         * @cfg {String} pattern (required)
         */
        pattern: null
    },
 
    _cached: false,
    _lastEditablePos: null,
    _mask: null,
 
    statics: {
        active: {},
 
        from: function(value, existing) {
            var active = InputMask.active,
                ret;
 
            if (value === null) {
                ret = null;
            }
            else if (typeof value !== 'string') {
                if (existing && !existing._cached) {
                    ret = existing;
                    ret.setConfig(value);
                }
                else {
                    ret = new InputMask(value);
                }
            }
            else if (!(ret = active[value])) {
                // No one is currently using this mask, but check the cache of
                // recently used masks. We remove the mask from the cache and
                // move it to the active set... if it was there.
                if (!(ret = InputMask.cache.remove(value))) {
                    ret = new InputMask({
                        pattern: value
                    });
                }
 
                active[value] = ret;
                ret._cached = 1; // this is the first user either way
            }
            else {
                // The mask was found in the active set so we can reuse it
                // (just bump the counter).
                ++ret._cached;
            }
 
            return ret;
        }
    },
 
    constructor: function(config) {
        this.initConfig(config);
    },
 
    release: function() {
        var me = this,
            cache = InputMask.cache,
            key;
 
        if (me._cached && !--me._cached) {
            key = me.getPattern();
            
            //<debug>
            if (InputMask.active[key] !== me) {
                Ext.raise('Invalid call to InputMask#release (not active)');
            }
            
            if (cache.map[key]) {
                Ext.raise('Invalid call to InputMask#release (already cached)');
            }
            //</debug>
 
            delete InputMask.active[key];
            cache.add(key, me);
            cache.trim(cache.maxSize);
        }
        //<debug>
        else if (me._cached === 0) {
            Ext.raise('Invalid call to InputMask#release (already released)');
        }
        //</debug>
    },
 
    clearRange: function(value, start, len) {
        var me = this,
            blank = me.getBlank(),
            end = start + len,
            n = value.length,
            s = '',
            i, mask, prefixLen;
 
        if (!blank) {
            prefixLen = me._prefix.length;
 
            for (= 0; i < n; ++i) {
                if (< prefixLen || i < start || i >= end) {
                    s += value[i];
                }
            }
 
            s = me.formatValue(s);
        }
        else {
            mask = me.getPattern();
 
            for (= 0; i < n; ++i) {
                if (< start || i >= end) {
                    s += value[i];
                }
                else if (me.isFixedChar(i)) {
                    s += mask[i];
                }
                else {
                    s += blank;
                }
            }
        }
 
        return s;
    },
 
    formatValue: function(value) {
        var me = this,
            blank = me.getBlank(),
            i, length, mask, prefix, s;
 
        if (!blank) {
            prefix = me._prefix;
            length = prefix.length;
 
            s = this.insertRange('', value, 0);
 
            for (= s.length; i > length && me.isFixedChar(- 1);) {
                --i;
            }
 
            s = (< length) ? prefix : s.slice(0, i - 1);
        }
        else if (value) {
            s = me.formatValue('');
            s = me.insertRange(s, value, 0);
        }
        else {
            mask = me.getPattern();
            s = '';
 
            for (= 0, length = mask.length; i < length; ++i) {
                if (me.isFixedChar(i)) {
                    s += mask[i];
                }
                else {
                    s += blank;
                }
            }
        }
 
        return s;
    },
 
    getEditPosLeft: function(pos) {
        var i;
        
        for (= pos; i >= 0; --i) {
            if (!this.isFixedChar(i)) {
                return i;
            }
        }
 
        return null;
    },
 
    getEditPosRight: function(pos) {
        var mask = this._mask,
            len = mask.length,
            i;
 
        for (= pos; i < len; ++i) {
            if (!this.isFixedChar(i)) {
                return i;
            }
        }
 
        return null;
    },
 
    getFilledLength: function(value) {
        var me = this,
            blank = me.getBlank(),
            c, i;
 
        if (!blank) {
            return value.length;
        }
 
        for (= value && value.length; i-- > 0;) {
            c = value[i];
 
            if (!me.isFixedChar(i) && me.isAllowedChar(c, i)) {
                break;
            }
        }
 
        return ++|| me._prefix.length;
    },
 
    getSubLength: function(value, substr, pos) {
        var me = this,
            mask = me.getPattern(),
            k = 0,
            maskLen = mask.length,
            substrLen = substr.length,
            i;
 
        for (= pos; i < maskLen && k < substrLen;) {
            if (!me.isFixedChar(i) || mask[i] === substr[k]) {
                if (me.isAllowedChar(substr[k++], i, true)) {
                    ++i;
                }
            }
            else {
                ++i;
            }
        }
 
        return i - pos;
    },
 
    insertRange: function(value, substr, pos) {
        var me = this,
            mask = me.getPattern(),
            blank = me.getBlank(),
            filled = me.isFilled(value),
            prefixLen = me._prefix.length,
            maskLen = mask.length,
            substrLen = substr.length,
            s = value,
            ch, fixed, i, k;
 
        if (!blank && pos > s.length) {
            s += mask.slice(s.length, pos);
        }
 
        for (= pos, k = 0; i < maskLen && k < substrLen;) {
            fixed = me.isFixedChar(i);
 
            if (!fixed || mask[i] === substr[k]) {
                ch = substr[k++];
 
                if (me.isAllowedChar(ch, i, true)) {
                    if (< s.length) {
                        if (blank || filled || i < prefixLen) {
                            s = s.slice(0, i) + ch + s.slice(+ 1);
                        }
                        else {
                            s = me.formatValue(s.substr(0, i) + ch + s.substr(i));
                        }
                    }
                    else if (!blank) {
                        s += ch;
                    }
 
                    ++i;
                }
            }
            else {
                if (!blank && i >= s.length) {
                    s += mask[i];
                }
                else if (blank && fixed && substr[k] === blank) {
                    ++k;
                }
 
                ++i;
            }
        }
 
        return s;
    },
 
    isAllowedChar: function(character, pos, allowBlankChar) {
        var me = this,
            mask = me.getPattern(),
            c, characters, rule;
 
        if (me.isFixedChar(pos)) {
            return mask[pos] === character;
        }
 
        c = mask[pos];
        characters = me.getCharacters();
        rule = characters[c];
 
        return !rule || rule.test(character || '') ||
               (allowBlankChar && character === me.getBlank());
    },
 
    isEmpty: function(value) {
        var i, len;
        
        for (= 0, len = value.length; i < len; ++i) {
            if (!this.isFixedChar(i) && this.isAllowedChar(value[i], i)) {
                return false;
            }
        }
 
        return true;
    },
 
    // TODO This function would benefit from optimization
    // Used during validation and range insert
    isFilled: function(value) {
        return this.getFilledLength(value) === this._mask.length;
    },
 
    isFixedChar: function(pos) {
        return Ext.Array.indexOf(this._fixedCharPositions, pos) > -1;
    },
 
    setCaretToEnd: function(field, value) {
        var filledLen = this.getFilledLength(value),
            pos = this.getEditPosRight(filledLen);
 
        if (pos !== null) {
            // Because we are called during a focus event, we have to delay pushing
            // down the new caret position to the next frame or else the browser will
            // position the caret at the end of the text. Note, Ext.asap() does *not*
            // work reliably for this.
            Ext.raf(function() {
                if (!field.destroyed) {
                    field.setCaretPos(pos);
 
                    Ext.raf(function() {
                        if (!field.destroyed) {
                            field.setCaretPos(pos);
                        }
                    });
                }
            });
        }
    },
 
    //---------------------------------------------------------------------
    // Event Handling
 
    onBlur: function(field, value) {
        if (field.getAutoHideInputMask() !== false) {
            if (this.isEmpty(value)) {
                field.maskProcessed = true;
                field.setValue('');
            }
        }
    },
 
    onFocus: function(field, value) {
        // On focus we have to show the mask and move caret to the last editable position
        // If field has autoHideInputMask === false, inputMask is always shown so we only
        // move the caret
        if (field.getAutoHideInputMask() !== false) {
            if (!value) {
                field.maskProcessed = true;
                field.setValue(this._mask);
            }
        }
 
        this.setCaretToEnd(field, value);
    },
 
    onChange: function(field, value, oldValue) {
        var me = this,
            s;
 
        if (field.maskProcessed || value === oldValue) {
            field.maskProcessed = false;
            
            return true;
        }
 
        if (value) {
            s = me.formatValue(value);
            field.maskProcessed = true;
            field.setValue(s);
        }
    },
 
    processAutocomplete: function(field, value) {
        var me = this,
            s;
 
        if (value) {
            if (value.length > me._mask.length) {
                value = value.substr(0, me._mask.length);
            }
 
            s = me.formatValue(value);
            field.maskProcessed = true;
            field.inputElement.dom.value = s; // match DOM
            field.setValue(s);
            
            this.setCaretToEnd(field, value);
        }
    },
 
    /**
     * @private
     * @param field
     * @param adjustCaret {Boolean} move caret to the first editable position
     */
    showEmptyMask: function(field, adjustCaret) {
        var s = this.formatValue();
 
        field.maskProcessed = true;
        field.setValue(s);
 
        if (adjustCaret) {
            this.setCaretToEnd(field);
        }
    },
 
    onKeyDown: function(field, value, event) {
        if (event.ctrlKey || event.metaKey) {
            return;
        }
 
        // eslint-disable-next-line vars-on-top
        var me = this,
            // key = event.key(), // Does not work on mobile
            key = event.keyCode === event.DELETE,
            del = key === 'Delete',
            handled = del || (event.keyCode === event.BACKSPACE),
            s = value,
            caret, editPos, len, prefixLen, textSelection, start;
 
        if (handled) {
            caret = field.getCaretPos();
            prefixLen = me._prefix.length;
            textSelection = field.getTextSelection();
            start = textSelection[0];
            len = textSelection[1] - start;
 
            if (len) {
                s = me.clearRange(value, start, len);
            }
            else if (caret < prefixLen || (!del && caret === prefixLen)) {
                caret = prefixLen;
            }
            else {
                editPos = del ? me.getEditPosRight(caret) : me.getEditPosLeft(caret - 1);
 
                if (editPos !== null) {
                    s = me.clearRange(value, editPos, 1);
                    caret = editPos;
                }
            }
 
            if (!== value) {
                field.maskProcessed = true;
                field.setValue(s);
            }
 
            event.preventDefault();
            field.setCaretPos(caret);
        }
    },
 
    onKeyPress: function(field, value, event) {
        var me = this,
            key = event.keyCode,
            ch = event.getChar(),
            mask = me.getPattern(),
            prefixLen = me._prefix.length,
            s = value,
            caretPos, pos, start, textSelection;
 
        if (key === event.ENTER || key === event.TAB || event.ctrlKey || event.metaKey) {
            return;
        }
 
        // TODO Windows Phone may need to return here
 
        caretPos = field.getCaretPos();
        textSelection = field.getTextSelection();
 
        if (me.isFixedChar(caretPos) && mask[caretPos] === ch) {
            s = me.insertRange(s, ch, caretPos);
            ++caretPos;
        }
        else {
            pos = me.getEditPosRight(caretPos);
 
            if (pos !== null && me.isAllowedChar(ch, pos)) {
                start = textSelection[0];
 
                s = me.clearRange(s, start, textSelection[1] - start);
                s = me.insertRange(s, ch, pos);
                caretPos = pos + 1;
            }
        }
 
        if (!== value) {
            field.maskProcessed = true;
            field.setValue(s);
        }
 
        event.preventDefault();
 
        if (caretPos < me._lastEditablePos && caretPos > prefixLen) {
            caretPos = me.getEditPosRight(caretPos);
        }
 
        field.setCaretPos(caretPos);
    },
 
    onPaste: function(field, value, event) {
        // TODO Android browser issues
        // https://bugs.chromium.org/p/chromium/issues/detail?id=369101
        var text,
            clipdData = event.browserEvent.clipboardData;
 
        if (clipdData && clipdData.getData) {
            text = clipdData.getData('text/plain');
        }
        else if (Ext.global.clipboardData && Ext.global.clipboardData.getData) {
            text = Ext.global.clipboardData.getData('Text'); // IE
        }
 
        if (text) {
            this.paste(field, value, text, field.getTextSelection());
        }
 
        event.preventDefault();
    },
 
    paste: function(field, value, text, selection) {
        var me = this,
            caretPos = selection[0],
            len = selection[1] - caretPos,
            s = len ? me.clearRange(value, caretPos, len) : value,
            textLen = me.getSubLength(s, text, caretPos);
 
        s = me.insertRange(s, text, caretPos);
        caretPos += textLen;
        caretPos = me.getEditPosRight(caretPos) || caretPos;
 
        if (!== value) {
            field.maskProcessed = true;
            field.setValue(s);
        }
 
        field.setCaretPos(caretPos);
    },
 
    syncPattern: function(field) {
        var fieldValue = field.getValue(),
            s;
 
        if (field.getAutoHideInputMask() === false) {
            // show blank input mask if there is no initial value
            if (!fieldValue) {
                this.showEmptyMask(field);
            }
            else {
                // format any value and combine with mask
                s = this.formatValue(fieldValue);
                field.maskProcessed = true;
                field.setValue(s);
            }
        }
        else {
            // field has auto hide mask, but there might be an initial value
            // don't process empty value as that will set value to match the mask
            if (fieldValue) {
                s = this.formatValue(fieldValue);
                field.maskProcessed = true;
                field.setValue(s);
            }
        }
    },
 
    //---------------------------------------------------------------------
    // Configs
 
    applyCharacters: function(map) {
        var ret = {},
            flags = this.getIgnoreCase() ? 'i' : '',
            c, v;
 
        for (in map) {
            v = map[c];
 
            if (typeof v === 'string') {
                v = new RegExp(v, flags);
            }
 
            ret[c] = v;
        }
 
        return ret;
    },
 
    updatePattern: function(mask) {
        var me = this,
            characters = me.getCharacters(),
            lastEditablePos = 0,
            n = mask && mask.length,
            blank = me.getBlank(),
            fixedPosArr = [],
            prefix = '',
            str = '',
            c, i;
 
        for (= 0; i < n; ++i) {
            c = mask[i];
 
            if (!characters[c]) {
                fixedPosArr.push(str.length);
                str += c;
            }
            else {
                lastEditablePos = str.length + 1;
                str += blank;
            }
        }
 
        me._lastEditablePos = lastEditablePos;
        me._mask = str;
        me._fixedCharPositions = fixedPosArr;
 
        // Now that _fixedCharPositions are populated, isFixedChar can be used
        for (= 0; i < str.length && me.isFixedChar(i); ++i) {
            prefix += str[i];
        }
 
        me._prefix = prefix;
    }
};
}, function(InputMask) {
    InputMask.cache = new Ext.util.LRU();
    InputMask.cache.maxSize = 100;
});