/**
 * Abstract widget.
 */
class Widget {
    /** @type {string} */
    id;
    /** @type {string} */
    type;
    /** @type {Renderer} */
    renderer;
    /** @type {object} */
    data;

    /**
     * @param {object} data 
     * @param {Renderer} renderer 
     */
    constructor(data, renderer) {
        this.id = data.id;
        this.type = data.type;
        this.data = data;
        this.renderer = renderer;
    }

    /**
     * Build HTML tree from widget.
     * 
     * Should be overriden.
     * @returns {HTMLElement | null}
     */
    build() {
        return null;
    }

    /**
     * Handle update.
     * 
     * Should be overriden.
     * @param {object} data 
     */
    update(data) {
        for (const [k, v] of Object.entries(data))
            this.data[k] = v;
    }

    /**
     * Handle previous set (with ack) was timed out.
     * 
     * Should be overriden.
     */
    _handleSetError(err) {}

    /**
     * Handle ack for previous set.
     * 
     * Should be overriden.
     */
    _handleAck() {}

    /**
     * Handle renderer closing. 
     * 
     * Should be overriden to stop timers (if any).
     */
    close() {}

    /**
     * Set widget value.
     * 
     * Should be called from subclass.
     * @param {any} value 
     * @param {boolean} ack 
     * @returns {Promise<undefined>}
     */
    set(value, ack = true) {
        this.renderer._set(this, value, ack);
    }

    /**
     * Register an UI file to load.
     * 
     * Should be called from subclass.
     * @param {string} path
     * @param {string} type
     * @param {(string) => undefined} callback 
     */
    addFile(path, type, callback) {
        this.renderer._addFile(this, path, type, callback);
    }

    _handleFileProgress(perc) {}
    _handleFileError(err) {}
    _handleFileLoaded(res) {}
}

/**
 * Widget with container.
 */
class BaseWidget extends Widget {
    /** @type {HTMLDivElement} */
    #root;
    /** @type {HTMLDivElement} */
    #inner;

    /** @type {HTMLDivElement} */
    #cont;

    /** @type {HTMLSpanElement} */
    #hint;
    /** @type {HTMLSpanElement} */
    #label;
    /** @type {HTMLSpanElement} */
    #plabel;
    /** @type {HTMLSpanElement} */
    #suffix;

    /** @type {HTMLDivElement} */
    #container;

    /**
     * @param {object} data 
     * @param {Renderer} renderer 
     */
    constructor(data, renderer) {
        super(data, renderer);

        this.#root = createElement(this, {
            type: 'div',
            class: 'widget_main',
            style: {
                width: data.wwidth_t + '%',
            }
        });

        this.#inner = createElement(this, {
            type: 'div',
            class: 'widget_inner'
        });
        this.#root.append(this.#inner);

        this.#cont = createElement(this, {
            type: 'div',
            class: 'widget_label'
        });
        this.#inner.append(this.#cont);

        this.#hint = createElement(this, {
            type: 'span',
            class: 'whint',
            text: '?',
            style: {
                display: 'none',
            },
            also($hint) {
                $hint.addEventListener('click', () => asyncAlert($hint.title));
            }
        });
        this.#cont.append(this.#hint);

        this.#label = createElement(this, {
            type: 'span',
            text: data.type.toUpperCase(),
        });
        this.#cont.append(this.#label);

        this.#plabel = createElement(this, {
            type: 'span',
        });
        this.#cont.append(this.#plabel);

        this.#suffix = createElement(this, {
            type: 'span',
            class: 'wsuffix',
        });
        this.#cont.append(this.#suffix);


        this.#container = createElement(this, {
            type: 'div',
            class: 'widget_body',
            style: {
                minHeight: data.wheight && data.wheight > 25 ? data.wheight + 'px' : '',
            }
        });
        this.#inner.append(this.#container);
    }

    build() {
        return this.#root;
    }

    /**
     * Initialize widget layout.
     * 
     * Should be called from subclass constructor.
     * @param {object[]} obj 
     */
    makeLayout(...obj) {
        this.#container.replaceChildren(...obj.map(o => createElement(this, o)));
    }

    /**
     * Handle update.
     * 
     * Subclass should override this method and call super.update(data) from it.
     * @param {object} data 
     */
    update(data) {
        super.update(data);
        if ('label' in data) {
            this.#label.textContent = data.label.length ? data.label : this.type.toUpperCase();
        }
        if ('suffix' in data) {
            this.#suffix.textContent = data.suffix;
        }
        if ('nolabel' in data) {
            if (data.nolabel) this.#cont.classList.add('wnolabel');
            else this.#cont.classList.remove('wnolabel');
        }
        if ('square' in data) {
            if (data.square) this.#root.classList.add('wsquare');
            else this.#root.classList.remove('wsquare');
        }
        if ('notab' in data) {
            if (data.notab) this.#inner.classList.add('widget_notab');
            else this.#inner.classList.remove('widget_notab');
        }
        if ('disable' in data) {
            if (data.disable) this.#container.classList.add('widget_dsbl');
            else this.#container.classList.remove('widget_dsbl');
        }
        if ('hint' in data) {
            const htext = 'name: ' + this.id + '\n' + (data.hint ?? '');
            this.#label.title = htext;
            this.#hint.title = htext;
            this.#hint.style.display = (data.hint && data.hint.length) ? 'inline-block' : 'none';
        }
    }

    disable(el, disable) {
        if (disable) {
            el.setAttribute('disabled', '1');
            el.classList.add('disable');
        } else {  // null/undefined/0/false
            el.removeAttribute('disabled');
            el.classList.remove('disable');
        }
    }

    align(align) {
        this.#container.style.justifyContent = ["flex-start", "center", "flex-end"][Number(align ?? 1)];
    }

    setPlabel(text = null) {
        this.#plabel.textContent = text ?? '';
    }

    setSuffix(text = null) {
        this.#suffix.textContent = text ?? '';
    }

    _handleSetError(err){
        this.setPlabel("[ERR]");
        showPopupError(`Widget ${this.id}: ` + getError(err));
    }

    _handleAck() {
        this.setPlabel();
    }

    _handleFileProgress(perc) {
        this.setPlabel(`[${perc}%]`);
    }

    _handleFileError(err) {
        this.setPlabel("[ERR]");
        showPopupError(`Widget ${this.id}: ` + getError(err));
    }

    _handleFileLoaded() {
        this.setPlabel();
    }
}
