/**
 * @since Last modified: 2024-09-04 19:51:03
 * @namespace validTools
 * @description Form value validation tools.
 * @see {@link https://codepen.io/axui/pen/mdvwQYo|demo @ codepen}
 */
'use strict';
import { T_fn, T_obj, T_sel } from '../../../types/ax';
import ax from '../ax';
import config from '../global/config';
import Message from '../modules/Message';
import combineArr from './combineArr';
import delay from './delay';
import getDataType from './getDataType';
import getEl from './getEl';
import isEmpty from './isEmpty';
import preventDft from './preventDft';
import renderTpl from './renderTpl';
import sliceFrags from './sliceFrags';
import toLocalTime from './toLocalTime';
import trim from './trim';
type T_result = {
    passed: boolean;
    succ: string;
    fail: string;
}
type T_valid = {
    test: T_fn;
    expired: T_fn;
    than: T_fn;
    getStrength: T_fn;
    parseLength: T_fn;
    parseSpecific: T_fn;
    parseCombine: T_fn;
    validForm: T_fn;
    listenSubmit: T_fn;
    factory: T_fn;
}
const validTools: T_valid = {
    /**
    * @memberOf validTools
    * @description Validate values using a regexp expression.
    * @param {string} value - text to be verified.
    * @param {RegExp | string} pattern - A regexp expression.
    * @returns {boolean} Return validate result,true or false.
    * @see {@link https://codepen.io/axui/pen/QWoWGRm|demo @ codepen}
    * @example
    * ax.validTools.test('123',/^\d+\.\d+\.\d+\.\d+$/);
    * <!--Return false-->
    */
    test: (value: string, pattern: RegExp | string): boolean => {
        if (isEmpty(value) || isEmpty(pattern)) { return false }
        return (typeof pattern === 'string' ? new RegExp(pattern) : pattern).test(value);
    },
    /**
    * @memberOf validTools
    * @description Validate values using a regexp expression.
    * @param {string|Date} value - Date string or date object.
    * @param {string|Date|any[]} data - The date of dates being compared.if type is any[],determine whether it falls within the intvl,else compare.
    * @returns {boolean} Return validate result,-1,1 or 0.
    * @see {@link https://codepen.io/axui/pen/QWoWGRm|demo @ codepen}
    * @example
    * ax.validTools.expired('2022-12-3','2026-5-12');
    * <!--Return -1-->
    */
    expired: (value: string | Date, data: string | Date | any[]): number[] => {
        let getTime = (v: any) => typeof v === 'string' ? toLocalTime(v) : v.getTime(),
            valueTime = getTime(value),
            dataType = getDataType(data),
            result: number[] = [-2, -2],
            compare = (a: number, b: number) => {
                return a < b ? -1 :
                    a === b ? 0 :
                        a > b ? 1 : -2;
            };
        if (isEmpty(value) || isEmpty(data)) { return result }
        if (dataType === 'Array' && (data as any[]).length === 2) {
            let dates = (data as any[]).map((k: any) => getTime(k)).sort();
            result[0] = compare(valueTime, dates[0]);
            result[1] = compare(valueTime, dates[1]);
        } else {
            let dataTime = getTime(data);
            result[0] = compare(valueTime, dataTime);
        }
        return result;
    },
    /**
   * @memberOf validTools
   * @description Compare numerical values.
   * @param {number} value - The number value.
   * @param {number|number[]} data - The date of dates being compared.if type is number[],determine whether it falls within the intvl,else compare.
   * @returns {boolean} Return validate result,-1,1 or 0.
   * @see {@link https://codepen.io/axui/pen/QWoWGRm|demo @ codepen}
   * @example
   * ax.validTools.than(25,[10,50]);
   * <!--Return 1-->
   */
    than: (value: number | string, data: number | string | any[]): number[] => {
        let result = [-2, -2],
            dataType = getDataType(data),
            valueParse = parseFloat(value as string),
            dataParse: any,
            compare = (a: number, b: number) => {
                return a < b ? -1 :
                    a === b ? 0 :
                        a > b ? 1 : -2;
            };
        if (dataType === 'Array') {
            if ((data as any[]).length === 2) {
                dataParse = (data as any[]).map((k: any) => parseFloat(k)).sort();
                result[0] = compare(valueParse, dataParse[0]);
                result[1] = compare(valueParse, dataParse[1]);
            }
        } else {
            dataParse = parseFloat(data as string);
            result[0] = compare(valueParse, dataParse);
        }
        return result;
    },
    /**
   * @memberOf validTools
   * @description The strength of the check value. The maximum strength depends on how many valid keys there are in the data.
   * @param {sting} value - The number value.
   * @param {object} [data] - Data for verification.Customizable properties,Do it in "value.match(new RegExp(datas[k]))" wary except "length" property.
   * @param {string} [data.lowerCase='[a-z]'] - The regexp of lower case.
   * @param {string} [data.upperCase='[A-Z]'] - The regexp of upper case.
   * @param {string} [data.digital='[0-9]'] - The regexp of digital.
   * @param {string} [data.chars=ax.regChars] - The regexp of special characters,can set it at ax.regChars.
   * @param {string} [data.local=config.lang.valid.regLocal] - The regexp of local characters,can set it at config.lang.valid.regLocal.
   * @param {number} [data.length=6] - The mini length of value.
   * @returns {number} Return value of strength.
   * @see {@link https://codepen.io/axui/pen/QWoWGRm|demo @ codepen}
   * @example
   * ax.validTools.getStrength('df$er445SA');
   * <!--Return 5-->
   */
    getStrength: (value: string, data?: T_obj): number => {
        let datas = Object.assign({ lowerCase: '[a-z]', upperCase: '[A-Z]', digital: '[0-9]', chars: `[${config.valid.regChars}]`, local: `[${config.lang.valid.regLocal}]`, length: config.valid.lengthStr }, data),
            strength = 0;
        if (isEmpty(value) || typeof value !== 'string') { return strength };
        for (let k in datas) {
            if (k === 'length') {
                value.trim().length >= datas[k] ? strength++ : null;
            } else {
                if (datas.hasOwnProperty(k) && datas[k]) {
                    value.match(new RegExp(datas[k])) ? strength++ : null;
                }
            }
        }
        return strength;
    },
    /**
       * @memberOf validTools
       * @description The strength of the check value. The maximum strength depends on how many valid keys there are in the data.
       * @param {sting} value - The origin string.
       * @returns {number} Return RegExp about length.
       * @see {@link https://codepen.io/axui/pen/QWoWGRm|demo @ codepen}
       * @example
       * ax.validTools.parseLength('6,');
       * <!--Return "{6,}"-->
       */
    parseLength: (value: string): T_obj => {
        let regex = '', text = '';
        if (value) {
            let arr = value.replace('{', '').replace('}', '').split(',').map((k: string) => ~~k);
            if (arr.length === 2) {
                regex = (arr[1] && arr[1] > arr[0]) ? `{${arr[0]},${arr[1]}}` :
                    (arr[1] && arr[1] === arr[0]) ? `{${arr[0]}}` : `{${arr[0]},}`;
                if (arr[1]) {
                    if (arr[0] === 0) {
                        text = renderTpl(config.lang.valid['length<='], { label: '', data: arr[1] });
                    } else {
                        text = renderTpl(config.lang.valid['length>=<='], { label: '', data: arr });
                    }
                } else {
                    text = renderTpl(config.lang.valid['length>='], { label: '', data: arr[0] });
                }
            } else {
                regex = `{${arr[0]}}`;
                text = renderTpl(config.lang.valid['length='], { label: '', data: arr[0] });
            }
        }
        return { regex, text };
    },
    /**
    * @memberOf validTools
    * @description The strength of the check value. The maximum strength depends on how many valid keys there are in the data.
    * @param {sting} value - The origin string.
    * @param {object} [data] - The data of validattion.
    * @param {object} [data.regChars=config.valid.regChars] - Special characters RegExp.
    * @param {object} [data.regLocal=config.lang.valid.regLocal] - local characters RegExp.
    * @param {string} [data.label] - Alias of field.
    * @param {string} [data.name] - Name attribute of field.
    * @param {object} [data.types=config.lang.valid.types] - Charactors type map.
    * @param {*} [data.tpl=config.lang.valid.specific] - Template string to render text.
    * @returns {string} Return specific RegExp and validated text:{regex,text}.
    * @see {@link https://codepen.io/axui/pen/QWoWGRm|demo @ codepen}
    * @example
    * ax.validTools.parseSpecific('a=2&A');
    * <!--Return {regex:"^(?=.*?[a-z].*?[a-z])(?=.*?[A-Z]).{0,}$",text:'要求小写字母至少2个，大写字母至少1个'}-->
    */
    parseSpecific: function (value: string, data?: T_obj): T_obj {
        value = trim(value, 'global');
        let regex = '',
            typeStr = value,
            rangeStr = '',
            text = '',
            strData: T_obj = {},
            regTypes = Object.assign({ ...config.lang.valid.types }, data?.types),
            tplStr = data?.tpl || config.lang.valid.specific,
            tplData: T_obj = {};
        if (value) {
            let rangeTemp = sliceFrags({ str: value });
            if (rangeTemp.length > 0) {
                rangeStr = rangeTemp[0];
                typeStr = value.replace(rangeStr, '');
            }
            regex = '^';
            let arr = typeStr.split('&');
            arr.forEach((k: string) => {
                let val = k.split('='),
                    reg = '';
                reg = (val[0] === 'a') ? `.*?[a-z]` :
                    (val[0] === 'A') ? `.*?[A-Z]` :
                        (val[0] === 'd') ? `.*?[0-9]` :
                            (val[0] === '~') ? `.*?[${data?.regChars || config.valid.regChars}]` :
                                (val[0] === '@') ? `.*?[${data?.regLocal || config.lang.valid.regLocal}]` : '';
                regex += (reg ? '(?=' + Array((val[1] ? ~~val[1] : 1) + 1).join(reg) + ')' : '');
                strData[regTypes[val[0]]] = val[1] || 1;
            });
            tplData = { label: data?.label || '', name: data?.name || '', value: data?.value || '', data: strData };
            regex += `.${this.parseLength(rangeStr).regex || '{0,}'}$`;
            text = renderTpl(tplStr, tplData) + (rangeStr ? this.parseLength(rangeStr).text : '');
        }
        return { regex, text };
    },
    /**
    * @memberOf validTools
    * @description Verify the character type of the permutation and combination.
    * @param {sting} value - The origin string.
    * @param {object} [data] - The data of validattion.
    * @param {object} [data.regChars=config.valid.regChars] - Special characters RegExp.
    * @param {object} [data.regLocal=config.lang.valid.regLocal] - local characters RegExp.
    * @param {string} [data.label] - Alias of field.
    * @param {string} [data.name] - Name attribute of field.
    * @param {object} [data.types=config.lang.valid.types] - Charactors type map.
    * @param {*} [data.tpl=config.lang.valid.combine] - Template string to render text.
    * @returns {object} Return combined RegExp and validated text:{regex,text}.
    * @see {@link https://codepen.io/axui/pen/QWoWGRm|demo @ codepen}
    * @example
    * ax.validTools.parseCombine('aAd:2');
    * <!--Return {regex:"^(?![a-z]+$)(?![A-Z]+$)(?![0-9]+$)[a-zA-Z0-9]{0,}$",text:'要求小写字母、大写字母、数字至少两种'}-->
    */
    parseCombine: function (value: string, data?: T_obj): T_obj {
        value = trim(value, 'global');
        let regChars = data?.regChars || config.valid.regChars,
            regLocal = data?.regLocal || config.lang.valid.regLocal,
            regex = '',
            typeStr = value,
            rangeStr = '',
            text = '',
            strData: { types: string[], total: number } = { types: [], total: 0 },
            regTypes = Object.assign({ ...config.lang.valid.types }, data?.types),
            tplStr = data?.tpl || config.lang.valid.combine,
            tplData = {};
        if (value) {
            let rangeTemp = sliceFrags({ str: value });
            if (rangeTemp.length > 0) {
                rangeStr = rangeTemp[0];
                typeStr = value.replace(rangeStr, '');
            }
            let arr = typeStr.split(':'),
                types = ~~arr[1] ? ~~arr[1] : 2,
                regs: string[] = [];
            arr[0].includes('a') ? (regs.push('a-z'), strData.types.push(regTypes['a'])) : null;
            arr[0].includes('A') ? (regs.push('A-Z'), strData.types.push(regTypes['A'])) : null;
            arr[0].includes('d') ? (regs.push('0-9'), strData.types.push(regTypes['d'])) : null;
            arr[0].includes('~') ? (regs.push(regChars), strData.types.push(regTypes['~'])) : null;
            arr[0].includes('@') ? (regs.push(regLocal), strData.types.push(regTypes['@'])) : null;
            types = Math.min(types, regs.length);
            if (types === 1) {
                regex = `^[${regs.join('')}]`;
            } else {
                regex += '^';
                combineArr({ origin: regs, length: types - 1 }).forEach((k: string[]) => {
                    regex += `(?![${k.join('')}]+$)`;
                });
                regex += `[${regs.join('')}]`;
            }
            strData.total = types;
            tplData = { label: data?.label || '', name: data?.name || '', value: data?.value || '', data: strData };
            regex += `${this.parseLength(rangeStr).regex || '{0,}'}$`;
            text = renderTpl(tplStr, tplData) + (rangeStr ? this.parseLength(rangeStr).text : '');
        }
        return { regex, text };
    },
    /**
    * @memberOf validTools
    * @description Verify the character type of the permutation and combination.
    * @param {object} obj - The parameters.
    * @param {getEl} obj.el - The form node.
    * @param {object} [obj.fail] - Call back of failure.No parameters.
    * @param {object} [obj.succ] - Call back of success.No parameters.
    * @param {object} [obj.intvl=100] - Interval of loop validation.
    * @returns {object} Return Promise(async/await).Response is "{passed,fails}","passed" return true/false,"fail" is the failure items.
    * @see {@link https://codepen.io/axui/pen/QWoWGRm|demo @ codepen}
    * @example
    * ax.validTools.validForm({el:'form'});
    * <!--Return Promise of async-->
    */
    validForm: async ({ el, fail, succ, intvl = 100 }: { el: T_sel, fail?: T_fn, succ?: T_fn, intvl?: number }): Promise<T_obj> => {
        let parentDom = getEl(el) || document.body,
            validItem = ax.valids.find((k: any) => k.parent === parentDom),
            children = validItem?.children || [],
            passed = true,
            fails: any[] = [],
            loopValid = async () => {
                for (let i = 0; i < children.length; i++) {
                    let v = children[i];
                    await delay({
                        duration: intvl * i
                    }).then(async () => {
                        await v.ins.do().then((res: any) => {
                            !res.passed ? (passed = res.passed, fails.push(v)) : null;
                        });
                    });
                }
            };
        if (ax.valids.length === 0 || !validItem) {
            new Message({
                content: config.lang.valid.message.noValids,
                iconShow: true,
                status: 'info',
            }).show();
            return { passed: true, fails: [] };
        }
        await loopValid().then(() => {
            !passed ? (fail && fail(fails)) : (succ && succ());
        });
        return { passed, fails };
    },
    /**
   * @memberOf validTools
   * @description Add a submit listener to all forms with verification items on the current page. When a form triggers the submit event, verify the verification items in this form again.
   * @param {object} obj - The parameters.
   * @param {object|array|getEl} [obj.item=ax.valids] - A item of ax.valids or items of ax.valids or a form node.
   * @param {object} [obj.fail] - Call back of failure.Support parameter:parent(a item of ax.valid) and failure child.
   * @param {object} [obj.succ] - Call back of success.Support parameters:parent(a item of ax.valid).
   * @param {object} [obj.cb] - Call back of validation.Support parameters:parent and validation result:{passed,fails}.
   * @returns {null} - null.
   * @see {@link https://codepen.io/axui/pen/QWoWGRm|demo @ codepen}
   * @example
   * ax.validTools.listenSubmit();
   * <!--Return null-->
   */
    listenSubmit: function ({ item, fail, succ, cb }: { item?: any, fail?: T_fn, succ?: T_fn, cb?: T_fn }): void {
        let allItems = ax.valids,
            getItems = () => {
                let itemType = getDataType(item), result: any[] = [];
                if (isEmpty(item)) {
                    result = allItems;
                } else {
                    if (itemType === 'Array') {
                        result = item;
                    } else {
                        if (itemType.includes('HTML')) {
                            result = [allItems.find((k: any) => k.parent === item)];
                        } else if (itemType === 'String') {
                            let temp = getEl(item);
                            temp ? result = [allItems.find((k: any) => k.parent === temp)] : null;
                        } else {
                            result = [item];
                        }
                    }
                }
                return result;
            },
            usableItems = getItems().filter((k: any) => k?.parent?.nodeName === 'FORM');
        if (usableItems.length > 0) {
            usableItems.forEach((k: any) => {
                k.parent.onsubmit = (e: any) => {
                    preventDft(e);
                    this.validForm({
                        el: k.parent,
                        fail: (fails: any) => {
                            fail && fail(k, fails);
                        },
                        succ: (fails: any) => {
                            succ && succ(k, fails);
                            k.parent.submit();
                        }
                    }).then((response: any) => {
                        cb && cb(k, response);
                    });
                }
            });
        }
    },
    /**
     * @memberOf validTools
     * @description The verification factory passes in the verification value and verification rules and returns the verification results.
     * @param {object} data - Data participating in verification.Format is:{value:100,label:'',name:'',ins:'',rules:{'rule01':()=>{},'rule02':()=>{}...},text:{succ:'',fail：''}}
     * @param {string} data.value - The value to be verified.
     * @param {object} data.rules - The rules to be verified.
     * @param {object} [data.text] - The default text.
     * @param {string} [data.text.succ] - The default text of success.
     * @param {string} [data.text.fail] - The default text of failure.
     * @returns  {promise} - A promise,the parameter of resolve is {passed,success,error}.
     * @see {@link https://codepen.io/axui/pen/vYPYyzN|demo @ codepen}
     * @example
     * ax.validTools.factory({value:100,rules:{'compare':function(){return {passed:this<200}}}});
     * "this" in handler is "{value,name,label,ins}".
     * <!--Return {passed:true,succ:'...',fail:'...'}-->
     */
    factory: async (data: any): Promise<T_result> => {
        let textSucc = data?.text?.succ || config.lang.valid.succ,
            textFail = data?.text?.fail || config.lang.valid.fail,
            result: T_result = { passed: true, succ: renderTpl(textSucc, { label: data.label, name: data.name }), fail: renderTpl(textFail, { label: data.label, name: data.name }) };
        if (data.rules.length === 0) { return result }
        try {
            for (let k in data.rules) {
                if (data.rules.hasOwnProperty(k)) {
                    let item = data.rules[k],
                        param = { value: data.value, name: data.name, label: data.label, type: k, ins: data.ins, data: data.ins.types[k] },
                        output = await item(param),
                        dataType = getDataType(output);
                    if (dataType === 'Object') {
                        if (output?.content?.hasOwnProperty('passed')) {
                            Object.assign(result, output.content);
                        } else {
                            Object.assign(result, output);
                        }
                        if (!result.passed) {
                            break;
                        };
                    } else {
                        throw new Error('Return value is not a standard object : {passed,succ,fail}!');
                    }
                }
            }
        } catch (err) {
            result.passed = false;
            new Message({
                status: 'info',
                content: config.lang.valid.message.wrongRule,
            }).show();
            console.error(err);
        }
        data.cb && data.cb(result);
        return result;
    }
}
export default validTools;
