import { UID } from "../core/uid";
import { IWidget, WUIEvent, WUICallback } from "../interfaces/widget.interface";
import { Vector2D } from "../types/vector2d.type";
import { WidgetEventProps, WidgetProps, WidgetAlignTypes, WidgetTypes } from "./widget.types";
import { addNewWidget, connectWidget } from "./widget.collection";

export class Widget implements IWidget {
    readonly id: string;

    subscribers: Map<string, WUICallback>;

    padding: number;

    left: number;
    top: number;
    width: number;
    height: number;

    initialWidth: number;
    initialHeight: number;

    overflow: boolean;

    fixedSize: number | null;

    type: WidgetTypes;
    align: WidgetAlignTypes;

    visible: boolean;
    enabled: boolean;

    parent: IWidget | null;
    childs: IWidget[];
    bodyTagName: string;
    body: HTMLElement;

    constructor(
        id: string | null = "",
        bodyTagName: string = "div",
        parent: IWidget | null = null
    ) {
        this.id = id ? id : UID();

        this.overflow = false;

        this.subscribers = new Map<string, WUICallback>();

        this.padding = 0;

        this.left = 0;
        this.top = 0;
        this.width = 0;
        this.height = 0;
        this.initialWidth = 0;
        this.initialHeight = 0;
        this.fixedSize = null;

        this.type = WidgetTypes.FREE;
        this.align = WidgetAlignTypes.VERTICAL;

        this.visible = true;
        this.enabled = true;

        this.parent = parent;
        this.childs = [];

        this.bodyTagName = bodyTagName;
        this.body = document.createElement(this.bodyTagName);
        this.body.id = `WUI.${id}.body`;

        if (parent) {
            //this.setType(WidgetTypes.CUSTOM);
            parent.addChild(this);
            parent.getBody().appendChild(this.body);
        } else {
            this.setType(WidgetTypes.FREE);
            document.body.appendChild(this.body);
        }

        this.body.addEventListener("click", (e) => {
            this.subscribers.forEach((callback) => {
                if (callback.event == "click") {
                    callback.then(e, this);
                }
            });
        });

        this.body.addEventListener("mousedown", (e) => {
            this.subscribers.forEach((callback) => {
                if (callback.event == "mousedown") {
                    callback.then(e, this);
                }
            });
        });

        this.body.addEventListener("mouseup", (e) => {
            this.subscribers.forEach((callback) => {
                if (callback.event == "mouseup") {
                    callback.then(e, this);
                }
            });
        });

        this.body.addEventListener("mousemove", (e) => {
            this.subscribers.forEach((callback) => {
                if (callback.event == "mousemove") {
                    callback.then(e, this);
                }
            });
        });

        this.body.addEventListener("wheel", (e) => {
            this.subscribers.forEach((callback) => {
                if (callback.event == "wheel") {
                    callback.then(e, this);
                }
            });
        });

        this.body.addEventListener("mouseout", (e) => {
            this.subscribers.forEach((callback) => {
                if (callback.event == "mouseout") {
                    callback.then(e, this);
                }
            });
        });

        this.body.addEventListener("mouseleave", (e) => {
            this.subscribers.forEach((callback) => {
                if (callback.event == "mouseleave") {
                    callback.then(e, this);
                }
            });
        });

        this.init();

        this.getMaxZIndex();

        addNewWidget(this.id, this);
    }

    public run(eventId: WUIEvent): void {
        this.subscribers.forEach((callback) => {
            if (callback.event == eventId) {
                callback.then(new Event(eventId), this);
            }
        });
    }

    public subscribe(cb: WUICallback) {
        const randomId =
            Math.random().toString(36).substring(2, 15) +
            Math.random().toString(36).substring(2, 15);

        this.subscribers.set(`${randomId}.${cb.event}`, cb);
    }

    public unsubscribe(event: WUIEvent) {
        this.subscribers.delete(`${event}`);
    }

