import { Color } from 'three';
import * as ThreeMeshUI from 'three-mesh-ui'
import type { DocumentedOptions as ThreeMeshUIEveryOptions } from "three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js";

import { serializable } from '../../engine/engine_serialization_decorator.js';
import { getParam } from '../../engine/engine_utils.js';
import { Canvas } from './Canvas.js';
import { Graphic } from './Graphic.js';
import { type ICanvas, type ICanvasEventReceiver, type IHasAlphaFactor } from './Interfaces.js';
import { updateRenderSettings } from './Utils.js';

const debug = getParam("debugtext");

export enum TextAnchor {
    UpperLeft = 0,
    UpperCenter = 1,
    UpperRight = 2,
    MiddleLeft = 3,
    MiddleCenter = 4,
    MiddleRight = 5,
    LowerLeft = 6,
    LowerCenter = 7,
    LowerRight = 8,
}

export enum VerticalWrapMode {
    Truncate = 0,
    Overflow = 1,
}
enum HorizontalWrapMode {
    Wrap = 0,
    Overflow = 1,
}

export enum FontStyle {
    Normal = 0,
    Bold = 1,
    Italic = 2,
    BoldAndItalic = 3,
}

/**
 * [Text](https://engine.needle.tools/docs/api/Text) displays text content in the UI. Supports custom fonts, colors,
 * alignment, and basic rich text formatting.
 *
 * **Text properties:**
 * - `text` - The string content to display
 * - `fontSize` - Size of the text in pixels
 * - `color` - Text color (inherited from Graphic)
 * - `alignment` - Text anchor position (UpperLeft, MiddleCenter, etc.)
 *
 * **Fonts:**
 * Set the `font` property to a URL pointing to a font file.
 * Supports MSDF (Multi-channel Signed Distance Field) fonts for crisp rendering.
 *
 * @example Update text at runtime
 * ```ts
 * const text = myLabel.getComponent(Text);
 * text.text = "Score: " + score;
 * text.fontSize = 24;
 * text.color = new RGBAColor(1, 1, 1, 1);
 * ```
 *
 * @summary Display text in the UI
 * @category User Interface
 * @group Components
 * @see {@link Canvas} for the UI root
 * @see {@link TextAnchor} for alignment options
 * @see {@link FontStyle} for bold/italic styles
 */
export class Text extends Graphic implements IHasAlphaFactor, ICanvasEventReceiver {

    @serializable()
    alignment: TextAnchor = TextAnchor.UpperLeft;
    @serializable()
    verticalOverflow: VerticalWrapMode = VerticalWrapMode.Truncate;
    @serializable()
    horizontalOverflow: HorizontalWrapMode = HorizontalWrapMode.Wrap;
    @serializable()
    lineSpacing: number = 1;
    @serializable()
    supportRichText: boolean = false;
    @serializable(URL)
    font?: string;
    @serializable()
    fontStyle: FontStyle = FontStyle.Normal;

    // private _alphaFactor : number = 1;
    setAlphaFactor(factor: number): void {
        super.setAlphaFactor(factor);
        this.uiObject?.set({ fontOpacity: this.color.alpha * this.alphaFactor });
        this.markDirty();
    }

    @serializable()
    get text(): string {
        return this._text;
    }

    set text(val: string) {
        if (val !== this._text) {
            this._text = val;
            this.feedText(this.text, this.supportRichText);
            this.markDirty();
            this.context.accessibility.updateElement(this, { label: this.text });
        }
    }

    private set_text(val: string) {
        this.text = val;
    }

    @serializable()
    get fontSize(): number {
        return this._fontSize;
    }

    set fontSize(val: number) {
        // Setting that kind of property in a parent, would cascade to each 'non-overrided' children.
        this._fontSize = val;
        this.uiObject?.set({ fontSize: val });
    }

    private sRGBTextColor: Color = new Color(1, 0, 1);
    protected onColorChanged(): void {
        this.sRGBTextColor.copy(this.color);
        this.sRGBTextColor.convertLinearToSRGB();
        this.uiObject?.set({ color: this.sRGBTextColor, fontOpacity: this.color.alpha });
    }

