import { DirectionalLight, Light } from "../scene/index.js";
import {} from "./Geometry.js";
import {} from "./ForwardPass.js";
import {} from "./RenderDevice.js";
import {} from "./RenderPass.js";
import { ShadowMap } from "./ShadowMap.js";
import { ShadowPass } from "./ShadowPass.js";
import {} from "./ShaderLibrary.js";
export class CascadedShadowMaps {
    cascades;
    constructor(options) {
        const size = options.size ?? 1024;
        const bias = options.bias ?? 0.001;
        const label = options.label ?? "csm";
        this.cascades = CascadedShadowMaps.computeSplits(options).map((split) => ({
            index: split.index,
            split,
            shadowMap: new ShadowMap({
                size,
                bias,
                filter: options.filter,
                pcfRadius: options.pcfRadius,
                pcfSamples: options.pcfSamples,
                label: `${label}-cascade-${split.index}`
            })
        }));
    }
    static computeSplits(options) {
        const { cascadeCount, near, far, lambda = 0.5 } = options;
        if (!Number.isInteger(cascadeCount) || cascadeCount <= 0) {
            throw new Error("Cascade count must be a positive integer");
        }
        if (near <= 0 || far <= near) {
            throw new Error("Cascade near/far range is invalid");
        }
        if (lambda < 0 || lambda > 1) {
            throw new Error("Cascade lambda must be between 0 and 1");
        }
        const splits = [];
        let previous = near;
        for (let index = 1; index <= cascadeCount; index += 1) {
            const ratio = index / cascadeCount;
            const logarithmic = near * (far / near) ** ratio;
            const uniform = near + (far - near) * ratio;
            const cascadeFar = index === cascadeCount ? far : lambda * logarithmic + (1 - lambda) * uniform;
            splits.push({
                index: index - 1,
                near: previous,
                far: cascadeFar
            });
            previous = cascadeFar;
        }
        return splits;
    }
    get cascadeCount() {
        return this.cascades.length;
    }
    getCascades() {
        return this.cascades;
    }
    computeStableCameraFits(options) {
        return this.cascades.map((cascade) => computeShadowCameraFit(cascade, options));
    }
    resize(size) {
        const first = this.cascades[0];
        if (!first) {
            throw new Error("Cannot resize cascaded shadow maps without cascades");
        }
        const resized = Object.create(CascadedShadowMaps.prototype);
        Object.assign(resized, {
            cascades: this.cascades.map((cascade) => ({
                index: cascade.index,
                split: cascade.split,
                shadowMap: cascade.shadowMap.resize(size)
            }))
        });
        return resized;
    }
    dispose() {
        for (const cascade of this.cascades) {
            cascade.shadowMap.dispose();
        }
    }
}
function computeShadowCameraFit(cascade, options) {
    const padding = options.padding ?? 0.25;
    if (!Number.isFinite(padding) || padding < 0) {
        throw new Error("Shadow camera fit padding must be finite and non-negative");
    }
    const basis = lightBasis(options.lightDirection);
    const frustumCornersWorld = cameraFrustumCorners(options.camera, cascade.split);
    const casterBoundsWorld = mergeItemBounds(options.casters);
    const receiverBoundsWorld = mergeItemBounds(options.receivers ?? []);
    const combinedBoundsWorld = expandBounds(mergeBounds([boundsFromPoints(frustumCornersWorld), casterBoundsWorld, receiverBoundsWorld]), padding);
    const lightSpacePoints = corners(combinedBoundsWorld).map((point) => projectToLightSpace(point, basis));
    const lightSpaceBounds = boundsFromPoints(lightSpacePoints);
    const centerLightSpace = [
        (lightSpaceBounds.min[0] + lightSpaceBounds.max[0]) / 2,
        (lightSpaceBounds.min[1] + lightSpaceBounds.max[1]) / 2,
        (lightSpaceBounds.min[2] + lightSpaceBounds.max[2]) / 2
    ];
    const width = Math.max(1e-6, lightSpaceBounds.max[0] - lightSpaceBounds.min[0]);
    const height = Math.max(1e-6, lightSpaceBounds.max[1] - lightSpaceBounds.min[1]);
    const extent = Math.max(width, height);
    const texelSize = extent / cascade.shadowMap.size;
    const snappedCenterLightSpace = options.stabilize === false ? centerLightSpace : [
        snap(centerLightSpace[0], texelSize),
        snap(centerLightSpace[1], texelSize),
        centerLightSpace[2]
    ];
    const halfExtent = extent / 2;
    const depthPadding = Math.max(padding, extent * 0.05);
    return {
        cascadeIndex: cascade.index,
        split: cascade.split,
        mapSize: cascade.shadowMap.size,
        texelSize,
        lightDirection: normalize(options.lightDirection),
        basis,
        frustumCornersWorld,
        casterBoundsWorld,
        receiverBoundsWorld,
        combinedBoundsWorld,
        lightSpaceBounds,
        centerLightSpace,
        snappedCenterLightSpace,
        stableOffsetLightSpace: [
            snappedCenterLightSpace[0] - centerLightSpace[0],
            snappedCenterLightSpace[1] - centerLightSpace[1],
            snappedCenterLightSpace[2] - centerLightSpace[2]
        ],
        orthographic: {
            left: snappedCenterLightSpace[0] - halfExtent,
            right: snappedCenterLightSpace[0] + halfExtent,
            bottom: snappedCenterLightSpace[1] - halfExtent,
            top: snappedCenterLightSpace[1] + halfExtent,
            near: lightSpaceBounds.min[2] - depthPadding,
            far: lightSpaceBounds.max[2] + depthPadding
        },
        casterCount: options.casters.length,
        receiverCount: options.receivers?.length ?? 0
    };
}
function cameraFrustumCorners(camera, split) {
    if (!Number.isFinite(camera.fovYRadians) || camera.fovYRadians <= 0 || camera.fovYRadians >= Math.PI) {
        throw new Error("Shadow camera fit requires a finite perspective fov in radians");
    }
    if (!Number.isFinite(camera.aspect) || camera.aspect <= 0) {
        throw new Error("Shadow camera fit requires a finite positive aspect ratio");
    }
    const forward = normalize(sub(camera.target, camera.position));
    const right = normalize(cross(forward, camera.up ?? [0, 1, 0]));
    const up = normalize(cross(right, forward));
    return [split.near, split.far].flatMap((distance) => {
        const halfHeight = Math.tan(camera.fovYRadians / 2) * distance;
        const halfWidth = halfHeight * camera.aspect;
        const center = add(camera.position, scale(forward, distance));
        return [
            add(add(center, scale(right, -halfWidth)), scale(up, -halfHeight)),
            add(add(center, scale(right, halfWidth)), scale(up, -halfHeight)),
            add(add(center, scale(right, halfWidth)), scale(up, halfHeight)),
            add(add(center, scale(right, -halfWidth)), scale(up, halfHeight))
        ];
    });
}
function mergeItemBounds(items) {
    const bounds = items.map(worldBoundsForItem);
    return bounds.length > 0 ? mergeBounds(bounds) : null;
}
function worldBoundsForItem(item) {
    return boundsFromPoints(corners(item.geometry.bounds).map((point) => transformPoint(point, item.modelMatrix)));
}
function transformPoint(point, matrix) {
    if (!matrix) {
        return point;
    }
    if (matrix.length < 16) {
        throw new Error("RenderItem modelMatrix must contain 16 numbers for shadow camera fitting");
    }
    return [
        matrix[0] * point[0] + matrix[4] * point[1] + matrix[8] * point[2] + matrix[12],
        matrix[1] * point[0] + matrix[5] * point[1] + matrix[9] * point[2] + matrix[13],
        matrix[2] * point[0] + matrix[6] * point[1] + matrix[10] * point[2] + matrix[14]
    ];
}
function lightBasis(lightDirection) {
    const forward = normalize(scale(normalize(lightDirection), -1));
    const fallbackUp = Math.abs(forward[1]) > 0.94 ? [0, 0, 1] : [0, 1, 0];
    const right = normalize(cross(fallbackUp, forward));
    const up = normalize(cross(forward, right));
    return { right, up, forward };
}
function projectToLightSpace(point, basis) {
    return [dot(point, basis.right), dot(point, basis.up), dot(point, basis.forward)];
}
function mergeBounds(bounds) {
    const filtered = bounds.filter((value) => value !== null);
    if (filtered.length === 0) {
        throw new Error("Cannot merge empty shadow fitting bounds");
    }
    const min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
    const max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
    for (const bound of filtered) {
        for (let axis = 0; axis < 3; axis += 1) {
            min[axis] = Math.min(min[axis], bound.min[axis]);
            max[axis] = Math.max(max[axis], bound.max[axis]);
        }
    }
    return { min, max };
}
function expandBounds(bounds, padding) {
    return {
        min: [bounds.min[0] - padding, bounds.min[1] - padding, bounds.min[2] - padding],
        max: [bounds.max[0] + padding, bounds.max[1] + padding, bounds.max[2] + padding]
    };
}
function boundsFromPoints(points) {
    if (points.length === 0) {
        throw new Error("Cannot compute shadow bounds from no points");
    }
    const min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
    const max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
    for (const point of points) {
        for (let axis = 0; axis < 3; axis += 1) {
            min[axis] = Math.min(min[axis], point[axis]);
            max[axis] = Math.max(max[axis], point[axis]);
        }
    }
    return { min, max };
}
function corners(bounds) {
    const { min, max } = bounds;
    return [
        [min[0], min[1], min[2]],
        [max[0], min[1], min[2]],
        [max[0], max[1], min[2]],
        [min[0], max[1], min[2]],
        [min[0], min[1], max[2]],
        [max[0], min[1], max[2]],
        [max[0], max[1], max[2]],
        [min[0], max[1], max[2]]
    ];
}
function snap(value, increment) {
    if (!Number.isFinite(increment) || increment <= 0) {
        return value;
    }
    return Math.round(value / increment) * increment;
}
function add(a, b) {
    return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
}
function sub(a, b) {
    return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}
