/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */
import { VertexBuffer, Mesh, Color4 } from './babylonjs-import';
import { Materials } from './materials';

/**
 * Represents a single object (hence a single pixel) in the GPU picking texture.
 * Should be created by calling to {@link Materials.createGpuTextureObject} instead of using
 * constructor directly.
 */
export class TextureObject {
    private static _tmp = {
        colorA: [0, 0, 0, 0] as [number, number, number, number]
    };
    private _isDisposed = false;

    /**
     * 0b10000000000000000; Bit 16 is set to 1 indicates that its not a IFC product
     * this is used in shaders that tell whether the object being rendered is a IFC product or not.
     */
    public static readonly IsNotIfcProductFlag = 65536;
    /**
     * Constructor.
     * @internal Only for internal use. Use {@link Materials.createGpuTextureObject} instead.
     * @param _materials Materials instance
     * @param id Unique object identifier (represents location in GPU picking texture).
     */
    public constructor(
        private readonly _materials: Materials,
        public readonly id: number
    ) {
        // this._materials.deleteGpuTextureObject()
    }

    /** Gets the color of the texture object (pixel). */
    public get color(): Color4 {
        return Color4.FromInts(...this._materials.intToGpuPickRgbaInPlace(this.id, 255, TextureObject._tmp.colorA));
    }

    /**
     * Replaces the UV coordiates on the specified mesh. Combined with a call to {@link assignGpuPickingMaterial}
     * this will make sure that the mesh is rendered with a unique color.
     * @param mesh Mesh to replace UV coordinates on.
     */
    assignUVKind(mesh: Mesh): void {
        this.throwOnIsDisposed();
        if (!mesh.isVertexBufferUpdatable) {
            throw new Error('mesh must be updatable');
        }

        const uvLength = (mesh.getVerticesData(VertexBuffer.PositionKind)?.length ?? 0) / 3;
        if (uvLength === 0) {
            throw new Error('mesh has no vertices');
        }

        const uvs = [];
        const posLen = uvLength;
        for (let idx = 0; idx < posLen; ++idx) {
            uvs.push(this.id, TextureObject.IsNotIfcProductFlag);
        }
        const vertexBuffer = new VertexBuffer(
            mesh.getEngine(),
            uvs,
            VertexBuffer.UVKind,
            false,
            false,
            2,
            mesh.isAnInstance
        );
        mesh.setVerticesBuffer(vertexBuffer);
        for (const child of mesh.getChildren((n) => n instanceof Mesh)) {
            this.assignUVKind(child as Mesh);
        }
    }

    /**
     * Replaces the material on the mesh with a GPU picking material. Combined with a
     * call to {@link assignUVKind} this will ensure that the mesh is rendered with a unique color.
     * @param mesh Mesh to relace material on.
     */
    assignGpuPickingMaterial(mesh: Mesh): void {
        this.throwOnIsDisposed();
        mesh.material = this._materials.getGpuPickingMaterial(mesh);
    }

    /**
     * Replaces both material and UV coordinates on the specified mesh. Ensures that the mesh
     * is rendered with a unique color.
     * @param mesh Mesh to render with a unique color.
     */
    buildGpuPickable(mesh: Mesh): void {
        this.throwOnIsDisposed();
        this.assignUVKind(mesh);
        this.assignGpuPickingMaterial(mesh);
    }

    /**
     * Releases the resource that ensures that this {@link TextureObject} has a unique color.
     * This makes it possible to create new {@link TextureObject} that represents this resource.
     * This does not mean that any mesh that was modified by {@link assignUVKind}, {@link assignGpuPickingMaterial}
     * or {@link buildGpuPickable} is restored in any way. They will still have the same UV coordinates and material.
     * It is up to the caller to ensure that the mesh gets other values if needed.
     * Same thing as calling {@link Materials.deleteGpuTextureObject}(this).
     */
    dispose(): void {
        this._materials.deleteGpuTextureObject(this);
        this._isDisposed = true;
    }

    private throwOnIsDisposed(): void {
        if (this._isDisposed) throw new Error(`TextureObject.Id: ${this.id} is disposed.`);
    }
}
