/**
 * @since Last modified: 2024-09-10 11:20:41
 * @class Tags
 * @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.Tags('#id');
 */
import getEl from '../utils/getEl';
import isEmpty from '../utils/isEmpty';
import createEl from '../utils/createEl';
import deepClone from '../utils/deepClone';
import PubBase from './PubBase';
import PubListen from './PubListen';
import PubUpdate from './PubUpdate';
import PubCommon from './PubCommon';
import pipe from '../utils/pipe';
import classes from '../utils/classes';
import optTags from '../options/optTags';
import prefix from '../global/prefix';
import getContent from '../utils/getContent';
import Observe from './Observe';
import unique from '../utils/unique';
import Message from './Message';
import findItem from '../utils/findItem';
import findItems from '../utils/findItems';
import config from '../global/config';
class Tags extends pipe(PubListen, PubUpdate, PubCommon)(PubBase) {
    constructor(elem, options = {}, initial = config.initial) {
        super();
        this.options = {};
        this.ready({
            options,
            maps: optTags,
            host: elem,
            component: true,
            spread: ['edit', 'empty']
        });
        /**
        * @name Tags.targetEl
        * @description Target element.
        * @type getEl
        */
        /**
        * @name Tags.rawOpts
        * @description Raw Parameters of the instance from map.
        * @type object
        */
        /**
         * @name Tags.dftOpts
         * @description Default Parameters of the instance from merging.The reset method requires this variable.
         * @type object
         */
        /**
         * @name Tags.options
         * @description Parameters of the instance.
         * @type object
         */
        /**
         * @name Tags.destroyed
         * @description Destruction state.
         * @type boolean
         */
        /**
        * @name Tags.plans
        * @description Plans for listening.
        * @type object
        */
        /**
        * @name Tags.storKeys
        * @description The set of parameter fields to save.
        * @type array
        */
        /**
         * @name Tags.storTmp
         * @description Cache object.The "updateCache" method will update the variable.
         * @type object
         */
        /**
        * @name Tags.storObs
        * @description Cache listener(proxy) that listens for "storTmp" changes and automatically updates the local storage.
        * @type any
        */
        /**
        * @name Tags.timestamp
        * @description Instance creation time.
        * @type number
        */
        /**
        * @function Tags.save
        * @description Storing settings locally.
        * @param {fn_cb_options} [cb] - Callback execution after saving local storage.
        * @returns {any} Return the instance itself
        */
        /**
        * @function Tags.reset
        * @description Re-initialization of old parameters.
        * @param {fn_cb_none} [cb] - Call back after reset settings.
        * @returns {*} Return the instance itself
        */
        this.timestamp = Date.now();
        this.maxIndex = 0;
        this.emptyEl = createEl('span', { ref: 'empty' }, this.options.empty.content);
        /**
        * @name Tags.content
        * @description Textual content of the tags, possibly HTML.
        * @type string
        */
        this.content = '';
        this.editEvent = async (e) => {
            let label = this.editEl.value.trim(), selectedItem = this.data.find((k) => k.selected);
            if (label) {
                this.editEl.style.width = label.length + 'em';
                if (this.options.editor.addable && e.code === 'Enter') {
                    this.add(label, () => {
                        this.editEl.value = '';
                        this.editEl.focus();
                    });
                }
                selectedItem && (selectedItem.selected = false);
            }
            else {
                this.editEl.style.cssText = this.editEl.style.cssText.replace('width:', '');
                if (this.options.editor.deletable && e.code === 'Backspace' && this.data.length > 0) {
                    let endItem = this.data.slice(-1)[0];
                    if (endItem.selected) {
                        this.remove(endItem);
                    }
                    else {
                        endItem.selected = true;
                    }
                    this.toggleEmpty();
                }
                else {
                    selectedItem && (selectedItem.selected = false);
                }
            }
        };
        super.listen({ name: 'ready' });
        initial && this.init();
    }
    /**
    * @description Initialization instance.
    * @param {fn_cb_none} [cb] - Callback execution after initialization.
    * @returns {*} Return the instance itself.
    */
    async init(cb) {
        super.listen({ name: 'launch' });
        this.options.b4Init && await this.options.b4Init.call(this);
        this.initContent();
        this.setEmpty();
        this.createEditor();
        await getContent.call(this, {
            content: this.content,
            contType: this.options.contType,
            contData: this.options.contData,
            ajax: {
                xhrName: 'contXhr',
                spinSel: this.targetEl,
                ...this.options.ajax
            },
            cb: (data) => {
                this.renderContent(data);
            }
        });
        this.setAttrs();
        this.renderFinish();
        super.listen({ name: 'init', cb });
        return this;
    }
    initContent() {
        if (this.options.content) {
            this.content = this.options.content;
        }
        else {
            this.content = this.rawHtml ? this.rawHtml : this.targetEl.innerHTML;
        }
    }
    async renderContent(data) {
        if (isEmpty(data)) {
            console.warn('No insertable content!');
        }
        if (this.options.b4Fill) {
            let resp = await this.options.b4Fill.call(this, data, this.targetEl);
            resp && (data = resp);
        }
        this.dataOrig = [];
        this.dataObs = new Observe(this.dataOrig, {
            deep: true,
            onSet: (obj) => {
                if (['selected', 'disabled'].includes(obj.key)) {
                    obj.value ? obj.target.wrapEl.setAttribute(obj.key, '') : obj.target.wrapEl.removeAttribute(obj.key);
                }
                else if (obj.key === 'theme') {
                    obj.target.wrapEl.setAttribute(obj.key, obj.value);
                }
                else if (obj.key === 'label') {
                    obj.target.labelEl.innerHTML = obj.value;
                }
                else if (['href', 'target', 'rel', 'download'].includes(obj.key) && this.labelEl.nodeName === 'A') {
                    obj.target.labelEl[obj.key] = obj.value;
                }
            },
            onComplete: () => {
                let tmp = {
                    content: !isEmpty(this.options.content) ? deepClone(this.dataOrig) : '',
                };
                super.updateCache(tmp);
            }
        });
        this.data = this.dataObs.proxy;
        this.data.push(...this.getSource(data));
        this.render(this.data);
    }
    getSource(obj) {
        let result = [];
        if (!isEmpty(obj)) {
            if (Array.isArray(obj)) {
                if (typeof obj[0] === 'string') {
                    result = obj.map(k => {
                        return { label: k, theme: '' };
                    });
                }
                else {
                    result = obj;
                }
            }
            else if (typeof obj === 'object') {
                result = [obj];
            }
            else if (typeof obj === 'string') {
                let tmp = createEl('div', {}, obj);
                let isA = tmp.querySelector('a');
                if (isA) {
                    [...tmp.children].forEach((k) => {
                        result.push({
                            label: k.innerHTML,
                            href: k.getAttribute('href'),
                            target: k.target,
                            rel: k.rel,
                            download: k.download,
                            disabled: k.hasAttribute('disabled'),
                            theme: '',
                        });
                    });
                }
                else {
                    let arr = obj.trim().split(this.options.hyphen);
                    result = arr.map(k => {
                        return { label: k, theme: '' };
                    });
                }
            }
        }
        result = result.filter(Boolean);
        result.forEach((k, i) => {
            !k.hasOwnProperty('id') ? k.id = this.maxIndex + i : null;
            !k.hasOwnProperty('theme') ? k.theme = '' : null;
        });
        this.maxIndex += result.length;
        return result;
    }
    render(content) {
        this.toggleEmpty();
        if (isEmpty(content)) {
            return false;
        }
        let fragment = document.createDocumentFragment();
        content.forEach(k => {
            k.wrapEl = this.createTag(k);
            fragment.appendChild(k.wrapEl);
        });
        this.targetEl.appendChild(fragment);
        if (this.options.editor.enable && !this.options.editor.selector) {
            this.targetEl.appendChild(this.editEl);
        }
    }
    setAttrs() {
        this.targetEl.classList.add(`${prefix}tags`);
        classes.add(this.targetEl, this.options.classes);
        this.options.theme && this.targetEl.setAttribute('theme', this.options.theme);
        this.options.type && this.targetEl.setAttribute('type', this.options.type);
        this.options.compact && this.targetEl.setAttribute('compact', '');
        this.options.shape && this.targetEl.setAttribute('shape', this.options.shape);
    }
    setEmpty() {
        this.targetEl.innerHTML = '';
    }
    createTag(obj) {
        let labelObj = { ref: 'label' };
        obj.hasOwnProperty('href') && (labelObj.href = obj.href);
        obj.hasOwnProperty('href') && obj.target && (labelObj.target = obj.target);
        obj.hasOwnProperty('href') && obj.rel && (labelObj.rel = obj.rel);
        obj.hasOwnProperty('href') && obj.download && (labelObj.download = obj.download);
        obj.labelEl = createEl(labelObj.hasOwnProperty('href') ? 'a' : 'i', labelObj, obj.label);
        obj.wrapEl = createEl('span', { class: `${prefix}tag` }, obj.labelEl);
        obj.removeEl = createEl('i', { ref: 'remove' });
        this.options.removable && obj.wrapEl.appendChild(obj.removeEl);
        !this.options.theme && obj.theme && obj.wrapEl.setAttribute('theme', obj.theme);
        obj.disabled && obj.wrapEl.setAttribute('disabled', '');
        obj.selected && obj.wrapEl.setAttribute('selected', '');
        return obj.wrapEl;
    }
    createEditor() {
        if (!this.options.editor.enable || this.destroyed) {
            return false;
        }
        let tmp = getEl(this.options.editor.selector);
        if (!tmp) {
            this.editEl = createEl('input', { type: 'text', placeholder: this.options.editor.placeholder });
        }
        else {
            this.editEl = tmp;
            !this.editEl.hasAttribute('placeholder') && this.editEl.setAttribute('placeholder', this.options.editor.placeholder);
        }
        this.editEl.removeEventListener('keyup', this.editEvent);
        this.editEl.addEventListener('keyup', this.editEvent);
    }
    toggleEmpty() {
        if (!this.options.empty.enable) {
            return this;
        }
        if (this.data.length === 0) {
            this.targetEl.insertAdjacentElement('afterbegin', this.emptyEl);
            this.maxIndex = 0;
        }
        else {
            this.emptyEl.remove();
        }
    }
    renderFinish() {
        this.data.forEach((k) => {
            k.wrapEl.onclick = () => {
                k.selected = true;
                this.data.filter((i) => i !== k).forEach((i) => {
                    i.selected = false;
                });
            };
            k.removeEl.onclick = () => {
                this.remove(k);
            };
        });
        this.last = this.dataOrig[this.data.length - 1];
    }
    /**
         * @description Update the contents of the POPUP, which will change "options.content".
         * @param {fn_cb_data} [cb] - Callback execution after updating contents.
         * @returns {Promise<any>} - Returns a proimise whose argument is the instance itself.
         */
    async updateCont(content, cb) {
        if (this.destroyed) {
            return this;
        }
        this.setEmpty();
        await getContent.call(this, {
            content,
            contType: this.options.contType,
            contData: this.options.contData,
            ajax: {
                xhrName: 'contXhr',
                spinSel: this.targetEl,
                ...this.options.ajax
            },
            cb: (data) => {
                this.renderContent(data);
                super.updateCache({ content });
                super.listen({ name: 'updateCont', cb, params: [this.dataOrig] });
            }
        });
        return this;
    }
    async add(data, cb) {
        if (isEmpty(data) || this.destroyed) {
            return this;
        }
        let arr = this.getSource(data), dupTags = [], oldLen = 0, oldValues = this.data.map((k) => k.label);
        if (this.options.unique) {
            arr = unique(arr, 'label');
            oldLen = arr.length;
            arr = arr.map((k) => {
                let output;
                if (oldValues.includes(k.label)) {
                    dupTags.push(k);
                }
                else {
                    output = k;
                }
                return output;
            }).filter(Boolean);
            if (oldLen !== arr.length) {
                new Message({
                    content: arr.length === 0 ? config.lang.tags.includeFull : config.lang.tags.includePart,
                    status: 'warn',
                    iconShow: true,
                }).show();
                super.listen({ name: 'duplicate', params: [dupTags] });
            }
        }
        if (arr.length === 0) {
            return this;
        }
        this.options.b4Add && await this.options.b4Add.call(this, arr);
        this.data.push(...arr);
        this.render(arr);
        this.renderFinish();
        super.listen({ name: 'add', cb, params: [arr] });
        return this;
    }
    async remove(data, cb) {
        if (isEmpty(data) || this.destroyed) {
            return this;
        }
        let tags = findItems(data, this.data);
        this.options.b4Remove && await this.options.b4Remove.call(this, tags);
        tags.forEach((k) => {
            k.wrapEl.remove();
        });
        this.data.forEach((k, i) => {
            tags.includes(k) && this.data.splice(i--, 1);
        });
        this.toggleEmpty();
        super.listen({ name: 'remove', cb, params: [tags] });
        return this;
    }
    async edit(item, data, cb) {
        if (isEmpty(item) || this.destroyed) {
            return this;
        }
        let tag = findItem(item, this.data);
        if (!tag) {
            return this;
        }
        this.options.b4Edit && await this.options.b4Edit.call(this, tag);
        Object.assign(tag, data);
        super.listen({ name: 'edit', cb, params: [tag] });
        return this;
    }
    async clear(cb) {
        if (this.destroyed) {
            return this;
        }
        this.options.b4Clear && await this.options.b4Clear.call(this);
        this.data.splice(0);
        this.maxIndex = 0;
        this.targetEl.innerHTML = '';
        this.options.editor.enable && this.targetEl.appendChild(this.editEl);
        this.toggleEmpty();
        super.listen({ name: 'clear', cb });
        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) {
        this.data.forEach((k) => {
            k.wrapEl.onclick = null;
            k.removeEl.onclick = null;
        });
        this.editEl.removeEventListener('keyup', this.editEvent);
        this.destroyed = true;
        super.listen({ name: 'destroy', cb });
        return this;
    }
}
Tags.hostType = 'node';
export default Tags;
