/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */

import { BimProductMesh } from './BimProductMesh';
import { IBimIfcLoaderElement } from './bim-ifc-loader-element';
import { rgba, RgbaComponent } from './rgba';
import { Material, AbstractMesh, Color4, Observable, Mesh } from './babylonjs-import';
import { Writeable } from '../Types';
import { TextureObject } from './texture-object';
import { NumberPool } from './NumberPool';
import { GpuPickingMaterial } from './CustomBabylonMaterials/GpuPickingMaterial';
import { IfcMaterial, IfcMaterialRenderProperty } from './CustomBabylonMaterials/IfcMaterial';
import { CullingMaterial } from './CustomBabylonMaterials/CullingMaterial';
import { Scene } from './babylonjs-import';
import { telemetry } from '../Telemetry';
import { Uint32Bits } from '../Uint32Bits';
import { LazyResizableRawTexture } from './LazyResizableRawTexture';
import { TransparentIfcMaterial } from './CustomBabylonMaterials/TransparentIfcMaterial';
import { DitheringTextureMode, DitherTexture } from './CustomBabylonMaterials/DitherTexture';
import { DitheringOptions } from './VisualSettings';
import { BabylonMeshDepthMaterial } from './CustomBabylonMaterials/BabylonMeshDepthMaterial';
import { IfcMeshDepthMaterial } from './CustomBabylonMaterials/IfcMeshDepthMaterial';
import { IndexedMap } from '../IndexedMap';
import { GlobalState } from './GlobalIfcProductMeshesRenderInfo';

/**
 * Stores the platform endianess.
 */
export const endianess = (() => {
    const uInt32 = new Uint32Array([0x11223344]);
    const uInt8 = new Uint8Array(uInt32.buffer);

    if (uInt8[0] === 0x44) {
        return 'little';
    } else if (uInt8[0] === 0x11) {
        return 'big';
    } else {
        return 'unknown';
    }
})();

// Log some information about endianness. We only support little endian platforms.
// big endian is supposedly rare but we do log a error if we detect such a platform.
// RGBA textures and bit fiddling will fail if we run on a big endian platform.
if (endianess !== 'little') {
    telemetry.trackTrace({
        message: `Unsupported endinaness '${endianess}'. Nothing will work.`,
        severityLevel: 4
    });
}

// Due to GPU picking (id of mesh/object is given by R,G,B, alpha is reserved
// for use as a visibility in gpu picking texture). We can only use 3 bytes
// that means max number of meshes and objects we can support in a scene is 2^24 (3 bytes)
// This also happens to be the nr of pixels in a 4096*4096 texture.
const maxNumberOfColorTexturePixelIndices = 1 << 24;

//Constants.MATERIAL_ClockWiseSideOrientation
enum MaterialOrientation {
    counterClockwise = 'counterClockwise',
    clockwise = 'clockwise'
}

interface CreateGpuPickingMaterialContext {
    readonly globalRenderState: GlobalState;
    readonly ifcProductColorTexture: LazyResizableRawTexture;
    readonly ifcProductPropertiesTexture: LazyResizableRawTexture;
    outlineDiscerningMaterial: boolean;
    sideOrientation: number;
    isIfcMesh: boolean;
}

/**
 * A cache for storing and retrieving materials.
 */
class MaterialCache {
    /**
     * A map to store materials with their corresponding keys.
     */
    private _materials = new Map<string, CullingMaterial>();

    /**
     * Creates an instance of MaterialCache.
     */
    constructor() {}

    dispose(): void {
        for (const material of this._materials.values()) {
            material.dispose();
        }
        this._materials.clear();
    }

    /**
     * Retrieves a material from the cache if it exists, otherwise creates a new material using the provided factory function,
     * adds it to the cache, and returns it.
     * @template T The type of the material, extending CullingMaterial.
     * @param key The key associated with the material.
     * @param scene The scene to which the material belongs.
     * @param factory A factory function to create a new material if it does not exist in the cache.
     * @returns The material associated with the given key.
     */
    getOrAddMaterial<T extends CullingMaterial, CTX = undefined>(
        key: string,
        scene: Scene,
        factory: (key: string, scene: Scene, context?: CTX) => T,
        context?: CTX
    ): T {
        // NOTE: Method is called a LOT so we avoid lambda allocation by not using getOrAdd pattern with lambda
        let ret = this._materials.get(key);
        if (ret === undefined) {
            ret = factory(key, scene, context);
            this._materials.set(key, ret);
        }
        return ret as T;
    }
}