function scale(value, amount) {
    return [value[0] * amount, value[1] * amount, value[2] * amount];
}
function dot(a, b) {
    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
function cross(a, b) {
    return [
        a[1] * b[2] - a[2] * b[1],
        a[2] * b[0] - a[0] * b[2],
        a[0] * b[1] - a[1] * b[0]
    ];
}
function normalize(value) {
    const length = Math.hypot(value[0], value[1], value[2]);
    if (!Number.isFinite(length) || length <= 1e-8) {
        throw new Error("Shadow camera fitting vector cannot be zero");
    }
    return [value[0] / length, value[1] / length, value[2] / length];
}
export class CascadedShadowPass {
    options;
    lastResult = null;
    constructor(options) {
        this.options = options;
    }
    execute(context) {
        const cascadeResults = this.options.cascades.getCascades().map((cascade) => {
            const shadowPass = new ShadowPass({
                light: this.options.light,
                casters: this.options.casters,
                shadowMap: cascade.shadowMap,
                ...(this.options.shaderLibrary ? { shaderLibrary: this.options.shaderLibrary } : {})
            });
            const result = shadowPass.execute(context);
            return {
                index: cascade.index,
                split: cascade.split,
                rendered: result.rendered,
                reason: result.reason,
                casterCount: result.casterCount,
                skippedTransparentCasters: result.skippedTransparentCasters,
                diagnostics: result.diagnostics,
                shadowMap: cascade.shadowMap
            };
        });
        this.lastResult = {
            rendered: cascadeResults.length > 0 && cascadeResults.every((cascade) => cascade.rendered),
            cascades: cascadeResults
        };
        return this.lastResult;
    }
    getLastResult() {
        return this.lastResult;
    }
}
export function supportsCascadedShadowLight(light) {
    return light instanceof DirectionalLight && light.visible && light.castsShadow;
}
//# sourceMappingURL=CascadedShadowMaps.js.map