import ax from "../ax";
import prefix from "../global/prefix";
import optScroll from "../options/optScroll";
import createEl from "../utils/createEl";
import debounce from "../utils/debounce";
import extend from "../utils/extend";
import getDataType from "../utils/getDataType";
import getEl from "../utils/getEl";
import isMobi from "../utils/isMobi";
import pipe from "../utils/pipe";
import transformTools from "../utils/transformTools";
import style from "../utils/style";
import Gesture from "./Gesture";
import PubBase from "./PubBase";
import PubCommon from "./PubCommon";
import PubListen from "./PubListen";
import PubUpdate from "./PubUpdate";
import delay from "../utils/delay";
import clampVal from "../utils/clampVal";
import toNumber from "../utils/toNumber";
class Scroll extends pipe(PubListen, PubUpdate, PubCommon)(PubBase) {
    constructor(elem, options = {}, inital = ax.config.inital) {
        super();
        this.options = {};
        this.ready({
            options,
            host: elem,
            maps: optScroll,
            component: false,
            spread: ['snap', 'drift', 'bar']
        });
        if (this.options.axis === 'x') {
            this.propsMap = { position: 'left', axis: 'x', overflow: 'overflowX', client: 'clientX', size: 'width', index: 4, offset: 'offsetLeft' };
        }
        else if (this.options.axis === 'y') {
            this.propsMap = { position: 'top', axis: 'y', overflow: 'overflowY', client: 'clientY', size: 'height', index: 5, offset: 'offsetTop' };
        }
        this.wrapEl = this.options.wrapper ? getEl(this.options.wrapper) : this.targetEl.firstElementChild;
        if (!this.wrapEl)
            return this;
        this.snappeds = [];
        this.unsnappeds = [];
        this.snapped = null;
        this.started = false;
        this.scrolled = true;
        super.listen({ name: 'ready' });
        inital ? this.init() : null;
    }
    /**
    * @description Initialization instance.
    * @param {fn_cb_none} [cb] - Callback execution after initialization.
    * @returns {*} Return the instance itself.
    */
    init(cb) {
        super.listen({ name: 'start' });
        this.setEmpty();
        this.setAttrs();
        this.transNow = this.transLast = this.options.paddingStart;
        this.createBar();
        this.updateDiff();
        this.initSnapArgs();
        this.getSnappeds();
        this.gestureIns = new Gesture(this.wrapEl, extend({
            target: {
                wheel: {
                    enable: true,
                    mode: 'translate',
                    axis: this.options.axis,
                    value: this.options.wheel.value,
                    linkage: false,
                    duration: this.options.duration,
                },
                drift: {
                    enable: this.options.drift.enable,
                    coef: 1,
                },
                initialVal: { [this.options.axis]: this.transNow },
                onInit: () => { },
                onWheel: (data) => {
                    this.getWheelVal(data.wheel.direction[this.options.axis]);
                },
                onWheeled: (data) => {
                    this.scrollTo({
                        target: data.translate.value[this.options.axis],
                        duration: this.options.duration,
                        snap: true,
                    });
                },
                onTranslate: (data) => {
                    super.listen({ name: 'do', params: [data.translate] });
                },
                onTranslating: (data) => {
                    if (data.translate.diff.h > 2) {
                        this.transNow = data.translate.value[this.propsMap.axis];
                        let cross = this.crossLinkage();
                        if (cross.includes('baby')) {
                            if (data.pointer === 'wheel')
                                return this;
                            if (cross === 'baby-start') {
                                this.transNow = this.getDmpVals('start+');
                            }
                            else if (cross === 'baby-start-reverse') {
                                this.transNow = this.getDmpVals('start-');
                            }
                        }
                        else if (cross === 'teen-start') {
                            if (data.pointer === 'wheel') {
                                this.toStart();
                                return this;
                            }
                            this.transNow = this.getDmpVals('start+');
                        }
                        else if (cross === 'teen-end') {
                            if (data.pointer === 'wheel') {
                                this.toEnd();
                                return this;
                            }
                            this.transNow = this.getDmpVals('end+');
                        }
                        else if (cross === 'adult-start') {
                            if (data.pointer === 'wheel') {
                                this.toStart();
                                return this;
                            }
                            this.transNow = this.getDmpVals('start+');
                        }
                        else if (cross === 'adult-end') {
                            if (data.pointer === 'wheel') {
                                this.toEnd();
                                return this;
                            }
                            this.transNow = this.getDmpVals('end+');
                        }
                        transformTools.set({
                            el: this.wrapEl,
                            data: { translate: { [this.options.axis]: this.transNow } }
                        });
                        this.exceedHandle({ linkage: cross });
                        super.listen({ name: 'doing', params: [data.translate] });
                    }
                },
                onTranslated: (data) => {
                    this.transLast = this.transNow;
                    this.exceedHandle({
                        start: async (ratio) => {
                            this.options.b4ToStart && await this.options.b4ToStart.call(this, ratio);
                            this.toStart();
                            this.driftable = false;
                        },
                        end: async (ratio) => {
                            this.options.b4ToEnd && await this.options.b4ToEnd.call(this, ratio);
                            this.toEnd();
                            this.driftable = false;
                        },
                        normal: () => {
                            this.options.snap.enable && this.setSnapped();
                            this.driftable = true;
                        }
                    });
                    super.listen({ name: 'done', params: [data.translate] });
                },
                onDrift: (data) => {
                    if (this.driftable) {
                        data.drift.value[this.options.axis] = this.getDriftDist(data.translate.direction[this.options.axis], this.getEdgeValue(data.drift.value[this.options.axis]));
                        let target = (this.options.snap.enable && this.options.snap.mode === 'auto') ? this.getLatestSnap(data.drift.value[this.options.axis]).latest : data.drift.value[this.options.axis];
                        this.scrollTo({
                            target,
                            snap: true,
                        });
                    }
                },
            },
            source: this.options.gesture,
        }));
        this.setObserver();
        this.scrollTo({
            target: this.rangeEnd,
            snap: true,
        });
        super.listen({ name: 'init', cb });
        return this;
    }
    initSnapArgs() {
        if (!this.options.snap.enable)
            return this;
        if (this.options.snap.mode === 'auto') {
            !this.options.snap.selector && (this.options.snap.selector = '[unalignable]');
        }
        else {
            !this.options.snap.selector && (this.options.snap.selector = '[alignable]');
            !this.options.snap.offset && (this.options.snap.offset = 50);
        }
    }
    setEmpty() {
    }
    setAttrs() {
        this.targetEl.classList.add(`${prefix}scroll`);
        this.wrapEl.classList.add(`${prefix}scroll-wrap`);
        this.targetEl.setAttribute('axis', this.options.axis);
        this.options.gridded && this.targetEl.setAttribute('gridded', '');
    }
    createBar() {
        if (this.options.bar.enable && !isMobi) {
            if (!this.trackEl) {
                this.thumbEl = createEl('span');
                this.trackEl = createEl('div', { class: `${prefix}scroll-bar` }, this.thumbEl);
                this.options.bar.sticky && this.trackEl.setAttribute('shown', '');
                this.targetEl.appendChild(this.trackEl);
            }
            if (this.options.bar.triggerable) {
                let prog = 0;
                this.barIns = new Gesture(this.trackEl, {
                    onTranslate: () => {
                        prog = this.progress;
                        this.trackEl.setAttribute('scrolling', '');
                    },
                    onTranslating: (data) => {
                        this.locateTo({
                            target: prog + data.translate.value[this.options.axis] / this.barSize.diff,
                            type: 'ratio',
                        });
                    },
                    onTranslated: () => {
                        this.trackEl.removeAttribute('scrolling');
                    }
                });
            }
        }
    }
    setObserver() {
        this.wrapStyleObs = new MutationObserver(mutations => {
            mutations.forEach(k => {
                if (k.type === "attributes") {
                    this.updateProg();
                    this.options.bar.enable && this.updateBarProg();
                }
                if (k.type === "childList") {
                    this.response();
                    this.options.snap.enable && this.getSnappeds();
                    super.listen({ name: 'refactored' });
                }
            });
        });
        this.wrapStyleObs.observe(this.wrapEl, {
            childList: true,
            attributes: true,
            attributeFilter: ['style']
        });
        this.targetSizeObs = new ResizeObserver(debounce((entries) => {
            for (let k of entries) {
                if (!k.contentRect.width && !k.contentRect.height)
                    return;
                this.response();
                super.listen({ name: 'resized', params: [k, this.targetEl] });
            }
        }));
        this.targetSizeObs.observe(this.targetEl);
        this.wrapSizeObs = new ResizeObserver(debounce((entries) => {
            for (let k of entries) {
                if (!k.contentRect.width && !k.contentRect.height)
                    return;
                this.response();
                super.listen({ name: 'resized', params: [k, this.wrapEl] });
            }
        }));
        this.wrapSizeObs.observe(this.wrapEl);
    }
    response() {
        this.updateDiff();
        if (this.isBaby()) {
            this.toStart();
        }
        else if (this.wrapEl[this.propsMap.offset] > this.paddingStart) {
            this.toStart();
        }
        else if (this.transNow < this.rangeStart) {
            this.toEnd();
        }
        else {
            this.options.snap.enable && this.setSnapped();
        }
    }
    getDmpVals(placement) {
        let val = 0, dmp = this.options.dmpRatio ** 2;
        if (placement === 'start+') {
            val = this.options.bounce ? this.paddingStart + (this.transNow - this.paddingStart) * dmp : this.paddingStart;
        }
        else if (placement === 'end+') {
            val = this.options.bounce ? this.rangeStart + (this.transNow - this.rangeStart) * dmp : this.rangeStart;
        }
        else if (placement === 'start-') {
            val = this.options.bounce ? (this.transNow - this.paddingStart) * dmp : this.paddingStart;
        }
        return val;
    }
    updateDiff() {
        let targetStyle = style(this.targetEl), wrapStyle = style(this.wrapEl), targetHeight = parseInt(targetStyle.height), wrapHeight = parseInt(wrapStyle.height), targetWidth = parseInt(targetStyle.width), wrapWidth = parseInt(wrapStyle.width), outerSize = 0, innerSize = 0;
        if (this.options.axis === 'x') {
            outerSize = targetWidth;
            innerSize = wrapWidth;
        }
        else if (this.options.axis === 'y') {
            outerSize = targetHeight;
            innerSize = wrapHeight;
        }
        this.targetSize = outerSize;
        this.wrapSize = innerSize;
        if (Math.abs(this.options.paddingStart) + Math.abs(this.options.paddingEnd) >= outerSize) {
            this.paddingStart = this.paddingEnd = 0;
            console.warn('The combined padding values on both sides exceed the container size; padding values have been removed!');
        }
        else {
            this.paddingStart = Math.abs(this.options.paddingStart);
            this.paddingEnd = Math.abs(this.options.paddingEnd);
        }
        this.rangeEnd = this.paddingStart;
        this.rangeStart = outerSize - innerSize - this.paddingEnd;
        this.activeSize = this.rangeEnd - this.rangeStart;
        this.baseSize = this.targetSize - this.paddingStart - this.paddingEnd;
        this.updateProg();
        this.getBarSize();
        this.updateBarProg();
    }
    getNowTargetDist(target) {
        if (!target)
            return 0;
        let targetType = getDataType(target);
        return targetType === 'Number' ? Math.abs(target - this.transNow) : targetType.includes('HTML') ? Math.abs(this.getOffset(target) - this.rangeEnd + this.transNow) : 0;
    }
    getDriftDist(dir, value) {
        let result = value;
        if (this.options.drift.mode === 'ratio') {
            let dft = this.options.drift.value || 1;
            result = this.transNow + this.baseSize * dft * dir;
        }
        else if (this.options.drift.mode === 'const') {
            this.options.drift.value && (result = this.transNow + this.options.drift.value * dir);
        }
        else if (this.options.drift.mode === 'snap' && this.options.snap.enable && this.options.snap.mode === 'auto') {
            let idx = this.snappeds.findIndex((k) => k === this.snapped);
            if (idx > -1) {
                let dist = dir === 1 ? this.getNowTargetDist(this.snappeds[idx + 1]) : dir === -1 ? this.getNowTargetDist(this.snappeds[idx - 1]) * -1 : 0;
                result = this.transNow + dist;
            }
        }
        return result;
    }
    getWheelVal(dir) {
        if (this.options.wheel.mode === 'ratio') {
            let dft = this.options.wheel.value || 0.25, val = this.baseSize * dft;
            this.gestureIns.wheelVal = val;
        }
        else if (this.options.wheel.mode === 'const') {
            this.options.wheel.value && (this.gestureIns.wheelVal = this.options.wheel.value);
        }
        if (this.options.wheel.mode === 'snap' && this.options.snap.enable && this.options.snap.mode === 'auto') {
            let idx = this.snappeds.findIndex((k) => k === this.snapped);
            if (idx > -1) {
                this.gestureIns.wheelVal = dir === 1 ? this.getNowTargetDist(this.snappeds[idx + 1]) : dir === -1 ? this.getNowTargetDist(this.snappeds[idx - 1]) : 0;
            }
        }
    }
    getEdgeValue(data, type = 'translate') {
        let value = this.rangeEnd;
        if (type === 'ratio') {
            let ratio = clampVal({ min: 0, max: 1, val: data });
            value = this.rangeEnd - this.activeSize * ratio;
        }
        else if (type === 'el') {
            let el = getEl(data);
            el && (value = -this.getOffset(el) + this.rangeEnd);
        }
        else {
            value = data;
        }
        if (value || value === 0) {
            if (value >= this.rangeEnd || this.isBaby()) {
                value = this.rangeEnd;
            }
            else if (value <= this.rangeStart) {
                value = this.rangeStart;
            }
        }
        return value;
    }
    updateProg(value = this.transNow) {
        if (this.activeSize <= 0) {
            this.progress = 1;
        }
        else {
            this.progress = (this.rangeEnd - value) / this.activeSize;
        }
    }
    getBarSize() {
        if (this.options.bar.enable) {
            let track = parseInt(style(this.trackEl)[this.propsMap.size]), ratio = Math.min(this.baseSize / this.wrapSize, 1), thumb = track * ratio;
            this.barSize = { track, thumb, diff: track - thumb };
        }
    }
    updateBarProg() {
        if (this.options.bar.enable) {
            let exceedRatio = this.progress < 0 ? Math.abs(this.progress) : this.progress > 1 ? this.progress - 1 : 0, progress = (this.progress >= 0 || this.progress <= 1) ? this.progress : 1, exceedSize = this.barSize.thumb * (1 - exceedRatio ** 2), thumbPos = (this.barSize.track - exceedSize) * progress;
            this.thumbEl.style[this.propsMap.size] = exceedSize + 'px';
            this.thumbEl.style[this.propsMap.position] = thumbPos + 'px';
        }
    }
    exceedListen(ratio, placement) {
        this.listen({ name: 'exceed', params: [ratio, placement] });
        ratio >= 1 && this.listen({ name: 'completed', params: [ratio, placement] });
    }
    exceedHandle({ start, end, normal, linkage }) {
        let ratio = 0, cross = linkage || this.crossLinkage();
        if (cross.includes('start')) {
            ratio = Math.abs(this.transNow / this.options.completedThr);
            start && start(ratio.toFixed(2));
            linkage && this.exceedListen(toNumber(ratio, { places: 2 }), 'start');
        }
        else if (cross.includes('end')) {
            ratio = Math.abs(this.transNow - this.rangeStart) / this.options.completedThr;
            end && end(ratio.toFixed(2));
            linkage && this.exceedListen(toNumber(ratio, { places: 2 }), 'end');
        }
        else {
            normal && normal();
        }
    }
    crossLinkage(value = this.transNow) {
        let result = 'regular';
        if (this.rangeStart > 0) {
            if (this.isBaby()) {
                if (value > this.paddingStart) {
                    result = 'baby-start';
                }
                else {
                    result = 'baby-start-reverse';
                }
            }
            else {
                if (value > this.paddingStart) {
                    result = 'teen-start';
                }
                else if (value < this.rangeStart) {
                    result = 'teen-end';
                }
            }
        }
        else {
            if (value > this.paddingStart) {
                result = 'adult-start';
            }
            else if (value < this.rangeStart) {
                result = 'adult-end';
            }
        }
        return result;
    }
    isBaby() {
        return this.rangeStart - this.paddingStart > 0;
    }
    rollback(dist, duration = this.options.duration) {
        setTimeout(() => {
            duration && (this.wrapEl.style.transitionDuration = duration + 'ms');
            transformTools.set({
                el: this.wrapEl,
                data: { translate: { [this.options.axis]: dist } }
            });
        }, 0);
    }
    toStart(duration) {
        if (this.destroyed || this.transNow === this.rangeEnd)
            return this;
        this.transNow = this.transLast = this.paddingStart;
        this.scrollTo({
            snap: true,
            duration,
            after: () => {
                super.listen({ name: 'toStart' });
            }
        });
    }
    toEnd(duration) {
        if (this.destroyed || this.transNow === this.rangeStart)
            return this;
        this.transNow = this.transLast = this.rangeStart;
        this.scrollTo({
            snap: true,
            duration,
            after: () => {
                super.listen({ name: 'toEnd' });
            }
        });
    }
    getSnappeds() {
        if (this.options.snap.enable) {
            if (this.options.snap.mode === 'auto') {
                this.unsnappeds = super.single2Els(this.options.snap.selector, this.wrapEl);
                this.snappeds = [...this.wrapEl.children].filter(k => !this.unsnappeds.includes(k));
            }
            else {
                this.snappeds = super.single2Els(this.options.snap.selector, this.wrapEl);
            }
        }
    }
    getOffset(child, parent = this.wrapEl) {
        return child.getBoundingClientRect()[this.propsMap.position] - parent.getBoundingClientRect()[this.propsMap.position];
    }
    getLatestSnap(val = this.transNow) {
        let snappeds = this.snappeds.map((k) => {
            return { el: k, offset: Math.abs(val - this.paddingStart + this.getOffset(k)) };
        }), latest = null;
        snappeds.sort((a, b) => a.offset - b.offset);
        if (this.options.snap.mode === 'auto') {
            if (this.options.snap.offset) {
                let temp = snappeds.find((k) => k.offset <= this.options.snap.offset);
                latest = temp ? temp.el : null;
            }
            else {
                latest = snappeds[0].el;
            }
        }
        else {
            latest = snappeds[0].offset <= this.options.snap.offset ? snappeds[0].el : null;
        }
        return { latest, snappeds };
    }
    setSnapped(options = {}) {
        if (this.destroyed || this.wrapEl.children.length === 0 || !this.options.snap.enable)
            return this;
        this.scrolled = false;
        let opt = Object.assign({ force: this.options.snap.force }, options), current = this.getLatestSnap(), snappeds = current.snappeds, latest = current.latest, toEl = opt.target ? getEl(opt.target, this.wrapEl) : null, target = toEl || latest;
        if (!latest || !target)
            return this;
        if (target !== this.snapped || opt.force) {
            this.toggleSnappedAttr(target, snappeds);
            super.listen({ name: 'snap', cb: opt.do, params: [target] });
        }
        this.scrollTo({
            target,
            type: 'el',
            duration: opt.duration,
            after: () => {
                if (target === this.snapped) {
                    super.listen({ name: 'snapped', cb: opt.done, params: [target] });
                }
            }
        });
    }
    toggleSnappedAttr(target, snappeds = this.getLatestSnap().snappeds) {
        let el = getEl(target, this.wrapEl), last;
        if (el) {
            el.setAttribute('snapped', '');
            last = snappeds.find(k => k.el !== el && k.el.hasAttribute('snapped'));
            last && last.el.removeAttribute('snapped');
            this.snapped = el;
        }
    }
    scrollTo({ target = this.transNow, duration, before, after, snap = false, type = 'translate' }) {
        if (this.destroyed)
            return this;
        this.scrolled = false;
        let now = this.transNow, trans = this.getEdgeValue(target, type), compare = Math.abs(this.transNow - trans) / this.targetSize, dur = duration || duration == 0 ? duration : compare > 1 ? Math.trunc(this.options.duration * compare) : this.options.duration;
        this.wrapEl.style.transitionDuration = dur + 'ms';
        transformTools.set({ el: this.wrapEl, data: { translate: { [this.options.axis]: trans } } });
        super.listen({ name: 'scroll', before, params: [trans, now] });
        this.transLast = this.transNow = trans;
        delay({
            duration: dur,
            done: () => {
                snap && this.options.snap.enable && this.setSnapped();
                super.listen({ name: 'scrolled', after, params: [trans, now] });
                this.scrolled = true;
            }
        });
    }
    locateTo({ target = this.transNow, before, after, snap = false, type = 'translate' }) {
        if (this.destroyed)
            return this;
        let now = this.transNow, trans = this.getEdgeValue(target, type);
        this.wrapEl.style.transitionDuration = '0ms';
        transformTools.set({ el: this.wrapEl, data: { translate: { [this.options.axis]: trans } } });
        super.listen({ name: 'locate', before, params: [trans, now] });
        this.transLast = this.transNow = trans;
        snap && this.options.snap.enable && this.setSnapped();
        super.listen({ name: 'located', after, params: [trans, now] });
    }
    /**
     * @description Destroy instance and prohibit use.
     * @param {fn_cb_none} [cb] - Callback execution after destruction.
     * @returns {any} - Return the instance itself.
     */
    destroy(cb) {
        if (this.destroyed)
            return this;
        this.wrapSizeObs.disconnect();
        this.targetSizeObs.disconnect();
        this.wrapStyleObs.disconnect();
        this.options.bar.enable && this.options.bar.triggerable && !isMobi && this.barIns.destroy();
        this.gestureIns.destroy();
        this.destroyed = true;
        super.listen({ name: 'destroy', cb });
        return this;
    }
}
Scroll.hostType = 'el';
export default Scroll;
