import { Matrix4, Object3D, Quaternion, Vector2, Vector3 } 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 { foreachComponentEnumerator } from "../../engine/engine_gameobject.js";
import { serializable } from "../../engine/engine_serialization_decorator.js";
import { getParam } from "../../engine/engine_utils.js";
import { GameObject } from '../Component.js';
import { BaseUIComponent } from "./BaseUIComponent.js";
import { type ICanvas, type IRectTransform, type IRectTransformChangedReceiver } from "./Interfaces.js";
import { onChange } from "./Utils.js";

const debug = getParam("debugui");
const debugLayout = getParam("debuguilayout");

export class Size {
    width!: number;
    height!: number;
}

export class Rect {
    x!: number;
    y!: number;
    width!: number;
    height!: number;
}

const tempVec = new Vector3();
const tempMatrix = new Matrix4();
const tempQuaternion = new Quaternion();

/**
* [RectTransform](https://engine.needle.tools/docs/api/RectTransform) defines a rectangle for UI layout with anchoring, pivoting, and positioning capabilities.
* @summary UI Rectangle Transform
* @category User Interface
* @group Components
*/
export class RectTransform extends BaseUIComponent implements IRectTransform, IRectTransformChangedReceiver {

    get parent() {
        return this._parentRectTransform;
    }

    // @serializable(Object3D)
    // root? : Object3D;

    get translation() { return this.gameObject.position; }
    get rotation() { return this.gameObject.quaternion; }
    get scale(): Vector3 { return this.gameObject.scale; }

    private _anchoredPosition!: Vector2;

    @serializable(Vector2)
    get anchoredPosition() {
        if (!this._anchoredPosition) this._anchoredPosition = new Vector2();
        return this._anchoredPosition;
    }
    private set anchoredPosition(value: Vector2) {
        this._anchoredPosition = value;
    }

    @serializable(Vector2)
    sizeDelta: Vector2 = new Vector2(100, 100);

    @serializable(Vector2)
    pivot: Vector2 = new Vector2(.5, .5);

    @serializable(Vector2)
    anchorMin: Vector2 = new Vector2(0, 0);
    @serializable(Vector2)
    anchorMax: Vector2 = new Vector2(1, 1);

    // @serializable(Vector2)
    // offsetMin: Vector2 = new Vector2(0, 0);
    // @serializable(Vector2)
    // offsetMax: Vector2 = new Vector2(0, 0);

    /** Optional min width in pixel, set to undefined to disable it */
    minWidth?: number;
    /** Optional min height in pixel, set to undefined to disable it */
    minHeight?: number;

    get width() {
        let width = this.sizeDelta.x;
        if (this.anchorMin.x !== this.anchorMax.x) {
            if (this._parentRectTransform) {
                const parentWidth = this._parentRectTransform.width;
                const anchorWidth = this.anchorMax.x - this.anchorMin.x;
                width = parentWidth * anchorWidth;
                width += this.sizeDelta.x;
            }
        }
        if(this.minWidth !== undefined && width < this.minWidth) return this.minWidth;
        return width
    }
    get height() {
        let height = this.sizeDelta.y;
        if (this.anchorMin.y !== this.anchorMax.y) {
            if (this._parentRectTransform) {
                const parentHeight = this._parentRectTransform.height;
                const anchorHeight = this.anchorMax.y - this.anchorMin.y;
                height = parentHeight * anchorHeight;
                height += this.sizeDelta.y;
            }
        }
        if(this.minHeight !== undefined && height < this.minHeight) return this.minHeight;
        return height;
    }

    // private lastMatrixWorld!: Matrix4;
    private lastMatrix!: Matrix4;
    private rectBlock!: Object3D;
    private _transformNeedsUpdate: boolean = false;
    private _initialPosition!: Vector3;
    private _parentRectTransform?: RectTransform;
    private _lastUpdateFrame: number = -1;

    awake() {
        super.awake();
        this._lastUpdateFrame = -1;
        this._parentRectTransform = undefined;

        this.rectBlock = new Object3D();
        this.rectBlock.name = this.name;
        this.lastMatrix = new Matrix4();
        this._lastAnchoring = null!;
        // TODO: get rid of the initial position
        this._initialPosition = this.gameObject.position.clone();
        this._initialPosition.z = 0;
        // this is required if an animator animated the transform anchoring
        if (!this._anchoredPosition) this._anchoredPosition = new Vector2();



        // TODO: we need to replace this with the watch that e.g. Rigibody is using (or the one in utils?)
        // perhaps we can also just manually check the few properties in the update loops?
        // TODO: check if value actually changed, this is called on assignment
        onChange(this, "_anchoredPosition", () => { this.markDirty(); });
        onChange(this, "sizeDelta", () => { this.markDirty(); });
        onChange(this, "pivot", () => { this.markDirty(); });
        onChange(this, "anchorMin", () => { this.markDirty(); });
        onChange(this, "anchorMax", () => { this.markDirty(); });
    }