    onParentRectTransformChanged(): void {
        super.onParentRectTransformChanged();
        if (this.uiObject) {
            this.updateOverflow();
        }
    }

    // onBeforeRender(): void {
    //     // TODO TMUI @swingingtom this is so we don't have text clipping
    //     if (this.uiObject && (this.Canvas?.screenspace || this.context.isInVR)) {
    //         this.updateOverflow();
    //     }
    // }
    onBeforeCanvasRender(_canvas: ICanvas) {
        // ensure the text clipping matrix is updated (this was a problem with multiple screenspace canvases due to canvas reparenting)
        this.updateOverflow();
    }

    private updateOverflow() {
        // HACK: force the text overflow to update
        const overflow = (this.uiObject as any)?._overflow;
        if (overflow) {
            overflow._needsUpdate = true;
            // the screenspace canvas does force an update, no need to mark dirty here 
        }
    }

    protected onCreate(_opts: any): void {
        if (debug) console.log(this);

        if (this.horizontalOverflow == HorizontalWrapMode.Overflow) {
            // Only line characters in the textContent (\n,\r\t) would be able to multiline the text
            _opts.whiteSpace = 'pre';
        }

        if (this.verticalOverflow == VerticalWrapMode.Truncate) {
            this.context.renderer.localClippingEnabled = true;
            _opts.overflow = 'hidden';
        }


        // @marwie : this combination is currently KO. See sample "Overflow Overview"
        if (this.horizontalOverflow == HorizontalWrapMode.Overflow && this.verticalOverflow == VerticalWrapMode.Truncate) {
            // This could fix this combination, but would require anchors updates to replace element
            // _opts.width = 'auto';
        }


        _opts.lineHeight = this.lineSpacing;

        // @marwie : Should be fixed. Currently _opts are always fed with :
        //          backgroundOpacity : color.opacity
        //          backgroundColor : color
        delete _opts.backgroundOpacity;
        delete _opts.backgroundColor;

        // helper to show bounds of text element
        if (debug) {
            _opts.backgroundColor = 0xff9900;
            _opts.backgroundOpacity = 0.5;
        }

        const rt = this.rectTransform;

        // Texts now support both options, block and inline, and inline has all default to inherit
        _opts = { ..._opts, ...this.getTextOpts() };

        this.getAlignment(_opts);

        if (debug) {
            _opts.backgroundColor = Math.random() * 0xffffff;
            _opts.backgroundOpacity = 0.1;
        }

        this.uiObject = rt.createNewText(_opts);
        this.feedText(this.text, this.supportRichText);

    }

    onAfterAddedToScene() {
        super.onAfterAddedToScene();
        this.handleTextRenderOnTop();
    }

    private _text: string = "";
    private _fontSize: number = 12;

    private _textMeshUi: Array<ThreeMeshUI.Inline> | null = null;


    private getTextOpts(): object {
        const fontSize = this.fontSize;
        // if (this.canvas) {
        //     fontSize /= this.canvas?.scaleFactor;
        // }


        const textOpts = {
            color: this.color,
            fontOpacity: this.color.alpha,
            fontSize: fontSize,
            fontKerning: "normal",

        };
        this.setFont(textOpts as ThreeMeshUIEveryOptions, this.fontStyle);
        return textOpts;
    }


    onEnable(): void {
        super.onEnable();

        this.context.accessibility.updateElement(this, {
            role: "text",
            label: this.text,
            hidden: false
        });

        this._didHandleTextRenderOnTop = false;
        if (this.uiObject) {
            // @ts-ignore

            // @TODO :  Evaluate the need of keeping it anonymous.
            //          From v7.x afterUpdate can be removed but requires a reference
            this.uiObject.addAfterUpdate(() => {
                // We need to update the shadow owner when the text updates
                // because once the font has loaded we get new children (a new mesh)
                // which is the text, it needs to be linked back to this component
                // to be properly handled by the EventSystem
                // since the EventSystem looks for shadow component owners to handle events
                this.setShadowComponentOwner(this.uiObject);
                this.markDirty();
            });
        }

        setTimeout(() => this.markDirty(), 10);
        this.canvas?.registerEventReceiver(this);
    }
    onDisable(): void {
        super.onDisable();
        this.canvas?.unregisterEventReceiver(this);
        this.context.accessibility.updateElement(this, { hidden: true });
    }
    onDestroy(): void {
        super.onDestroy();
        this.context.accessibility.removeElement(this);
    }