/**
 * Manages materials and textures used by Twinfinity for rendering of IFC products and other objects.
 */
export class Materials {
    public readonly globalRenderState = new GlobalState();
    private static readonly _outOfColorTexturePixelIndices = -1;
    private readonly _tmp: {
        gpuPickingMAterialContext: CreateGpuPickingMaterialContext;
    };

    private readonly _ifcProductColorTexture = new LazyResizableRawTexture('bimProducts', true);

    private readonly _ifcProductPropertiesTexture = new LazyResizableRawTexture(
        'properties',
        false,
        4096,
        (colorPixelIndex, current, prevValue) => {
            // Update the render property info of the IfcProductMesh instance that corresponds to the colorPixelIndex.
            // All IfcProductMesh that shares the same mergeId will have the same render property info.
            // If they have the same mergeId they will belong to the same BJS mesh. Therefore we will know (per BJS mesh)
            // how many visible, outlined and ghostOutlined meshes there are in that BJS mesh. This is usefull to determine
            // if a BJS mesh shall render or not. For example. Render a BJS mesh only if there are visible meshes in the BJS mesh.
            const rPi = this._bimObjectLookup.get(colorPixelIndex)?._renderPropertyInfo;
            rPi && rPi.updateFromRenderProperty(current, prevValue);
            this.globalRenderState.updateFromRenderProperty(current, prevValue);
        }
    );

    private readonly _ditheringTexture = new DitherTexture(DitheringTextureMode.blueNoise, 4);

    private readonly _materialCache = new MaterialCache();

    /**
     * Lookup for BimProductMesh instances. Used to find a BimProductMesh instance by colorTexturePixelIndex.
     * We use a IndexedMap instead of a Map because we know that the keys are sequential numbers starting from 0.
     * Lookups are faster in a IndexedMap than in a Map and we do a lot of them.
     * We may get holes in the map if meshes are
     */
    private readonly _bimObjectLookup = new IndexedMap<BimProductMesh>();

    /**
     * Pool of colorPixelIdex identifiers. _colorPixelIndexPool.get() will start returning -1 when no more
     * indices can be created (maxCount reached).
     * 0 is reserved and is typically used to indicate that a object does not have a color texture pixel index.
     */
    private readonly _colorTexturePixelIndexPool = new NumberPool(1, maxNumberOfColorTexturePixelIndices);

    // Changed whenever there are pending texture changes.
    private _state = {};

    private static readonly _reservedGpuPickClearColorRgba = [255, 255, 255, 255];

    private _onIsBackfaceCullingEnabled = new Observable<boolean>();

    /**
     * Materials constructor.
     */
    public constructor(private _isBackfaceCullingEnabled = true) {
        this._tmp = {
            gpuPickingMAterialContext: {
                globalRenderState: this.globalRenderState,
                ifcProductColorTexture: this._ifcProductColorTexture,
                ifcProductPropertiesTexture: this._ifcProductPropertiesTexture,
                outlineDiscerningMaterial: false,
                sideOrientation: Material.ClockWiseSideOrientation,
                isIfcMesh: false
            }
        };

        // Whenever the textures change we need to update the _state member so that we can
        this._ifcProductColorTexture.onUpdateObservable.add(() => this.onTextureUpdate());
        this._ifcProductPropertiesTexture.onUpdateObservable.add(() => this.onTextureUpdate());
    }

    /**  Color used to as scene.clearColor when doing gpu picking. */
    public readonly reservedGpuPickClearColor = Color4.FromInts(
        Materials._reservedGpuPickClearColorRgba[RgbaComponent.Red],
        Materials._reservedGpuPickClearColorRgba[RgbaComponent.Green],
        Materials._reservedGpuPickClearColorRgba[RgbaComponent.Blue],
        Materials._reservedGpuPickClearColorRgba[RgbaComponent.Alpha]
    );

    /**
     * Gets a value indicating whether backface culling is enabled.
     * Backface culling is a rendering technique used to improve performance
     * by not drawing polygons facing away from the camera.
     * @returns {boolean} `true` if backface culling is enabled; otherwise, `false`.
     */
    public get isBackfaceCullingEnabled(): boolean {
        return this._isBackfaceCullingEnabled;
    }

    /**
     * Sets the backface culling state for the material.
     * @param value A boolean indicating whether backface culling is enabled.
     * If true, backface culling is enabled; otherwise, it is disabled.
     */
    public set isBackfaceCullingEnabled(value: boolean) {
        if (value !== this._isBackfaceCullingEnabled) {
            this._isBackfaceCullingEnabled = value;
            this._onIsBackfaceCullingEnabled.notifyObservers(value);
        }
    }

