import {
    clipping_volume_matrix4_project
} from "../../../../../core/geom/3d/frustum/clipping_volume_matrix4_project.js";
import { hexahedron_from_aabb } from "../../../../../core/geom/3d/frustum/hexahedron_from_aabb.js";

const scratch_local_planes = new Float32Array(24);
const scratch_world_planes = new Float32Array(24);
const scratch_range = { near: 0, far: 0 };

// Half-extent of the z range used for the caster query. We want any object in
// the column above the receivers regardless of how far it sits along the light
// direction, so the query volume is effectively unbounded in z; the BVH walk's
// AABB-vs-frustum test is happy with a wide-but-finite range and avoids
// infinities propagating through plane math.
const CASTER_QUERY_HALF_RANGE = 1e7;

/**
 * Push the shadow camera's near plane back toward the light to include any
 * occluder whose shadow would fall within the camera's x/y bounds, even when
 * the occluder itself sits outside the main view frustum.
 *
 * Without this step the textbook "fit shadow camera to view frustum" leaves
 * the near plane at the closest receiver — a brick floating just above the
 * view frustum gets clipped out of the shadow map and casts no shadow on the
 * ground below.
 *
 * Algorithm:
 *   1. Take the shadow camera's existing left/right/top/bottom (already fit
 *      to receivers) as the x/y extents of a light-local query box. Extend z
 *      to a wide range so any caster along the light direction is captured.
 *   2. Transform the 6 box planes to world space via the shadow camera's
 *      matrixWorld.
 *   3. Walk every render layer's BVH through `compute_depth_range` against
 *      this world-space volume, measured along the shadow camera's view
 *      direction.
 *   4. If the closest depth found is smaller than the camera's current near,
 *      replace near with it.
 *
 * Far is intentionally left alone: casters behind the farthest receiver (in
 * light direction) can't shadow anything in the view.
 *
 * @param {THREE.OrthographicCamera} shadow_camera
 * @param {RenderLayerManager} layers
 */
export function extend_shadow_camera_near_for_casters(shadow_camera, layers) {
    if (layers === null) {
        return;
    }

    const x0 = shadow_camera.left;
    const x1 = shadow_camera.right;
    const y0 = shadow_camera.bottom;
    const y1 = shadow_camera.top;

    hexahedron_from_aabb(
        scratch_local_planes, 0,
        x0, y0, -CASTER_QUERY_HALF_RANGE,
        x1, y1, CASTER_QUERY_HALF_RANGE
    );

    clipping_volume_matrix4_project(
        scratch_world_planes, 0,
        scratch_local_planes, 0,
        6,
        shadow_camera.matrixWorld.elements
    );

    // Depth plane: world-space, normal along the shadow camera's view direction
    // (-Z column of matrixWorld), passing through the camera position. A point's
    // distance to this plane equals its depth in front of the camera.
    const m = shadow_camera.matrixWorld.elements;
    const nx = -m[8];
    const ny = -m[9];
    const nz = -m[10];
    const plane_constant = -(nx * m[12] + ny * m[13] + nz * m[14]);

    scratch_range.near = Number.POSITIVE_INFINITY;
    scratch_range.far = Number.NEGATIVE_INFINITY;

    layers.traverse((layer) => {
        if (!layer.state.visible) {
            return;
        }

        const compute = layer.compute_depth_range;

        if (compute === null) {
            return;
        }

        compute(scratch_range, scratch_world_planes, nx, ny, nz, plane_constant);
    });

    if (isFinite(scratch_range.near) && scratch_range.near < shadow_camera.near) {
        shadow_camera.near = scratch_range.near;
    }
}
