import { AbstractMesh, Matrix, Scene, Vector2 } from '../babylonjs-import';
import { ClipPlane } from '../ClipPlane';
import { LazyResizableRawTexture } from '../LazyResizableRawTexture';
import { ProductMeshesGroupRenderInfo } from '../ProductMeshesGroupRenderInfo';
import {
    DEPTH_WRITE_IGNORES_VISIBILITY_PROPERTY_DEFINE,
    PROPERTIES_SAMPLER,
    REQUIRES_WORLD_FRAGMENT,
    VISIBILITY_PROPERTY_DEFINE
} from '../ShaderConstants';
import { ShortFrustumProjectionMatrixState } from '../ShortFrustumProjectionMatrixState';
import {
    clipPlanesStrings,
    CullingMaterial,
    DIFFUSE_SAMPLER,
    SIZE,
    UniformsAndDefinesAndSamplers
} from './CullingMaterial';
import { customDepthShader } from './shaders/CustomDepthShader';

export const DISCARD_DITHER_TRANSPARENT = 'discardDitherTransparent';
export const IS_BABYLON_MESH = 'isBabylonMesh';
export const BABYLON_MESH_ALPHA = 'babylonMeshAlpha';

const WORLD = 'worldMatrix';
const VIEW_PROJECTION = 'viewProjectionMatrix';
const WORLD_VIEW_PROJECTION = 'worldViewProjectionMatrix_custom';
const DEPTH_VALUES = 'depthValues';
const NONLINEARDEPTH = 'nonLinearDepth';
const DEPTH_NEAR = 'depthNear';
const DEPTH_FAR = 'depthFar';

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

export abstract class CustomDepthMaterial extends CullingMaterial {
    public storeNonLinearDepth = true;

    private _requiresWorldSpace: boolean;
    private _shortFrustumProjectionMatrixState: ShortFrustumProjectionMatrixState;

    public getFragmentSource(): string {
        const fragmentSource = customDepthShader.fragment({
            clipPlaneUniforms: (): string => {
                return clipPlanesStrings.CLIP_PLANE_UNIFORMS;
            },
            clipPlaneDiscarding: (): string => {
                return clipPlanesStrings.CLIP_PLANE_DISCARDING;
            }
        });

        return fragmentSource;
    }

    public getVertexSource(): string {
        const vertexSource = customDepthShader.vertex();
        return vertexSource;
    }

    constructor(
        name: string,
        scene: Scene,
        renderingInfo: ProductMeshesGroupRenderInfo,
        public readonly diffuseTexture: LazyResizableRawTexture,
        public readonly propertiesTexture?: LazyResizableRawTexture
    ) {
        super(
            name,
            scene,
            renderingInfo,
            {
                attributes: ['position', 'uv'],
                samplers: [DIFFUSE_SAMPLER, PROPERTIES_SAMPLER],
                defines: [],
                uniforms: []
            },
            false,
            true
        );

        if (!scene.activeCamera)
            throw Error('Scene must have a active camera otherwise the custom depth material cannot be created.');
        else {
            this._shortFrustumProjectionMatrixState = scene.activeCamera.twinfinity.shortFrustum;
        }
    }

    getUniformsAndDefinesAndSamplers(): UniformsAndDefinesAndSamplers {
        const uniformsDefinesSamplers = super.getUniformsAndDefinesAndSamplers();

        const defines: string[] = [VISIBILITY_PROPERTY_DEFINE, DEPTH_WRITE_IGNORES_VISIBILITY_PROPERTY_DEFINE];

        const uniforms = [DEPTH_VALUES, SIZE];

        const clipPlanes = ClipPlane.values(this.visualSettings.ifc.clipPlanes);

        this._requiresWorldSpace = false;
        for (const clipPlane of clipPlanes) {
            if (clipPlane.enabled) {
                this._requiresWorldSpace = true;
                break;
            }
        }

        if (this._requiresWorldSpace) {
            uniforms.push(WORLD, VIEW_PROJECTION);
            defines.push(REQUIRES_WORLD_FRAGMENT);
        } else {
            uniforms.push(WORLD_VIEW_PROJECTION);
        }

        if (this.storeNonLinearDepth) {
            defines.push(NONLINEARDEPTH);
            uniforms.push(DEPTH_NEAR, DEPTH_FAR);
        }

        const samplers = [DIFFUSE_SAMPLER];

        return {
            uniforms: [...uniformsDefinesSamplers.uniforms, ...uniforms],
            defines: [...uniformsDefinesSamplers.defines, ...defines],
            samplers: [...uniformsDefinesSamplers.samplers, ...samplers]
        };
    }

    public setLinearDepthMode(storeNonLinearDepth: boolean): void {
        this.storeNonLinearDepth = storeNonLinearDepth;
    }

    override bindFunction(mesh: AbstractMesh): void {
        super.bindFunction(mesh);
        const scene = this.getScene();
        const combinedMatrix = new Matrix();
        const effect = this.getEffect();

        effect.setVector2(
            DEPTH_VALUES,
            new Vector2(
                this._shortFrustumProjectionMatrixState.depthRenderCameraMinZ,
                this._shortFrustumProjectionMatrixState.depthRenderCameraMaxZ
            )
        );

        if (this.storeNonLinearDepth) {
            effect.setFloat(DEPTH_NEAR, this._shortFrustumProjectionMatrixState.depthRenderCameraMinZ);
            effect.setFloat(DEPTH_FAR, this._shortFrustumProjectionMatrixState.depthRenderCameraMaxZ);
        }

        const camera = scene.activeCamera;
        combinedMatrix.reset();
        if (camera) {
            const meshWorldMatrix = mesh.getWorldMatrix();

            if (this._requiresWorldSpace) {
                camera
                    .getViewMatrix()
                    .multiplyToRef(this._shortFrustumProjectionMatrixState.getProjectionMatrix(), combinedMatrix);

                effect.setMatrix(WORLD, meshWorldMatrix);
                effect.setMatrix(VIEW_PROJECTION, combinedMatrix);
            } else {
                meshWorldMatrix.multiplyToRef(camera.getViewMatrix(), combinedMatrix);
                combinedMatrix.multiplyToRef(
                    this._shortFrustumProjectionMatrixState.getProjectionMatrix(),
                    combinedMatrix
                );

                effect.setMatrix(WORLD_VIEW_PROJECTION, combinedMatrix);
            }
        } else {
            throw new Error('No camera?');
        }
    }
}