    /** Maximum number of Ifc products and {@link TextureObject} instances material can hold. */
    public get maxObjectCount(): number {
        return this._colorTexturePixelIndexPool.maxCount;
    }

    /**
     * Number of objects (IFC products or {@link TextureObject}) this instance currently holds. Cannot be
     * higher than {@link maxObjectCount}.
     */
    public get objectCount(): number {
        return this._colorTexturePixelIndexPool.popCount;
    }

    /** Number of objects (IFC products or {@link TextureObject}) that can still be added to this instance  */
    public get availableObjectCount(): number {
        return this._colorTexturePixelIndexPool.availableCount;
    }

    /**
     * Changed whenever colors or properties on objects, managed by this {@link Materials} instance, change.
     * Useful when performing checks (for exemple during rendering) to determine if something has changed between one frame and another.
     * Let your code keep a reference to {@link state}. Whenever
     * you need to check whether something has changed just compare your local reference with {@link state}. If they differ
     * then colors or properties of objects have changed since the last frame. Dont forget to update the local reference afterwards.
     */
    public get state(): Record<string, never> {
        return this._state;
    }

    /**
     * Makes any animated textures change to the next animation frame.
     * @hidden
     * @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported.
     * */
    public updateAnimatedTextures(): void {
        this._ditheringTexture.updateAnimatedFrame();
    }

    /**
     * Deletes a {@link TextureObject}.
     * @param textureObject Object to delete.
     * @returns True if deleted, otherwise false.
     */
    public deleteGpuTextureObject(textureObject: TextureObject | number): boolean {
        const colorPixelIndex = typeof textureObject === 'number' ? textureObject : textureObject.id;
        if (!this._colorTexturePixelIndexPool.push(colorPixelIndex)) {
            return false;
        }
        return true;
    }

    /**
     * Create a {@link TextureObject}.
     * @returns A valid {@link TextureObject} or undefined if the maximum number of gpu texture objects have been reached.
     */
    public createGpuTextureObject(): TextureObject | undefined {
        const colorPixelIndex = this._colorTexturePixelIndexPool.pop();
        if (colorPixelIndex === Materials._outOfColorTexturePixelIndices) {
            return undefined;
        }

        return new TextureObject(this, colorPixelIndex);
    }

    public add(ifcFiles: IBimIfcLoaderElement[]): void {
        // TODO Handle versions correctly if we load same ifc file of different versions

        // Calculate number of new (not reused) colorPixelIndices we will allocate. This allows us to calculate
        // exactly how much extra memory we need to allocate. We know that each mesh requires a colorPixelIndice
        let newMeshCount = 0;
        ifcFiles.forEach((f) => (newMeshCount += f.meshCount));

        if (newMeshCount <= 0) {
            return;
        }

        const maxColorPixelIndice = this._colorTexturePixelIndexPool.peek(newMeshCount);

        if (maxColorPixelIndice === this._colorTexturePixelIndexPool.outOfNumbers) {
            throw new Error(
                `Adding #${newMeshCount} objects would result in more than supported #${this._colorTexturePixelIndexPool.maxCount} objects.`
            );
        }

        const textureObjectCount = maxColorPixelIndice + 1;
        // Ensure we have room in the textures for the maximum colorPixelIndice that we will allocate.

        this._ifcProductColorTexture.setMinSize(textureObjectCount);
        this._ifcProductPropertiesTexture.setMinSize(textureObjectCount);

        // NOTE Avoid allocations in this loop like the plague because it is a very hot loop (many iterations)
        // and allocations can trigger GC which will bring down performance significantly.
        for (const f of ifcFiles) {
            for (const p of f.products) {
                if (!p.hasGeometry) {
                    continue;
                }

                let isVisible = true;
                if (f.loadOptions?.isIfcObjectVisiblePredicate) {
                    isVisible = f.loadOptions.isIfcObjectVisiblePredicate(p);
                }

                for (const mesh of p.productMeshes) {
                    // We should never run out of pixel indices since we have already checked for that earlier.
                    const colorTexturePixelIndex = this._colorTexturePixelIndexPool.pop();
                    // mesh.colorTexturePixelIndex is converted to a uv coordinate in ColorPixelIndexMaterial Shader and in GpuPickingMaterial.
                    // colorTexturePixelIndice points to a single pixel in ColorPixelIndiceMaterial.diffuseTexture which is then used to determine
                    // the color of the object. In GpuPickingMaterial it is treated as the unique color that the object shall be rendered with during
                    // gpu picking.
                    (mesh.colorTexturePixelIndex as number) = colorTexturePixelIndex;

                    const style = mesh.style;
                    this._ifcProductColorTexture.bytes(mesh.colorTexturePixelIndex).copyFrom(style.color);
                    this.globalRenderState.incrementMeshCount();
                    // All ifc meshes start out as visible unless they have alpha 0 but have no other effects.
                    this.renderPropertiesFor(mesh).set(IfcMaterialRenderProperty.visibility, isVisible && style.a > 0);

                    // Build lookup so we can find a ifcproduct and a one of its meshes just by having the colorTexturePixelIndex value.
                    this._bimObjectLookup.set(mesh.colorTexturePixelIndex, mesh);
                }
            }
        }
    }