    /**
     * Sets the padding value for the object.
     *
     * @param {number} p - The padding value to set.
     * @return {void} This function does not return a value.
     */
    public setPadding(p: number): void {
        this.padding = p;
    }

    /**
     * Sets the value of x and updates the left position of the element.
     *
     * @param {number} x - The new value of x.
     * @return {void} This function does not return anything.
     */
    public setX(x: number): void {
        this.left = x;
        this.body.style.left = `${x}px`;
    }

    /**
     * Sets the value of the 'y' property and updates the 'top' and 'body.style.top' properties accordingly.
     *
     * @param {number} y - The new value for the 'y' property.
     * @return {void} This function does not return a value.
     */
    public setY(y: number): void {
        this.top = y;
        this.body.style.top = `${y}px`;
    }

    /**
     * Sets the width of the element and updates the corresponding inline style.
     *
     * @param {number} w - The new width value.
     * @return {void} This function does not return a value.
     */
    public setW(w: number): void {
        //this.width = w;
        this.body.style.width = `${w}px`;
    }

    /**
     * Sets the initial value of the width property.
     *
     * @param {number} w - The initial width value.
     * @return {void} This function does not return a value.
     */
    public setInitialW(w: number): void {
        this.initialWidth = w;
    }

    /**
     * Sets the value of the height property and updates the height of the element.
     *
     * @param {number} h - The new height value.
     * @return {void} This function does not return any value.
     */
    public setH(h: number): void {
        //this.height = h;
        this.body.style.height = `${h}px`;
    }

    /**
     * Sets the initial height of the object.
     *
     * @param {number} h - The initial height value to set.
     * @return {void} This function does not return a value.
     */
    public setInitialH(h: number): void {
        this.initialHeight = h;
    }

    /**
     * Sets the width and height of the element.
     *
     * @param {number} w - The width to set.
     * @param {number} h - The height to set.
     * @return {void} This function does not return anything.
     */
    public setWH(w: number, h: number): void {
        this.width = w;
        this.height = h;
        this.body.style.width = `${w}px`;
        this.body.style.height = `${h}px`;

        //ejecutar los subscriptores
        this.subscribers.forEach((callback) => {
            if (callback.event == "resize") {
                callback.then(new Event("resize"), this);
            }
        });
    }

    public setFixedSize(s: number | null): void {
        this.fixedSize = s;
    }

    /**
     * Sets the type of the widget.
     *
     * @param {WidgetTypes} type - The type of widget to set.
     * @return {void} This function does not return anything.
     */
    public setType(type: WidgetTypes): void {
        this.type = type;

        let freeStyle = false;

        if (this.type === WidgetTypes.FREE) {
            freeStyle = true;
        }

        const parent = this.getParent();

        if (parent) {
            if (parent.type === WidgetTypes.FREE) {
                freeStyle = true;
            }
        }

        if (freeStyle) {
            this.deleteClass("WUINonFreePosition");
            this.addClass("WUIFixPosition");
            this.body.style.left = "";
            this.body.style.top = "";
            this.body.style.width = "";
            this.body.style.height = "";

            this.body.style.bottom = "";
            this.body.style.right = "";
        } else {
            if (this.type === WidgetTypes.CUSTOM || this.type === WidgetTypes.FILL) {
                // this.body.style.position = "absolute";
                // this.body.style.overflow = "hidden";
                this.deleteClass("WUIFixPosition");
                this.addClass("WUINonFreePosition");
            }
        }

        if (this.initialWidth != 0) {
            this.body.style.width = `${this.initialWidth}px`;
        }
        if (this.initialHeight != 0) {
            this.body.style.height = `${this.initialHeight}px`;
        }
    }

    /**
     * Sets the alignment of the widget.
     *
     * @param {WidgetAlignTypes} align - The alignment to set.
     * @return {void}
     */
    public setAlign(align: WidgetAlignTypes): void {
        this.align = align;
    }

    /**
     * Sets the visibility of the element.
     *
     * @param {boolean} visible - The visibility state of the element.
     * @return {void}
     */
    public setVisible(visible: boolean): void {
        this.visible = visible;
        this.body.style.display = visible ? "" : "none";
    }

