/**  
 * @since Last modified: 2024-09-10 11:21:12
 * @class Retrieval
 * @description A bubble attached to a target node.
 * @param {getEl} elem - Target element.    
 * @param {object} [options] - Parameters 
 * 
 * @param {string} [options.insName] - The name of the instance, which can be retrieved through the "ax.instance.find" method after filling in, it should be a unique value.
 * @param {string} [options.storName] - The name of the cache, after filling in, it can automatically save the parameters of this instance, and refresh the page can also keep the original state, it should be a unique value.
 * @param {string[]} [options.storKeys=[]] - The fields of the cache, property set from options, default is null that is, automatically save the whole options object, you can customize the property set of options that need to be saved by this parameters, for example: storKeys:['content','placement']
 * @param {breakpoints} [options.breakpoints={}] - The breakpoints settings, set options for different terminals or different screen sizes, please refer to the function: "ax.breakpoints".
 * @param {object} [options.lang={}] - The module language packs, after filling in, will override the corresponding module language packs under "config.lang", through "config.lang" to see the language items that can be set in this module.
 * @param {function} [options.onReady=null] - The listener function, which is executed after completion of the constructor and before initialization begins, supports one parameter i.e. the callback function, which has no parameters.
 * @param {function} [options.onStart=null] - The listener function, after the completion of the constructor and just after the initialization of the execution, supports one parameter i.e. the callback function, which has no parameters.
 * @param {function} [options.onInit=null] - The listener function, executed after the completion of the initialization, supports one parameter i.e. the callback function, which has no parameters.
 * @param {function} [options.onDestroy=null] - The listener function, executed after the destruction of the instance, supports one parameter i.e. callback function, which has no parameters.
 * @param {function} [options.onReset=null] - The listener function,executed after resetting the parameters, supports one parameter i.e. callback function, which has no parameters.
 * @param {function} [options.onUpdate=null] - The listener function, which executes after updating the parameters, supports one two parameters: the user parameters and the callback function, which has one parameter i.e. the user parameters.
 * @param {function} [options.onSave=null] - The listener function, which executes after saving the parameters, supports one parameter i.e. the callback function which has one parameter i.e. the parameter being saved.
 * 
 * @param {string} [options.heading=''] - The retrievaltitle, optional, if not fill in the title node will not be displayed.
 * @param {boolean} [options.divider=false] - If or not show the split line between title and content, default is false that is not show, can use true that is show. 
 * @param {'top-start'|'top-center'|'top-end'|'bottom-start'|'bottom-center'|'bottom-end'|'left-start'|'left-center'|'left-end'|'right-start'|'right-center'|'right-end'|'top-max'|'bottom-max'|'left-max'|'right-max'|'max'|'center'|'left'|'right'|'top'|'bottom'} [options.placement='top'] - The display position of the retrieval.
 * @param {'sm'|'md'|'lg'|'xl'|'xxl'} [options.size='sm'] - The retrievalwidth.
 * @param {number} [options.duration=0] - The animation duration to open and close the retrieval, if it is 0 then use the animation duration defined in css, in ms.
 * @param {boolean} [options.asleep=false] - Does the retrievalnot close after opening.
 * @param {classes} [options.classes=''] - The classes of the retrieval, can fill in more than one, write the same "ax.classes.add"
 * @param {string|boolean} [options.padding=true] - Whether or not to show the inner padding of the retrieval, can be a boolean value or a class name, default with padding, can be filled with false,true,"_p","_p-xxl", etc.
 * @param {getContent} [options.content=''] - The retrievalcontent, can be filled with a string (#id or html),a node,a json,an object,an array,a function name,a Promise or an AsyncFunction, the same as "getContent" function.
 * @param {'text'|'html'|'image'|'video'|'audio'|'iframe'|'form'|'async'|'auto'} [options.contType='text'] - Specify the type of retrievalcontent.If you fill in "auto", it means that the content is not text, and the system will automatically determine the content type, including "Function", "Promise", "AsyncFunction", "Object", "Array" and "HTMLElement".
 * @param {'post'|'get'} [options.ajaxType='post'] - The asynchronous submission method.
 * @param {object} [options.contData={}] - This parameter can be filled in if "contType" is "async" and "dataType" is "post"; this parameter can be used if you need to append attributes to the media node.
 * @param {string} [options.actClass='_opened'] - The class name added by target after opening the retrieval.
 * @param {string} [options.tplStr=''] - If the data obtained is an object or an array, the content needs to be created automatically by the template, the parameter can be filled with a template id or a complete template text.
 * @param {function|null} [options.tplEng=null] - If or not using a third-party template engine, fill in "template" for Art-template engine, "mustache.render" for Mustache engine, "_.template" for Underscore engine, "ejs.render" for EJS engine.
 * @param {number} [options.zIndex=0] - The retrievallevel, the default is 0 that is, not set, if with other components occurs to obscure please set your own.
 * @param {object} [options.position={}] - The parameters of the positioning engine "ax.Position".
 * @param {object} [options.media={}] - The media data, used when contType is "image", "video", "audio" or "iframe".
 * @param {object} [options.media.caption] - The media caption/title.
 * @param {object} [options.media.brief] - The media brief/description.Multi-line text is allowed.
 * @param {object} [options.wing] - Setting up additional buttons to open the retrieval.
 * @param {string} [options.wing.actClass=''] - The class name added by wing node after opening the retrieval.
 * @param {object|boolean} [options.tools=true] - The tools Data(In the upper right corner of the retrieval).
 * @param {boolean} [options.tools.enable=true] - Whether to display the tools.
 * @param {'inside'|'outside'} [options.tools.placement='inside'] - The placement of tools.
 * @param {createTools} [options.tools.children=['close']] - The placement of tools.Write the same as "ax.createTools".
 * @param {object|boolean} [options.footer=true] - The footer Data(In the bottom of the retrieval).
 * @param {boolean} [options.footer.enable=true] - Whether to display the footer.
 * @param {'center'|'plain'|'right'|'left'} [options.footer.layout='plain'] - Buttons of footer Layout.
 * @param {createTools} [options.footer.note=false] - Show or not show note.Write the same as "ax.createTools".
 * @param {boolean} [options.footer.divider=false] - Whether or not to show the splitter line between footer and content.
 * @param {boolean} [options.footer.padding=false] - Whether to show the inner padding of the footer.
 * @param {createTools} [options.footer.children=['cancel', 'confirm']] - The placement of tools.Write the same as "ax.createTools".
 * 
 * @param {function|null} [options.onBeforeFill=null] - The listener function, executed after getting the content and before setting it, takes one argument, content, which may be a string, an object, or an array.
 * @param {function|null} [options.onBeforeShow=null] - The listener function, executed before the retrievalis displayed, no parameters.
 * @param {function|null} [options.onBeforeHide=null] - The listener function, executed before the retrievalis closed, no parameters.
 * @param {function|null} [options.onShow=null] - The listener function, executed after the retrievalis displayed, no parameters.
 * @param {function|null} [options.onHide=null] - The listener function, executed after the retrievalis closed, no parameters.
 * @param {function|null} [options.onValid=null] - The listener function, executed after form validation, support for a parameter that is the result of validation.
 * @param {function|null} [options.onUpdateCont=null] - The listener function, executed after the content is updated (asynchronous behavior), support for a parameter content that is the content of the update.
 * @param {function|null} [options.onChangePosition=null] - The listener function, executed when the retrievalposition is updated, supports one parameter placement.
 * @param {function|null} [options.onTransferPosition=null] - The listener function, executed after the replacement retrievalhost node is updated, supports two parameters i.e. the new host node and placement.
 * @param {function|null} [options.onConfirm=null] - The listener function, executed when the confirm button is clicked, supports one parameter, the button itself.
 * @param {function|null} [options.onCancel=null] - The listener function, executed when the cancel button is clicked, supports one parameter, the button itself.
 * 
 * @see {@link https://codepen.io/axui/pen/yLwbrvm|demo @ codepen}
 * @example
 * ax.Retrieval('#id');
 */
