import { Frustum as ThreeFrustum } from "three";
import { read_three_planes_to_array } from "../../../../core/geom/3d/frustum/read_three_planes_to_array.js";
import { frustum_from_camera } from "./frustum_from_camera.js";

const CLIPPING_EPSILON = 0.001;
const CLIPPING_NEAR_MIN = 0.5;
const CLIPPING_FAR_DEFAULT = 100;

const scratch_three_frustum = new ThreeFrustum();
const scratch_frustum_planes = new Float32Array(24);
const scratch_range = { near: 0, far: 0 };

/**
 * Hysteresis prevents the clipping planes from thrashing as content moves
 * slightly. A shrink is only honored when it's at least this fraction of the
 * current near-to-far span; smaller shrinks are ignored, so an object briefly
 * crossing the frustum boundary doesn't trigger a re-projection.
 *
 * Loosenings (when content moves further away or closer) always apply
 * immediately — never delay including content.
 *
 * @type {number}
 */
const HYSTERESIS = 0.33;

/**
 * Tighten the camera's near/far planes to the world-space depth range of all
 * BVH-backed render layers' content visible inside the camera's frustum. Falls
 * back to (clip_near, clip_far) when no layer contributes content.
 *
 * @param {Camera} c camera ECS component (its `object` is the THREE.Camera)
 * @param {RenderLayerManager} layers
 */
export function auto_set_camera_clipping_planes(c, layers) {
    const camera = c.object;

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

    // Run visibility against the user-specified outer bounds, not the previous
    // frame's tightened range — otherwise objects could be culled before we
    // ever see them and the autoClip would shrink toward nothing.
    camera.near = c.clip_near;
    camera.far = c.clip_far;

    frustum_from_camera(camera, scratch_three_frustum, true);
    read_three_planes_to_array(scratch_three_frustum.planes, scratch_frustum_planes);

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

    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_frustum_planes, nx, ny, nz, plane_constant);
    });

    let new_near = scratch_range.near - CLIPPING_EPSILON;
    let new_far = scratch_range.far + CLIPPING_EPSILON;

    if (!isFinite(new_near) || new_near < CLIPPING_NEAR_MIN) {
        new_near = CLIPPING_NEAR_MIN;
    }
    if (!isFinite(new_far) || new_far <= new_near) {
        new_far = Math.max(new_near + CLIPPING_EPSILON, CLIPPING_FAR_DEFAULT);
    }

    // Clamp inside the user-specified outer bounds so the tightened range
    // never expands past what the camera is configured to render.
    if (new_near < c.clip_near) {
        new_near = c.clip_near;
    }
    if (new_far > c.clip_far) {
        new_far = c.clip_far;
    }

    // Hysteresis: only shrink each plane if the shrink is meaningful relative
    // to the current span. Loosening always applies immediately.
    const old_near = camera.near;
    const old_far = camera.far;
    const old_span = old_far - old_near;
    const shrink_threshold = HYSTERESIS * old_span;

    if (new_near > old_near && new_near - old_near < shrink_threshold) {
        new_near = old_near;
    }
    if (new_far < old_far && old_far - new_far < shrink_threshold) {
        new_far = old_far;
    }

    camera.near = new_near;
    camera.far = new_far;

    // Rebuild the projection matrix from the tightened range — CameraView
    // reads camera.projectionMatrix directly when capturing the frame's
    // frustum, and the wide-range matrix from the depth-query step above is
    // now stale.
    camera.updateProjectionMatrix();
}