    /**
     * Sets the enabled status of the object.
     *
     * @param {boolean} enabled - The new enabled status.
     * @return {void} This function does not return a value.
     */
    public setEnabled(enabled: boolean): void {
        this.enabled = enabled;
    }

    /**
     * Sets the parent of the widget.
     *
     * @param {IWidget | null} parent - The parent widget or null if there is no parent.
     * @return {void} This function does not return anything.
     */
    public setParent(parent: IWidget | null): void {
        if (this.parent) {
            if (this.body) {
                if (this.body.parentNode) {
                    this.body.parentNode.removeChild(this.body);
                }
            }
        }
        this.parent = parent;
        this.parent?.body.appendChild(this.body);

        this.setType(this.type); //TODO: revisar, se coloca para tratar de que reconozca al padre como freestyle en caso de serlo.

        //this.parent?.addChild(this);
    }

    /**
     * Sets the body of the element.
     *
     * @param {HTMLElement} body - The new body element.
     * @return {void} This function does not return anything.
     */
    public setBody(body: HTMLElement): void {
        if (this.body.parentNode) {
            this.body.parentNode.removeChild(this.body);
        }

        this.body = body;
        this.parent?.getBody().appendChild(this.body);
    }

    /**
     * Sets the array of child widgets for this widget.
     *
     * @param {IWidget[]} childs - The array of child widgets to set.
     * @return {void}
     */
    public setChilds(childs: IWidget[]): void {
        this.childs = childs;
    }

    /**
     * Sets the overflow value.
     *
     * @param {boolean} overflow - The new value for the overflow.
     * @return {void} This function does not return a value.
     */
    setOverflow(overflow: boolean): void {
        this.overflow = overflow;
        this.getBody().style.overflow = this.overflow ? "auto" : "hidden";
    }

    /**
     * Adds a CSS class to the list of CSS classes.
     *
     * @param {string} cssClass - The CSS class to add.
     * @return {void}
     */
    public addClass(cssClass: string): void {
        this.body.classList.add(cssClass);
    }

    /**
     * Deletes a CSS class from the list of CSS classes.
     *
     * @param {string} cssClass - The CSS class to be deleted.
     * @return {void} This function does not return a value.
     */
    public deleteClass(cssClass: string): void {
        this.body.classList.remove(cssClass);
    }

    public deleteAllClasses(): void {
        this.body.classList.forEach((cssClass) => {
            this.deleteClass(cssClass);
        });
    }

    /**
     * Retrieves the padding value.
     *
     * @return {number} The padding value.
     */
    getPadding(): number {
        return this.padding;
    }

    /**
     * Retrieves the value of the 'left' property.
     *
     * @return {number} The value of the 'left' property.
     */
    public getX(recursive: boolean = false): number {
        if (!recursive) {
            return this.left;
        }

        const parent = this.getParent();

        if (parent) {
            return this.left + parent.getX(true);
        }
        return this.left;
    }

    /**
     * Retrieves the value of Y.
     *
     * @return {number} The value of Y.
     */
    public getY(recursive: boolean = false): number {
        if (!recursive) {
            return this.top;
        }

        const parent = this.getParent();

        if (parent) {
            return this.top + parent.getY(true);
        }
        return this.top;
    }

    /**
     * Get the width of the object.
     *
     * @return {number} The width of the object.
     */
    public getW(): number {
        //return this.width;
        return this.getBody().clientWidth;
    }

    /**
     * Returns the value of the height property.
     *
     * @return {number} The value of the height property.
     */
    public getH(): number {
        //return this.height;
        return this.getBody().clientHeight;
    }