    onEnable() {
        super.onEnable();
        
        if (!this.rectBlock) this.rectBlock = new Object3D();
        if (!this.lastMatrix) this.lastMatrix = new Matrix4();
        if (!this._lastAnchoring) this._lastAnchoring = new Vector2();
        if (!this._initialPosition) this._initialPosition = new Vector3();
        if (!this._anchoredPosition) this._anchoredPosition = new Vector2();

        this.addShadowComponent(this.rectBlock);
        this._transformNeedsUpdate = true;
        this.canvas?.registerTransform(this);
        // this.onApplyTransform("enable");
    }

    onDisable() {
        super.onDisable();
        this.removeShadowComponent();
        this.canvas?.unregisterTransform(this);
    }

    onParentRectTransformChanged(comp: IRectTransform) {
        if (this._transformNeedsUpdate) return;
        // When the parent rect transform changes we have to to recalculate our transform
        this.onApplyTransform(debugLayout ? `${comp.name} changed` : undefined);
    }

    get isDirty() {
        if (!this._transformNeedsUpdate) this._transformNeedsUpdate = !this.lastMatrix.equals(this.gameObject.matrix);
        return this._transformNeedsUpdate;
    }

    // private _copyMatrixAfterRender: boolean = false;

    markDirty() {
        if (this._transformNeedsUpdate) return;
        if (debugLayout) console.warn("RectTransform markDirty()", this.name)
        this._transformNeedsUpdate = true;
        // If mark dirty is called explictly we want to allow updating the transform again when updateTransform is called
        // if we dont reset it here we get delayed layout updates
        this._lastUpdateFrame = -1;
    }


    /** Will update the transforms if it changed or is dirty */
    updateTransform() {
        // TODO: instead of checking matrix again it would perhaps be better to test if position, rotation or scale have changed individually?
        const transformChanged = this._transformNeedsUpdate || !this.lastMatrix.equals(this.gameObject.matrix);// || !this.lastMatrixWorld.equals(this.gameObject.matrixWorld);
        if (transformChanged && this.canUpdate()) {
            this.onApplyTransform(this._transformNeedsUpdate ? "Marked dirty" : "Matrix changed");
        }
    }


    private canUpdate() {
        return this._transformNeedsUpdate && this.activeAndEnabled && this._lastUpdateFrame !== this.context.time.frame;
    }

    private onApplyTransform(reason?: string) {
        // TODO: need to improve the update logic, with this UI updates have some frame delay but dont happen exponentially per hierarchy
        if (this.context.time.frameCount === this._lastUpdateFrame) return;
        this._lastUpdateFrame = this.context.time.frameCount;

        const uiobject = this.shadowComponent;
        if (!uiobject) return;
        if (this.gameObject.parent)
            this._parentRectTransform = GameObject.getComponentInParent(this.gameObject.parent, RectTransform) as RectTransform;
        else
            this._parentRectTransform = undefined;
        this._transformNeedsUpdate = false;

        if (debugLayout) console.warn("RectTransform → ApplyTransform", this.name + " because " + reason);

        if (!this.isRoot()) {
            // Reset temp matrix
            uiobject.matrix.identity();
            uiobject.matrixAutoUpdate = false;
            // calc pivot and apply
            tempVec.set(0, 0, 0);
            this.applyPivot(tempVec);
            uiobject.matrix.setPosition(tempVec.x, tempVec.y, 0);
            // calc rotation matrix and apply (we can skip this if it's not rotated)
            if (this.gameObject.quaternion.x || this.gameObject.quaternion.y || this.gameObject.quaternion.z) {
                tempQuaternion.copy(this.gameObject.quaternion);
                tempQuaternion.x *= -1;
                tempQuaternion.z *= -1;
                tempMatrix.makeRotationFromQuaternion(tempQuaternion);
                uiobject.matrix.premultiply(tempMatrix);
            }
            // calc anchors and offset and apply
            tempVec.set(0, 0, 0);
            this.applyAnchoring(tempVec);
            if(this.canvas?.screenspace) tempVec.z += .1;
            else tempVec.z += .01;
            tempMatrix.identity();
            tempMatrix.setPosition(tempVec.x, tempVec.y, tempVec.z);
            uiobject.matrix.premultiply(tempMatrix);
            // apply scale if necessary
            uiobject.matrix.scale(this.gameObject.scale);
        }
        else {
            // We have to rotate the canvas when it's in worldspace
            const canvas = this.Root as any as ICanvas;
            if (!canvas.screenspace) uiobject.rotation.y = Math.PI;
        }

        this.lastMatrix.copy(this.gameObject.matrix);

        // iterate other components on this object that might need to know about the transform change
        // e.g. Graphic components should update their width and height
        const includeChildren = true;
        for (const comp of foreachComponentEnumerator(this.gameObject, BaseUIComponent, includeChildren, 1)) {
            if (comp === this) continue;
            if (!comp.activeAndEnabled) continue;
            const callback = comp as any as IRectTransformChangedReceiver;
            if (callback.onParentRectTransformChanged) {
                // if (debugLayout) console.log(`RectTransform ${this.name} → call`, comp.name + "/" + comp.constructor.name)
                callback.onParentRectTransformChanged(this);
            }
        }

        // const layout = GameObject.getComponentInParent(this.gameObject, ILayoutGroup);
    }

