import { Texture, Constants, RawTexture, Observable } from './babylonjs-import';
import { LazyResizableTypedArray } from '../LazyResizableTypedArray';
import { Uint32Bits } from '../Uint32Bits';
import { telemetry } from '../Telemetry';
import { SeverityLevel } from '@microsoft/applicationinsights-web';
/**
 * A resizable RGBA raw texture where bits and bytes can be manipulated freely. The texture
 * will always be square.
 */
export class LazyResizableRawTexture {
    name;
    hasAlpha;
    _maxTextureSize;
    _onPixelChanged;
    _rgba = new LazyResizableTypedArray((len) => new Uint32Array(len));
    _texture;
    _hasTextureChanges = false;
    _beginFrameObserver = null;
    _scene;
    _propertyBits = new Uint32Bits(this._rgba, (offset, current, prevValue) => {
        // This callback is ONLY called if current.value <> prevValue. Hence we know that
        // we have a texture change
        this._hasTextureChanges = true;
        this._onPixelChanged(offset, current, prevValue);
    });
    onUpdateObservable = new Observable();
    /**
     * Constructor
     * @param name Name of texture
     * @param hasAlpha Whether texture has alpha or not.
     * @param _maxTextureSize Maximum size of the texture. Default is 4096. May actually become less than the specified value if the GPU does not support it.
     * @param _onPixelChanged Callback invoked when a pixel has changed its value.
     */
    constructor(name, hasAlpha, _maxTextureSize = 4096, _onPixelChanged = () => { }) {
        this.name = name;
        this.hasAlpha = hasAlpha;
        this._maxTextureSize = _maxTextureSize;
        this._onPixelChanged = _onPixelChanged;
        // Why _onPixelChanged instead of just an Observable? Because this event is called for every pixel change
        // and that can be a lot. Calling Observeable.notifyObservers for every pixel change comes with overhead
        // which adds up quickly when we have millions of pixels.
    }
    /** @inheritDoc */
    get scene() {
        if (!this._scene)
            throw new Error('Scene is not available. Make sure to call attachToScene before calling getTexture.');
        return this._scene;
    }
    /** @inheritDoc */
    register() {
        this.attachToScene(this.scene);
    }
    /** @inheritDoc */
    rebuild() {
        this.updateIfTextureHasChanges(this.scene, true);
    }
    /** @inheritDoc */
    dispose() {
        this.reset();
        if (this._scene) {
            if (this._beginFrameObserver) {
                this._scene.getEngine().onBeginFrameObservable.remove(this._beginFrameObserver);
                this._beginFrameObserver = null;
            }
            // NOTE Would like to remove from scene._beforeRenderTargetClearStage but it is unclear how to do that.
            this._scene = undefined;
        }
    }
    /**
     * Total size in bytes of the RGBA texture
     */
    get sizeInBytes() {
        return this._rgba.uint8.byteLength;
    }
    /**
     * Size in pixels (RGBA) of texture.
     */
    get size() {
        return this._rgba.buffer.length;
    }
    /**
     * Sets the minimum size of the texture. If the texture is already
     * larger then size will not be changed.
     * @param sz Minimum size.
     * @returns The actual size.
     */
    setMinSize(sz) {
        const oldLen = this._rgba.buffer.length;
        if (this._rgba.setMinLength(sz)) {
            telemetry.trackTrace({
                message: `Resized texture`,
                severityLevel: SeverityLevel.Information
            }, {
                oldPixelLength: oldLen,
                newPixelLength: this._rgba.buffer.length
            });
        }
        return this.size;
    }
    /**
     * Updates the texture (uploads it to the GPU) if required. Will only be updated
     * if there actually is changes in the texture since the last time that  {@link updateIfTextureHasChanges} was called.
     * @returns `true` if texture was updated, otherwise `false`.
     */
    updateIfTextureHasChanges(scene, forceRecreate = false) {
        const { maxTextureSize } = scene.getEngine().getCaps();
        const textureWidth = Math.min(this._maxTextureSize, Math.ceil(Math.sqrt(this.size)));
        const textureHeight = textureWidth;
        if (textureHeight > maxTextureSize) {
            telemetry.trackTrace({ message: 'Texture size is too large.', severityLevel: SeverityLevel.Error }, { textureHeight, maxTextureSize });
            throw new Error(`Texture size is too large. Max supported is ${maxTextureSize}`);
        }
        const currentTextureSize = this._texture?.getSize() ?? { width: -1, height: -1 };
        // If there is no texture or if it is changed in size then create a new one
        const recreateTexture = forceRecreate || currentTextureSize.width < textureWidth || currentTextureSize.height < textureHeight;
        if (recreateTexture) {
            // Possibly resize the array to fit the new texture size
            this.setMinSize(textureWidth * textureHeight);
            this._hasTextureChanges = false;
            this._texture?.dispose();
            telemetry.trackTrace({ message: 'Re/creating lazy RGBA texture.', severityLevel: SeverityLevel.Verbose }, { textureWidth, textureHeight });
            this._texture = RawTexture.CreateRGBATexture(this._rgba.uint8, textureWidth, textureHeight, scene, false, false, Texture.NEAREST_NEAREST, Constants.TEXTURETYPE_UNSIGNED_INT, Constants.TEXTURE_CREATIONFLAG_STORAGE);
            this._texture.hasAlpha = this.hasAlpha;
            this._texture.anisotropicFilteringLevel = 1;
            this._texture.wrapU = Texture.CLAMP_ADDRESSMODE;
            this._texture.wrapV = Texture.CLAMP_ADDRESSMODE;
            this._texture.name = this.name;
            this.onUpdateObservable.notifyObservers(this);
            return true;
        }
        if (this._hasTextureChanges) {
            this._hasTextureChanges = false;
            this._texture.update(this._rgba.uint8);
            this.onUpdateObservable.notifyObservers(this);
            return true;
        }
        return false;
    }
    /**
     * Resets texture. back to 0 size.
     */
    reset() {
        this._texture?.dispose();
        this._texture = undefined;
        this._rgba.reset();
        this._hasTextureChanges = false;
    }
    /**
     * Attaches the texture to a scene and ensures it is updated before rendering frames or to a render target.
     * A texture can only be attached to one scene.
     * @param scene - The scene to attach the texture to.
     * @returns The {@link LazyResizableRawTexture} instance.
     * @throws Error if the engine is not available.
     */
    attachToScene(scene) {
        if (this._scene)
            return this;
        this._scene = scene;
        this.updateIfTextureHasChanges(scene);
        // Ensure texture is up to date on GPU before we render a frame.
        this._beginFrameObserver = scene.getEngine().onBeginFrameObservable.add(() => {
            this.updateIfTextureHasChanges(scene);
        });
        // Ensure texture is up to date on GPU before we render to a texture (render target in BJS).
        scene._beforeRenderTargetClearStage.registerStep(1, this, () => {
            this.updateIfTextureHasChanges(scene);
        });
        return this;
    }
    /**
     * Gets a BJS `RawTexture` that represents the current {@link LazyResizableRawTexture} instance.
     * @param _scene BJS scene.
     * @returns A BJS `RawTexture`.
     */
    getTexture(_scene) {
        if (!this._texture)
            throw new Error('Texture is not available. Make sure to call attachToScene before calling getTexture.');
        return this._texture;
    }
    /**
     * Gets a {@link Uint32Bits} instance that can be used to manipulate the bits of the RGBA
     * pixel stored at the offset specified by `pixelOffset`
     * WARNING: Never ever store a reference to the {@link Uint32Bits} instance. It is reused for all pixels
     * in the texture. whenever {@link bits} or {@link bytes} are called again the {@link Uint32Bits} instance
     * will point to a new pixel. This behavior is because of performace.
     * @param pixelOffset Pixel offset.
     * @returns {@link Uint32Bits} instance
     */
    bits(pixelOffset) {
        this._propertyBits.dataOffset = pixelOffset;
        return this._propertyBits;
    }
    /**
     * Gets a {@link Uint32Bytes} instance that can be used to manipulate the bytes of the RGBA
     * pixel stored at the offset specified by `pixelOffset`
     * WARNING: Never ever store a reference to the {@link Uint32Bytes} instance. It is reused for all pixels
     * in the texture. whenever {@link bits} or {@link bytes} are called again the {@link Uint32Bytes} instance
     * will point to a new pixel. This behavior is because of performace.
     * @param pixelOffset Pixel offset.
     * @returns {@link Uint32Bits} instance
     */
    bytes(pixelOffset) {
        return this.bits(pixelOffset).bytes;
    }
}
//# sourceMappingURL=LazyResizableRawTexture.js.map