/**  
 * @since Last modified: 2024-08-13 18:24:47
 * @class Valid
 * @description Form and form field validation.
 * @param {getEl} elem - The target node.The writing is the same as getEl.
 * @param {Object} [options] - The parameters 
 * @see {@link https://codepen.io/axui/pen/VwRaKxe|demo @ codepen}
 * @example
 * ax.Valid('#demo')
 * <!--Return Promise of valid -->
 */
import { T_obj, T_opt_base, T_required, T_sel } from "../../../types/ax";
import createEl from "../utils/createEl";
import extend from "../utils/extend";
import getEl from "../utils/getEl";
import instance from "../utils/instance";
import pipe from "../utils/pipe";
import PubBase from "./PubBase";
import PubListen from "./PubListen";
import ax from "../ax";
import PubCommon from "./PubCommon";
import isEmpty from "../utils/isEmpty";
import renderTpl from "../utils/renderTpl";
import validTools from "../utils/validTools";
import deepClone from "../utils/deepClone";
import regExps from "../utils/regExps";
import isDateStr from "../utils/isDateStr";
import Message from "./Message";
import paramToJson from "../utils/paramToJson";
import ajax from "../utils/ajax";
import isSubset from "../utils/isSubset";
import icons from "../utils/icons";
import elState from "../utils/elState";
import Observe from "./Observe";
import Popup from "./Popup";
import getDataType from "../utils/getDataType";
import fieldTools from "../utils/fieldTools";
import elProps from "../utils/elProps";
import { T_valid } from "../../../types/valid";
import PubUpdate from "./PubUpdate";
import optValid from "../options/optValid";
import prefix from "../global/prefix";
import isNull from "../utils/isNull";
class Valid extends pipe(PubListen, PubUpdate, PubCommon)(PubBase) {
    options: Required<T_valid> | T_obj = {};
    targetEl: any;
    useRules: any;
    dftRules: any;
    inputEl: any;
    static hostType = 'el';
    constructor(elem: T_sel, options?: T_valid, inital: boolean = ax.config.inital) {
        super();
        this.ready({
            options,
            host: elem,
            maps: optValid,
        });
        this.timestamp = new Date().getTime();
        this.targetType = fieldTools.getType(this.targetEl);
        this.inputEl = this.targetType === 'native' ? this.targetEl :
            this.targetType === 'cmpt-group' ? this.targetEl : this.targetEl.querySelector('TEXTAREA,SELECT,INPUT');
        if (!this.inputEl) {
            throw new Error('The target node must contain a form field!');
        }
        if (!this.inputEl.getAttribute('name')) {
            throw new Error('The element to be verified must have name attribute!');
        }
        /**
        * @name Valid.name
        * @description The name of form field.
        * @type string
        */
        this.name = this.inputEl.getAttribute('name');
        /**
        * @name Valid.label
        * @description The alias of form field.Come from "options.label",if empty,name instead.
        * @type string
        */
        this.label = this.options.label || this.name;
        /**
        * @name Valid.parentEl
        * @description The parent node of input,Come from "options.parent",if empty,closeat form node instead.
        * @type HTMLElement
        */
        this.parentEl = getEl(this.options.parent) || this.inputEl.closest('form') || document.body;
        /**
        * @name Valid.valid
        * @description The validation object of instance,that will be push to ax.valids.The format is:"{el,name,ins}"
        * @type object
        */
        this.valid = { input: this.inputEl, target: this.targetEl, name: this.name, ins: this };
        this.hostValid = null;
        this.pushValids();
        /**
        * @name Valid.msgEl
        * @description The message node of validation message.The format is:"<div class="_valid"></div>".
        * @type HTMLElement
        */
        this.createMsgEl();
        this.otherBox = getEl(this.options.placement);
        this.createObs();
        this.checkFun = async () => {
            await this.do();
        }
        this.dftRules = {
            /*
            *required validation
            */
            'required': (data: any) => {
                let fail = renderTpl(this.options.lang.required, { label: data.label });
                return !isEmpty(data.value) ? { passed: true } : { passed: false, fail, type: 'required' };
            },
            /*
             *Commonly used regExps validation
             */
            ...this.loopTestRules(['email', 'cellphone', 'landline', 'ip', 'id', 'zip', 'url', 'locale', 'password', 'letter', 'string', 'ymdhms', 'ymd', 'hms', 'ym', 'y', 'm', 'd', 'integer', 'number']),
            'plate': (data: any) => {
                let regex = data.value.length === 8 ? regExps.plate.nev : regExps.plate.oil;
                return this.testRegex({ regex, value: data.value, name: 'plate' });
            },
            'date': (data: any) => {
                let flag = isDateStr(data.value),
                    fail = renderTpl(this.options.lang['date'], { label: data.label, value: data.value });
                return flag ? { passed: true } : { passed: false, fail, type: 'date' };
            },
            /*
             *date range validation
             */
            ...this.loopDateRules(['date=', 'date<', 'date<=', 'date>', 'date>=', 'date><', 'date><=', 'date>=<', 'date>=<=']),
            /*
             * number range validation
             */
            ...this.loopThanRules(['than=', 'than>', 'than>=', 'than<', 'than<=', 'than><', 'than><=', 'than>=<', 'than>=<=',]),
            /*
             *string length validation
             */
            ...this.loopLengthRules(['length=', 'length>', 'length>=', 'length<', 'length<=', 'length><', 'length><=', 'length>=<', 'length>=<=']),
            /**
             * include and exclude validation
             */
            ...this.loopIncludeRules(['include', 'exclude']),
            /**
             * value total validation
             */
            ...this.loopValuesRules(['count=', 'count>', 'count>=', 'count<', 'count<=', 'count><', 'count><=', 'count>=<', 'count>=<=']),
            /**
            * same value validation
            */
            ...this.loopSameRules(['same', 'different']),
            /**
             * strength validation (0~6)
             */
            'strength': (data: any) => {
                if (typeof data.value !== 'string') {
                    return { passed: false, fail: ax.lang.valid.strFormat }
                }
                let strength = this.getStrength(data.value),
                    fail = renderTpl(this.options.lang['strength'], { label: data.label, name: data.name, value: strength, data: this.types['strength'] });
                return strength >= this.types['strength'] ? { passed: true } : { passed: false, fail, type: 'strength' };
            },
            /**
             * specific validation
             */
            'specific': (data: any) => {
                if (typeof data.value !== 'string') {
                    return { passed: false, fail: ax.lang.valid.strFormat }
                }
                let parse = validTools.parseSpecific(this.types['specific'], { label: data.label, name: data.name, value: data.value, tpl: this.options.lang['specific'] }),
                    regExp = parse.regex,
                    regText = parse.text;
                return validTools.test(data.value, regExp) ? { passed: true } : { passed: false, fail: regText, type: 'specific' };
            },
            /**
            * combine validation
            */
            'combine': (data: any) => {
                if (typeof data.value !== 'string') {
                    return { passed: false, fail: ax.lang.valid.strFormat }
                }
                let parse = validTools.parseCombine(this.types['combine'], { label: data.label, name: data.name, ins: this, value: data.value, tpl: this.options.lang['combine'] }),
                    regExp = parse.regex,
                    regText = parse.text;
                return validTools.test(data.value, regExp) ? { passed: true } : { passed: false, fail: regText, type: 'combine' };
            },
            /**
             * async validation,data={label,name,value,strength}
             */
            'async': (data: any) => {
                return ajax.call(this, Object.assign({
                    url: this.types['async'],
                    data: {
                        label: data.label,
                        name: data.name,
                        value: data.value,
                        strength: this.getStrength(data.value),
                    },
                }, this.options.ajax));
            },
            ...this.options.extend
        };
        validTools.listenSubmit({ item: this.parentEl });
        super.listen({ name: 'ready' });
        inital ? this.init() : null;
    }
    /**
     * @description Initialization instance.No parameters.
     * @param {fn_cb_init} [cb] - Call back after initialization.
     * @returns {Promise} Return Promise of instance.The response of Promise is the instance itself.
     */
    async init(cb?: any): Promise<any> {
        super.listen({ name: 'start' });
        this.setEmpty();
        /**
        * @name Valid.types
        * @description All validation types from "options.type".The format is:"{required:true,email:true,range:{min:100,max:200}}".
        * @type object
        */
        this.types = typeof this.options.type === 'string' ? paramToJson(this.options.type) : this.options.type;
        this.getUserRules();
        if (['change', 'blur', 'input'].includes(this.options.trigger)) {
            if (this.targetType === 'cmpt-group') {
                this.inputEl.on('change', this.checkFun)
            } else {
                this.inputEl.removeEventListener(this.options.trigger, this.checkFun);
                this.inputEl.addEventListener(this.options.trigger, this.checkFun, false);
            }
        } else if (this.options.trigger === 'load') {
            await this.do();
        }
        super.listen({ name: 'init', cb });
        return this;
    }
    setEmpty() {
        this.msgEl ? this.msgEl.innerHTML = '' : null;
        this.popupIns ? this.popupIns.destroy : null;
        this.msgIns ? this.msgIns.destroy : null;
    }
    loopTestRules(types: string[]) {
        let result: T_obj = {};
        types.forEach((k: string) => {
            if (regExps.hasOwnProperty(k)) {
                result[k] = (data: any) => {
                    if (Array.isArray(data.value)) {
                        return { passed: false, fail: ax.lang.valid.strFormat, id: 'strFormat' };
                    }
                    return this.testRegex({ regex: regExps[k], value: data.value, name: k });
                }
            }
        });
        return result;
    }
    loopIncludeRules(types: string[]) {
        let result: T_obj = {};
        types.forEach((k: any) => {
            result[k] = (data: any) => {
                if (!Array.isArray(data.data)) {
                    return { passed: false, fail: ax.lang.valid.arrFormat };
                };
                let value = Array.isArray(data.value) ? data.value : data.value.split(this.options.separator),
                    flag = isSubset(value, data.data),
                    fail = renderTpl(this.options.lang[k], { label: data.label, name: data.name, value: data.value, data: data.data });
                k === 'exclude' && (flag = !flag);
                return flag ? { passed: true } : { passed: false, fail, type: k };
            }
        });
        return result;
    }
    loopSameRules(types: string[]) {
        let result: T_obj = {};
        types.forEach((k: any) => {
            result[k] = (data: any) => {
                if (!Array.isArray(data.data)) {
                    return { passed: false, fail: ax.lang.valid.arrFormat };
                };
                let referField = this.parentEl.querySelector(`[name="${this.types[k][0]}"]`),
                    referValue = this.getVals(referField, 'string'),
                    targetValue = this.getVals(this.inputEl, 'string'),
                    fail = renderTpl(this.options.lang[k], { label: data.label, name: data.name, value: targetValue, data: data.data }),
                    condition = k === 'same' ? (referValue === targetValue) : (referValue !== targetValue);
                return condition ? { passed: true } : { passed: false, fail, type: k };
            }
        });
        return result;
    }
    loopDateRules(types: string[]) {
        let result: T_obj = {};
        types.forEach((k: any) => {
            result[k] = (data: any) => {
                if (Array.isArray(data.value)) {
                    return { passed: false, fail: ax.lang.valid.strFormat };
                }
                let flag = validTools.expired(data.value, data.data).join(''),
                    fail = renderTpl(this.options.lang[k], { label: data.label, value: data.value, data: data.data }),
                    condition = k === 'date=' ? (flag === '0-2') :
                        k === 'date<' ? (flag === '-1-2') :
                            k === 'date<=' ? (flag === '0-2' || flag === '-1-2') :
                                k === 'date>' ? (flag === '1-2') :
                                    k === 'date>=' ? (flag === '1-2' || flag === '0-2') :
                                        k === 'date><' ? (flag === '1-1') :
                                            k === 'date><=' ? (flag === '1-1' || flag === '10') :
                                                k === 'date>=<' ? (flag === '1-1' || flag === '0-1') :
                                                    k === 'date>=<=' ? (['1-1', '10', '0-1', '00'].includes(flag)) : true;
                return condition ? { passed: true } : { passed: false, fail, type: k };
            }
        });
        return result;
    }
    loopThanRules(types: string[]) {
        let result: T_obj = {};
        types.forEach((k: any) => {
            result[k] = (data: any) => {
                if (Array.isArray(data.value)) {
                    return { passed: false, fail: ax.lang.valid.strFormat };
                }
                let flag = validTools.than(data.value, data.data).join(''),
                    fail = renderTpl(this.options.lang[k], { label: data.label, value: data.value, data: data.data }),
                    condition = k === 'than>' ? (flag === '1-2') :
                        k === 'than>=' ? (flag === '1-2' || flag === '0-2') :
                            k === 'than<' ? (flag === '-1-2') :
                                k === 'than<=' ? (flag === '0-2' || flag === '-1-2') :
                                    k === 'than=' ? (flag === '0-2') :
                                        k === 'than><' ? (flag === '1-1') :
                                            k === 'than><=' ? (flag === '1-1' || flag === '10') :
                                                k === 'than>=<' ? (flag === '1-1' || flag === '0-1') :
                                                    k === 'than>=<=' ? (['1-1', '10', '0-1', '00'].includes(flag)) : true;
                return condition ? { passed: true } : { passed: false, fail, type: k };
            }
        });
        return result;
    }
    loopLengthRules(types: string[]) {
        let result: T_obj = {};
        types.forEach((k: any) => {
            result[k] = (data: any) => {
                if (Array.isArray(data.value)) {
                    return { passed: false, fail: ax.lang.valid.strFormat, id: 'strFormat' };
                }
                let pattern = k === 'length=' ? this.types[k] :
                    k === 'length>' ? `${this.types[k] + 1},` :
                        k === 'length>=' ? `${this.types[k]},` :
                            k === 'length<' ? `0,${this.types[k] - 1}` :
                                k === 'length<=' ? `0,${this.types[k]}` :
                                    k === 'length><' ? `${this.types[k][0] + 1},${this.types[k][1] - 1}` :
                                        k === 'length><=' ? `${this.types[k][0] + 1},${this.types[k][1]}` :
                                            k === 'length>=<' ? `${this.types[k][0]},${this.types[k][1] - 1}` :
                                                k === 'length>=<=' ? `${this.types[k][0]},${this.types[k][1]}` : '',
                    regex = new RegExp(`^.{${pattern}}$`);
                return this.testRegex({ regex, value: data.value, name: k });
            }
        });
        return result;
    }
    loopValuesRules(types: string[]) {
        let result: T_obj = {};
        types.forEach((k: any) => {
            result[k] = (data: any) => {
                let value = (Array.isArray(data.value)) ? data.value : data.value.split(this.options.separator).filter(Boolean),
                    fail = renderTpl(this.options.lang[k], { label: data.label, name: data.name, value: value.length, data: data.data }),
                    condition = k === 'count>=<=' ? (value.length >= this.types[k][0] && value.length <= this.types[k][1]) :
                        k === 'count><=' ? (value.length > this.types[k][0] && value.length <= this.types[k][1]) :
                            k === 'count>=<' ? (value.length >= this.types[k][0] && value.length < this.types[k][1]) :
                                k === 'count><' ? (value.length > this.types[k][0] && value.length < this.types[k][1]) :
                                    k === 'count=' ? `${value.length} === ${this.types[k]}` : `${value.length} ${k.replace('count', '')} ${this.types[k]}`;
                return new Function(`return ${condition}`)() ? { passed: true } : { passed: false, fail, type: k };
            }
        });
        return result;
    }
    testRegex({ regex, value, name }: { regex: RegExp, value: string, name: string }) {
        let flag = validTools.test(value, regex),
            fail = renderTpl(this.options.lang[name], { label: this.label, value, data: this.types[name] });
        ((name === 'length<' || name === 'length<=') && !value) && (flag = true);
        return flag ? { passed: true } : { passed: false, fail, type: name };
    }
    extendRules(rules: any) {
        Object.assign(this.dftRules, rules);
    }
    getUserRules() {
        this.userRules = {};
        for (let k in this.types) {
            if (this.types.hasOwnProperty(k) && this.dftRules.hasOwnProperty(k)) {
                this.userRules[k] = this.dftRules[k];
                this.userRules[k].data = this.types[k];
            }
        }
    }
    /**
     * @description Get the value of the form field.
     * @returns {*} Return value of field.
     */
    getVals(field: any = this.inputEl, format = ''): any {
        return fieldTools.getVals({ el: field, parent: this.parentEl, format });
    }
    /**
     * @description Get the value strength of the form field.
     * @returns {number} Return value strength(0~6).
     */
    getStrength(value: any = this.getVals(this.inputEl)): number {
        return validTools.getStrength(value, {
            chars: `[${this.options.regChars}]`,
            local: `[${this.options.regLocal}]`,
            length: this.options.lengthStr
        });
    }
    /**
     * @description Validate the field.
     * @returns {Promise} Return Promise of validition,the response is validation result,the format is:"{passed,succ,fail,type}".
     */
    do(): any {
        if (this.destroyed) {
            return this;
        }
        let value = this.getVals(this.inputEl);
        if (isNull(value)) {
            console.warn('No values obtained from the input, stopping validation!');
            return this;
        }
        return validTools.factory({
            value,
            name: this.name,
            label: this.label,
            ins: this,
            rules: this.userRules,
            text: {
                succ: this.options.succ,
                fail: this.options.fail,
            },
            cb: (resp: any) => {
                /**
                * @name Valid.result
                * @description Verification result,the format is : {passed,succ,fail,type}.
                * @type object
                */
                this.result = resp;
                /**
                * @name Valid.output
                * @description Data from external calls,the format is : {passed,succ,fail,type,value,ins,strength}.
                * @type object
                */
                this.output = { ...resp, name: this.name, label: this.label, value, ins: this, strength: this.getStrength(value) };
                super.listen({ name: 'trigger', params: [this.output] });
                this.observer.proxy.passed = resp.passed;
                this.observer.proxy.type = resp.type;
            }
        });
    }
    toggleStyle(passed: boolean) {
        if (passed) {
            this.msgEl.classList.remove(`${prefix}c-error`);
            this.msgEl.classList.add(`${prefix}c-succ`);
            this.targetEl.setAttribute('validate', 'passed');
            if (this.options.styleHost) {
                this.targetEl.classList.remove(`${prefix}c-error`);
                this.targetEl.classList.add(`${prefix}c-succ`);
            }
        } else {
            this.msgEl.classList.remove(`${prefix}c-succ`);
            this.msgEl.classList.add(`${prefix}c-error`);
            this.targetEl.setAttribute('validate', 'failed');
            if (this.options.styleHost) {
                this.targetEl.classList.remove(`${prefix}c-succ`);
                this.targetEl.classList.add(`${prefix}c-error`);
            }
        }
    }
    render(msg: any) {
        this.toggleStyle(msg.passed);
        if (msg.passed) {
            this.msgIcon = this.options.iconShow ? icons.font["succ-f"] : '';
            this.msgText = msg.succ;
        } else {
            this.msgIcon = this.options.iconShow ? icons.font["error-f"] : '';
            this.msgText = msg.fail;
        }
        this.msgEl.innerHTML = this.msgIcon + this.msgText;
        if (this.otherBox) {
            elState(this.msgEl).isVirtual && this.otherBox.appendChild(this.msgEl);
        } else {
            if (typeof this.options.placement !== 'string') { return this }
            this.options.placement = this.options.placement.trim();
            if (this.options.placement === 'down') {
                let box = this.inputEl.closest(`.${prefix}box-input`)?.querySelector('[role="input"]');
                box ? box.insertAdjacentElement('afterEnd', this.msgEl) : null;
            } else if (this.options.placement === 'right') {
                let box = this.inputEl.closest(`.${prefix}box-input`)?.querySelector('[role="help"]');
                box ? box.appendChild(this.msgEl) : null;
            } else if (this.options.placement === 'popup') {
                if (!this.popupIns) {
                    this.popupIns = new Popup(
                        this.targetType.include('cmpt') ? this.targetEl : this.inputEl,
                        {
                            content: this.msgEl,
                            placement: 'bottom-start',
                            keepShow: true,
                            footer: false,
                            tools: false,
                            arrow: {
                                enable: false,
                                gap: 0,
                            },
                            classes: `${prefix}plain`,
                            trigger: 'sticky'
                        });
                }
            } else if (this.options.placement === 'message') {
                if (!this.msgIns) {
                    this.msgIns = new Message(
                        {
                            content: this.msgText,
                            iconShow: true,
                            status: msg.passed ? 'succ' : 'error',
                        }).show();
                } else {
                    this.msgIns.update({
                        content: this.msgText,
                        status: msg.passed ? 'succ' : 'error',
                    }).show();
                }
            } else {
                elState(this.msgEl).isVirtual ? this.targetEl.insertAdjacentElement('afterend', this.msgEl) : null;
            }
        }
    }
    createMsgEl() {
        /**
        * @name Valid.msgEl
        * @description The message node of validation message.The format is:"<div class="_valid"></div>".
        * @type HTMLElement
        */
        this.msgEl = this.hostValid ? this.hostValid.ins.msgEl : createEl(this.options.nodeName, { class: `${prefix}valid` });
    }
    createObs() {
        /**
        * @name Valid.obsProps
        * @description Props of observer.The format is:"{passed,type}".
        * @type object
        */
        this.obsProps = this.hostValid ? this.hostValid.ins.obsProps : {};
        this.observer = new Observe(this.obsProps);
        this.observer.ins.on('complete', () => {
            this.render(this.result);
            super.listen({ name: 'change', params: [this.output] });
        });
    }
    pushValids() {
        if (ax.valids.length === 0) {
            (ax.valids as any[]).push({ parent: this.parentEl, children: [this.valid] });
        } else {
            let index = (ax.valids as any[]).findIndex(k => (k.parent === this.parentEl));
            if (index > -1) {
                let valids = (ax.valids as any[])[index].children,
                    temp = valids.find((k: any) => k.name === this.name);
                !temp ? valids.push(this.valid) : (this.hostValid = temp);
            } else {
                (ax.valids as any[]).push({ parent: this.parentEl, children: [this.valid] });
            }
        }
    }
    /**
     * @description Hidden message el.
     * @param {fn_cb_clear} [cb] - Call back after clear.
     * @returns {*} Return the instance itself
     */
    hide(cb: any): any {
        if (this.destroyed) {
            return this;
        }
        if (this.options.placement === 'message') {
        } else if (this.options.placement === 'popup') {
            this.popupIns.hide();
        } else {
            this.msgEl.remove();
        }
        super.listen({ name: 'hide', cb });
        return this;
    }
    show(cb: any): any {
        if (this.destroyed) {
            return this;
        }
        if (this.otherBox) {
            elState(this.msgEl).isVirtual ? this.otherBox.appendChild(this.msgEl) : null;
        } else {
            if (this.options.placement === 'message') {
                this.msgIns.show();
            } else if (this.options.placement === 'popup') {
                this.popupIns.show();
            } else {
                elState(this.msgEl).isVirtual ? this.targetEl.insertAdjacentElement('afterend', this.msgEl) : null;
            }
        }
        super.listen({ name: 'show', cb });
        return this;
    }
    /**
     * @description Destroy instance and prohibit use.
     * @param {fn_cb_destroy} [cb] - Call back after destroy.
     * @returns {*} Return the instance itself
     */
    destroy(cb?: any): any {
        if (this.destroyed) {
            return this;
        }
        if(['change', 'blur', 'input'].includes(this.options.trigger)){
            if(this.targetType==='cmpt-group'){
                this.inputEl.off('change');
            }else{
                this.inputEl.removeEventListener(this.options.trigger, this.checkFun)
            }
        }
        this.destroyed = true;
        super.listen({ name: 'destroy', cb });
        return this;
    }
}
export default Valid;
