/**  
 * @since Last modified: 2024-09-11 11:42:16
 * @class Spy
 * @description Warnings and tips.
 * @param {getEl} elem - Target element.    
 * @param {object} [options] - Parameters 
 * @see {@link https://codepen.io/axui/pen/mdgyYwO|demo @ codepen}
 * @example
 * ax.Spy('#id');
 */
import getEl from '../utils/getEl';
import PubBase from './PubBase';
import PubListen from './PubListen';
import PubUpdate from './PubUpdate';
import PubCommon from './PubCommon';
import pipe from '../utils/pipe';
import ax from '../ax';
import classes from '../utils/classes';
import { T_spy } from '../../../types/spy';
import { T_el, T_fn, T_obj, T_required, T_sel } from '../../../types/ax';
import elProps from '../utils/elProps';
import optSpy from '../options/optSpy';
import Message from './Message';
import preventDft from '../utils/preventDft';
import scrollTo from '../utils/scrollTo';
import debounce from '../utils/debounce';
import getDataType from '../utils/getDataType';
import allToEls from '../utils/allToEls';
import attrToJson from '../utils/attrToJson';
import renderTpl from '../utils/renderTpl';
import isEmpty from '../utils/isEmpty';
import config from '../global/config';
import prefix from '../global/prefix';
import alias from '../global/alias';
class Spy extends pipe(PubListen, PubCommon)(PubBase) {
    options: Required<T_spy> | T_obj = {};
    destroyed: any;
    plans: any;
    storKeys: any;
    storObs: any;
    storTmp: any;
    rawOpts: any;
    static hostType = 'node';
    constructor(elem: T_sel, options?: T_spy) {
        super();
        this.ready({
            options,
            maps: optSpy,
            host: elem,
            component: false,
        });
        /**
        * @name Spy.targetEl
        * @description Target element.
        * @type getEl
        */
        /**
        * @name Spy.rawOpts
        * @description Raw Parameters of the instance from map.
        * @type object
        */
        /**
         * @name Spy.dftOpts
         * @description Default Parameters of the instance from merging.The reset method requires this variable.
         * @type object
         */
        /**
         * @name Spy.options
         * @description Parameters of the instance.
         * @type object
         */
        /**
         * @name Spy.destroyed
         * @description Destruction state.
         * @type boolean
         */
        /**
        * @name Spy.plans
        * @description Plans for listening.
        * @type object
        */
        /**
        * @name Spy.timestamp
        * @description Instance creation time.
        * @type number
        */
        this.navEl = this.options.navSel ? getEl(this.options.navSel) : null;
        this.subEls = this.navEl && this.options.subSel ? this.navEl.querySelectorAll(this.options.subSel) : [];
        this.hide2Debounce = debounce((el: T_el) => {
            if (el.spy.state === 'hidden') {
                if (el.spy?.out) {
                    classes.add(el, el.spy?.out);
                }
                if (el.spy?.in) {
                    classes.remove(el, el.spy?.in);
                    !this.options.visible && classes.add(el, `${prefix}o`);
                }
                if (this.active) {
                    el.spy.wing.forEach((k: any) => {
                        this.actIsAttr ? k.removeAttribute(this.active[0]) : classes.remove(k, this.active);
                    });
                }
            }
        }, this.options.intvl);
        this.rootEl = getEl(this.options.root);
        this.getActive();
        super.listen({ name: 'ready' });
        this.init();
    }
    /**
    * @description Initialization instance.
    * @param {fn_cb_none} [cb] - Callback execution after initialization.
    * @returns {*} Return the instance itself.
    */
    async init(cb?: T_fn): Promise<any> {
        super.listen({ name: 'launch' });
        this.options.b4Init && await this.options.b4Init.call(this);
        this.items = [];
        this.interactIns && this.interactIns.disconnect();
        this.interactIns = new IntersectionObserver(entries => {
            entries.forEach(i => {
                let args: T_obj = { target: i.target, ratio: i.intersectionRatio, ins: this.interactIns, entry: i };
                if (i.isIntersecting) {
                    args.intersecting = true;
                    if (i.intersectionRatio === 0) {
                        (i.target as T_obj).spy.state = 'show';
                        super.listen({ name: 'show', params: [args] });
                    } else if (i.intersectionRatio === 1) {
                        (i.target as T_obj).spy.state = 'shown';
                        super.listen({ name: 'shown', params: [args] });
                        this.setActiveOnly(i.target);
                    } else {
                        (i.target as T_obj).spy.state = 'showing';
                        super.listen({ name: 'showing', params: [args] });
                    }
                    if ((i.target as T_obj).spy.ratio < i.intersectionRatio) {
                        super.listen({ name: 'in', params: [args] });
                        !this.options.visible && classes.remove(i.target, `${prefix}o`);
                        if ((i.target as T_obj).spy.state = 'showing') {
                            if ((i.target as T_obj).spy?.in) {
                                classes.add(i.target, (i.target as T_obj).spy?.in, (name: string) => i.target.classList.contains(name) ? false : true);
                            }
                        }
                        !this.options.repeat && this.interactIns.unobserve(i.target);
                    }
                    (i.target as T_obj).spy.ratio = i.intersectionRatio;
                } else {
                    args.intersecting = false;
                    (i.target as T_obj).spy.state = 'hidden';
                    if (this.options.repeat) {
                        this.hide2Debounce.cancel()(i.target);
                    }
                    super.listen({ name: 'out', params: [args] });
                    (i.target as T_obj).spy.ratio = 0;
                }
            });
        }, {
            root: this.rootEl,
            rootMargin: this.options.margin,
            threshold: this.options.threshold,
        });
        if (this.options.spySel) {
            this.items = [...this.targetEl.querySelectorAll(this.options.spySel)];
        } else {
            this.targetEl && this.items.push(this.targetEl);
        }
        if (this.options.b4Add) {
            let resp = await this.options.b4Add.call(this, this.items);
            resp && (this.items = resp);
        }
        if (!this.options.manual && this.items.length) {
            this.items.forEach((k: any) => {
                this.setSpyAttr(k);
                this.interactIns.observe(k);
            });
        }
        super.listen({ name: 'init', cb });
        return this;
    }
    getActive() {
        let activeTrim = this.options.active.trim();
        if (activeTrim.startsWith('[') && activeTrim.endsWith(']')) {
            this.actIsAttr = true;
            this.active = activeTrim.replace('[', '').replace(']', '').replace(' ', '').split('=');
        } else {
            this.actIsAttr = false;
            this.active = this.options.active;
        }
    }
    setActiveOnly(el: T_el) {
        if (!isEmpty(this.active)) {
            el.spy.wing.forEach((k: any) => {
                if (this.actIsAttr) {
                    if (this.active.length > 0) {
                        k.setAttribute(this.active[0], this.active[1] || '');
                    }
                } else {
                    classes.add(k, this.active, (name: string) => {
                        return k.classList.contains(name) ? false : true;
                    });
                }
            });
            let other = this.items.filter((k: any) => k !== el && k.spy),
                otherWings = other.map((k: any) => k.spy.wing).flat(Infinity);
            otherWings.forEach((k: any) => {
                this.actIsAttr ? k.removeAttribute(this.active[0]) : classes.remove(k, this.active);
            });
        }
    }
    setSpyAttr(el: T_el) {
        if (!getDataType(el).includes('HTML')) return;
        let attrs: T_obj = attrToJson(el, 'spy');
        el.spy = {
            observed: true,
            wing: this.getWings(el),
            in: attrs.in || this.options.in,
            out: attrs.out || this.options.out,
            ratio: 0,
        }
        this.options.classes && classes.add(el, this.options.classes);
        !this.options.visible && classes.add(el, `${prefix}o`);
    }
    getWings(el: T_el) {
        let arrFromId = [], arrFromSpy: T_el[] = [], arrFromOpt: T_el[] = [], wingArr = [];
        if (el.id) {
            if (this.subEls.length > 0 && this.subEls[0].hasAttribute('href')) {
                let navWing = this.subEls.find((k: any) => k.getAttribute('href') === `#${k.id}`);
                navWing && arrFromId.push(navWing) ;
            } else {
                arrFromId = [...document.querySelectorAll(`[href='#${el.id}']`)];
            }
        } else {
            let index = this.items.findIndex((k: any) => k == el);
            index !== -1 && this.subEls[index] ? arrFromId.push(this.subEls[index]) : null;
        }
        if (el.hasAttribute('spy')) {
            let attrs: T_obj = attrToJson(el, 'spy');
            if (attrs.wing) {
                arrFromSpy = allToEls(attrs.wing);
            }
        }
        if (this.options.wing.selector) {
            arrFromOpt = allToEls(this.options.wing.selector);
        }
        wingArr = [...arrFromId, ...arrFromSpy, ...arrFromOpt].filter(Boolean);
        wingArr.forEach(k => {
            k.onclick = (e: any) => {
                preventDft(e);
                scrollTo(el, { root: this.rootEl, duration: this.options.wing.duration, offset: this.options.wing.offset });
            }
        });
        return wingArr;
    }
    getEls(data: any) {
        let dataType = getDataType(data),
            els = [];
        if (dataType === 'Array') {
            els = data.map((k: any) => getEl(k)).filter(Boolean);
        } else if (dataType === 'String') {
            els = allToEls(data).filter(Boolean);
        }
        return els;
    }
    async add(data: any, cb: T_fn) {
        if (this.destroyed) return;
        let els = this.getEls(data);
        if (!els.length) return this;
        if (this.options.b4Add) {
            let resp = await this.options.b4Add.call(this, els);
            resp && (els = resp);
        }
        els.forEach((i: any) => {
            if (!i.spy) {
                this.setSpyAttr(i);
                this.interactIns.observe(i);
                !this.items.includes(i) && this.items.push(i) ;
            } else {
                new Message({
                    content: renderTpl(config.lang.spy.isObserved, { src: i.dataset.src || i.src }),
                    iconShow: true,
                    status: 'info',
                }).show();
            }
        });
        super.listen({ name: 'add', cb, params: [els] });
        return this;
    }
    async remove(data: any, cb: T_fn) {
        if (this.destroyed) return;
        let els = this.getEls(data);
        this.options.b4Remove && await this.options.b4Remove.call(this, els);
        els.forEach((i: any) => {
            if (this.items.includes(i)) {
                if (i.spy) {
                    Reflect.deleteProperty(i, 'spy');
                    this.interactIns.unobserve(i);
                } else {
                    new Message({
                        content: renderTpl(config.lang.spy.isUnobserved, { src: i.dataset.src || i.src }),
                        iconShow: true,
                        status: 'info',
                    }).show();
                }
            }
        });
        super.listen({ name: 'remove', cb, params: [els] });
        return this;
    }
    /**
     * @description Destroy instance and prohibit use.
     * @param {fn_cb_none} [cb] - Callback execution after destruction.
     * @returns {any} - Return the instance itself.
     */
    destroy(cb?: any): any {
        if (this.destroyed) return;
        this.interactIns.disconnect();
        this.items.forEach((k: any) => {
            k.spy.wing.forEach((i: any) => {
                i.onclick = null;
            });
        });
        this.destroyed = true;
        super.listen({ name: 'destroy', cb });
        return this;
    }
}
export default Spy;