    public clear(): void {
        this._colorTexturePixelIndexPool.clear();
        this._bimObjectLookup.clear();
        this._ifcProductColorTexture.reset();
        this._ifcProductPropertiesTexture.reset();
        this.globalRenderState.clearState();
    }

    public dispose(): void {
        this.clear();
        this._materialCache.dispose();
        this._ifcProductColorTexture.dispose();
        this._ifcProductPropertiesTexture.dispose();
        this._ditheringTexture.dispose();
    }

    /**
     * Retrieves a BimProductMesh object by its ID.
     * @param id The ID of the  {@link BimProductMesh}. The alpha component is masked away as it is not part of the ID.
     * The ID is converted to an unsigned value to handle values above 2^31 correctly.
     * @returns The {@link BimProductMesh} object associated with the given ID, or undefined if not found.
     */
    public getBimProductAndMeshById(id: number): BimProductMesh | undefined {
        // Mask away alpha as that is not part of the id. Need >>> 0 to make it a unsigned value (otherwise everything
        // above 2^31 will be incorrect)
        return this._bimObjectLookup.get((id & 0xffffff) >>> 0);
    }

    public getGpuPickingMaterial(sceneOrMesh: Scene | AbstractMesh): Material {
        return this.getIdWriterMaterial(sceneOrMesh, false);
    }

    public getOutlineMaterial(sceneOrMesh: Scene | AbstractMesh): Material {
        return this.getIdWriterMaterial(sceneOrMesh, true);
    }

    private getIdWriterMaterial(sceneOrMesh: Scene | AbstractMesh, outlineDiscerningMaterial = false): Material {
        let orientation: MaterialOrientation | undefined = undefined;
        let scene: Scene;
        let isIfcMesh: boolean;
        if (sceneOrMesh instanceof Scene) {
            scene = sceneOrMesh;
            isIfcMesh = false;
        } else {
            scene = sceneOrMesh.getScene();
            const materialOrientation = sceneOrMesh.material?.sideOrientation;
            if (materialOrientation === Material.ClockWiseSideOrientation) {
                orientation = MaterialOrientation.clockwise;
            } else if (materialOrientation === Material.CounterClockWiseSideOrientation) {
                orientation = MaterialOrientation.counterClockwise;
            }

            if (sceneOrMesh && sceneOrMesh instanceof Mesh) {
                isIfcMesh = !!sceneOrMesh.twinfinity.ifc;
            } else {
                isIfcMesh = false;
            }
        }

        orientation =
            orientation ||
            // If we have no orientation fall back to how babylonjs calculates default orientation (check material.ts)
            (scene.useRightHandedSystem ? MaterialOrientation.clockwise : MaterialOrientation.counterClockwise);

        const key = `gpuPickingMaterial:${orientation}${isIfcMesh}${outlineDiscerningMaterial}`;
        // NOTE: Method is called a LOT so we avoid lambda allocation by not using getOrAdd pattern with lambda. Instead we use a
        // reusable context object. Before this allocations showed up in profile traces as taking a non significant time.
        const { gpuPickingMAterialContext } = this._tmp;
        gpuPickingMAterialContext.outlineDiscerningMaterial = outlineDiscerningMaterial;
        gpuPickingMAterialContext.sideOrientation = this.toSideOrientation(orientation);
        gpuPickingMAterialContext.isIfcMesh = isIfcMesh;
        return this._materialCache.getOrAddMaterial(
            key,
            scene,
            Materials.createGpuPickingMaterial,
            gpuPickingMAterialContext
        );
    }