    private getAlignment(opts: ThreeMeshUIEveryOptions): ThreeMeshUIEveryOptions {

        opts.flexDirection = "column";

        switch (this.alignment) {
            case TextAnchor.UpperLeft:
            case TextAnchor.MiddleLeft:
            case TextAnchor.LowerLeft:
                opts.textAlign = "left";
                break;
            case TextAnchor.UpperCenter:
            case TextAnchor.MiddleCenter:
            case TextAnchor.LowerCenter:
                opts.textAlign = "center";

                break;
            case TextAnchor.UpperRight:
            case TextAnchor.MiddleRight:
            case TextAnchor.LowerRight:
                opts.textAlign = "right";
                break;
        }


        switch (this.alignment) {
            default:
            case TextAnchor.UpperLeft:
            case TextAnchor.UpperCenter:
            case TextAnchor.UpperRight:
                opts.alignItems = "start";
                break;
            case TextAnchor.MiddleLeft:
            case TextAnchor.MiddleCenter:
            case TextAnchor.MiddleRight:
                opts.alignItems = "center";
                break;
            case TextAnchor.LowerLeft:
            case TextAnchor.LowerCenter:
            case TextAnchor.LowerRight:
                opts.alignItems = "end";
                break;
        }

        return opts;
    }

    private feedText(text: string, richText: boolean): void {
        // if (!text || text.length <= 0) return;
        // if (!text ) return;
        if (debug) console.log("feedText", this.uiObject, text, richText);

        if (!this.uiObject) return;
        if (!this._textMeshUi)
            this._textMeshUi = [];

        // this doesnt work and produces errors when length is 0:
        // this.uiObject.textContent = " "; 

        // reset the current text (e.g. when switching from "Hello" to "Hello <b>World</b>")
        // @TODO swingingtom: this is a hack to reset the text content, not sure how to do that right
        this.uiObject.children.length = 0;

        if (!richText || text.length === 0) {
            //@TODO: @swingingtom how would the text content be set?
            //@ts-ignore
            this.uiObject.textContent = text;
        } else {
            let currentTag = this.getNextTag(text);
            if (!currentTag) {
                //@ts-ignore
                // we have to set it to empty string, otherwise TMUI won't update it @swingingtom
                this.uiObject.textContent = ""; // < 
                this.setOptions({ textContent: text });
                return;
            } else if (currentTag.startIndex > 0) {
                // First segment should also clear children inlines
                for (let i = this.uiObject.children.length - 1; i >= 0; i--) {
                    const child = this.uiObject.children[i];
                    // @ts-ignore
                    if (child.isUI) {
                        this.uiObject.remove(child as any);
                        child.clear();
                    }
                }
                const el = new ThreeMeshUI.Inline({ textContent: text.substring(0, currentTag.startIndex), color: 'inherit' });
                this.uiObject.add(el as any);
            }

            const stackArray: Array<TagStackEntry> = [];
            while (currentTag) {
                const next = this.getNextTag(text, currentTag.endIndex);

                const opts = {
                    fontFamily: this.uiObject?.get('fontFamily'),
                    color: 'inherit',
                    textContent: ""
                }

                if (next) {
                    opts.textContent = this.getText(text, currentTag, next);
                    this.handleTag(currentTag, opts, stackArray);
                    const el = new ThreeMeshUI.Inline(opts);
                    this.uiObject?.add(el as any)

                } else {
                    opts.textContent = text.substring(currentTag.endIndex);
                    this.handleTag(currentTag, opts, stackArray);
                    const el = new ThreeMeshUI.Inline(opts);
                    this.uiObject?.add(el as any);
                }
                currentTag = next;
            }
        }
    }

    private _didHandleTextRenderOnTop: boolean = false;

    private handleTextRenderOnTop() {
        if (this._didHandleTextRenderOnTop) return;
        this._didHandleTextRenderOnTop = true;
        this.startCoroutine(this.renderOnTopCoroutine());
    }

