import isEmpty from '../utils/isEmpty';
import createEl from '../utils/createEl';
import PubBase from './PubBase';
import PubListen from './PubListen';
import PubUpdate from './PubUpdate';
import PubCommon from './PubCommon';
import pipe from '../utils/pipe';
import optVirtualize from '../options/optVirtualize';
import prefix from '../global/prefix';
import classes from '../utils/classes';
import toPixel from '../utils/toPixel';
import Observe from './Observe';
import getDataType from '../utils/getDataType';
import getContent from '../utils/getContent';
import tplToEls from '../utils/tplToEls';
import Spy from './Spy';
import debounce from '../utils/debounce';
import style from '../utils/style';
import transformTools from '../utils/transformTools';
import clampVal from '../utils/clampVal';
import getElSpace from '../utils/getElSpace';
class Virtualize extends pipe(PubListen, PubUpdate, PubCommon)(PubBase) {
    constructor(elem, options, initial = true) {
        super();
        this.options = {};
        this.ready({
            options,
            maps: optVirtualize,
            host: elem,
            component: false,
            spread: ['spy']
        });
        if (this.options.axis === 'x') {
            this.propsMap = { position: 'left', axis: 'x', overflow: 'overflowX', client: 'clientX', content: 'clientWidth', size: 'width', scroll: 'scrollLeft', dim: 'offsetWidth', index: 4, offset: 'offsetLeft', gap: 'marginLeft', reverse: { size: 'height', gap: 'marginTop' } };
        }
        else if (this.options.axis === 'y') {
            this.propsMap = { position: 'top', axis: 'y', overflow: 'overflowY', client: 'clientY', content: 'clientHeight', size: 'height', scroll: 'scrollTop', dim: 'offsetHeight', index: 5, offset: 'offsetTop', gap: 'marginTop', reverse: { size: 'width', gap: 'marginLeft' } };
        }
        this.contReq = {
            content: this.options.content,
            contType: this.options.contType,
            contData: this.options.contData,
            ajax: {
                error: (resp) => {
                    super.listen({ name: 'error', params: [resp] });
                },
                xhrName: 'contXhr',
                ...this.options.ajax,
            },
        };
        this.autoRenderEvt = () => {
            this.renderFromStart();
        };
        this.obsRenderEvt = debounce(() => {
            this.initialized && this.renderFromStart();
        });
        this.avgSize = Math.max(parseInt(style(document.body).lineHeight), toPixel(this.options.size));
        super.listen({ name: 'ready' });
        initial && this.init();
    }
    async init(cb) {
        super.listen({ name: 'launch' });
        this.options.b4Init && await this.options.b4Init.call(this);
        this.setAttrs();
        this.targetSize = this.targetEl[this.propsMap.content];
        this.useTpl();
        this.getNodesTree();
        await this.getInitData();
        this.setSizesObs();
        this.setSpy();
        this.targetEl.addEventListener("scroll", this.autoRenderEvt, false);
        this.itemObs = new MutationObserver(this.obsRenderEvt);
        this.targetObs = new ResizeObserver(this.obsRenderEvt);
        this.targetObs.observe(this.targetEl);
        this.renderFromStart(this.options.index);
        super.listen({ name: 'init', cb });
        return this;
    }
    setAttrs() {
        this.targetEl.classList.add(`${prefix}virt`);
        this.targetEl.setAttribute('axis', this.options.axis);
        classes.add(this.targetEl, this.options.classes);
    }
    getAvgArr(len) {
        return Array(len).fill(null).map(() => { return { size: this.avgSize, rendered: false }; });
    }
    setSizesObs() {
        this.sizesObs = new Observe(this.getAvgArr(this.content.length), {
            deep: true,
            onComplete: () => {
                if (this.itemSizes.findIndex((k) => !k.rendered) < 0) {
                    super.listen({ name: 'exhausted', params: [this.content] });
                }
            }
        });
        this.itemSizes = this.sizesObs.proxy;
    }
    setSpy() {
        this.startSpyIns = new Spy(this.startSpaceEl, {
            onIn: () => {
                super.listen({ name: 'toStart' });
            },
        });
        this.endSpyIns = new Spy(this.endSpaceEl, {
            onIn: () => {
                super.listen({ name: 'toEnd' });
            },
        });
    }
    setNodeMutation(el) {
        this.itemObs.observe(el, {
            childList: true,
            subtree: true,
            attributes: true,
        });
    }
    getNodesTree() {
        let tmpWrap = this.targetEl.querySelector(`.${prefix}virt-wrap`);
        if (tmpWrap) {
            this.wrapEl = tmpWrap;
        }
        else {
            this.wrapEl = createEl(this.options.names.wrap, { class: `${prefix}virt-wrap` });
            this.targetEl.appendChild(this.wrapEl);
        }
        let tmpCont = this.wrapEl.querySelector(`.${prefix}virt-cont`);
        if (tmpCont) {
            this.contEl = tmpWrap;
        }
        else {
            this.contEl = createEl(this.options.names.cont, { class: `${prefix}virt-cont` });
            this.wrapEl.appendChild(this.contEl);
        }
        let tmpList = this.contEl.querySelector(`.${prefix}virt-list`);
        if (tmpList) {
            this.wrapEl = tmpList;
        }
        else {
            this.listEl = createEl(this.options.names.list, { class: `${prefix}virt-list` });
            this.contEl.appendChild(this.listEl);
        }
        this.startSpaceEl = createEl('div', { class: `${prefix}virt-space` }, `${this.options.lang.preload}`);
        this.endSpaceEl = createEl('div', { class: `${prefix}virt-space` }, `${this.options.lang.preload}`);
        this.contEl.insertAdjacentElement('afterbegin', this.startSpaceEl);
        this.contEl.appendChild(this.endSpaceEl);
    }
    getWrapSize() {
        return this.options.immutable ? this.avgSize * this.itemSizes.length : this.itemSizes.reduce((sum, cur) => sum + cur.size, 0);
    }
    get2SetWrapSize() {
        this.wrapSize = this.getWrapSize();
        this.wrapEl.style[this.propsMap.size] = this.wrapSize + "px";
    }
    async getInitData() {
        let resp = await this.getData();
        this.content = resp.content;
    }
    async getData(data, cb) {
        let resp = await getContent.call(this, data ? Object.assign(this.contReq, data) : this.contReq), respType = getDataType(resp), result = { content: [], source: resp };
        this.respSource = resp;
        if (respType === 'Array') {
            result.content = resp;
        }
        else if (respType === 'Object' && resp.hasOwnProperty('data')) {
            result.content = resp.data;
        }
        else if (respType === 'String') {
            result.content = tplToEls(resp);
        }
        super.listen({ name: 'getCont', cb, params: [result] });
        return result;
    }
    getItemNode(data) {
        let dataType = getDataType(data), result;
        if (dataType.includes('HTML')) {
            result = data;
        }
        else {
            result = createEl(this.options.names.item, {});
            result.innerHTML = dataType === 'Object' ? super.getTplcont(data) : data.toString();
        }
        this.setNodeMutation(result);
        return result;
    }
    renderFromStart(start) {
        if (this.destroyed)
            return;
        this.listEl.innerHTML = '';
        this.listSize = 0;
        this.paddingtStart = 0;
        this.startIdx = 0;
        this.endIdx = this.itemSizes.length - 1;
        this.offsetVal = 0;
        this.scrollVal = this.targetEl[this.propsMap.scroll];
        this.nodes = [];
        if (start) {
            this.startIdx = start - 1;
        }
        else {
            if (this.scrollVal) {
                for (let i = 0; i < this.itemSizes.length; i++) {
                    this.paddingtStart += this.itemSizes[i].size;
                    /*  if (!this.itemSizes[i].rendered && this.itemSizes[i - 1]?.rendered) {
                         this.startIdx = i;
                         this.paddingtStart -= this.itemSizes[i].size;
                         this.targetEl[this.propsMap.scroll] = this.paddingtStart;
                         this.scrollVal = this.paddingtStart;
                         break;
                     } else {
  */
                    if (this.paddingtStart > this.scrollVal) {
                        this.startIdx = i;
                        break;
                    }
                    else if (i === this.itemSizes.length - 1 && this.paddingtStart <= this.scrollVal) {
                        return;
                    }
                }
            }
        }
        this.paddingEnd = 0;
        for (let i = this.startIdx; i < this.itemSizes.length; i++) {
            this.paddingEnd += this.itemSizes[i].size;
            if (this.paddingEnd > this.targetSize) {
                this.endIdx = i;
                break;
            }
        }
        this.compensate();
        /* this.sliceData = this.content.slice(this.startIdx, this.endIdx + 1);
        let tmpEls: T_obj = new WeakMap();
        for (let [i, k] of this.sliceData.entries()) {
            let itemEl = this.getItemNode(k);
            tmpEls.set(this.itemSizes[this.startIdx + i], itemEl);
            this.listEl.appendChild(itemEl);
        }
        this.updateSize(tmpEls);
 */
        for (let i = this.startIdx; i <= this.endIdx; i++) {
            this.appendUpdateSize(i, 'beforeend', (size) => {
                this.listSize += size;
            });
        }
        let fillObj = {};
        this.fillBlank(this.listSize, (val) => {
            fillObj = val;
        });
        this.get2SetWrapSize();
        if (start) {
            this.updateScrollVal(start + (fillObj?.indexDiff || 0));
        }
        else {
            fillObj?.padding && (this.paddingtStart -= fillObj.padding);
        }
        this.get2SetContOffset();
        super.listen({ name: 'rendered', params: [{ startIdx: this.startIdx, endIdx: this.endIdx, nodes: this.nodes, scrollVal: this.scrollVal, offsetVal: this.offsetVal, wrapSize: this.wrapSize }] });
    }
    fillBlank(total = this.listSize, cb) {
        let obj = { startIdx: 0, indexDiff: 0, padding: 0 };
        if (total < this.targetSize) {
            for (let i = 1; i < this.content.length; i++) {
                let index = this.startIdx - i;
                if (index < 0)
                    break;
                this.appendUpdateSize(index, 'afterbegin', (size) => {
                    total += size;
                    obj.padding += size;
                });
                if (total >= this.targetSize) {
                    obj.startIdx = index;
                    obj.indexDiff = index - this.startIdx;
                    this.startIdx = index;
                    this.listSize = total;
                    break;
                }
            }
            cb && cb(obj);
        }
    }
    appendUpdateSize(index, dir = 'afterbegin', cb) {
        if (index < 0 || index >= this.content.length)
            return;
        let itemEl = this.getItemNode(this.content[index]);
        this.nodes.push({ el: itemEl, index });
        this.listEl.insertAdjacentElement(dir, itemEl);
        !this.options.immutable && (this.itemSizes[index].size = getElSpace((itemEl), this.options.axis));
        this.itemSizes[index].rendered = true;
        cb && cb(this.itemSizes[index].size);
    }
    renderFromEnd() {
        this.listSize = 0;
        this.listEl.innerHTML = '';
        this.nodes = [];
        this.endIdx = this.content.length - 1;
        for (let i = this.endIdx; i >= 0; i--) {
            this.appendUpdateSize(i, 'afterbegin', (size) => {
                this.listSize += size;
            });
            if (this.listSize > this.targetSize) {
                for (let k = 1; k <= this.options.spill; k++) {
                    this.appendUpdateSize(i - k, 'afterbegin', (size) => {
                        this.listSize += size;
                    });
                }
                this.startIdx = i - this.options.spill;
                break;
            }
        }
        this.get2SetWrapSize();
        this.offsetVal = this.wrapSize - this.listSize;
        this.setContOffset();
        this.setScrollVal(this.wrapSize);
        super.listen({ name: 'rendered', params: [{ startIdx: this.startIdx, endIdx: this.endIdx, nodes: this.nodes, scrollVal: this.scrollVal, offsetVal: this.offsetVal, wrapSize: this.wrapSize }] });
    }
    getPadStartFromIdx(index) {
        if (this.options.immutable) {
            return this.avgSize * index;
        }
        else {
            let result = 0;
            for (let i = 0; i < index; i++) {
                result += this.itemSizes[i].size;
            }
            return result;
        }
    }
    setScrollVal(val = this.paddingtStart) {
        this.targetEl.removeEventListener('scroll', this.autoRenderEvt);
        this.targetEl[this.propsMap.scroll] = val;
        this.scrollVal = val;
        setTimeout(() => {
            this.targetEl.addEventListener('scroll', this.autoRenderEvt, false);
        }, 0);
    }
    updateScrollVal(start) {
        this.paddingtStart = this.getPadStartFromIdx(start);
        this.setScrollVal();
    }
    updateSize(els, start = this.startIdx, end = this.endIdx) {
        for (let i = start; i <= end; i++) {
            !this.options.immutable && (this.itemSizes[i].size = getElSpace(els.get(this.itemSizes[i]), this.options.axis));
            this.itemSizes[i].rendered = true;
        }
    }
    compensate() {
        this.endIdx = clampVal({ val: this.endIdx + this.options.spill, min: 0, max: this.itemSizes.length - 1 });
        this.startIdx = clampVal({ val: this.startIdx - this.options.spill, min: 0, max: this.itemSizes.length - 1 });
    }
    get2SetContOffset() {
        if (!this.startIdx) {
            this.offsetVal = 0;
        }
        else {
            this.offsetVal = this.paddingtStart;
            if (this.options.immutable) {
                this.offsetVal -= this.avgSize * (this.options.spill + 1);
            }
            else {
                for (let i = 0; i <= this.options.spill; i++) {
                    this.offsetVal -= this.itemSizes[this.startIdx + i].size;
                }
            }
        }
        this.setContOffset();
    }
    setContOffset(val = this.offsetVal) {
        transformTools.set({
            el: this.contEl,
            data: {
                translate: { [this.options.axis]: Math.max(0, val) }
            }
        });
    }
    scrollTo(index = 0, cb) {
        if (!index) {
            this.targetEl[this.propsMap.scroll] = 0;
        }
        else {
            this.renderFromStart(index);
        }
        cb && cb.call(this, index);
    }
    toStart() {
        this.scrollTo();
    }
    toEnd() {
        this.renderFromEnd();
    }
    async append(data, cb) {
        if (this.destroyed)
            return;
        let resp = await this.getData(data);
        if (resp.content.length) {
            if (this.options.b4Append) {
                let tmp = await this.options.b4Append.call(this, resp.content);
                tmp && (resp.content = tmp);
            }
            this.content.push(...resp.content);
            this.itemSizes.push(...this.getAvgArr(resp.content.length));
            this.renderFromStart();
            super.listen({ name: 'append', cb, params: [resp] });
        }
        return this;
    }
    async updateItem(data = { index: 0, override: true }) {
        if (this.destroyed)
            return;
        let item = this.content[data.index], itemType = getDataType(item), contType = getDataType(data.content);
        if (itemType.includes('HTML')) {
            data.override && (item.innerHTML = '');
            if (contType.includes('HTML')) {
                item.appendChild(data.content);
            }
            else {
                item.insertAdjacentHTML('beforeend', data.content.toString());
            }
        }
        else if (['String', 'Number'].includes(itemType) && ['String', 'Number'].includes(contType)) {
            data.override && (this.content[data.index] = '');
            this.content[data.index] += (data.content + '');
        }
        else if (itemType === 'Object' && contType === 'Object') {
            data.override && (this.content[data.index] = {});
            Object.assign(this.content[data.index], data.content);
        }
        if (this.options.b4UpdateItem) {
            let resp = await this.options.b4UpdateItem.call(this, this.content[data.index]);
            resp && (this.content[data.index] = resp);
        }
        if (itemType.includes('HTML') && !isEmpty(data.attrs)) {
            for (let k in data.attrs) {
                item.setAttribute(k, data.attrs[k]);
            }
        }
        this.obsRenderEvt.cancel();
        this.obsRenderEvt();
        super.listen({ name: 'updateItem', cb: data.cb, params: [this.content[data.index]] });
    }
    async clear(cb) {
        if (this.destroyed)
            return;
        this.options.b4Clear && await this.options.b4Clear.call(this);
        this.content = [];
        this.itemSizes = [];
        this.renderFromStart();
        super.listen({ name: 'clear', cb });
        return this;
    }
    /**
     * @description Destroy instance.Prohibit use.
     * @param {fn_cb_destroy} cb - Call back after destroy.
     * @returns {*} Return the instance itself
     */
    destroy(cb) {
        if (this.destroyed)
            return;
        this.targetEl.removeEventListener("scroll", this.autoRenderEvt);
        this.startSpyIns.destroy();
        this.endSpyIns.destroy();
        this.itemObs.disconnect();
        this.targetObs.unobserve(this.targetEl);
        this.contXhr && this.contXhr.abort();
        this.destroyed = true;
        super.listen({ name: 'destroy', cb });
        return this;
    }
}
Virtualize.hostType = 'node';
export default Virtualize;
