import html2canvas from 'html2canvas';
import { Axis, Color3, MeshBuilder, Observable, Plane, StandardMaterial, Texture, Vector3, Matrix } from '../loader/babylonjs-import';
import { CoordinateTracker } from './CoordinateTracker';
/**
 * Renders HTML elements to a plane in 3D space.
 * WARNING: This class does NOT sanitize its contents. Make sure to sanitize your HTML in the htmlConversion parameter or before passing it
 * to this class.
 */
export class Html2CanvasPlane {
    _api;
    id;
    _htmlConversion;
    _position;
    _normal;
    _scale;
    rotationAroundNormal;
    _backgroundColor;
    _pointFactory;
    _parent;
    _maxTextureSize = 512 * 256;
    /**
     * @hidden
     */
    _pointTracker;
    onPointTrackableScreen;
    _html;
    _originalHtml;
    _mesh;
    _minScale = 0.01;
    _texture;
    _cornerCoordinatesLocal = new Map();
    _cornerPoints = [];
    _centerPoint;
    onHtmlChangeObservable = new Observable();
    onBackgroundColorChangeObservable = new Observable();
    _onParentWorldMatrixUpdateObserver;
    _containerDiv = document.createElement('div');
    static _parser = new DOMParser();
    _tmpVecA = Vector3.Zero();
    _tmpVecB = Vector3.Zero();
    _tmpVecC = Vector3.Zero();
    _tmpMatrix = Matrix.Identity();
    /**
     * Creates a new Html2CanvasPlane and adds it to the scene.
     * @param api BimApi instance.
     * @param id unique id
     * @param html The original HTML
     * @param htmlConversion The method for modifying the HTML string. NOTE: This class does not sanitize HTML so this method is a good place to do so.
     * @param position The position of the plane.
     * @param normal The normal of the plane.
     * @param scale The scale of the plane. If a number is passed in the scale will automatically adjust for aspect ratio and parent scale. If a Vector3 is passed in it is the absolute scaling of the plane.
     * @param rotationAroundNormal How much the plane should be rotated around the normal in radians.
     * @param backgroundColor Background color for the resulting canvas which is then applied to the plane.
     * @param pointFactory Factory method for the points the user can drag to resize the plane etc.
     * @param parent Babylonjs parent node.
     * @returns The new a promise with the Html2CanvasPlane.
     */
    static async create(api, id, html, htmlConversion, position, normal, scale, rotationAroundNormal, backgroundColor, pointFactory, parent) {
        new Texture(null, api.viewer.scene, false, true, Texture.TRILINEAR_SAMPLINGMODE);
        const h2cPlane = new Html2CanvasPlane(api, id, html, htmlConversion, position, normal, scale, rotationAroundNormal, backgroundColor, pointFactory, parent);
        await h2cPlane.renderToPlane();
        return h2cPlane;
    }
    constructor(_api, id, html, _htmlConversion, _position, _normal, _scale, rotationAroundNormal, _backgroundColor, _pointFactory, _parent) {
        this._api = _api;
        this.id = id;
        this._htmlConversion = _htmlConversion;
        this._position = _position;
        this._normal = _normal;
        this._scale = _scale;
        this.rotationAroundNormal = rotationAroundNormal;
        this._backgroundColor = _backgroundColor;
        this._pointFactory = _pointFactory;
        this._parent = _parent;
        //to avoid z fighting.
        this._position.addInPlace(this._normal.scale(0.02));
        this._texture.hasAlpha = true;
        this._originalHtml = html;
        this._html = this._htmlConversion(html);
        this._pointTracker = new CoordinateTracker(this._api, true);
        this.onPointTrackableScreen = this._pointTracker.onUpdateObservable;
        for (let i = 0; i < 4; i++) {
            this._cornerPoints.push(this._pointFactory(this._api, Plane.FromPositionAndNormal(this._position, this._normal)));
            this._cornerPoints[i]._parent = this;
            this._pointTracker.track(this._cornerPoints[i], this._cornerPoints[i]);
        }
        this._cornerCoordinatesLocal.set(this._cornerPoints[0], new Vector3(-0.5, 0, -0.5));
        this._cornerCoordinatesLocal.set(this._cornerPoints[1], new Vector3(0.5, 0, -0.5));
        this._cornerCoordinatesLocal.set(this._cornerPoints[2], new Vector3(0.5, 0, 0.5));
        this._cornerCoordinatesLocal.set(this._cornerPoints[3], new Vector3(-0.5, 0, 0.5));
        this._centerPoint = this._pointFactory(this._api, Plane.FromPositionAndNormal(this._position, this._normal));
        this._centerPoint._parent = this;
        this._pointTracker.track(this._centerPoint, this._centerPoint);
        this.registerParentWorldMatrixCallback();
        //default style
        this._containerDiv.style.position = 'absolute';
        // this._containerDiv.style.width = 'fit-content';
        this._containerDiv.style.overflowWrap = 'anywhere';
        this._containerDiv.style.paddingRight = '0.5em';
        this._containerDiv.style.paddingLeft = '0.5em';
        // this._containerDiv.style.maxWidth = '600px';
        this._containerDiv.style.opacity = '0.0';
        this._containerDiv.id = `containerDiv_${this.id}`;
    }
    registerParentWorldMatrixCallback() {
        if (this._parent) {
            let updateFlag = this._parent.getWorldMatrix().updateFlag;
            this._onParentWorldMatrixUpdateObserver = this._parent.onAfterWorldMatrixUpdateObservable.add(() => {
                if (this._parent && updateFlag !== this._parent.getWorldMatrix().updateFlag && this._mesh) {
                    for (const p of this._cornerPoints) {
                        p.recalculateWorldCoords = true;
                        updateFlag = this._parent.getWorldMatrix().updateFlag;
                    }
                    this._mesh.computeWorldMatrix();
                    this.updatePointsFromLocalSpaceCoordinates();
                }
            });
        }
    }
    /**
     * Sets the parent of the Html2CanvasPlane without keeping the position in world space.
     * @param node new parent for the Html2CanvasPlane.
     */
    set parent(node) {
        if (this._onParentWorldMatrixUpdateObserver && this.parent) {
            this.parent.onAfterWorldMatrixUpdateObservable.remove(this._onParentWorldMatrixUpdateObserver);
        }
        this._parent = node;
        this._mesh.parent = node;
        this.registerParentWorldMatrixCallback();
        this._mesh.computeWorldMatrix();
        for (const p of this._cornerPoints) {
            p.recalculateWorldCoords = true;
        }
        this._centerPoint.recalculateWorldCoords = true;
    }
    /**
     * Gets the parent TransformNode.
     */
    get parent() {
        return this._parent;
    }
    /**
     * Gets the position of the plane in world space.
     */
    get position() {
        return this._mesh.position;
    }
    /**
     * Gets the scaling of the plane.
     */
    get scaling() {
        return this._mesh.scaling;
    }
    /**
     * @returns The worldmatrix of the plane mesh.
     */
    worldMatrix() {
        if (!this._mesh) {
            throw new Error('Can not get world matrix, no mesh exists.');
        }
        return this._mesh.computeWorldMatrix();
    }
    parseHtml(html) {
        const htmlDoc = Html2CanvasPlane._parser.parseFromString(html, 'text/html');
        return htmlDoc;
    }
    /**
     * Renders the HTML to a plane in 3D space.
     * @returns The mesh on which the HTML is rendered.
     */
    async renderToPlane() {
        await this.updateTexture();
        this._mesh = MeshBuilder.CreateGround(`Html2CanvasPlane_${this.id}`, { subdivisions: 1 }, this._api.viewer.scene);
        this._mesh.parent = this._parent;
        this._mesh.id = this.id;
        const materialGround = new StandardMaterial(`Html2CanvasPlaneMaterial_${this.id}`, this._api.viewer.scene);
        materialGround.specularColor = Color3.Black();
        materialGround.emissiveColor = Color3.Black();
        materialGround.diffuseTexture = this._texture;
        materialGround.useAlphaFromDiffuseTexture = true;
        this._mesh.material = materialGround;
        this._api.viewer.selectables.attach(this._mesh);
        this._mesh.position.copyFrom(this._position);
        Vector3.CrossToRef(this._normal, this._mesh.up, this._tmpVecA);
        const ang = Vector3.GetAngleBetweenVectors(this._normal, this._mesh.up, this._tmpVecA);
        this._mesh.rotate(this._tmpVecA, ang);
        this._mesh.rotate(this._normal, Math.PI + this.rotationAroundNormal);
        if (this._scale instanceof Vector3) {
            this._mesh.scaling.x = this._scale.x;
            this._mesh.scaling.z = this._scale.z;
        }
        else {
            this._mesh.scaling.x = this._scale * (this._texture.getSize().width / this._texture.getSize().height);
            this._mesh.scaling.z = this._scale;
            if (this._parent) {
                this._parent.getWorldMatrix().invertToRef(this._tmpMatrix);
                Vector3.TransformCoordinatesToRef(this._mesh.scaling, this._tmpMatrix, this._mesh.scaling);
            }
        }
        let cornerCoordLocal = Vector3.Zero();
        for (const p of this._cornerPoints) {
            cornerCoordLocal = this._cornerCoordinatesLocal.get(p);
            p.setLocals(cornerCoordLocal.x, cornerCoordLocal.y, cornerCoordLocal.z);
            this._pointTracker.track(p, p);
        }
        this._centerPoint.setLocals(0, 0, 0);
        this._pointTracker.track(this._centerPoint, this._centerPoint);
        this._mesh.material?.freeze();
        return this._mesh;
    }
    /**
     * Updates the container div element with the current HTML.
     */
    updateContainerDiv() {
        //clear innerhtml so we dont end up with duplicate html.
        this._containerDiv.innerHTML = '';
        const htmlDoc = this.parseHtml(this._html);
        htmlDoc.body.childNodes.forEach((node) => {
            this._containerDiv.appendChild(node);
        });
        return this._containerDiv;
    }
    /**
     * Renders the containerdiv to the plane.
     * @returns A promise that will resolve when the texture is rendered.
     */
    async renderContainerDivToPlane() {
        document.body.appendChild(this._containerDiv);
        const size = this._containerDiv.clientHeight * this._containerDiv.clientWidth;
        const maxScale = Math.sqrt(this._maxTextureSize / size);
        const canvas = await html2canvas(this._containerDiv, {
            onclone: (doc) => {
                doc.getElementById(this._containerDiv.id).style.opacity = '1.0';
            },
            scale: maxScale,
            backgroundColor: this._backgroundColor.toHexString()
        });
        document.body.removeChild(this._containerDiv);
        const dataURL = canvas.toDataURL();
        return new Promise((resolve) => {
            this._texture.updateURL(dataURL, undefined, () => {
                URL.revokeObjectURL(dataURL);
                resolve(this._texture);
            });
        });
    }
    /**
     * Updates the container div and renders the container div to the plane.
     * @returns A promise which resolves when the texture is updated.
     */
    async updateTexture() {
        this.updateContainerDiv();
        return this.renderContainerDivToPlane();
    }
    /**
     * @hidden
     */
    rotate(point) {
        const cornerCoordLocal = this._cornerCoordinatesLocal.get(point);
        this._tmpVecA.set(cornerCoordLocal.x, cornerCoordLocal.y, cornerCoordLocal.z); //previous point position in local space
        this._tmpVecB.set(point.localX, point.localY, point.localZ); //current point position in local space
        let angle = Vector3.GetAngleBetweenVectorsOnPlane(this._tmpVecA, this._tmpVecB, Axis.Y);
        //This is needed for rotations to work when the plane does not have a 1:1 aspect ratio.
        angle =
            angle /
                (this._mesh.scaling.z > this._mesh.scaling.x
                    ? this._mesh.scaling.z / this._mesh.scaling.x
                    : this._mesh.scaling.x / this._mesh.scaling.z);
        this._mesh.rotate(Axis.Y, angle);
        this.rotationAroundNormal += angle % (Math.PI * 2);
        this.updatePointsFromLocalSpaceCoordinates();
    }
    /**
     * @hidden
     */
    scale(point, retainAspectRatio = true) {
        const cornerCoordLocal = this._cornerCoordinatesLocal.get(point);
        if (!cornerCoordLocal) {
            throw new Error(`Local corner coordinate could not be found for point ${point}`);
        }
        this._tmpVecA.set(cornerCoordLocal.x, cornerCoordLocal.y, cornerCoordLocal.z); //previous point position in local space
        this._tmpVecB.set(point.localX, point.localY, point.localZ); //current point position in local space
        const prevX = this._mesh.scaling.x;
        const prevZ = this._mesh.scaling.z;
        const angle = Vector3.GetAngleBetweenVectorsOnPlane(this._tmpVecA, this._tmpVecB, Vector3.Cross(this._tmpVecA, this._tmpVecB));
        if (angle < Math.PI / 2) {
            if (retainAspectRatio) {
                let ratio = this._tmpVecB.length() / this._tmpVecA.length();
                //this is done since we need to halve the amount that we scale the mesh by.
                //but if we just divide by 2, 1.2 would become 0.6, but we want it to become 1.1
                //since we only want to divide the scaling factor by 2.
                ratio = (ratio - 1) / 2 + 1;
                const min = Math.min(this._mesh.scaling.x, this._mesh.scaling.z);
                const maxRatio = min / this._minScale;
                this._mesh.scaling.x *= Math.max(ratio, 1 / maxRatio);
                this._mesh.scaling.z *= Math.max(ratio, 1 / maxRatio);
            }
            else {
                let ratioX = this._tmpVecB.x / this._tmpVecA.x;
                let ratioZ = this._tmpVecB.z / this._tmpVecA.z;
                ratioX = (ratioX - 1) / 2 + 1;
                ratioZ = (ratioZ - 1) / 2 + 1;
                const maxRatioX = this._mesh.scaling.x / this._minScale;
                this._mesh.scaling.x *= Math.max(ratioX, 1 / maxRatioX);
                const maxRatioZ = this._mesh.scaling.z / this._minScale;
                this._mesh.scaling.z *= Math.max(ratioZ, 1 / maxRatioZ);
            }
            const deltaX = this._mesh.scaling.x - prevX;
            const deltaZ = this._mesh.scaling.z - prevZ;
            this._tmpVecC.set(deltaX * cornerCoordLocal.x, 0, deltaZ * cornerCoordLocal.z);
            Vector3.TransformCoordinatesToRef(this._tmpVecC, this._mesh.getWorldMatrix().getRotationMatrix(), this._tmpVecC);
            this._mesh.position.addInPlace(this._tmpVecC);
        }
        this.updatePointsFromLocalSpaceCoordinates();
    }
    updatePointsFromLocalSpaceCoordinates() {
        for (const p of this._cornerPoints) {
            const localSpace = this._cornerCoordinatesLocal.get(p);
            p.setLocals(localSpace.x, localSpace.y, localSpace.z);
            this._pointTracker.track(p, p);
        }
        //again, what happens if the user changes the pivot point of the mesh?
        this._centerPoint.setLocals(0, 0, 0);
        this._pointTracker.track(this._centerPoint, this._centerPoint);
        this._api.viewer.wakeRenderLoop();
    }
    /**
     * @hidden
     */
    translate() {
        if (this._parent) {
            this._parent.getWorldMatrix().invertToRef(this._tmpMatrix);
            Vector3.TransformCoordinatesFromFloatsToRef(this._centerPoint.x, this._centerPoint.y, this._centerPoint.z, this._tmpMatrix, this._tmpVecA);
            this._mesh.position.copyFrom(this._tmpVecA);
        }
        else {
            this._mesh.position.set(this._centerPoint.x, this._centerPoint.y, this._centerPoint.z);
        }
        this.updatePointsFromLocalSpaceCoordinates();
    }
    /**
     * Sets the HTML and applies it to the texture
     * @param html The new HTML to be rendered.
     * @param htmlConversion The conversion function for the HTML, note that it is recommended to santize the input first.
     */
    async setHtmlAndUpdate(html, htmlConversion) {
        this._originalHtml = html;
        this._html = htmlConversion(html);
        await this.updateTexture();
        const textureSize = this._texture.getSize();
        if (this._scale instanceof Vector3) {
            this._mesh.scaling.x = this._scale.z * (textureSize.width / textureSize.height);
        }
        else {
            this._mesh.scaling.z = this._scale;
            this._mesh.scaling.x = this._scale * (textureSize.width / textureSize.height);
            if (this._parent) {
                this._parent.getWorldMatrix().invertToRef(this._tmpMatrix);
                Vector3.TransformCoordinatesToRef(this._mesh.scaling, this._tmpMatrix, this._mesh.scaling);
            }
        }
        this.updatePointsFromLocalSpaceCoordinates();
        this.onHtmlChangeObservable.notifyObservers(this._originalHtml);
    }
    async setHtml(html, htmlConversion) {
        this._originalHtml = html;
        this._html = htmlConversion(html);
        this.updateContainerDiv();
    }
    /**
     * Sets the background color and updates the texture which is rendered on the plane mesh.
     * @param col New background color.
     */
    async setBackgroundColorAndUpdateTexture(col) {
        this._backgroundColor = col;
        await this.updateTexture();
        this.onBackgroundColorChangeObservable.notifyObservers(this._backgroundColor);
    }
    /**
     * Returns the original HTML before it was put through the htmlConversion parameter method.
     */
    get originalHtml() {
        return this._originalHtml;
    }
    get containerDiv() {
        return this._containerDiv;
    }
    /**
     * @returns the babylonjs mesh
     */
    get mesh() {
        return this._mesh;
    }
    clear() {
        this._pointTracker.clear();
        this._mesh.setEnabled(false);
    }
    /**
     * Get the current minScale of the plane.
     */
    get minScale() {
        return this._minScale;
    }
    /**
     * Set the minimum scale of the plane. This value will be applied on all scaling axes.
     * @param scale New minimum scale to apply to the scaling axes.
     */
    set minScale(scale) {
        this._minScale = scale;
    }
    /**
     * Disposes the Html2CanvasPlane.
     */
    dispose() {
        this.clear();
        this._mesh.dispose();
    }
    /**
     * Sets the max size of the texture used to render the html.
     * NOTE: Increasing the max texture size will have a negative performance impact
     * @param size new max texture size (width * height)
     */
    set maxTextureSize(size) {
        this._maxTextureSize = size;
    }
    /**
     * Gets the max texture size (width * height).
     */
    get maxTextureSize() {
        return this._maxTextureSize;
    }
    centerPoint() {
        return this._centerPoint;
    }
    /**
     * Gets the style of the HTML element that will be rendered to the plane.
     * NOTE: Do not change background color here as the canvas renderer has its own background color property,
     * use {@link setBackgroundColorAndUpdateTexture} instead.
     */
    get style() {
        return this._containerDiv.style;
    }
}
//# sourceMappingURL=Html2CanvasPlane.js.map