    /**
     * Returns the position of the element relative to the viewport.
     *
     * @param {boolean} scroll - Indicates whether to account for scrolling in the position calculation. Defaults to false.
     * @return {Vector2D} An object with x and y properties representing the position of the element.
     */
    public getPosition(scroll: boolean = false): Vector2D {
        var box = this.getBody().getBoundingClientRect();
        var body = document.body;
        var docElem = document.documentElement;
        var scrollTop = scroll ? docElem.scrollTop || body.scrollTop : 0;
        var scrollLeft = scroll ? docElem.scrollLeft || body.scrollLeft : 0;
        var clientTop = docElem.clientTop || body.clientTop || 0;
        var clientLeft = docElem.clientLeft || body.clientLeft || 0;
        var top = box.top + scrollTop - clientTop;
        var left = box.left + scrollLeft - clientLeft;
        return { x: Math.round(left), y: Math.round(top) };
    }

    public getFixedSize(): number | null {
        return this.fixedSize;
    }

    /**
     * Returns the type of the widget.
     *
     * @return {WidgetTypes} The type of the widget.
     */
    public getType(): WidgetTypes {
        return this.type;
    }

    /**
     * Retrieves the alignment of the widget.
     *
     * @return {WidgetAlignTypes} The alignment of the widget.
     */
    public getAlign(): WidgetAlignTypes {
        return this.align;
    }

    /**
     * Retrieves the value of the enabled flag.
     *
     * @return {boolean} The value of the enabled flag.
     */
    public getEnabled(): boolean {
        return this.enabled;
    }

    /**
     * Returns the visibility status of the element.
     *
     * @return {boolean} The visibility status of the element.
     */
    public getVisible(): boolean {
        return this.visible;
    }

    /**
     * Retrieves the parent widget of this widget.
     *
     * @return {IWidget | null} The parent widget or null if there is no parent.
     */
    public getParent(): IWidget | null {
        return this.parent;
    }

    /**
     * Returns the body element of the object.
     *
     * @return {HTMLElement} The body element.
     */
    public getBody(): HTMLElement {
        return this.body;
    }

    /**
     * Retrieves the array of child widgets.
     *
     * @return {IWidget[]} The array of child widgets.
     */
    public getChilds(): IWidget[] {
        return this.childs;
    }

    /**
     * Returns the value of the 'overflow' property.
     *
     * @return {boolean} The value of the 'overflow' property.
     */
    public getOverflow(): boolean {
        return this.overflow;
    }

    /**
     * Adds a child widget to the current widget.
     *
     * @param {IWidget} child - The widget to be added.
     * @return {void} - This function does not return anything.
     */
    public addChild(child: IWidget | null = null): void {
        if (!child) return;
        this.childs.push(child);

        child.setParent(this);

        child.init();
        /*child.resize();
        child.render();*/
    }

    public populate(): void {
        //se agrega a si mismo a window.w y a sus hijos.
        if (!window.w.get(this.id)) {
            window.w.set(this.id, this);
        }

        for (const child of this.childs) {
            child.populate();
        }
    }

    /**
     * Disables the selection feature.
     *
     * @param {void} -
     * @return {void} -
     */
    public disableSelection(): void {
        console.log("disableSelection");
    }

    /**
     * Enables selection.
     *
     */
    public enableSelection(): void {
        console.log("enableSelection");
    }

    /**
     * Hides the element by setting its visibility to false.
     *
     * @param {void} -
     * @return {void} -
     */
    public hide(): void {
        this.setVisible(false);
    }

    /**
     * Initializes the function.
     *
     * @param {type} paramName - description of parameter
     * @return {type} description of return value
     */
    public init(): void {
        let freeStyle = false;

        if (this.type === WidgetTypes.FREE) {
            freeStyle = true;
        }

        const parent = this.getParent();

        if (parent) {
            if (parent.type === WidgetTypes.FREE) {
                freeStyle = true;
            }
        }

        if (!freeStyle) {
            this.deleteClass("WUIFixPosition");
            this.addClass("WUINonFreePosition");
            // this.body.style.position = "absolute";
            // this.body.style.overflow = "hidden";
        }

        this.initPosition();
    }