import getEl from '../utils/getEl';
import extend from '../utils/extend';
import isEmpty from '../utils/isEmpty';
import instance from '../utils/instance';
import preventDft from '../utils/preventDft';
import { T_el, T_fn, T_fn_obj, T_obj, T_required, T_sel } from '../../../types/ax';
import contains from '../utils/contains';
import getEls from '../utils/getEls';
import getContent from '../utils/getContent';
import getDataType from '../utils/getDataType';
import Position from './Position';
import createFooter from '../utils/createFooter';
import renderTpl from '../utils/renderTpl';
import tplToEl from '../utils/tplToEl';
import isMobi from '../utils/isMobi';
import style from '../utils/style';
import easeHeight from '../utils/easeHeight';
import PubBase from './PubBase';
import pipe from '../utils/pipe';
import PubListen from './PubListen';
import PubUpdate from './PubUpdate';
import PubCommon from './PubCommon';
import Hover from './Hover';
import createTools from '../utils/createTools';
import ax from '../ax';
import classes from '../utils/classes';
import deepClone from '../utils/deepClone';
import validTools from '../utils/validTools';
import formTools from '../utils/formTools';
import { T_retrieval } from '../../../types/retrieval';
import elState from '../utils/elState';
import { T_footer, T_btn_ft } from '../../../types/utils';
import delay from '../utils/delay';
import setContent from '../utils/setContent';
import optRetrieval from '../options/optRetrieval';
import prefix from '../global/prefix';
import alias from '../global/alias';
import Bubble from './Bubble';
import bulletinTools from '../utils/bulletinTools';
import valToArr from '../utils/valToArr';
import createEl from '../utils/createEl';
import arrSearch from '../utils/arrSearch';
import debounce from '../utils/debounce';
import fieldTools from '../utils/fieldTools';
import getIntArr from '../utils/getIntArr';
import clearRegx from '../utils/clearRegx';
class Retrieval extends pipe(PubListen, PubUpdate, PubCommon)(PubBase) {
    targetEl: any;
    options: T_required<T_retrieval<0>> | T_obj = {};
    destroyed: any;
    plans: any;
    storKeys: any;
    storObs: any;
    storTmp: any;
    dftOpts: any;
    rawOpts: any;
    mainEl: any;
    static hostType = 'none';
    constructor(options: T_retrieval = {}, initial: boolean = true) {
        super();
        super.ready({
            options,
            maps: optRetrieval,
            component: false,
            spread: ['status', 'highlight']
        });
        /**
        * @name Retrieval.targetEl
        * @description Target element.
        * @type getEl
        */
        /**
        * @name Retrieval.rawOpts
        * @description Raw Parameters of the instance from map.
        * @type object
        */
        /**
         * @name Retrieval.dftOpts
         * @description Default Parameters of the instance from merging.The reset method requires this variable.
         * @type object
         */
        /**
         * @name Retrieval.options
         * @description Parameters of the instance.
         * @type object
         */
        /**
         * @name Retrieval.destroyed
         * @description Destruction state.
         * @type boolean
         */
        /**
        * @name Retrieval.plans
        * @description Plans for listening.
        * @type object
        */
        /**
        * @name Retrieval.storKeys
        * @description The set of parameter fields to save.
        * @type array
        */
        /**
         * @name Retrieval.storTmp
         * @description Cache object.The "updateCache" method will update the variable.
         * @type object
         */
        /**
        * @name Retrieval.storObs
        * @description Cache listener(proxy) that listens for "storTmp" changes and automatically updates the local storage.
        * @type any
        */
        /**
        * @name Retrieval.timestamp
        * @description Instance creation time.
        * @type number
        */
        /**
        * @function Retrieval.save
        * @description Storing settings locally.
        * @param {fn_cb_options} [cb] - Callback execution after saving local storage.
        * @returns {any} Return the instance itself
        */
        /**
        * @function Retrieval.reset
        * @description Re-initialization of old parameters.
        * @param {fn_cb_none} [cb] - Call back after reset settings.
        * @returns {*} Return the instance itself
        */
        this.targetEl = getEl(this.options.target);
        this.inputEl = getEl(this.options.editor);
        this.inputType = this.inputEl ? fieldTools.getType(this.inputEl) : '';
        this.renderCount = 0;
        this.statusEl = createEl('div', { class: `${prefix}retrieval-status` });
        this.inputEvent = debounce(() => {
            let keys = this.getInputVals();
            this.updateKeys(keys);
        }, this.options.delay);
        this.inputEvent4Cmpt = () => {
            this.inputEvent();
        }
        super.listen({ name: 'ready' });
        initial && this.init() ;
    }
    /**
    * @description Initialization instance.
    * @param {fn_cb_none} [cb] - Callback execution after initialization.
    * @returns {Promise<any>} - Returns a proimise whose argument is the instance itself.
    */
    async init(cb?: T_fn): Promise<any> {
        super.listen({ name: 'launch' });
        this.options.b4Init && await this.options.b4Init.call(this);
        this.setEmpty();
        this.keys = this.options.keys || this.getInputVals();
        this.useTpl();
        await getContent.call(this, {
            content: this.options.content,
            contType: this.options.contType,
            contData: this.options.contData,
            ajax: {
                xhrName: 'contXhr',
                spinSel: this.targetEl,
                ...this.options.ajax
            },
            cb: (data) => {
                if (isEmpty(data)) return this;
                this.data = this.getStdData(data);
                super.listen({ name: 'standardize', params: [this.data] });
                this.searchData();
                this.render();
            }
        });
        if (elState(this.statusEl).isVirtual && this.options.status.enable) {
            if (this.options.status.placement === 'list-top') {
                this.targetEl.insertAdjacentElement('beforebegin', this.statusEl);
            } else if (this.options.status.placement === 'list-btm') {
                this.targetEl.insertAdjacentElement('afterend', this.statusEl);
            } else if (this.options.status.placement === 'editor-top') {
                this.editorEl && this.editorEl.insertAdjacentElement('beforebegin', this.statusEl);
            } else if (this.options.status.placement === 'editor-btm') {
                this.editorEl && this.editorEl.insertAdjacentElement('afterend', this.statusEl);
            } else {
                let target = getEl(this.options.status.placement);
                target && target.appendChild(this.statusEl);
            }
        }
        this.input2Render();
        super.listen({ name: 'init', cb });
        return this;
    }
    setEmpty() {
        this.targetEl && (this.targetEl.innerHTML = '');
    }
    setAttrs() {
        if (this.targetEl) {
            this.targetEl.classList.add(`${prefix}retrieval`)
            this.options.classes && classes.add(this.targetEl, this.options.classes) ;
        }
    }
    getStdData(data: string | string[] | T_obj[]) {
        let dataType = getDataType(data), result: T_obj[] | void[] = [];
        if (dataType === 'String') {
            let tmp = valToArr(data, this.options.hyphen);
            result = tmp.filter(Boolean).map((k: any) => {
                let obj = { label: k, value: k }
                return obj;
            });
        } else if (dataType === 'Array') {
            if (data.length === 0) return [];
            let childType = getDataType(data[0]);
            if (['String', 'Number'].includes(childType)) {
                result = (data as string[]).filter(Boolean).map((k: any) => {
                    let obj = { label: k + '', value: k + '' }
                    return obj;
                });
            } else if (childType === 'Object') {
                if (data[0].hasOwnProperty('label')) {
                    result = (data as T_obj[]).filter(Boolean).map((k: any) => {
                        !k.hasOwnProperty('value') && (k.value = k.label);
                        return k;
                    });
                } else {
                    console.error(`The object array must contain the "label" property.`);
                    result = [];
                }
            } else {
                console.error(`The items in the object array must be either strings or objects containing the "label" property.`);
                result = [];
            }
        } else {
            console.error(`The data type received is incorrect; a string, string array, or object array is required.`);
            result = [];
        }
        return result;
    }
    searchData(keys: string | string[] = this.keys, data: T_obj[] = this.data) {
        if (this.destroyed) return this;
        if (isEmpty(keys) && this.options.nullHandle === 'reset') {
            this.output = data.map((k: any) => {
                return { source: k, weight: 0, keys: [] }
            });
        } else {
            this.output = arrSearch({
                keys,
                props: this.options.props,
                source: data,
                hyphen: this.options.hyphen,
                fuzzy: this.options.fuzzy,
                ignore: this.options.ignore,
                order: this.options.order,
                limit: this.options.limit
            });
        }
        for (let k of this.output) {
            k.source.el = this.createItemEl(k);
            k.source.action && k.source.action.call(this, k);
        }
        this.updateStatus();
        super.listen({ name: 'search', params: [this.output] });
    }
    useTpl() {
        let tplEl = getEl(this.options.tplStr);
        this.tplEng = this.options.tplEng || renderTpl;
        this.tplStr = tplEl?.innerHTML || this.options.tplStr || bulletinTools.getItemTpl(this.options.list);
    }
    createItemEl(item: T_obj) {
        let tplData = this.options.highlight.enable ? this.getHighlightData(item) : item.source,
            html = super.getTplcont(tplData);
        return tplToEl(html);
    }
    getHighlightData(data: T_obj) {
        if (!this.options.highlight.enable) return data;
        let props = getIntArr([valToArr(this.options.highlight.props), valToArr(this.options.props)]),
            copy = { ...data.source },
            keys = data.keys.map((k: string) => clearRegx(k)).filter(Boolean);
        for (let k in copy) {
            if (props.includes(k) && keys.length !== 0) {
                let regx = new RegExp(`(${keys.join('|')})`, 'gi'),
                    str = `<${this.options.highlight.node} class="${this.options.highlight.classes}">$1</${this.options.highlight.node}>`;
                copy[k] = (copy[k] + '').replace(regx, str);
            }
        }
        return copy;
    }
    async render(data: T_obj[] = this.output) {
        if (this.destroyed) return this;
        isEmpty(data) && console.warn('There is no content to render, but no errors occurred!');
        if (this.options.b4Fill) {
            let resp = await this.options.b4Fill.call(this, data, this.targetEl);
            resp && (data = resp);
        }
        if (this.targetEl) {
            this.targetEl.innerHTML = '';
            data.length > 0 && this.targetEl.append(...data.map((k: any) => k.source.el));
            super.listen({ name: 'render', params: [data, this.targetEl] });
        }
        this.renderCount++;
    }
    updateKeys(keys: string | string[] = this.getInputVals()) {
        if (this.destroyed || keys.toString() == this.keys.toString()) return this;
        this.keys = keys;
        this.searchData();
        this.render();
        super.listen({ name: 'updateKeys', params: [keys] });
    }
    input2Render() {
        if (!this.inputEl || this.destroyed) return this;
        if (this.inputType === 'native') {
            this.inputEl.removeEventListener('input', this.inputEvent);
            this.inputEl.addEventListener('input', this.inputEvent, false);
        } else if (this.inputType.includes('cmpt')) {
            this.inputEl.on('change', this.inputEvent4Cmpt);
        }
    }
    getInputVals() {
        return this.inputEl ? (this.inputEl?.value || this.inputEl?.properties?.value || '') : '';
    }
    resetList() {
        if (this.destroyed) return this;
        this.output = this.data.map((k: any) => {
            return { source: k, weight: 0, keys: [] }
        });
        for (let k of this.output) {
            k.source.el = this.createItemEl(k);
            k.source.action && k.source.action.call(this, k);
        }
        this.render();
    }
    clearList() {
        if (this.destroyed) return this;
        if (this.inputEl) {
            if (this.inputType === 'native') {
                this.inputEl.value = '';
            } else if (this.inputType.includes('cmpt')) {
                this.inputEl.setAttribute('value', '');
            }
        }
        this.keys = '';
        this.output = [];
        this.render();
    }
    updateStatus() {
        let data = { value: this.output.length, keys: this.keys };
        this.statusEl.innerHTML = isEmpty(data.keys) ? this.options.lang.nullKeys : renderTpl(this.options.lang.status, data);
    }
    async updateCont(content: any, cb?: T_fn): Promise<any> {
        if (this.destroyed) return this;
        await getContent.call(this, {
            content,
            contType: this.options.contType,
            contData: this.options.contData,
            ajax: {
                xhrName: 'contXhr',
                spinSel: this.targetEl,
                ...this.options.ajax
            },
            request: (data) => {
                this.listen({ name: 'request', params: [data] });
            },
            cb: (data) => {
                if (isEmpty(data)) return this;
                this.data = this.getStdData(data);
                super.listen({ name: 'standardize', params: [this.data] });
                this.searchData();
                this.render();
                this.updateCache({ content });
                this.listen({ name: 'updateCont', cb, params: [data] });
            }
        });
        return this;
    }
    destroy(cb?: any): any {
        if (this.inputEl) {
            if (this.inputType === 'native') {
                this.inputEl.removeEventListener(this.options.trigger, this.inputEvent);
            } else if (this.inputType.includes('cmpt')) {
                this.inputEl.off('change', this.inputEvent4Cmpt);
            }
        }
        this.contXhr && this.contXhr.abort() ;
        this.destroyed = true;
        super.listen({ name: 'destroy', cb });
        return this;
    }
}
export default Retrieval;