    // waits for all the text objects to be ready to set the render on top setting
    // @THH :  this isn't true anymore. We can set mesh and material properties before their counterparts are created.
    //         Values would automatically be passed when created. Not sure for depthWrite but it can be added;
    private * renderOnTopCoroutine() {
        if (!this.canvas) return;
        const updatedRendering: boolean[] = [];
        const canvas = this.canvas as Canvas;
        const settings = {
            renderOnTop: canvas.renderOnTop,
            depthWrite: canvas.depthWrite,
            doubleSided: canvas.doubleSided
        };
        while (true) {
            let isWaitingForElementToUpdate = false;
            if (this._textMeshUi) {
                for (let i = 0; i < this._textMeshUi.length; i++) {
                    if (updatedRendering[i] === true) continue;
                    isWaitingForElementToUpdate = true;
                    const textMeshObject = this._textMeshUi[i];
                    // text objects have this textContent which is the mesh
                    // it is not ready immediately so we have to check if it exists 
                    // and only then setting the render on top property works
                    if (!textMeshObject["textContent"]) continue;
                    updateRenderSettings(textMeshObject, settings);
                    updatedRendering[i] = true;
                    // console.log(textMeshObject);
                }
            }
            if (!isWaitingForElementToUpdate) break;
            yield;
        }
    }

    private handleTag(tag: TagInfo, opts: any, stackArray: Array<TagStackEntry>) {
        // console.log(tag);
        if (!tag.isEndTag) {
            if (tag.type.includes("color")) {
                const stackEntry = new TagStackEntry(tag, { color: opts.color });
                stackArray.push(stackEntry);
                if (tag.type.length > 6) // color=
                {
                    const col = parseInt("0x" + tag.type.substring(7));
                    opts.color = col;
                } else {
                    // if it does not contain a color it is white
                    opts.color = new Color(1, 1, 1);
                }
            } else if (tag.type == "b") {
                this.setFont(opts, FontStyle.Bold);
                const stackEntry = new TagStackEntry(tag, {
                    fontWeight: 700,
                });
                stackArray.push(stackEntry);
            } else if (tag.type == "i") {
                this.setFont(opts, FontStyle.Italic);
                const stackEntry = new TagStackEntry(tag, {
                    fontStyle: 'italic'
                });
                stackArray.push(stackEntry);

            }
        }
    }

    private getText(text: string, start: TagInfo, end: TagInfo) {
        return text.substring(start.endIndex, end.startIndex);
    }

    private getNextTag(text: string, startIndex: number = 0): TagInfo | null {
        const start = text.indexOf("<", startIndex);
        const end = text.indexOf(">", start);
        if (start >= 0 && end >= 0) {
            const tag = text.substring(start + 1, end);
            return { type: tag, startIndex: start, endIndex: end + 1, isEndTag: tag.startsWith("/") };
        }
        return null;
    }

    /**
     * Update provided opts to have a proper fontDefinition : family+weight+style
     * Ensure Family and Variant are registered in FontLibrary
     *
     * @param opts
     * @param fontStyle
     * @private
     */
    private setFont(opts: ThreeMeshUIEveryOptions, fontStyle: FontStyle) {

        // @TODO : THH could be useful to uniformize font family name :
        //         This would ease possible html/vr matching
        //              -   Arial instead of assets/arial
        //              -   Arial should stay Arial instead of arial
        if (!this.font) return;
        const fontName = this.font;
        const familyName = this.getFamilyNameWithCorrectSuffix(fontName, fontStyle);
        if (debug) console.log("Selected font family:" + familyName);

        // ensure a font family is register under this name
        let fontFamily = ThreeMeshUI.FontLibrary.getFontFamily(familyName as string);
        if (!fontFamily)
            fontFamily = ThreeMeshUI.FontLibrary.addFontFamily(familyName as string);

        // @TODO: @swingingtom how should the font be set?
        //@ts-ignore
        opts.fontFamily = fontFamily;

        switch (fontStyle) {
            default:
            case FontStyle.Normal:
                opts.fontWeight = 400;
                opts.fontStyle = "normal";
                break

            case FontStyle.Bold:
                opts.fontWeight = 700;
                opts.fontStyle = "normal";
                break;

            case FontStyle.Italic:
                opts.fontWeight = 400;
                opts.fontStyle = "italic"
                break;

            case FontStyle.BoldAndItalic:
                opts.fontStyle = 'italic';
                opts.fontWeight = 400;
        }


        // Ensure a fontVariant is registered
        //@TODO: @swingingtom add type for fontWeight
        let fontVariant = fontFamily.getVariant(opts.fontWeight as any as string, opts.fontStyle);
        if (!fontVariant) {
            let jsonPath = familyName;
            if (!jsonPath?.endsWith("-msdf.json")) jsonPath += "-msdf.json";
            let texturePath = familyName;
            if (!texturePath?.endsWith(".png")) texturePath += ".png";

            //@TODO: @swingingtom add type for fontWeight
            //@TODO: @swingingtom addVariant return type is wrong (should be FontVariant)
            fontVariant = fontFamily.addVariant(opts.fontWeight as any as string, opts.fontStyle, jsonPath, texturePath as string) as any as ThreeMeshUI.FontVariant;
            /** @ts-ignore */
            fontVariant?.addEventListener('ready', () => {
                this.markDirty();
            });
        }

    }

