import { GefestEngine } from './engine';
import { GefestStyle } from './style';

// The general class of Gefest
export abstract class GefestElement {
    style: GefestStyle | null = null;
    isHidden: boolean = false;
    //onClick: (() => void) | null = null;
    readonly gefestId: string;
    protected content: (GefestElement | string)[] | string;
    protected attributes: Record<string, string> = {};
    protected classList: Set<string> = new Set();

    protected clickHandler: (() => void) | null = null;

    set onClick(handler: (() => void) | null) {
        if (handler) {
            this.clickHandler = handler;
        } else {
            this.clickHandler = null;
        }
    }

    get onClick(): (() => void) | null {
        if (this.clickHandler === null)
            return null;
        return () => {
            this.clickHandler!();
            GefestEngine.activateOnClick();
        };
    }

    /**
     * Creates a new instance of the element from an HTML string.
     * @param html The HTML string to parse.
     * @returns The created GefestElement.
     */
    static fromHTML(html: string): GefestElement {
        const htmlElement = new DOMParser().parseFromString(html, 'text/html').body.firstChild as HTMLElement;
        const element = new (class extends GefestElement {
            protected wrapHTML(content: string): string {
                return content;
            }
        })(htmlElement.innerHTML);

        for (const attr of htmlElement.attributes) {
            try {
                element.setAttribute(attr.name, attr.value);
            } catch (_) {}
        }

        for (const className of htmlElement.classList) {
            element.addClass(className);
        }

        return element;
    }

    /**
     * Creates a new instance of the element.
     * @param content The content for the element. Can be a string, an array of strings, an array of GefestElements or a combination of both.
     */
    constructor (content : (GefestElement | string)[] | string = []) {
        this.gefestId = GefestEngine.register(this);
        this.content = content;
    }

    /** 
     * Builds the element and returns it as a string. If the element has a style, it applies the style to the rendered HTML.
     * @param isFromStyle Indicates if the build is being called from a style application. This prevents infinite recursion when applying styles.
     * @returns The built element as a string.
    */
    build (isFromStyle: boolean = false): string {
        if (this.isHidden)
            this.setAttribute('hidden', '');
        if (!isFromStyle)
            return this.applyStyle(this.render(), this);
        return this.render();
    };

    /**
     * Checks if an attribute key is reserved.
     * @param key The attribute key.
     */
    checkReservedAttribute(key: string): void {
        if (({ "style": true, "class": true })[key]) 
            throw new Error(`The attribute "${key}" is reserved. Use the setStyle method for styles and setClass method for classes.`);
    }

    /**
     * Sets a personal style for the element. This will be applied on top of any style that is set for the element.
     * @param style The personal style to set. If null, the personal style will be removed.
     */
    setPersonalStyle(style: string | null): void {
        if (style === null) {
            delete this.attributes.style;
            return;
        }
        this.attributes.style = style;
    }

    /** 
     * Adds a class to the element's class list and updates the "class" attribute accordingly.
     * @param className The class name to add.
    */
    addClass(className: string): void {
        this.classList.add(className);
        this.attributes.class = Array.from(this.classList).join(' ');
    }

    /** 
     * Removes a class from the element's class list and updates the "class" attribute accordingly.
     * @param className The class name to remove.
    */
    removeClass(className: string): void {
        this.classList.delete(className);
        this.attributes.class = Array.from(this.classList).join(' ');
    }

    /**
     * Gets the list of classes for the element.
     * @returns The array of class names.
     */
    getClassList(): string[] {
        return Array.from(this.classList);
    }

    /**
     * Sets an attribute for the element.
     * @param key The attribute key.
     * @param value The attribute value.
     */
    setAttribute (key: string, value: string): void {
        this.checkReservedAttribute(key);
        this.attributes[key] = value;
    }

    /**
     * Gets an attribute value by its key.
     * @param key The attribute key.
     * @returns The attribute value or undefined if not found.
     */
    getAttribute (key: string): string | undefined {
        return this.attributes[key];
    }

    /**
     * Removes an attribute by its key.
     * @param key The attribute key.
     */
    removeAttribute (key: string): void {
        this.checkReservedAttribute(key);
        delete this.attributes[key];
    }

    /**
     * Applies the element's style to the given HTML.
     * @param html The HTML to apply the style to.
     * @returns The styled HTML.
     */
    protected applyStyle(html: string, element?: GefestElement): string {
        if (this.style)
            return this.style.applyStyle(html, element);
        return html;
    }

    /**
     * Converts the element's attributes to a string.
     * @returns The attributes string.
     */
    protected attributesToString (): string {
        return Object.entries(this.attributes)
            .map(([key, value]) => `${key}="${value}"`)
            .join(' ');
    }

    /**
     * Wraps the given content in HTML tags.
     * @param content The content to wrap.
     * @returns The wrapped HTML.
     */
    protected wrapHTML (content: string): string {
        const attrString = this.attributesToString();
        return `<div ${attrString}>${content}</div>`;
    }

    /**
     * Renders the element and returns it as a string.
     * @returns The rendered element as a string.
     */
    protected render (): string {
        if (typeof this.content === 'string') {
            return this.wrapHTML(this.content);
        }
        const rendered : string[] = [];
        for (const item of this.content) {
            if (typeof item === 'string') {
                rendered.push(this.applyStyle(this.wrapHTML(item)));
            }
            else if (item instanceof GefestElement) {
                if (!item.style && this.style) {
                    item.style = this.style;
                }
                rendered.push(item.build());
            }
        }
        return this.wrapHTML(rendered.join(''));
    }
}