import { Matrix4x4, Frustum, Vector3, AABB, Vector4, retainObject, CubeFace } from '@zephyr3d/base';
import { Camera } from '../camera/camera.js';
import { SSM } from './ssm.js';
import { ESM } from './esm.js';
import { VSM } from './vsm.js';
import { PCFPD } from './pcf_pd.js';
import { PCFOPT } from './pcf_opt.js';
import { LIGHT_TYPE_NONE, LIGHT_TYPE_DIRECTIONAL, LIGHT_TYPE_POINT } from '../values.js';
import { ShaderHelper } from '../material/shader/helper.js';
import { getDevice } from '../app/api.js';

const tmpMatrix = new Matrix4x4();
const tmpFrustum = new Frustum(Matrix4x4.identity());
// const zeroPosition = Vector3.zero();
/**
 * The shadow map generator
 * @public
 */ class ShadowMapper {
    /** @internal */ static _snapMatrix = new Matrix4x4();
    /** @internal */ static _target = new Vector3();
    /** @internal */ static _up = new Vector3();
    /** @internal */ static _frustumMin = new Vector3();
    /** @internal */ static _frustumMax = new Vector3();
    /** @internal */ static _frustumCenter = new Vector3();
    /** @internal */ static _lightCameras = new WeakMap();
    /** @internal */ static _shadowMapParams = [];
    /** @internal */ _light;
    /** @internal */ _config;
    /** @internal */ _resourceDirty;
    /** @internal */ _shadowMode;
    /** @internal */ _shadowDistance;
    /** @internal */ _impl;
    /** @internal */ _pdSampleCount;
    /** @internal */ _pdSampleRadius;
    /** @internal */ _pcfKernelSize;
    /** @internal */ _vsmBlurKernelSize;
    /** @internal */ _vsmBlurRadius;
    /** @internal */ _vsmDarkness;
    /** @internal */ _esmBlur;
    /** @internal */ _esmBlurKernelSize;
    /** @internal */ _esmBlurRadius;
    /** @internal */ _esmDepthScale;
    /** @internal */ _shadowRegion;
    /** @internal */ _autoShadowRegion;
    /**
   * Creates an instance of ShadowMapper
   * @param light - The light that is used to generate shadow map
   */ constructor(light){
        this._light = light;
        this._config = {
            shadowMapSize: 1024,
            numCascades: 1,
            splitLambda: 0.5,
            depthBias: 0.5,
            normalBias: 0.2,
            nearClip: 1
        };
        this._resourceDirty = true;
        this._shadowMode = 'hard';
        this._shadowDistance = 2000;
        this._impl = null;
        this._pdSampleCount = 12;
        this._pdSampleRadius = 4;
        this._pcfKernelSize = 5;
        this._vsmBlurKernelSize = 5;
        this._vsmBlurRadius = 4;
        this._vsmDarkness = 0.3;
        this._esmBlur = true;
        this._esmBlurKernelSize = 5;
        this._esmBlurRadius = 4;
        this._esmDepthScale = 200;
        this._shadowRegion = null;
        this._autoShadowRegion = false;
        this.applyMode(this._shadowMode);
    }
    copyFrom(other) {
        this.shadowMapSize = other.shadowMapSize;
        this.shadowRegion = other.shadowRegion ? new AABB(other.shadowRegion) : null;
        this.shadowDistance = other.shadowDistance;
        this.numShadowCascades = other.numShadowCascades;
        this.splitLambda = other.splitLambda;
        this.depthBias = other.depthBias;
        this.normalBias = other.normalBias;
        this.nearClip = other.nearClip;
        this.mode = other.mode;
        this.pdSampleCount = other.pdSampleCount;
        this.pdSampleRadius = other.pdSampleRadius;
        this.pcfKernelSize = other.pcfKernelSize;
        this.vsmBlurKernelSize = other.vsmBlurKernelSize;
        this.vsmBlurRadius = other.vsmBlurRadius;
        this.vsmDarkness = other.vsmDarkness;
        this.esmBlur = other.esmBlur;
        this.esmBlurKernelSize = other.esmBlurKernelSize;
        this.esmBlurRadius = other.esmBlurRadius;
        this.esmDepthScale = other.esmDepthScale;
    }
    /** The light that is used to generate shadow map */ get light() {
        return this._light;
    }
    /** Size of the shadow map */ get shadowMapSize() {
        return this._config.shadowMapSize;
    }
    set shadowMapSize(num) {
        if (!Number.isInteger(num) || num < 1) {
            console.error(`invalid shadow map size: ${num}`);
            return;
        }
        if (this._config.shadowMapSize !== num) {
            this._config.shadowMapSize = num;
            this._resourceDirty = true;
        }
    }
    /** Shadow region for directional light */ get shadowRegion() {
        return this._shadowRegion;
    }
    set shadowRegion(region) {
        this._shadowRegion = region;
        this._autoShadowRegion = false;
    }
    /** @internal */ updateDirectionalShadowRegion(force = false) {
        if (!this._light.isDirectionLight()) {
            return;
        }
        if (!force && !this._autoShadowRegion) {
            return;
        }
        const scene = this._light.scene;
        if (!scene) {
            return;
        }
        const aabb = new AABB();
        aabb.beginExtend();
        scene.rootNode.iterate((child)=>{
            if ((child.isMesh() || child.isClipmapTerrain()) && child.castShadow) {
                const bbox = child.getWorldBoundingVolume()?.toAABB();
                if (bbox) {
                    aabb.extend(bbox.minPoint);
                    aabb.extend(bbox.maxPoint);
                }
            }
        });
        this._shadowRegion = aabb.isValid() ? aabb : null;
        this._autoShadowRegion = true;
    }
    /** Maximum distance from the camera, shadow will not be rendered beyond this range */ get shadowDistance() {
        return this._shadowDistance;
    }
    set shadowDistance(val) {
        this._shadowDistance = Math.max(0, val);
    }
    /** Count of the cascades, The maximum value is 4 */ get numShadowCascades() {
        return this._config.numCascades;
    }
    set numShadowCascades(num) {
        if (num !== 1 && num !== 2 && num !== 3 && num !== 4) {
            console.error(`invalid shadow cascade number: ${num}`);
            return;
        }
        if (!this._light.isDirectionLight() && num > 1) {
            console.error(`only directional light can have more than one shadow cascades`);
            return;
        }
        if (num !== this._config.numCascades) {
            this._config.numCascades = num;
            this._resourceDirty = true;
        }
    }
    /** The split lambda for cascaded shadow mapping */ get splitLambda() {
        return this._config.splitLambda;
    }
    set splitLambda(val) {
        if (this._config.splitLambda !== val) {
            this._config.splitLambda = val;
        }
    }
    /** Depth bias for the shadow map */ get depthBias() {
        return this._config.depthBias;
    }
    set depthBias(val) {
        this._config.depthBias = val;
    }
    /** Normal bias for the shadow map */ get normalBias() {
        return this._config.normalBias;
    }
    set normalBias(val) {
        this._config.normalBias = val;
    }
    /** Near clip plane */ get nearClip() {
        return this._config.nearClip;
    }
    set nearClip(val) {
        if (this._config.nearClip !== val) {
            this._config.nearClip = val;
        }
    }
    /** Shadow map mode */ get mode() {
        return this._shadowMode;
    }
    set mode(mode) {
        if (mode !== this._shadowMode) {
            this._shadowMode = mode;
            this.applyMode(this._shadowMode);
        }
    }
    /**
   * Applies a practical shadow preset for common scenarios.
   *
   * `character-small`: higher contact quality and tighter distance for character-centric scenes.
   * `outdoor-large`: longer distance coverage for open scenes.
   */ applyQualityPreset(preset) {
        if (preset === 'character-small') {
            this.mode = 'pcf-opt';
            this.shadowMapSize = 2048;
            this.shadowDistance = 120;
            this.splitLambda = 0.75;
            this.depthBias = 0.18;
            this.normalBias = 0.3;
            this.nearClip = 0.2;
            this.pcfKernelSize = 5;
            this.numShadowCascades = this._light.isDirectionLight() ? 3 : 1;
            return;
        }
        this.mode = 'pcf-opt';
        this.shadowMapSize = 2048;
        this.shadowDistance = 800;
        this.splitLambda = 0.6;
        this.depthBias = 0.35;
        this.normalBias = 0.22;
        this.nearClip = 1;
        this.pcfKernelSize = 3;
        this.numShadowCascades = this._light.isDirectionLight() ? 4 : 1;
    }
    /** @internal */ getShaderHash(shadowMapParams) {
        return `${shadowMapParams.impl.constructor.name}_${shadowMapParams.impl.getShaderHash()}_${shadowMapParams.lightType}_${shadowMapParams.shadowMap.target}_${Number(shadowMapParams.numShadowCascades > 1)}_${Number(getDevice().getDeviceCaps().textureCaps.getTextureFormatInfo(shadowMapParams.shadowMap.format).filterable)}`;
    }
    /** Sample count for poisson disc PCF */ get pdSampleCount() {
        return this._pdSampleCount;
    }
    set pdSampleCount(val) {
        val = Math.min(Math.max(1, Number(val) >> 0), 64);
        if (val !== this._pdSampleCount) {
            this._pdSampleCount = val;
            const pcfpd = this.asPCFPD();
            if (pcfpd) {
                pcfpd.tapCount = this._pdSampleCount;
            }
        }
    }
    /** Radius for poisson disc PCF */ get pdSampleRadius() {
        return this._pdSampleRadius;
    }
    set pdSampleRadius(val) {
        val = Math.max(0, Number(val) >> 0);
        if (val !== this._pdSampleRadius) {
            this._pdSampleRadius = val;
            this.asPCFPD()?.setDepthScale(this._pdSampleRadius);
        }
    }
    /** Kernel size for optimized PCF */ get pcfKernelSize() {
        return this._pcfKernelSize;
    }
    set pcfKernelSize(val) {
        val = val !== 3 && val !== 5 && val !== 7 ? 5 : val;
        if (val !== this._pcfKernelSize) {
            this._pcfKernelSize = val;
            const pcfopt = this.asPCFOPT();
            if (pcfopt) {
                pcfopt.kernelSize = this._pcfKernelSize;
            }
        }
    }
    /** Kernel size of VSM */ get vsmBlurKernelSize() {
        return this._vsmBlurKernelSize;
    }
    set vsmBlurKernelSize(val) {
        val = Math.max(3, Number(val) >> 0) | 1;
        if (val !== this._vsmBlurKernelSize) {
            this._vsmBlurKernelSize = val;
            const vsm = this.asVSM();
            if (vsm) {
                vsm.kernelSize = this._vsmBlurKernelSize;
            }
        }
    }
    /** Blur radius for VSM */ get vsmBlurRadius() {
        return this._vsmBlurRadius;
    }
    set vsmBlurRadius(val) {
        val = Math.max(0, Number(val) || 0);
        if (val !== this._vsmBlurRadius) {
            this._vsmBlurRadius = val;
            const vsm = this.asVSM();
            if (vsm) {
                vsm.blurSize = this._vsmBlurRadius;
            }
        }
    }
    /** Darkness for VSM */ get vsmDarkness() {
        return this._vsmDarkness;
    }
    set vsmDarkness(val) {
        val = Math.min(0.999, Math.max(0, Number(val) || 0));
        if (val !== this._vsmDarkness) {
            this._vsmDarkness = val;
            this.asVSM()?.setDepthScale(this._vsmDarkness);
        }
    }
    /** Whether to enable ESM blur */ get esmBlur() {
        return this._esmBlur;
    }
    set esmBlur(val) {
        if (!!val !== this.esmBlur) {
            this._esmBlur = !!val;
            const esm = this.asESM();
            if (esm) {
                esm.blur = this._esmBlur;
            }
        }
    }
    /** Kernel size for ESM */ get esmBlurKernelSize() {
        return this._esmBlurKernelSize;
    }
    set esmBlurKernelSize(val) {
        val = Math.max(3, Number(val) >> 0) | 1;
        if (val !== this._esmBlurKernelSize) {
            this._esmBlurKernelSize = val;
            const esm = this.asESM();
            if (esm) {
                esm.kernelSize = this._esmBlurKernelSize;
            }
        }
    }
    /** Blur radius for ESM */ get esmBlurRadius() {
        return this._esmBlurRadius;
    }
    set esmBlurRadius(val) {
        val = Math.max(0, Number(val) || 0);
        if (val !== this._esmBlurRadius) {
            this._esmBlurRadius = val;
            const esm = this.asESM();
            if (esm) {
                esm.blurSize = this._esmBlurRadius;
            }
        }
    }
    /** Depth scale for ESM */ get esmDepthScale() {
        return this._esmDepthScale;
    }
    set esmDepthScale(val) {
        val = Math.max(0, Number(val) || 0);
        if (val !== this._esmDepthScale) {
            this._esmDepthScale = val;
            this.asESM()?.setDepthScale(this._esmDepthScale);
        }
    }
    /** @internal */ computeShadow(shadowMapParams, scope, shadowVertex, NdotL) {
        return this._impl.computeShadow(shadowMapParams, scope, shadowVertex, NdotL);
    }
    /** @internal */ computeShadowCSM(shadowMapParams, scope, shadowVertex, NdotL, split) {
        return this._impl.computeShadowCSM(shadowMapParams, scope, shadowVertex, NdotL, split);
    }
    /** @internal */ static releaseTemporalResources(ctx) {
        if (ctx.shadowMapInfo) {
            for (const k of ctx.shadowMapInfo.keys()){
                const shadowMapParams = ctx.shadowMapInfo.get(k);
                //ctx.device.pool.releaseFrameBuffer(shadowMapParams.shadowMapFramebuffer);
                shadowMapParams.lightType = LIGHT_TYPE_NONE;
                shadowMapParams.depthClampEnabled = false;
                shadowMapParams.shaderHash = '';
                shadowMapParams.numShadowCascades = 1;
                shadowMapParams.shadowMapFramebuffer = null;
                shadowMapParams.impl = null;
                shadowMapParams.shadowMap = null;
                shadowMapParams.shadowMapSampler = null;
                shadowMapParams.implData = null;
                this._shadowMapParams.push(shadowMapParams);
            }
            ctx.shadowMapInfo = null;
        }
    }
    /** @internal */ static computeShadowBias(shadowMapParams, scope, z, NdotL, linear) {
        const pb = scope.$builder;
        const depthBiasParam = ShaderHelper.getDepthBiasValues(scope);
        if (shadowMapParams.lightType === LIGHT_TYPE_DIRECTIONAL) {
            return pb.dot(pb.mul(depthBiasParam.xy, pb.vec2(1, pb.sub(1, NdotL))), pb.vec2(1, 1));
        } else {
            const nearFar = ShaderHelper.getShadowCameraParams(scope).xy;
            const linearDepth = linear ? z : ShaderHelper.nonLinearDepthToLinearNormalized(scope, z, nearFar);
            const biasScaleFactor = pb.mix(1, depthBiasParam.w, linearDepth);
            return pb.dot(pb.mul(depthBiasParam.xy, pb.vec2(1, pb.sub(1, NdotL)), biasScaleFactor), pb.vec2(1, 1));
        }
    }
    /** @internal */ static computeShadowBiasCSM(shadowMapParams, scope, NdotL, split) {
        const pb = scope.$builder;
        const depthBiasParam = ShaderHelper.getDepthBiasValues(scope);
        const splitFlags = pb.vec4(pb.float(pb.equal(split, 0)), pb.float(pb.equal(split, 1)), pb.float(pb.equal(split, 2)), pb.float(pb.equal(split, 3)));
        const depthBiasScale = pb.dot(ShaderHelper.getDepthBiasScales(scope), splitFlags);
        return pb.dot(pb.mul(depthBiasParam.xy, pb.vec2(1, pb.sub(1, NdotL)), depthBiasScale), pb.vec2(1, 1));
    }
    /** @internal */ isTextureInvalid(texture, target, format, width, height) {
        return texture && (texture.target !== target || texture.format !== format || texture.width !== width || texture.height !== height || texture.depth !== this.numShadowCascades);
    }
    /** @internal */ createTexture(target, format, width, height, depth) {
        const device = getDevice();
        const options = {
            mipmapping: false
        };
        switch(target){
            case '2d':
                return device.createTexture2D(format, width, height, options);
            case 'cube':
                return device.createCubeTexture(format, width, options);
            case '2darray':
                return device.createTexture2DArray(format, width, height, depth, options);
            default:
                throw new Error(`Invalid target ${target}`);
        }
    }
    /** @internal */ static fetchTemporalFramebuffer(autoRelease, lightType, numCascades, width, height, colorFormat, depthFormat, mipmapping) {
        const device = getDevice();
        const useTextureArray = numCascades > 1 && device.type !== 'webgl';
        const colorAttachments = colorFormat ? useTextureArray ? [
            device.pool.fetchTemporalTexture2DArray(false, colorFormat, width, height, numCascades, mipmapping)
        ] : lightType === LIGHT_TYPE_POINT ? [
            device.pool.fetchTemporalTextureCube(false, colorFormat, width, mipmapping)
        ] : [
            device.pool.fetchTemporalTexture2D(false, colorFormat, width, height, mipmapping)
        ] : null;
        const depthAttachment = depthFormat ? useTextureArray ? device.pool.fetchTemporalTexture2DArray(false, depthFormat, width, height, numCascades, false) : device.type !== 'webgl' && lightType === LIGHT_TYPE_POINT ? device.pool.fetchTemporalTextureCube(false, depthFormat, width, false) : device.pool.fetchTemporalTexture2D(false, depthFormat, width, height, false) : null;
        const fb = device.pool.createTemporalFramebuffer(autoRelease, colorAttachments, depthAttachment);
        if (colorAttachments) {
            device.pool.releaseTexture(colorAttachments[0]);
        }
        if (depthAttachment) {
            device.pool.releaseTexture(depthAttachment);
        }
        return fb;
    }
    /** @internal */ updateResources(shadowMapParams) {
        const device = getDevice();
        const colorFormat = shadowMapParams.impl.getShadowMapColorFormat(shadowMapParams);
        const depthFormat = shadowMapParams.impl.getShadowMapDepthFormat(shadowMapParams);
        const numCascades = shadowMapParams.numShadowCascades;
        const useTextureArray = numCascades > 1 && device.type !== 'webgl';
        const shadowMapWidth = numCascades > 1 && !useTextureArray ? 2 * this._config.shadowMapSize : this._config.shadowMapSize;
        const shadowMapHeight = numCascades > 2 && !useTextureArray ? 2 * this._config.shadowMapSize : this._config.shadowMapSize;
        /*
    const colorAttachments = colorFormat
      ? useTextureArray
        ? [device.pool.fetchTemporalTexture2DArray(false, colorFormat, shadowMapWidth, shadowMapHeight, numCascades, false)]
        : this._light.isPointLight()
          ? [device.pool.fetchTemporalTextureCube(false, colorFormat, shadowMapWidth, false)]
          : [device.pool.fetchTemporalTexture2D(false, colorFormat, shadowMapWidth, shadowMapHeight, false)]
      : null;
    const depthAttachment = depthFormat
      ? useTextureArray
        ? device.pool.fetchTemporalTexture2DArray(false, depthFormat, shadowMapWidth, shadowMapHeight, numCascades, false)
        : device.type !== 'webgl' && this._light.isPointLight()
          ? device.pool.fetchTemporalTextureCube(false, depthFormat, shadowMapWidth, false)
          : device.pool.fetchTemporalTexture2D(false, depthFormat, shadowMapWidth, shadowMapHeight, false)
      : null;
    shadowMapParams.shadowMapFramebuffer = device.pool.createTemporalFramebuffer(false, colorAttachments, depthAttachment);
    if (colorAttachments) {
      device.pool.releaseTexture(colorAttachments[0]);
    }
    if (depthAttachment) {
      device.pool.releaseTexture(depthAttachment);
    }
    */ shadowMapParams.shadowMapFramebuffer = ShadowMapper.fetchTemporalFramebuffer(true, this._light.lightType, numCascades, shadowMapWidth, shadowMapHeight, colorFormat, depthFormat);
        shadowMapParams.impl = this._impl;
        this._impl.updateResources(shadowMapParams);
    }
    /** @internal */ createLightCameraPoint(lightCamera) {
        //lightCamera.reparent(this._light);
        lightCamera.parent = lightCamera.scene.rootNode;
        lightCamera.position.setXYZ(0, 0, 0);
        lightCamera.rotation.identity();
        lightCamera.scale.setXYZ(1, 1, 1);
        lightCamera.setPerspective(Math.PI / 2, 1, this._config.nearClip, Math.min(this._shadowDistance, this._light.range));
        lightCamera.position.set(this._light.positionAndRange.xyz());
    }
    /** @internal */ createLightCameraSpot(lightCamera) {
        lightCamera.parent = this._light;
        lightCamera.position.setXYZ(0, 0, 0);
        lightCamera.rotation.identity();
        lightCamera.scale.setXYZ(1, 1, 1);
        lightCamera.setPerspective(2 * Math.acos(this._light.cutoff), 1, this._config.nearClip, Math.min(this._shadowDistance, this._light.range));
    }
    /** @internal */ createLightCameraRect(lightCamera) {
        const rect = this._light;
        lightCamera.parent = this._light;
        lightCamera.position.setXYZ(0, 0, 0);
        lightCamera.rotation.identity();
        lightCamera.scale.setXYZ(1, 1, 1);
        const halfW = rect.width * 0.5;
        const halfH = rect.height * 0.5;
        const far = Math.min(this._shadowDistance, rect.range);
        lightCamera.setOrtho(-halfW, halfW, -halfH, halfH, this._config.nearClip, Math.max(this._config.nearClip + 0.001, far));
    }
    /** @internal */ createLightCameraDirectional(sceneAABB, sceneCamera, lightCamera, cropMatrix, border) {
        let frustum = sceneCamera.frustumViewSpace;
        if (this._shadowDistance < sceneCamera.getFarPlane()) {
            tmpMatrix.set(sceneCamera.getProjectionMatrix());
            tmpMatrix.setNearFar(tmpMatrix.getNearPlane(), this._shadowDistance);
            //tmpMatrix.multiplyRight(sceneCamera.viewMatrix);
            tmpFrustum.initWithMatrix(tmpMatrix);
            frustum = tmpFrustum;
        }
        border = border || 0;
        const expand = (this.shadowMapSize - 2 * border) / this.shadowMapSize;
        //const frustum = sceneCamera.frustum;
        const frustumMin = ShadowMapper._frustumMin;
        const frustumMax = ShadowMapper._frustumMax;
        const frustumCenter = ShadowMapper._frustumCenter;
        const target = ShadowMapper._target;
        const up = ShadowMapper._up;
        // const frustum = new Frustum(sceneCamera.viewProjectionMatrix);
        frustumMin.setXYZ(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
        frustumMax.setXYZ(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
        frustum.corners.forEach((p)=>{
            frustumMin.inplaceMin(p);
            frustumMax.inplaceMax(p);
        });
        let radius = Vector3.distance(frustumMin, frustumMax) * 0.5 * expand;
        const center = sceneCamera.thisToWorld(Vector3.add(frustumMin, frustumMax, frustumCenter).scaleBy(0.5), frustumCenter);
        // Bounding sphere of the shadow camera should not be larger than bounding sphere of the scene.
        const sceneRadius = sceneAABB.diagonalLength * 0.5 * expand;
        if (sceneRadius < radius) {
            radius = sceneRadius;
            Vector3.add(sceneAABB.minPoint, sceneAABB.maxPoint, center).scaleBy(0.5);
        }
        target.setXYZ(center.x + this._light.directionAndCutoff.x, center.y + this._light.directionAndCutoff.y, center.z + this._light.directionAndCutoff.z);
        up.setXYZ(0, 1, 0);
        lightCamera.lookAt(center, target, up);
        lightCamera.position.set(center);
        lightCamera.setOrtho(-radius, radius, -radius, radius, -radius, Math.max(Vector3.distance(center, sceneCamera.getWorldPosition()), sceneCamera.getFarPlane()));
        center.setXYZ(0, 0, 0);
        lightCamera.viewProjectionMatrix.transformPointP(center, center);
        if (cropMatrix) {
            const tx = center.x * this.shadowMapSize * 0.5;
            const ty = center.y * this.shadowMapSize * 0.5;
            const rx = Math.round(tx);
            const ry = Math.round(ty);
            center.setXYZ((rx - tx) * 2 / this.shadowMapSize, (ry - ty) * 2 / this.shadowMapSize, 0);
            cropMatrix.translation(center);
        }
    /*
    const minx = frustumMin.x;
    const maxx = frustumMax.x;
    const miny = frustumMin.y;
    const maxy = frustumMax.y;
    const minz = frustumMin.z;
    const maxz = frustumMax.z;
    const texelSizeW = (maxx - minx) / this.shadowMapSize;
    const texelSizeH = (maxy - miny) / this.shadowMapSize;
    const cx = Math.floor((minx + maxx) / 2 / texelSizeW) * texelSizeW;
    const cy = Math.floor((miny + maxy) / 2 / texelSizeH) * texelSizeH;
    const cz = (minz + maxz) / 2;
    const hx = Math.floor(((maxx - minx) * (expand + 0.5)) / texelSizeW) * texelSizeW;
    const hy = Math.floor(((maxy - miny) * (expand + 0.5)) / texelSizeH) * texelSizeH;
    lightCamera.position.setXYZ(cx, cy, cz);
    lightCamera.setOrtho(
      -hx,
      hx,
      -hy,
      hy,
      sceneMax.z - maxz,
      sceneMax.z - minz + 1
    );
    if (cropMatrix) {
      // compute crop matrix
      let clipMaxX = 0,
        clipMaxY = 0;
      let clipMinX = Number.MAX_VALUE,
        clipMinY = Number.MAX_VALUE;
      frustum.corners.forEach((p) => {
        const clipPos = lightCamera.viewProjectionMatrix.transformPoint(p);
        clipPos.x = Math.min(1, Math.max(-1, clipPos.x / clipPos.w));
        clipPos.y = Math.min(1, Math.max(-1, clipPos.y / clipPos.w));
        if (clipPos.x > clipMaxX) {
          clipMaxX = clipPos.x;
        }
        if (clipPos.x < clipMinX) {
          clipMinX = clipPos.x;
        }
        if (clipPos.y > clipMaxY) {
          clipMaxY = clipPos.y;
        }
        if (clipPos.y < clipMinY) {
          clipMinY = clipPos.y;
        }
      });
      const clipW = clipMaxX - clipMinX;
      const clipH = clipMaxY - clipMinY;
      clipMinX -= expand * clipW;
      clipMinY -= expand * clipH;
      clipMaxX += expand * clipW;
      clipMaxY += expand * clipH;
      const scaleX = 2 / (clipMaxX - clipMinX);
      const scaleY = 2 / (clipMaxY - clipMinY);
      const offsetX = -0.5 * (clipMaxX + clipMinX) * scaleX;
      const offsetY = -0.5 * (clipMaxY + clipMinY) * scaleY;
      cropMatrix.identity();
      cropMatrix.m00 = scaleX;
      cropMatrix.m11 = scaleY;
      cropMatrix.m03 = offsetX;
      cropMatrix.m13 = offsetY;
    }
    */ }
    /** @internal */ static fetchShadowMapParams() {
        if (this._shadowMapParams.length > 0) {
            return this._shadowMapParams.pop();
        } else {
            return {
                lightType: LIGHT_TYPE_NONE,
                depthClampEnabled: false,
                shaderHash: '',
                cameraParams: new Vector4(),
                cascadeDistances: new Vector4(),
                depthBiasScales: new Vector4(),
                depthBiasValues: [
                    new Vector4(),
                    new Vector4(),
                    new Vector4(),
                    new Vector4()
                ],
                numShadowCascades: 1,
                shadowMatrices: new Float32Array(16 * 4),
                shadowMap: null,
                shadowMapSampler: null,
                shadowMapFramebuffer: null,
                impl: null,
                implData: null
            };
        }
    }
    /** @internal */ static fetchCameraForScene(scene) {
        const cameras = this._lightCameras.get(scene);
        if (!cameras || cameras.length === 0) {
            const camera = new Camera(scene);
            retainObject(camera);
            return camera;
        } else {
            const camera = cameras.pop();
            camera.parent = scene.rootNode;
            camera.position.setXYZ(0, 0, 0);
            camera.rotation.identity();
            camera.scale.setXYZ(1, 1, 1);
            camera.clipMask = 0;
            return camera;
        }
    }
    /** @internal */ static releaseCamera(camera) {
        let cameras = this._lightCameras.get(camera.scene);
        if (!cameras) {
            cameras = [];
            this._lightCameras.set(camera.scene, cameras);
        }
        camera.remove();
        cameras.push(camera);
    }
    /** @internal */ calcSplitDistances(nearPlane, farPlane, numCascades) {
        const result = [
            0,
            0,
            0,
            0,
            0
        ];
        for(let i = 0; i <= numCascades; ++i){
            const fIDM = i / numCascades;
            const fLog = nearPlane * Math.pow(farPlane / nearPlane, fIDM);
            const fUniform = nearPlane + (farPlane - nearPlane) * fIDM;
            result[i] = fLog * this._config.splitLambda + fUniform * (1 - this._config.splitLambda);
        }
        return result;
    }
    /** @internal */ calcDepthBiasParams(camera, shadowMapSize, depthBias, normalBias, depthScale, result) {
        const sizeNear = Math.min(camera.getProjectionMatrix().getNearPlaneWidth(), camera.getProjectionMatrix().getNearPlaneHeight());
        const sizeFar = Math.min(camera.getProjectionMatrix().getFarPlaneWidth(), camera.getProjectionMatrix().getFarPlaneHeight());
        const scaleFactor = sizeNear / shadowMapSize * 2;
        result.setXYZW(depthBias * scaleFactor, normalBias * 0.0001, depthScale, sizeFar / sizeNear);
    }
    /** @internal */ postRenderShadowMap(shadowMapParams) {
        this._impl.postRenderShadowMap(shadowMapParams);
    }
    /** @internal */ render(ctx, renderPass) {
        if (!ctx.shadowMapInfo) {
            ctx.shadowMapInfo = new Map();
        }
        const shadowMapParams = ShadowMapper.fetchShadowMapParams();
        shadowMapParams.impl = this._impl;
        shadowMapParams.lightType = this.light.lightType;
        shadowMapParams.numShadowCascades = shadowMapParams.lightType === LIGHT_TYPE_DIRECTIONAL ? this._config.numCascades ?? 1 : 1;
        ctx.shadowMapInfo.set(this.light, shadowMapParams);
        const scene = ctx.scene;
        const camera = ctx.camera;
        renderPass.light = this._light;
        this.updateResources(shadowMapParams);
        shadowMapParams.shaderHash = this.getShaderHash(shadowMapParams);
        const device = getDevice();
        const fb = shadowMapParams.shadowMapFramebuffer;
        shadowMapParams.depthClampEnabled = false;
        renderPass.clearColor = fb.getColorAttachments()[0] ? fb.getColorAttachments()[0].isFloatFormat() ? new Vector4(1, 1, 1, 1) : new Vector4(0, 0, 0, 1) : null;
        const depthScale = this._impl.getDepthScale();
        if (this._light.isDirectionLight() && this._autoShadowRegion) {
            this.updateDirectionalShadowRegion();
        }
        const shadowRegion = this._light.isDirectionLight() && this._shadowRegion && this._shadowRegion.isValid() ? this._shadowRegion : scene.boundingBox;
        if (this._light.isPointLight()) {
            const shadowMapRenderCamera = ShadowMapper.fetchCameraForScene(scene);
            this.createLightCameraPoint(shadowMapRenderCamera);
            this.calcDepthBiasParams(camera, this._config.shadowMapSize, this._config.depthBias, this._config.normalBias, depthScale, shadowMapParams.depthBiasValues[0]);
            shadowMapParams.cameraParams.setXYZW(shadowMapRenderCamera.getNearPlane(), shadowMapRenderCamera.getFarPlane(), this._config.shadowMapSize, this._shadowDistance);
            device.setFramebuffer(fb);
            shadowMapParams.shadowMatrices.set(Matrix4x4.transpose(shadowMapRenderCamera.viewMatrix));
            for (const face of [
                CubeFace.PX,
                CubeFace.NX,
                CubeFace.PY,
                CubeFace.NY,
                CubeFace.PZ,
                CubeFace.NZ
            ]){
                shadowMapRenderCamera.lookAtCubeFace(face);
                fb.setColorAttachmentCubeFace(0, face);
                fb.setDepthAttachmentCubeFace(face);
                renderPass.render(ctx, shadowMapRenderCamera);
            }
            shadowMapParams.shadowMatrices.set(Matrix4x4.identity());
            ShadowMapper.releaseCamera(shadowMapRenderCamera);
        } else {
            if (this._config.numCascades > 1) {
                const distances = this.calcSplitDistances(camera.getNearPlane(), Math.min(this._shadowDistance, camera.getFarPlane()), this._config.numCascades);
                const cascadeCamera = ShadowMapper.fetchCameraForScene(scene);
                const shadowMapRenderCamera = ShadowMapper.fetchCameraForScene(scene);
                const shadowMapCullCamera = ShadowMapper.fetchCameraForScene(scene);
                shadowMapCullCamera.clipMask = AABB.ClipLeft | AABB.ClipRight | AABB.ClipBottom | AABB.ClipTop;
                cascadeCamera.parent = camera;
                shadowMapParams.depthClampEnabled = getDevice().getDeviceCaps().shaderCaps.supportFragmentDepth;
                for(let split = 0; split < this._config.numCascades; split++){
                    cascadeCamera.setPerspective(camera.getFOV(), camera.getAspect(), distances[split], distances[split + 1]);
                    const snapMatrix = ShadowMapper._snapMatrix;
                    const border = shadowMapParams.impl.getShadowMapBorder(shadowMapParams); //20 / this._config.shadowMapSize;
                    this.createLightCameraDirectional(shadowRegion, cascadeCamera, shadowMapRenderCamera, snapMatrix, border);
                    this.createLightCameraDirectional(shadowRegion, cascadeCamera, shadowMapCullCamera, null, border);
                    this.calcDepthBiasParams(camera, this._config.shadowMapSize, this._config.depthBias, this._config.normalBias, depthScale, shadowMapParams.depthBiasValues[split]);
                    shadowMapParams.depthBiasScales[split] = shadowMapParams.depthBiasValues[0].x !== 0 ? shadowMapParams.depthBiasValues[split].x / shadowMapParams.depthBiasValues[0].x : 1;
                    shadowMapParams.cameraParams.setXYZW(shadowMapRenderCamera.getNearPlane(), shadowMapRenderCamera.getFarPlane(), this._config.shadowMapSize, this._shadowDistance);
                    let scissor = null;
                    if (fb.getColorAttachments()[0]?.isTexture2DArray() || fb.getDepthAttachment()?.isTexture2DArray()) {
                        shadowMapRenderCamera.setProjectionMatrix(Matrix4x4.multiply(snapMatrix, shadowMapRenderCamera.getProjectionMatrix()));
                        fb.setColorAttachmentLayer(0, split);
                        fb.setDepthAttachmentLayer(split);
                    } else {
                        const numRows = this._config.numCascades > 2 ? 2 : 1;
                        const numCols = this._config.numCascades > 1 ? 2 : 1;
                        const adjMatrix = new Matrix4x4();
                        const col = split % 2;
                        const row = split >> 1;
                        adjMatrix.setRowXYZW(0, 1.5 - 0.5 * numCols, 0, 0, 0);
                        adjMatrix.setRowXYZW(1, 0, 1.5 - 0.5 * numRows, 0, 0);
                        adjMatrix.setRowXYZW(2, 0, 0, 1, 0);
                        adjMatrix.setRowXYZW(3, col - 0.5 * numCols + 0.5, row - 0.5 * numRows + 0.5, 0, 1);
                        shadowMapRenderCamera.setProjectionMatrix(Matrix4x4.multiply(adjMatrix, Matrix4x4.multiply(snapMatrix, shadowMapRenderCamera.getProjectionMatrix())));
                        if (device.type === 'webgpu') {
                            scissor = [
                                col * this._config.shadowMapSize,
                                (numRows - 1 - row) * this._config.shadowMapSize,
                                this._config.shadowMapSize,
                                this._config.shadowMapSize
                            ];
                        } else {
                            scissor = [
                                col * this._config.shadowMapSize,
                                row * this._config.shadowMapSize,
                                this._config.shadowMapSize,
                                this._config.shadowMapSize
                            ];
                        }
                    }
                    device.setFramebuffer(fb);
                    device.setScissor(scissor);
                    renderPass.render(ctx, shadowMapRenderCamera, shadowMapCullCamera);
                    shadowMapParams.shadowMatrices.set(Matrix4x4.transpose(shadowMapRenderCamera.viewProjectionMatrix), split * 16);
                    shadowMapParams.cascadeDistances[split] = distances[split + 1];
                }
                ShadowMapper.releaseCamera(cascadeCamera);
                ShadowMapper.releaseCamera(shadowMapRenderCamera);
                ShadowMapper.releaseCamera(shadowMapCullCamera);
            } else {
                const shadowMapRenderCamera = ShadowMapper.fetchCameraForScene(scene);
                const snapMatrix = ShadowMapper._snapMatrix;
                shadowMapRenderCamera.clipMask = AABB.ClipLeft | AABB.ClipRight | AABB.ClipBottom | AABB.ClipTop;
                if (this._light.isDirectionLight()) {
                    this.createLightCameraDirectional(shadowRegion, camera, shadowMapRenderCamera, snapMatrix, shadowMapParams.impl.getShadowMapBorder(shadowMapParams));
                } else if (this._light.isRectLight()) {
                    this.createLightCameraRect(shadowMapRenderCamera);
                } else {
                    this.createLightCameraSpot(shadowMapRenderCamera);
                }
                this.calcDepthBiasParams(camera, this._config.shadowMapSize, this._config.depthBias, this._config.normalBias, depthScale, shadowMapParams.depthBiasValues[0]);
                shadowMapParams.cameraParams.setXYZW(shadowMapRenderCamera.getNearPlane(), shadowMapRenderCamera.getFarPlane(), this._config.shadowMapSize, this._shadowDistance);
                device.setFramebuffer(fb);
                shadowMapRenderCamera.setProjectionMatrix(Matrix4x4.multiply(snapMatrix, shadowMapRenderCamera.getProjectionMatrix()));
                renderPass.render(ctx, shadowMapRenderCamera);
                shadowMapParams.shadowMatrices.set(Matrix4x4.transpose(shadowMapRenderCamera.viewProjectionMatrix));
                ShadowMapper.releaseCamera(shadowMapRenderCamera);
            }
        }
        this.postRenderShadowMap(shadowMapParams);
    }
    /** @internal */ applyMode(mode) {
        if (mode !== 'hard' && mode !== 'vsm' && mode !== 'esm' && mode !== 'pcf-pd' && mode !== 'pcf-opt') {
            console.error(`ShadowMapper.setShadowMode() failed: invalid mode: ${mode}`);
            return;
        }
        this._impl = null;
        if (mode === 'hard') {
            this._impl = new SSM();
        } else if (mode === 'vsm') {
            this._impl = new VSM(this._vsmBlurKernelSize, this._vsmBlurRadius, this._vsmDarkness);
        } else if (mode === 'esm') {
            this._impl = new ESM(this._esmBlurKernelSize, this._esmBlurRadius, this._esmDepthScale);
        } else if (mode === 'pcf-pd') {
            this._impl = new PCFPD(this._pdSampleCount, this._pdSampleRadius);
        } else if (mode === 'pcf-opt') {
            this._impl = new PCFOPT(this._pcfKernelSize);
        }
    }
    /** @internal */ asVSM() {
        return this._impl?.getType() === 'vsm' ? this._impl : null;
    }
    /** @internal */ asESM() {
        return this._impl?.getType() === 'esm' ? this._impl : null;
    }
    /** @internal */ asPCFPD() {
        return this._impl?.getType() === 'pcf-pd' ? this._impl : null;
    }
    /** @internal */ asPCFOPT() {
        return this._impl?.getType() === 'pcf-opt' ? this._impl : null;
    }
}

export { ShadowMapper };
//# sourceMappingURL=shadowmapper.js.map
