ui/View.tstypescript
import { createEvents } from "../Events";
import type { EventCallback, EventsHandler } from "../Events";
import { parseHTML, stringTemplate } from "../utils/shared";

export interface IViewParams {
    model?: any;
    template?: string;
    parent?: View<any> | null;
    classList?: string[];
    initRender?: boolean;
}

export type ViewEventsList = ["render"];
const VIEW_EVENTS: ViewEventsList = ["render"];

class View<M> {
    static __counter__: number = 0;

    protected __id: number;

    public events: EventsHandler<ViewEventsList>;

    public model: M;

    public template: string;

    public parent: View<any> | null;

    public el: HTMLElement | null;

    protected _classList: string[];

    constructor(options: IViewParams = {}) {
        this.__id = View.__counter__++;
        this.events = createEvents<ViewEventsList>(VIEW_EVENTS);
        this.model = options.model || null;
        this.template = options.template || "";
        this.parent = options.parent || null;
        this._classList = options.classList || [];
        this.el = null;
        if (options.initRender) {
            this.render();
        }
    }

    public on(name: string, callback: EventCallback, sender?: any, priority?: number) {
        this.events.on(name, callback, sender, priority);
    }

    public off(name: string, callback: EventCallback) {
        this.events.off(name, callback);
    }

    static getHTML(template: string, params: any): string {
        return stringTemplate(template, params);
    }

    static parseHTML(htmlStr: string): HTMLElement[] {
        return parseHTML(htmlStr);
    }

    static insertAfter(newNodes: HTMLElement | HTMLElement[], referenceNode: Node): HTMLElement[] {
        if (!Array.isArray(newNodes)) {
            newNodes = [newNodes];
        }
        for (let i = 0; i < newNodes.length; i++) {
            if (referenceNode.parentNode) {
                referenceNode.parentNode.insertBefore(newNodes[i], referenceNode.nextSibling);
            }
        }
        return newNodes;
    }

    static insertBefore(newNodes: HTMLElement | HTMLElement[], referenceNode: Node): HTMLElement[] {
        if (!Array.isArray(newNodes)) {
            newNodes = [newNodes];
        }
        for (let i = 0; i < newNodes.length; i++) {
            if (referenceNode.parentNode) {
                referenceNode.parentNode.insertBefore(newNodes[i], referenceNode);
            }
        }
        return newNodes;
    }

    public insertBefore(view: View<any> | HTMLElement) {
        if (!this.el) {
            this.render();
        }
        if (this.el) {
            if (view instanceof HTMLElement && view.parentNode) {
                View.insertBefore(this.el, view);
            }
            if (view instanceof View && view.el && view.el.parentNode) {
                View.insertBefore(this.el, view.el);
            }
        }
    }

    public insertAfter(view: View<any> | HTMLElement) {
        if (!this.el) {
            this.render();
        }
        if (this.el) {
            if (view instanceof HTMLElement && view.parentNode) {
                View.insertAfter(this.el, view);
            }
            if (view instanceof View && view.el && view.el.parentNode) {
                View.insertAfter(this.el, view.el);
            }
        }
    }

    public isEqual(view: View<any>): boolean {
        return view.__id === this.__id;
    }

    public appendTo(node: HTMLElement, clean: boolean = false, firstPosition: boolean = false) {
        if (node) {
            if (!this.el) {
                this.beforeRender(node);
                this.render();
            }

            if (this.el && this.el.parentNode) {
                this.el.parentNode.removeChild(this.el);
            }

            if (clean) {
                node.innerHTML = "";
            }

            if (this.el) {
                if (firstPosition) {
                    if (node.childNodes[0]) {
                        View.insertBefore(this.el, node.childNodes[0]);
                    } else {
                        node.appendChild(this.el);
                    }
                } else {
                    node.appendChild(this.el);
                }
            }
            this.afterRender(node);
        }

        return this;
    }

    public afterRender(parentNode: HTMLElement) {}

    public beforeRender(parentNode: HTMLElement) {}

    public stopPropagation() {
        this.events.stopPropagation();
    }

    public renderTemplate(params: any): HTMLElement {
        return View.parseHTML(View.getHTML(this.template, params || {}))[0];
    }

    public render(params?: any): this {
        this.el = this.renderTemplate(params);
        for (let i = 0, len = this._classList.length; i < len; i++) {
            this.el.classList.add(this._classList[i]);
        }
        this.events.dispatch(this.events.render, this);
        return this;
    }

    public select<T extends HTMLElement>(queryStr: string): T | null {
        if (this.el) {
            return this.el.querySelector<T>(queryStr);
        }
        return null as T | null;
    }

    public selectRemove(queryStr: string): HTMLElement | undefined {
        if (this.el) {
            let r = this.select(queryStr);
            if (r && r.parentNode) {
                r.parentNode.removeChild(r);
                return r;
            }
        }
    }

    public selectAll(queryStr: string, callback?: Function) {
        if (this.el) {
            const res = this.el.querySelectorAll(queryStr);
            if (callback) {
                for (let i = 0, len = res.length; i < len; i++) {
                    callback(res[i], i);
                }
            }
            return res;
        }
    }

    public remove() {
        if (this.el && this.el.parentNode) {
            this.el.parentNode.removeChild(this.el);
        }
    }
}

export { View };