    private getFamilyNameWithCorrectSuffix(familyName: string, style: FontStyle): string {


        // the URL decorator resolves the URL to absolute URLs - we need to remove the domain part since we're only interested in the path
        if (familyName.startsWith("https:") || familyName.startsWith("http:")) {
            const url = new URL(familyName);
            familyName = url.pathname;
        }


        // we can only change the style for the family if the name has a suffix (e.g. Arial-Bold)
        const styleSeparator = familyName.lastIndexOf('-');
        if (styleSeparator < 0) return familyName;

        // Check if the font name contains a style that we don't support in the enum
        // e.g. -Medium, -Black, -Thin...
        const styleName = familyName.substring(styleSeparator + 1)?.toLowerCase();
        if (unsupportedStyleNames.includes(styleName)) {
            if (debug) console.warn("Unsupported font style: " + styleName);
            return familyName;
        }

        // Try find a suffix that matches the style
        // We assume that if the font name is "Arial-Regular" then the bold version is "Arial-Bold"
        // and if the font name is "arial-regular" then the bold version is "arial-bold"
        const pathSeparatorIndex = familyName.lastIndexOf("/");
        let fontBaseName = familyName;
        if (pathSeparatorIndex >= 0) {
            fontBaseName = fontBaseName.substring(pathSeparatorIndex + 1);
        }
        const isUpperCase = fontBaseName[0] === fontBaseName[0].toUpperCase();
        const fontNameWithoutSuffix = familyName.substring(0, styleSeparator > pathSeparatorIndex ? styleSeparator : familyName.length);
        if (debug) console.log("Select font: ", familyName, FontStyle[style], fontBaseName, isUpperCase, fontNameWithoutSuffix);

        switch (style) {
            case FontStyle.Normal:
                if (isUpperCase) return fontNameWithoutSuffix + "-Regular";
                else return fontNameWithoutSuffix + "-regular";
            case FontStyle.Bold:
                if (isUpperCase) return fontNameWithoutSuffix + "-Bold";
                else return fontNameWithoutSuffix + "-bold";
            case FontStyle.Italic:
                if (isUpperCase) return fontNameWithoutSuffix + "-Italic";
                else return fontNameWithoutSuffix + "-italic";
            case FontStyle.BoldAndItalic:
                if (isUpperCase) return fontNameWithoutSuffix + "-BoldItalic";
                else return fontNameWithoutSuffix + "-bolditalic";
            default:
                return familyName;
        }
    }
}

class TagStackEntry {
    tag: TagInfo;
    previousValues: object;
    constructor(tag: TagInfo, previousValues: object) {
        this.tag = tag;
        this.previousValues = previousValues;
    }
}

declare type TagInfo = {
    type: string,
    startIndex: number,
    endIndex: number,
    isEndTag: boolean
}

// const anyTag = new RegExp('<.+?>', 'g');
// const regex = new RegExp('<(?<type>.+?)>(?<text>.+?)<\/.+?>', 'g');


const unsupportedStyleNames = [
    "medium", "mediumitalic", "black", "blackitalic", "thin", "thinitalic", "extrabold", "light", "lightitalic", "semibold"
]