import { SeverityLevel } from '@microsoft/applicationinsights-web';
import { telemetry } from '../Telemetry';
import { AbstractMesh, Camera, Constants, DepthRenderer, Mesh, Scene, Texture } from './babylonjs-import';
import { ShortFrustumProjectionMatrixState } from './ShortFrustumProjectionMatrixState';
import { IfcMeshDepthMaterial } from './CustomBabylonMaterials/IfcMeshDepthMaterial';
import { BabylonMeshDepthMaterial } from './CustomBabylonMaterials/BabylonMeshDepthMaterial';
import { copySizeTo } from '../postprocess/CopySizeFunctions';

export class ShortFrustumDepthRenderer {
    private _referenceCount = 0;
    public readonly shortFrustumProjectionMatrixState: ShortFrustumProjectionMatrixState;
    public readonly depthRenderStoresNonLinearDepth: boolean = false;
    public readonly depthRenderer: DepthRenderer;
    private _disabled = false;
    private _meshRenderTargetMaterialAssigned = new WeakMap<AbstractMesh, boolean>();

    // This function is to it so that only the IFC meshes get the custom depth material, the ordinary Babylonjs meshes should have the ordinary depth material
    private filterAndApplyCustomDepthMaterial(mesh: AbstractMesh): void {
        if (this.depthRenderer) {
            if (mesh && mesh instanceof Mesh) {
                const isTwinfintySubMesh = !!mesh.twinfinity.ifc;

                if (isTwinfintySubMesh) {
                    this.depthRenderer.setMaterialForRendering(mesh, this._ifcMeshCustomDepthMaterial);
                } else {
                    this.depthRenderer.setMaterialForRendering(mesh, this._babylonjsMeshCustomDepthMaterial);
                }
            }
        } else {
            throw new Error('Should not be able to reach this place without a depth renderer');
        }
    }

    constructor(
        storeNonLinearDepth: boolean,
        scene: Scene,
        camera: Camera,
        private readonly _ifcMeshCustomDepthMaterial: IfcMeshDepthMaterial,
        private readonly _babylonjsMeshCustomDepthMaterial: BabylonMeshDepthMaterial
    ) {
        const MAX_PRECISION = scene.getEngine().getCaps().textureFloatRender;
        let textureType = Constants.TEXTURETYPE_FLOAT;

        if (!MAX_PRECISION) {
            textureType = Constants.TEXTURETYPE_UNSIGNED_BYTE;
            telemetry.trackTrace({
                message: 'Using less than 32 bit precision for depth effects, effects will not appear as intended',
                severityLevel: SeverityLevel.Warning
            });
        }
        // Hack to create a depth renderer without using Babylons default of trilinear filtering, which causes flickering issues on Chromium browsers
        // Any near filtering besides nearest will cause flickering on Chromium browsers
        // This is how you'd ordinary do it with Babylon: this.depthRenderer = scene.enableDepthRenderer(camera, storeNonLinearDepth, MAX_PRECISION);
        this.depthRenderer = new DepthRenderer(scene, textureType, camera, storeNonLinearDepth, Texture.NEAREST_LINEAR);
        if (!scene._depthRenderer) {
            scene._depthRenderer = {};
        }
        scene._depthRenderer[camera.id] = this.depthRenderer;

        this.shortFrustumProjectionMatrixState = new ShortFrustumProjectionMatrixState(camera);

        this._meshRenderTargetMaterialAssigned = new WeakMap<AbstractMesh, boolean>();

        this.depthRenderer.getDepthMap().onBeforeBindObservable.add((texture) =>
            copySizeTo({
                src: texture.getScene()!.getEngine(),
                dst: texture
            })
        );

        this.depthRenderer.getDepthMap().renderListPredicate = (abstractMesh: AbstractMesh): boolean => {
            if (abstractMesh instanceof Mesh) {
                const isTwinfintySubMesh = !!abstractMesh.twinfinity;

                // Side effect inside this function
                if (!this._meshRenderTargetMaterialAssigned.has(abstractMesh)) {
                    this.filterAndApplyCustomDepthMaterial(abstractMesh);
                    this._meshRenderTargetMaterialAssigned.set(abstractMesh, true);
                }

                if (abstractMesh.material?.forceDepthWrite) {
                    return true;
                }

                if (abstractMesh.material?.disableDepthWrite) {
                    return false;
                }

                if (isTwinfintySubMesh) {
                    if (!abstractMesh.twinfinity.ifc) {
                        // Exclude point cloud meshes. They have no index buffer and require
                        // drawArrays — incompatible with standard depth materials.
                        return !abstractMesh.material?.pointsCloud;
                    } else if (!abstractMesh.twinfinity.ifc.isTransparent) {
                        // When looking at DepthRenderer.ts in babylonjs source (https://github.com/BabylonJS/Babylon.js/blob/8726f72d4b5557b6e9f3e9ebf71b864ce77612d5/packages/dev/core/src/Rendering/depthRenderer.ts) I was not able to find any frustum culling being applied
                        // On line 192 it says "culling" but it only seems to set the back or frontface culling. Therefore the check for if the mesh is in the frustum is here
                        const isVisible =
                            abstractMesh.twinfinity.ifc.isVisible(
                                camera,
                                this.shortFrustumProjectionMatrixState.depthRenderCameraMaxZ
                            ) && abstractMesh.isInFrustum(scene.frustumPlanes);
                        return isVisible;
                    }
                }
            }

            return false;
        };

        this.depthRenderStoresNonLinearDepth = storeNonLinearDepth;

        this._referenceCount++;
    }

    public resize(width: number, height: number, scene: Scene): void {
        if (!this._disabled) {
            this.depthRenderer.getDepthMap().resize({ width: width, height: height });
            // this.assignMeshesDepthMaterial(scene);
            this._meshRenderTargetMaterialAssigned = new WeakMap<AbstractMesh, boolean>();
        }
    }

    public isDisabled(): boolean {
        return this._disabled;
    }

    public setPause(paused: boolean): void {
        this.depthRenderer.enabled = !paused;
    }

    public disable(): void {
        this._referenceCount--;

        if (this._referenceCount === 0) {
            if (this.depthRenderer) {
                this.depthRenderer.enabled = true;
                this.depthRenderer.dispose();
                this.depthRenderer.getDepthMap().dispose();
            }
            this._disabled = true;
        }
    }

    public dispose(): void {
        if (this.depthRenderer) {
            this.depthRenderer.dispose();
            this.depthRenderer.getDepthMap().dispose();
        }
        this._disabled = true;
    }
}