    private static createGpuPickingMaterial(
        key: string,
        scene: Scene,
        context?: CreateGpuPickingMaterialContext
    ): GpuPickingMaterial {
        if (!context) throw new Error('Context is required to create GpuPickingMaterial');
        const m = new GpuPickingMaterial(
            key,
            scene,
            context.globalRenderState,
            context.ifcProductColorTexture.attachToScene(scene),
            context.isIfcMesh ? context.ifcProductPropertiesTexture.attachToScene(scene) : undefined,
            context.outlineDiscerningMaterial
        );

        m.sideOrientation = context.sideOrientation;
        m.backFaceCulling = false;
        m.freeze();
        return m;
    }

    /**
     * Creates a custom depth material that is used for Babylon meshes for two reasons:
     * 1) It correctly excludes the invisible fragments from the IFC meshes
     * 2) It uses a camera with a nearplane that is further away, to increase precision and thereforce reduce z fighting for effects that use depth textures written to with this material
     * @param scene The scene for which  the depth material should be created
     * @param storeNonLinearDepth Whether the custom depth material should write the depth linearly or logarithmically
     * @returns The custom depth material, which was created for the scene
     */
    public getBabylonMeshCustomDepthMaterial(scene: Scene, storeNonLinearDepth: boolean): BabylonMeshDepthMaterial {
        const key = `babylonMeshDepthMaterial storeNonLinearDepth:${storeNonLinearDepth}`;
        const ret = this._materialCache.getOrAddMaterial(key, scene, (key, scene) => {
            const customDepthMaterial = new BabylonMeshDepthMaterial(
                key,
                scene,
                this.globalRenderState,
                this._ifcProductColorTexture.attachToScene(scene)
            );

            customDepthMaterial.backFaceCulling = this.isBackfaceCullingEnabled;
            customDepthMaterial.sideOrientation = Material.ClockWiseSideOrientation;
            customDepthMaterial.freeze();
            this._onIsBackfaceCullingEnabled.add((isEnabled) => {
                customDepthMaterial.unfreeze();
                customDepthMaterial.backFaceCulling = isEnabled;
                customDepthMaterial.freeze();
            });
            return customDepthMaterial;
        });

        return ret;
    }

    /**
     * Creates a custom depth material that is used for IFC meshes instead of Babylons normal depth material for two reasons:
     * 1) It correctly excludes the invisible fragments from the IFC meshes
     * 2) It uses a camera with a nearplane that is further away, to increase precision and thereforce reduce z fighting for effects that use depth textures written to with this material
     * @param scene The scene for which  the depth material should be created
     * @param storeNonLinearDepth Wheter the custom depth material should write the depth linearly or logarithmic
     * @returns The custom depth material, which was created for the scene
     */
    public getIFCMeshCustomDepthMaterial(scene: Scene, storeNonLinearDepth: boolean): IfcMeshDepthMaterial {
        const key = `ifcMeshDepthMaterial storeNonLinearDepth:${storeNonLinearDepth}`;
        const ret = this._materialCache.getOrAddMaterial(key, scene, (key, scene) => {
            const customDepthMaterial = new IfcMeshDepthMaterial(
                key,
                scene,
                this.globalRenderState,
                this._ifcProductColorTexture.attachToScene(scene),
                this._ifcProductPropertiesTexture.attachToScene(scene)
            );
            customDepthMaterial.backFaceCulling = this.isBackfaceCullingEnabled;
            customDepthMaterial.freeze();
            this._onIsBackfaceCullingEnabled.add((isEnabled) => {
                customDepthMaterial.unfreeze();
                customDepthMaterial.backFaceCulling = isEnabled;
                customDepthMaterial.freeze();
            });

            return customDepthMaterial;
        });

        return ret;
    }

    /**
     * Converts an integer pixel value to an RGBA array and sets the alpha value in place.
     * @template T A writable array-like type that will hold the RGBA values.
     * @param pixelValue The integer pixel value to be converted.
     * @param alpha The alpha value to be set in the resulting RGBA array.
     * @param dst The destination array where the RGBA values will be stored.
     * @returns The destination array with the RGBA values and the specified alpha value.
     */
    public intToGpuPickRgbaInPlace<T extends Writeable<ArrayLike<number>>>(
        pixelValue: number,
        alpha: number,
        dst: T
    ): T {
        rgba.toArray(pixelValue, dst, 3);
        dst[3] = alpha;
        return dst;
    }

