import {
    BufferAttribute,
    InstancedBufferGeometry,
    InstancedInterleavedBuffer,
    InterleavedBufferAttribute,
    Sphere,
    Vector3
} from "three";
import {
    VOXEL_POINT_OFFSET_ALBEDO,
    VOXEL_POINT_OFFSET_NORMAL,
    VOXEL_POINT_OFFSET_ORM,
    VOXEL_POINT_OFFSET_POSITION,
    VOXEL_POINT_STRIDE
} from "../VoxelImpostorDescription.js";


/**
 * Unit quad in the XY plane, centred on origin. Each instance picks
 * one of these four corners; the shader places it in view space at a
 * world-space offset around the instance's iPosition, producing a
 * camera-facing billboard.
 *
 * @type {Float32Array}
 */
const QUAD_POSITIONS = new Float32Array([
    -0.5, -0.5, 0,
     0.5, -0.5, 0,
     0.5,  0.5, 0,
    -0.5,  0.5, 0,
]);

const QUAD_UVS = new Float32Array([
    0, 0,
    1, 0,
    1, 1,
    0, 1,
]);

const QUAD_INDICES = new Uint16Array([
    0, 1, 2,
    0, 2, 3,
]);


/**
 * InstancedBufferGeometry with one instance per voxel. Each instance is
 * a unit quad whose vertex shader pushes the quad to a world-space
 * footprint around the voxel's iPosition. Because the splat's size is
 * specified in world units, it scales correctly with viewing distance:
 *
 *   - At the impostor's design distance (where one voxel projects to
 *     about one screen pixel), the quad covers a single pixel.
 *   - Closer in, the quad expands to cover the same voxel cell on
 *     screen — no holes.
 *   - Farther out, the quad shrinks below a pixel — no z-fighting.
 *
 * This is the camera-facing-billboard reading of the Witcher 4 "voxel
 * point" — the on-screen size of each voxel is whatever its world-space
 * cell projects to, not a fixed screen size. Shadows work for free
 * because the depth caster, rendered with the same shader from the
 * light's view, also produces world-sized splats in light space.
 *
 * The interleaved point buffer is shared across iPosition / iNormal /
 * iAlbedo / iORM via offsets — one GPU upload covers the lot.
 *
 * @param {VoxelImpostorDescription} description
 * @returns {InstancedBufferGeometry}
 */
export function make_voxel_impostor_geometry(description) {
    const geometry = new InstancedBufferGeometry();

    geometry.setAttribute('position', new BufferAttribute(QUAD_POSITIONS, 3));
    geometry.setAttribute('uv', new BufferAttribute(QUAD_UVS, 2));
    geometry.setIndex(new BufferAttribute(QUAD_INDICES, 1));

    const interleaved = new InstancedInterleavedBuffer(description.points, VOXEL_POINT_STRIDE);

    geometry.setAttribute('iPosition', new InterleavedBufferAttribute(interleaved, 3, VOXEL_POINT_OFFSET_POSITION));
    geometry.setAttribute('iNormal',   new InterleavedBufferAttribute(interleaved, 3, VOXEL_POINT_OFFSET_NORMAL));
    geometry.setAttribute('iAlbedo',   new InterleavedBufferAttribute(interleaved, 3, VOXEL_POINT_OFFSET_ALBEDO));
    geometry.setAttribute('iORM',      new InterleavedBufferAttribute(interleaved, 3, VOXEL_POINT_OFFSET_ORM));

    geometry.instanceCount = description.point_count;

    // The point cloud occupies the bake's bounding sphere; each splat
    // extends at most half a voxel-cell past its centre, which is
    // already absorbed by the sphere's bake-time radius. Add a small
    // margin so frustum culling doesn't pop voxels at the edge.
    geometry.boundingSphere = new Sphere(
        new Vector3(description.offset[0], description.offset[1], description.offset[2]),
        description.sphere_radius + description.voxel_size * 0.5
    );

    return geometry;
}