    /**
     * Initializes the position.
     *
     * @param {void} - No parameters required.
     * @return {void} - No return value.
     */
    initPosition(): void {
        if (this.type === WidgetTypes.FILL) {
            if (!this.parent) {
                this.setX(0);
                this.setY(0);
                this.setWH(window.innerWidth, window.innerHeight);
            } else {
                //Si tiene padre que lo controle el padre!
                return;
            }
        }
    }

    /**
     * Renders the component.
     *
     * @return {void}
     */
    public render(): void {
        //this.initPosition();

        const padding = this.padding;

        const size = this.align === WidgetAlignTypes.HORIZONTAL ? this.width : this.height;

        let currentPosition = padding;

        //Obtener el available size si hay elementos con fixed size
        let sumFixedSizes = 0;

        let totalFillElements = 0;
        for (const child of this.childs) {
            if (child.type === WidgetTypes.FILL) {
                totalFillElements++;
            }
        }

        let freeElements = totalFillElements;

        for (const child of this.childs) {
            if (child.getFixedSize() !== null) {
                sumFixedSizes += child.getFixedSize() as number;
                freeElements--;
            }
        }

        let availableSize = size - sumFixedSizes;

        /**
         * Elementos que tienen un padre LIBRE
         * ===================================
         */

        const myParent = this.getParent();
        if (myParent) {
            if (myParent.type === WidgetTypes.FREE) {
                //this.getBody().style.position = "relative";
                this.addClass("WUIFixPosition");
                this.getBody().style.left = "";
                this.getBody().style.top = "";
                this.getBody().style.height = "";
                this.getBody().style.width = "";
                if (this.initialWidth != 0) {
                    this.getBody().style.width = `${this.initialWidth}px`;
                }
                if (this.initialHeight != 0) {
                    this.getBody().style.height = `${this.initialHeight}px`;
                }
            }
        }
        /************************************ */

        for (const child of this.childs) {
            if (child.type !== WidgetTypes.FILL) {
                child.render();

                continue;
            }

            let elementSize = (availableSize - padding) / freeElements;

            if (child.getFixedSize() !== null) {
                elementSize = child.getFixedSize() as number;
            }

            if (this.align === WidgetAlignTypes.HORIZONTAL) {
                child.setY(padding);
                child.setX(currentPosition);

                if (child.type === WidgetTypes.FILL) {
                    child.setWH(elementSize - padding, this.getH() - padding * 2);
                } else {
                    child.setWH(elementSize - padding, this.getH() - padding * 2);
                }
            } else if (this.align === WidgetAlignTypes.VERTICAL) {
                child.setX(padding);
                child.setY(currentPosition);
                if (child.type === WidgetTypes.FILL) {
                    child.setWH(this.getW() - padding * 2, elementSize - padding);
                } else {
                    child.setWH(this.getW() - padding * 2, elementSize - padding);
                }
            }
            currentPosition += elementSize;
            child.render();
        }

        this.subscribers.forEach((callback) => {
            if (callback.event == "render") {
                callback.then(new Event("render"), this);
            }
        });
    }

    /**
     * Resizes the element and its child elements.
     */
    public resize(): void {
        // Initialize the position of the element
        this.initPosition();
        this.render();
    }

    /**
     * Displays the element on the screen.
     *
     * @return {void} - Does not return a value.
     */
    public display(): void {
        this.setVisible(true);
    }

    /**
     * Toggles the visibility of something.
     *
     * @return {void} Nothing is returned.
     */
    public toggle(): void {
        this.setVisible(!this.visible);
    }

    public renderHTML(content: any): HTMLElement {
        this.body.appendChild(content);
        return content as HTMLElement;
    }

    private getMaxZIndex(maxZindex: number = 0, node: ChildNode | null = null): number {
        const parent = node ? node : document.body;

        if (parent instanceof HTMLElement) {
            if (parseInt(window.getComputedStyle(parent).zIndex) > maxZindex) {
                maxZindex = parseInt(window.getComputedStyle(parent).zIndex);
            }
        }

        parent?.childNodes.forEach((child) => {
            maxZindex = this.getMaxZIndex(maxZindex, child);
        });

        return maxZindex;
    }

