/**  
 * @since Last modified: 2024-09-11 09:46:39
 * @class Progress
 * @description Folding too long text and omittind.
 * @param {*} elem - target element
 * @param {Object} [options] - Parameters 
 * @see {@link https://codepen.io/axui/pen/ZEwvpNL|demo @ codepen}
 * @example
 * ax.Progress('#id');
 */
import getEl from '../utils/getEl';
import isEmpty from '../utils/isEmpty';
import instance from '../utils/instance';
import breakpoints from '../utils/breakpoints';
import createEl from '../utils/createEl';
import deepClone from '../utils/deepClone';
import purifyHtml from '../utils/purifyHtml';
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 { T_progress } from '../../../types/progress';
import { T_el, T_fn, T_obj } from '../../../types/ax';
import optProgress from '../options/optProgress';
import elProps from '../utils/elProps';
import prefix from '../global/prefix';
import alias from '../global/alias';
import classes from '../utils/classes';
import toPixel from '../utils/toPixel';
import Observe from './Observe';
import getDataType from '../utils/getDataType';
import clampVal from '../utils/clampVal';
import toNumber from '../utils/toNumber';
import tplToEl from '../utils/tplToEl';
import number from '../../../../src/scripts/utils/number';
import delay from '../utils/delay';
import ease from '../utils/ease';
import isNull from '../utils/isNull';
import splitNum from '../utils/splitNum';
class Progress extends pipe(PubListen, PubUpdate, PubCommon)(PubBase) {
    targetEl: any;
    options: Required<T_progress<0>> | T_obj = {};
    destroyed: any;
    plans: any;
    storKeys: any;
    storObs: any;
    storTmp: any;
    rawOpts: any;
    vals: any;
    valNow: any;
    barNow: any;
    static hostType = 'node';
    constructor(elem: any, options?: T_progress, initial: boolean = true) {
        super();
        this.ready({
            options,
            maps: optProgress,
            host: elem,
            component: true,
            spread: ['label']
        });
        this.vals = {}
        this.valNow = 0;
        this.barNow = 0;
        this.toggleCtrl = () => {
            this.controlEl.classList.contains(`${prefix}icon-pause`) ? this.pause() : this.continue();
        }
        super.listen({ name: 'ready' });
        initial && this.init() ;
    }
    async init(cb?: T_fn): Promise<any> {
        super.listen({ name: 'launch' });
        this.options.b4Init && await this.options.b4Init.call(this);
        this.range = [...this.options.range].sort((a, b) => a - b);
        this.barSize = this.options.type === 'circle' ? 251.2 : this.options.type === 'semicircle' ? 125.6 : this.options.type === 'gapcircle' ? 167.6 : this.range[1];
        this.steps = this.options.steps.length ? [...this.options.steps].sort((a, b) => b.value - a.value) : [];
        this.getInitVals();
        this.completeEl = createEl('span', { [alias]: 'complete' }, this.options.lang.complete);
        this.textEl = createEl('div', { [alias]: 'text' });
        this.controlEl = createEl('i', { [alias]: 'control', class: `${prefix}icon-pause`, disabled: '' });
        this.controlEl.addEventListener('click', this.toggleCtrl, false);
        this.setObs();
        this.render();
        this.setAttrs();
        this.watchEnds();
        this.watchSteps();
        super.listen({ name: 'init', cb });
        return this;
    }
    setObs() {
        this.obsIns = new Observe({ paused: false, completed: false }, {
            onSet: (resp) => {
                if (resp.key === 'paused') {
                    /* this.controlEl.removeAttribute('disabled');
                    if (resp.value) {
                        classes.replace(this.controlEl, `${prefix}icon-play`, `${prefix}icon-pause`);
                        this.controlEl.onclick = () => {
                            this.continue();
                        }
                    } else {
                        classes.replace(this.controlEl, `${prefix}icon-pause`, `${prefix}icon-play`);
                        this.controlEl.onclick = () => {
                            this.puase();
                        }
                    } */
                } else if (resp.key === 'completed') {
                    if (resp.value) {
                        this.targetEl.setAttribute('completed', '');
                        this.labelEl ? this.labelEl.appendChild(this.completeEl) : null;
                    } else {
                        this.completeEl.remove();
                        this.targetEl.removeAttribute('completed');
                    }
                }
            }
        });
        this.obs = this.obsIns.proxy;
    }
    setControl(pause: boolean, disabled?: boolean) {
        if (pause) {
            classes.replace(this.controlEl, `${prefix}icon-play`, `${prefix}icon-pause`);
        } else {
            classes.replace(this.controlEl, `${prefix}icon-pause`, `${prefix}icon-play`);
        }
        if (disabled) {
            this.controlEl.setAttribute('disabled', '');
        } else {
            this.controlEl.removeAttribute('disabled');
        }
    }
    setAttrs() {
        this.targetEl.classList.add(`${prefix}progress`);
        this.targetEl.setAttribute('linecap', this.options.linecap);
        this.targetEl.setAttribute('type', this.options.type);
        this.options.thk && this.targetEl.setAttribute('thk', this.options.thk);
        this.options.size && this.targetEl.setAttribute('size', this.options.size);
        this.options.theme && this.targetEl.setAttribute('theme', this.options.theme);
        this.options.gradient && this.targetEl.setAttribute('gradient', 'true');
        this.options.simple && this.targetEl.setAttribute('simple', 'true');
        this.options.classes && classes.add(this.targetEl, this.options.classes);
        (this.options.rotate && this.options.type === 'circle') && this.wrapEl.classList.add(`${prefix}rotate360`, `${prefix}loop-inf`);
    }
    getInitVals() {
        this.vals = this.getVals();
        this.valNow = this.vals.val;
        this.barNow = this.vals.bar;
        this.initVals = { ...this.vals }
        this.startVals = {
            val: this.range[0],
            bar: this.barSize,
            ratio: 0,
            percent: 0,
            number: this.getNumber(this.range[0], 0),
        };
        this.endVals = {
            val: this.range[1],
            bar: 0,
            ratio: 1,
            percent: 100,
            number: this.getNumber(this.range[1], 100),
        };
    }
    getVal(val: number | string = this.vals.val) {
        return isNull(val) ? this.range[0] : clampVal({ val, min: this.range[0], max: this.range[1] });
    }
    getBarVal(val?: number) {
        let result;
        if (val == this.range[0]) {
            result = this.barSize;
        } else if (val == this.range[1]) {
            result = 0;
        } else {
            let value, ratio, tmp;
            if (isNull(val)) {
                value = this.vals.val;
                ratio = this.ratio;
            } else {
                value = clampVal({ val: val as number, min: this.range[0], max: this.range[1] });
                ratio = this.getRatio(value);
            }
            tmp = (this.options.type == 'line') ? (value - this.range[0]) : (this.barSize * (1 - ratio));
            result = toNumber(tmp, { mode: 'floor' });
        }
        return result;
    }
    getRatio(val: number = this.vals.val) {
        let result;
        if (val == this.range[1]) {
            result = 1;
        } else if (val == this.range[0]) {
            result = 0;
        } else {
            result = toNumber((val - this.range[0]) / (this.range[1] - this.range[0]), { places: 4, mode: 'floor' });
        }
        return result;
    }
    getNumber(value: number, percent: number) {
        let tmp = this.options.number.type === 'value' ? value : percent,
            val = this.options.number.trunc ? ~~tmp : toNumber(tmp, { places: 2, mode: 'floor', zero: true }),
            result = this.options.number.emphatic ? splitNum(val) : val;
        return result;
    }
    getVals(val: number | string = this.options.value) {
        let value = this.getVal(val),
            ratio = this.getRatio(value) as number,
            percent = ratio * 100,
            barVal = this.getBarVal(value),
            number = this.getNumber(value, percent);
        return {
            val: value,
            bar: barVal,
            ratio,
            percent,
            number,
        }
    }
    render() {
        this.targetEl.innerHTML = '';
        this.labelTpl = this.options.label.template || `<div ${alias}=label><div ${alias}=result><i ${alias}=number></i><i ${alias}=unit></i></div></div>`;
        if (this.options.type == 'line') {
            this.trackEl = createEl('div', { [alias]: 'track' });
            this.wrapEl = this.trackEl;
            this.barEl = createEl('div', { [alias]: 'bar', 'style': `width:${this.vals.percent}%` });
            this.wrapEl.appendChild(this.barEl);
        } else {
            let gradient: string = '', svg: string = '';
            if (this.options.gradient) {
                if (this.options.theme == 'prim') {
                    gradient = `
                <linearGradient id="svgbar-prim" >
                    <stop offset="0%" style="stop-color:var(--${prefix}c-prim);" ></stop>
                    <stop offset="100%" style="stop-color:var(--${prefix}c-prim-aj);" ></stop>
                </linearGradient>
              `;
                } else if (this.options.theme == 'error') {
                    gradient = `
                <linearGradient id="svgbar-error" >
                    <stop offset="0%" style="stop-color:var(--${prefix}c-error);" ></stop>
                    <stop offset="100%" style="stop-color:var(--${prefix}c-error-aj);" ></stop>
                </linearGradient>
              `;
                } else if (this.options.theme == 'succ') {
                    gradient = `
                <linearGradient id="svgbar-succ" >
                    <stop offset="0%" style="stop-color:var(--${prefix}c-succ);" ></stop>
                    <stop offset="100%" style="stop-color:var(--${prefix}c-succ-aj);" ></stop>
                </linearGradient>
              `;
                } else if (this.options.theme == 'warn') {
                    gradient = `
            <linearGradient id="svgbar-warn" >
                <stop offset="0%" style="stop-color:var(--${prefix}c-warn);" ></stop>
                <stop offset="100%" style="stop-color:var(--${prefix}c-warn-aj);" ></stop>
            </linearGradient>
          `;
                } else if (this.options.theme == 'info') {
                    gradient = `
                <linearGradient id="svgbar-info" >
                    <stop offset="0%" style="stop-color:var(--${prefix}c-info);" ></stop>
                    <stop offset="100%" style="stop-color:var(--${prefix}c-info-aj);" ></stop>
                </linearGradient>
              `;
                } else if (this.options.theme == 'issue') {
                    gradient = `
                <linearGradient id="svgbar-issue" >
                    <stop offset="0%" style="stop-color:var(--${prefix}c-issue);" ></stop>
                    <stop offset="100%" style="stop-color:var(--${prefix}c-issue-aj);"></stop>
                </linearGradient>
              `;
                }
            }
            if (this.options.type == 'circle') {
                svg = `<svg viewBox="0 0 100 100">
                            ${gradient}
                            ${!this.options.simple ? `<path d="M 50 50 m -40 0 a 40 40 0 1 1 80 0  a 40 40 0 1 1 -80 0" ${alias}="track"></path>` : ''}
                            <path d="M 50 50 m -40 0 a 40 40 0 1 1 80 0  a 40 40 0 1 1 -80 0" ${alias}=="bar" stroke-dasharray=${this.barSize} stroke-linecap=${this.options.linecap}  stroke-dashoffset=${this.vals.bar}></path>
                       </svg>
                        `;
            } else if (this.options.type == 'semicircle') {
                svg = `<svg viewBox="0 0 100 60">
                            ${gradient}
                            ${!this.options.simple ? `<path d="M 50 50 m -40 0 a 40 40 0 1 1 80 0" ${alias}=="track" stroke-linecap=${this.options.linecap}></path>` : ''}
                            <path d="M 50 50 m -40 0 a 40 40 0 1 1 80 0" ${alias}=="bar" stroke-dasharray=${this.barSize} stroke-linecap=${this.options.linecap} stroke-dashoffset=${this.vals.bar}></path>
                       </svg>
                        `;
            } else if (this.options.type == 'gapcircle') {
                svg = `<svg viewBox="0 0 100 80">
                            ${gradient}
                            ${!this.options.simple ? `<path d="M15.367,70.026C11.954,64.137,10,57.296,10,49.999 C10,27.909,27.909,10,50,10c22.092,0,40,17.909,40,39.999c0,7.295-1.952,14.134-5.363,20.022" ${alias}=="track" stroke-linecap=${this.options.linecap}></path>` : ''}
                            <path d="M15.367,70.026C11.954,64.137,10,57.296,10,49.999 C10,27.909,27.909,10,50,10c22.092,0,40,17.909,40,39.999c0,7.295-1.952,14.134-5.363,20.022" ${alias}=="bar" stroke-dasharray=${this.barSize} stroke-linecap=${this.options.linecap} stroke-dashoffset=${this.vals.bar}></path>
                       </svg>
                        `;
            }
            this.wrapEl = tplToEl(svg);
            this.barEl = this.wrapEl.querySelector(`[${alias}="bar"]`);
            this.trackEl = this.wrapEl.querySelector(`[${alias}="track"]`);
        }
        this.targetEl.appendChild(this.wrapEl);
        this.targetEl.appendChild(this.textEl);
        if (this.options.label.enable) {
            if (!this.options.label.placement) {
                this.textEl.insertAdjacentHTML('beforeEnd', this.labelTpl);
                this.labelEl = this.targetEl.querySelector(`[${alias}="label"]`);
            } else {
                let place = getEl(this.options.label.placement);
                if (place) {
                    place.insertAdjacentHTML('beforeEnd', this.labelTpl);
                    this.labelEl = place.querySelector(`[${alias}="label"]`);
                }
            }
            this.numEl = this.labelEl.querySelector(`[${alias}="number"]`);
            this.unitEl = this.labelEl.querySelector(`[${alias}="unit"]`);
            this.numEl && (this.numEl.innerHTML = this.vals.number);
            this.unitEl && (this.unitEl.innerHTML = this.options.label.unit);
            if (this.options.label.tips) {
                if (!this.labelEl.querySelector(`[${alias}="tips"]`)) {
                    this.tipsEl = createEl('span', { [alias]: 'tips' }, this.options.lang.tips);
                    this.labelEl.insertAdjacentElement('afterbegin', this.tipsEl);
                } else {
                    this.tipsEl = this.labelEl.querySelector(`[${alias}="tips"]`);
                    this.tipsEl.innerHTML = this.options.lang.tips;
                }
            }
        }
        this.options.control && this.textEl.appendChild(this.controlEl);
    }
    setAnim(val: number = this.vals.val, duration?: number) {
        if (this.obs.paused) return false;
        if (val === this.valNow) return false;
        this.animFrame && cancelAnimationFrame(this.animFrame);
        let vals = this.vals,
            dftTime = (size: number) => size * 3 + 200,
            barDiff = vals.bar - this.barNow,
            time = duration || dftTime(Math.abs(barDiff));
        this.setControl(false, false);
        ease({
            from: { val: this.valNow },
            to: { val },
            duration: time,
            curve: this.options.curve,
            before: (resp: T_obj) => {
                super.listen({ name: 'start', params: [{ ...vals }] });
            },
            progress: (resp: T_obj) => {
                vals = this.getVals(resp.value.val);
                this.durRest = resp.remaining;
                this.valNow = vals.val;
                this.barNow = vals.bar;
                if (this.options.type == 'line') {
                    this.barEl.style.width = vals.percent + '%';
                } else {
                    this.barEl.setAttribute('stroke-dashoffset', this.barNow);
                }
                this.numEl && (this.numEl.innerHTML = vals.number);
                this.animFrame = resp.frame;
                this.watchEnds();
                this.watchSteps();
            },
            doing: (resp: T_obj) => {
                super.listen({ name: 'move', params: [{ ...this.vals, valNow: this.valNow, barNow: this.barNow }] });
            },
            done: (resp: T_obj) => {
                this.setControl(false, true);
                super.listen({ name: 'end', params: [{ ...this.vals, valNow: this.valNow, barNow: this.barNow }] });
            },
        })
    }
    watchEnds() {
        if (this.valNow >= this.range[1]) {
            this.obs.completed = true;
            super.listen({ name: 'completed' });
        } else {
            this.obs.completed = false;
            (this.valNow <= this.range[0]) && super.listen({ name: 'zero' });
        }
    }
    watchSteps(val: number = this.valNow) {
        if (!this.steps.length) return;
        let step = this.steps.find((k: any) => k.value <= val);
        if (step) {
            if (step.color) {
                if (['prim', 'succ', 'error', 'info', 'warn', 'issue', 'text'].includes(step.color)) {
                    this.targetEl.setAttribute('theme', step.color);
                } else {
                    if (this.options.type === 'line') {
                        this.barEl.style.background = step.color;
                    } else {
                        this.barEl.style.stroke = step.color;
                    }
                }
            }
            step.text && (this.textEl.innerHTML = step.text);
            step.action && step.action.call(this, step.value, this.valNow);
        }
    }
    getStepVals(val: number | string) {
        let result;
        if (typeof val === 'string') {
            if (val === 'zero') {
                result = this.startVals;
            } else if (val === 'completed') {
                result = this.endVals;
            } else if (val.includes('/')) {
                let splits = val.split('/'),
                    num = ~~splits[0],
                    demom = ~~splits[1],
                    ratio = num / demom,
                    value = (this.range[1] - this.range[0]) * ratio + this.range[0];
                result = this.getVals(value);
            } else {
                result = this.getVals();
            }
        } else if (typeof val === 'number') {
            result = this.getVals(val);
        }
        return result;
    }
    async animateTo(val: number | string, opts?: { duration?: number, cb?: T_fn }) {
        if (this.destroyed) return this;
        this.obs.paused = false;
        this.vals = typeof val === 'string' ? this.getStepVals(val) : this.getVals(val);
        this.options.b4Set && await this.options.b4Set.call(this, this.vals);
        if (this.vals.val === this.valNow) return false;
        this.setAnim(this.vals.val, opts?.duration);
        super.listen({ name: 'set', cb: opts?.cb, params: [{ ...this.vals, valNow: this.valNow, barNow: this.barNow }] });
        return this;
    }
    async locateTo(val: number | string, opts?: { cb?: T_fn }) {
        this.vals = this.getStepVals(val);
        this.options.b4Set && await this.options.b4Set.call(this, this.vals);
        this.animFrame && cancelAnimationFrame(this.animFrame);
        this.obs.paused = false;
        this.valNow = this.vals.val;
        this.barNow = this.vals.bar;
        this.numEl && (this.numEl.innerHTML = this.vals.number);
        this.barEl.setAttribute('stroke-dashoffset', this.vals.bar);
        this.watchEnds();
        this.watchSteps();
        super.listen({ name: 'set', cb: opts?.cb, params: [{ ...this.vals, valNow: this.valNow, barNow: this.barNow }] });
        return this;
    }
    restore(cb?: T_fn) {
        if (this.destroyed) return this;
        this.locateTo('restore');
        super.listen({ name: 'restore', cb });
        return this;
    }
    setCompleted(cb?: T_fn) {
        if (this.destroyed) return this;
        this.locateTo('completed');
        cb && cb.call(this);
        return this;
    }
    setZero(cb?: T_fn) {
        if (this.destroyed) return this;
        this.locateTo('zero');
        cb && cb.call(this);
        return this;
    }
    continue(duration?: number, cb?: T_fn) {
        if (this.destroyed || this.valNow === this.vals.val) return this;
        if (!this.obs.paused) return false;
        this.obs.paused = false;
        this.setControl(false, false);
        this.setAnim(this.vals.val, duration || this.durRest);
        super.listen({ name: 'continue', cb, params: [{ ...this.vals, valNow: this.valNow, barNow: this.barNow, durRest: duration || this.durRest }] });
        return this;
    }
    pause(cb?: T_fn) {
        if (this.destroyed || this.valNow === this.vals.val) return this;
        if (this.animFrame) {
            cancelAnimationFrame(this.animFrame);
            this.obs.paused = true;
            this.setControl(true, false);
        }
        super.listen({ name: 'pause', cb, params: [{ ...this.vals, valNow: this.valNow, barNow: this.barNow }] });
        return this;
    }
    get() {
        return {
            ...this.vals,
            valNow: this.valNow,
            barNow: this.barNow,
            completed: this.obs.completed,
            paused: this.obs.paused,
            durRest: this.durRest || 0,
        };
    }
    /**
     * @description Destroy instance.Prohibit use.
     * @param {fn_cb_destroy} cb - Call back after destroy.
     * @returns {*} Return the instance itself
     */
    destroy(cb?: any): any {
        if (this.destroyed) return this;
        this.restore();
        this.controlEl.removeEventListener('click', this.toggleCtrl);
        this.obsIns.ins.destroy();
        super.listen({ name: 'destroy', cb });
        return this;
    }
}
export default Progress;