    /**
     * Converts RGB components of a pixel array to a 32-bit unsigned integer.
     * @param pixels An array-like object containing pixel data.
     * @param offset The offset within the pixel array to start reading RGB components.
     * @returns The 32-bit integer representation of the RGB components.
     */
    public gpuPickRgbToInt32(pixels: ArrayLike<number>, offset: number): number {
        // only use RGB components. Ie skip alpha
        return rgba.toInt32(pixels, 3, offset);
    }

    /**
     * Sets various on/off properties pertaining to rendering of a {@link BimProductMesh} instance.
     * A mask representing the bits that you want to flip,
     * Legend:
     * idw: Depth write, single bit, represents a bool, wheter the object is written into the postprocess depth buffer or not
     * v: Visibility, 1 bit wheter the object is visible or not
     * o: Outline, 1 bit wheter the object is outlined or not
     * h: Highlight, 2 bits for a total of 3 different highlight colors, or no highlight if all bits are turned off
     * cpi: Clip plane ignore
     * s: Shininess, 4 bits, represents a number
     * 0: unused bit
     * [0|0|0|0 0|0|0|0] [0|0|0|0 0|0|0|0] [0|0|0|0 s|s|s|s] [0|0|cpi|o h|h|v|idw]
     * @param mesh Mesh to read/write properties for.
     * @returns A {@link Uint32Bits} instance. Use to manipulate the bits in the property mask for the {@link BimProductAndMesh} instance.
     */
    public renderPropertiesFor(mesh: BimProductMesh): Uint32Bits {
        return this._ifcProductPropertiesTexture.bits(mesh.colorTexturePixelIndex);
    }

    public setColor(mesh: BimProductMesh, color: ArrayLike<number>): boolean {
        const rgba = this._ifcProductColorTexture.bytes(mesh.colorTexturePixelIndex);
        rgba.copyFrom(color);
        return rgba.isChanged;
    }

    public copyColorTo(mesh: BimProductMesh, dst: Writeable<ArrayLike<number>>, dstOffset = 0): void {
        this._ifcProductColorTexture.bytes(mesh.colorTexturePixelIndex).copyTo(dst, dstOffset);
    }

    public getColorComponent(mesh: BimProductMesh, colorComponent: RgbaComponent): number {
        return this._ifcProductColorTexture.bytes(mesh.colorTexturePixelIndex).get(colorComponent);
    }

    public get(isTransparent: boolean, scene: Scene): Material {
        const viewer = scene.twinfinity.viewer;

        if (!viewer) {
            throw new Error('No viewer for scene');
        }
        return this._materialCache.getOrAddMaterial(
            `bimProductsMaterial_${isTransparent ? 't' : 'o'}`,
            scene,
            (key, scene) => {
                let m: CullingMaterial;
                if (isTransparent) {
                    const transparentMaterial = new TransparentIfcMaterial(
                        key,
                        scene,
                        this.globalRenderState,
                        this._ifcProductColorTexture.attachToScene(scene),
                        this._ifcProductPropertiesTexture.attachToScene(scene),
                        this._ditheringTexture
                    );

                    transparentMaterial.updateAlphaMode();

                    m = transparentMaterial;

                    m.separateCullingPass = true;
                    m.needDepthPrePass = false;
                } else {
                    m = new IfcMaterial(
                        key,
                        scene,
                        this.globalRenderState,
                        this._ifcProductColorTexture.attachToScene(scene),
                        this._ifcProductPropertiesTexture.attachToScene(scene),
                        this._ditheringTexture
                    );
                }
                m.sideOrientation = Material.CounterClockWiseSideOrientation;
                m.backFaceCulling = this.isBackfaceCullingEnabled;
                this._onIsBackfaceCullingEnabled.add((isEnabled) => {
                    m.unfreeze();
                    m.backFaceCulling = isEnabled;
                    m.freeze();
                });

                m.freeze();
                return m;
            }
        );
    }

    public updateDitherTexture(ditherOptions: DitheringOptions): void {
        this._ditheringTexture.currentMode = ditherOptions.mode;
        this._ditheringTexture.animatedNoiseTextures = ditherOptions.animatedFrames;
    }

    private toSideOrientation(orientation: MaterialOrientation): number {
        if (orientation === MaterialOrientation.clockwise) {
            return Material.ClockWiseSideOrientation;
        } else if (orientation === MaterialOrientation.counterClockwise) {
            return Material.CounterClockWiseSideOrientation;
        }
        throw new Error(`orientation: ${orientation} is not supported.`);
    }

    private onTextureUpdate(): void {
        this._state = {};
    }
}