    // onAfterRender() {
    //     if (this._copyMatrixAfterRender) {
    //         // can we only have this event when the transform changed in this frame? Otherwise all RectTransforms will be iterated. Not sure what is better
    //         this.lastMatrixWorld.copy(this.gameObject.matrixWorld);
    //     }
    // }

    private _lastAnchoring!: Vector2;

    /** applies the position offset to the passed in vector */
    private applyAnchoring(pos: Vector3) {

        if (!this._lastAnchoring) this._lastAnchoring = new Vector2();
        const diff = this._lastAnchoring.sub(this._anchoredPosition)
        this.gameObject.position.x += diff.x;
        this.gameObject.position.y += diff.y;
        this._lastAnchoring.copy(this._anchoredPosition);

        pos.x += (this._initialPosition.x - this.gameObject.position.x);
        pos.y += (this._initialPosition.y - this.gameObject.position.y);
        pos.z += (this._initialPosition.z - this.gameObject.position.z);

        const parent = this._parentRectTransform;
        if (parent) {
            // Calculate vertical offset
            let oy = 0;
            const vert = 1 - this.anchorMax.y - this.anchorMin.y;
            oy -= (parent.height * .5) * vert;
            pos.y += oy;

            // calculate horizontal offset
            let ox = 0;
            const horz = 1 - this.anchorMax.x - this.anchorMin.x;
            ox -= (parent.width * .5) * horz;
            pos.x += ox;
        }
    }

    /** applies the pivot offset to the passed in vector */
    private applyPivot(vec: Vector3) {
        if (this.pivot && !this.isRoot()) {
            const pv = this.pivot.x - .5;
            vec.x -= pv * this.sizeDelta.x * this.gameObject.scale.x;
            const ph = this.pivot.y - .5;
            vec.y -= ph * this.sizeDelta.y * this.gameObject.scale.y;
        }
    }

    getBasicOptions(): ThreeMeshUIEveryOptions {

        // @TODO : instead of getBasicOptions for each component we could use once needleEngine initialized
        // ThreeMeshUI.DefaultValues.set({
        //     backgroundOpacity: 1,
        //     borderWidth: 0, // if we dont specify width here a border will automatically propagated to child blocks
        //     borderRadius: 0,
        //     borderOpacity: 0,
        // })

        const opts = {
            width: this.sizeDelta!.x,
            height: this.sizeDelta!.y,// * this.context.mainCameraComponent!.aspect,
            offset: 0,
            backgroundOpacity: 0,
            borderWidth: 0, // if we dont specify width here a border will automatically propagated to child blocks
            borderRadius: 0,
            borderOpacity: 0,
            letterSpacing: -0.03,
            // justifyContent: 'center',
            // alignItems: 'center',
            // alignContent: 'center',
            // backgroundColor: new Color(1, 1, 1),
        };
        this.ensureValidSize(opts);
        return opts;
    }

    // e.g. when a transform has the size 0,0 we still want to render the text
    private ensureValidSize(opts: Size, fallbackWidth = 0.0001): Size {
        if (opts.width <= 0) {
            opts.width = fallbackWidth;
        }
        if (opts.height <= 0) opts.height = 0.0001;
        return opts;
    }

    private _createdBlocks: ThreeMeshUI.Block[] = [];
    private _createdTextBlocks: ThreeMeshUI.Text[] = [];

    createNewBlock(opts?: ThreeMeshUIEveryOptions | object): ThreeMeshUI.Block {
        opts = {
            ...this.getBasicOptions(),
            ...opts
        };
        if (debug)
            console.log(this.name, opts);
        const block = new ThreeMeshUI.Block(opts as ThreeMeshUIEveryOptions);
        this._createdBlocks.push(block);
        return block;
    }

    createNewText(opts?: ThreeMeshUIEveryOptions | object): ThreeMeshUI.Block {
        if (debug)
            console.log(opts)
        opts = {
            ...this.getBasicOptions(),
            ...opts,
        };
        if (debug)
            console.log(this.name, opts);
        const block = new ThreeMeshUI.Text(opts as ThreeMeshUIEveryOptions);
        this._createdTextBlocks.push(block);
        return block;
    }
}