    public setZIndex(zIndex: number): void {
        this.getBody().style.zIndex = `${zIndex}`;
    }

    public raisteTop(): void {
        this.setZIndex(this.getMaxZIndex() + 1);
    }

    public raiseBottom(): void {
        this.setZIndex(0);
    }

    /**
     * Attaches a widget to the current widget and delete all pre-existents widgets
     *
     * @param {IWidget} guest - The widget to attach.
     * @return {void} This function does not return anything.
     */
    public attachWidget(guest: IWidget): void {
        this.removeAllChilds();
        this.addChild(guest);
        guest.setParent(this);
        guest.render();
        this.resize();
        this.render();
    }

    /**
     * Removes all child nodes from the current element.
     *
     * @return {void} No return value.
     */
    public removeAllChilds(): void {
        while (this.getBody().childNodes.length > 0) {
            const child = this.getBody().firstChild;
            if (child) this.getBody().removeChild(child);
        }

        this.childs = [];
    }

    public free(): void {
        if (this.childs) {
            for (const child of this.childs) {
                child.free();
            }
        }

        window.w.delete(this.id);

        const body = this.getBody();
        const parent = body.parentNode;
        if (!parent && !body) return;
        parent?.removeChild(body);
    }

    public dispose(): void {
        this.removeAllChilds();
        const body = this.getBody();
        const parent = body.parentNode;
        parent?.removeChild(body);
    }
}

export function getOnlyEventProps(props: WidgetProps): WidgetEventProps {
    const eventProps = {
        onClick: props.onClick,
        onDrag: props.onDrag,
        onResize: props.onResize,
        onMouseDown: props.onMouseDown,
        onMouseUp: props.onMouseUp,
        onMouseMove: props.onMouseMove,
        onMouseOut: props.onMouseOut,
        onMouseLeave: props.onMouseLeave,
        onWheel: props.onWheel,
        onRender: props.onRender,
    };

    return eventProps;
}

export function connectWidgetCallback(
    id: string | null | undefined,
    props: WidgetEventProps
): void {
    if (!id) return;
    connectWidget("widget-added-" + id, {
        event: "widget-load",
        then: (_e, _w) => {
            const widget = w.get(id) as Widget;

            if (widget) {
                widget.subscribe({
                    event: "click",
                    then: (_e, _w) => {
                        props.onClick ? props.onClick({}) : null;
                    },
                });

                widget.subscribe({
                    event: "drag",
                    then: (_e, _w) => {
                        props.onDrag ? props.onDrag({}) : null;
                    },
                });

                widget.subscribe({
                    event: "resize",
                    then: (_e, _w) => {
                        props.onResize ? props.onResize({}) : null;
                    },
                });

                widget.subscribe({
                    event: "mousedown",
                    then: (_e, _w) => {
                        props.onMouseDown ? props.onMouseDown({}) : null;
                    },
                });

                widget.subscribe({
                    event: "mouseup",
                    then: (_e, _w) => {
                        props.onMouseUp ? props.onMouseUp({}) : null;
                    },
                });

                widget.subscribe({
                    event: "mousemove",
                    then: (_e, _w) => {
                        props.onMouseMove ? props.onMouseMove({}) : null;
                    },
                });

                widget.subscribe({
                    event: "mouseout",
                    then: (_e, _w) => {
                        props.onMouseOut ? props.onMouseOut({}) : null;
                    },
                });

                widget.subscribe({
                    event: "mouseleave",
                    then: (_e, _w) => {
                        props.onMouseLeave ? props.onMouseLeave({}) : null;
                    },
                });

                widget.subscribe({
                    event: "wheel",
                    then: (_e, _w) => {
                        props.onWheel ? props.onWheel({}) : null;
                    },
                });

                widget.subscribe({
                    event: "render",
                    then: (_e, _w) => {
                        props.onRender ? props.onRender({}) : null;
                    },
                });
            }
        },
    });
}
