var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Matrix4, Object3D, Quaternion, Vector2, Vector3 } from "three";
import * as ThreeMeshUI from 'three-mesh-ui';
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 { onChange } from "./Utils.js";
const debug = getParam("debugui");
const debugLayout = getParam("debuguilayout");
export class Size {
    width;
    height;
}
export class Rect {
    x;
    y;
    width;
    height;
}
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 {
    get parent() {
        return this._parentRectTransform;
    }
    // @serializable(Object3D)
    // root? : Object3D;
    get translation() { return this.gameObject.position; }
    get rotation() { return this.gameObject.quaternion; }
    get scale() { return this.gameObject.scale; }
    _anchoredPosition;
    get anchoredPosition() {
        if (!this._anchoredPosition)
            this._anchoredPosition = new Vector2();
        return this._anchoredPosition;
    }
    set anchoredPosition(value) {
        this._anchoredPosition = value;
    }
    sizeDelta = new Vector2(100, 100);
    pivot = new Vector2(.5, .5);
    anchorMin = new Vector2(0, 0);
    anchorMax = 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;
    /** Optional min height in pixel, set to undefined to disable it */
    minHeight;
    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;
    lastMatrix;
    rectBlock;
    _transformNeedsUpdate = false;
    _initialPosition;
    _parentRectTransform;
    _lastUpdateFrame = -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) {
        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");
        }
    }
    canUpdate() {
        return this._transformNeedsUpdate && this.activeAndEnabled && this._lastUpdateFrame !== this.context.time.frame;
    }
    onApplyTransform(reason) {
        // 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);
        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;
            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;
            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);
    //     }
    // }
    _lastAnchoring;
    /** applies the position offset to the passed in vector */
    applyAnchoring(pos) {
        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 */
    applyPivot(vec) {
        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() {
        // @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
    ensureValidSize(opts, fallbackWidth = 0.0001) {
        if (opts.width <= 0) {
            opts.width = fallbackWidth;
        }
        if (opts.height <= 0)
            opts.height = 0.0001;
        return opts;
    }
    _createdBlocks = [];
    _createdTextBlocks = [];
    createNewBlock(opts) {
        opts = {
            ...this.getBasicOptions(),
            ...opts
        };
        if (debug)
            console.log(this.name, opts);
        const block = new ThreeMeshUI.Block(opts);
        this._createdBlocks.push(block);
        return block;
    }
    createNewText(opts) {
        if (debug)
            console.log(opts);
        opts = {
            ...this.getBasicOptions(),
            ...opts,
        };
        if (debug)
            console.log(this.name, opts);
        const block = new ThreeMeshUI.Text(opts);
        this._createdTextBlocks.push(block);
        return block;
    }
}
__decorate([
    serializable(Vector2)
], RectTransform.prototype, "anchoredPosition", null);
__decorate([
    serializable(Vector2)
], RectTransform.prototype, "sizeDelta", void 0);
__decorate([
    serializable(Vector2)
], RectTransform.prototype, "pivot", void 0);
__decorate([
    serializable(Vector2)
], RectTransform.prototype, "anchorMin", void 0);
__decorate([
    serializable(Vector2)
], RectTransform.prototype, "anchorMax", void 0);
//# sourceMappingURL=RectTransform.js.map