import { Color, ShaderMaterial, UniformsLib, UniformsUtils } from "three";


// PBR-lit variant of {@link VoxelImpostorShaderV0}. Each voxel renders
// as a camera-facing billboard sized in WORLD space; the per-instance
// baked normal drives the lighting equations and the per-instance ORM
// modulates roughness / metalness / AO. ShaderMaterial (not Raw) so
// three.js auto-injects modelMatrix / viewMatrix / normalMatrix /
// UniformsLib.lights via lights: true.
//
// Shadow handling — receiver side:
//   The vertex shader builds a worldPosition for <shadowmap_vertex>
//   that uses the splat CENTRE (every quad vertex shares the same
//   iPosition, so the shadow coord is uniform across the splat — what
//   you want for a single point sample), shifted along the surface
//   normal by exactly the splat's world-space radius. That's the
//   "50% of the quad size" normal-bias trick — at the impostor's
//   design viewing distance this is sub-pixel (~half a pixel) and so
//   invisible, while being more than enough to keep the splat from
//   shadowing itself.
//
// Caster side is the paired VoxelImpostorShaderDepthV0.
//
// GLSL 1.0 syntax (attribute / varying) so three.js can mix in its
// chunks; three.js auto-converts to GLSL 3.0 on WebGL2.
const shader_vx = `
    #define STANDARD

    attribute vec3 iPosition;
    attribute vec3 iNormal;
    attribute vec3 iAlbedo;
    attribute vec3 iORM;

    flat varying vec3 vAlbedo;
    flat varying vec3 vNormal_OS;
    flat varying vec3 vORM;
    varying vec3 vViewPosition;
    varying vec2 vUv;

    uniform float uPointDiameter;

    #include <common>
    #include <fog_pars_vertex>
    #include <shadowmap_pars_vertex>
    #include <logdepthbuf_pars_vertex>
    #include <clipping_planes_pars_vertex>

    void main() {
        vUv = uv;
        vAlbedo = iAlbedo;
        vORM = iORM;
        vNormal_OS = iNormal;

        // World-sized camera-facing billboard. Same as the unlit V0:
        // expand the quad in view space (which is rigid w.r.t. world)
        // by the OS diameter scaled into world units.
        float model_scale = length(modelMatrix[0].xyz);
        float world_diameter = uPointDiameter * model_scale;
        float world_radius   = world_diameter * 0.5;
        float radius_OS      = uPointDiameter * 0.5;

        // Inward-shift the splat centre so its outer edge sits at
        // the original surface. iPosition from the voxeliser is the
        // surface centroid (un-shifted) — see voxelize_samples.js.
        vec3 centre_OS = iPosition - iNormal * radius_OS;

        vec4 centre_view = modelViewMatrix * vec4(centre_OS, 1.0);
        vec2 corner = position.xy * world_diameter;
        vec4 pos_view = centre_view + vec4(corner, 0.0, 0.0);

        vViewPosition = -pos_view.xyz;
        gl_Position = projectionMatrix * pos_view;

        // Surface normal in view space — drives the lit fragment
        // pipeline and is consumed by <shadowmap_vertex> to compute a
        // world-space normal for its bias step.
        vec3 transformedNormal = normalize(normalMatrix * iNormal);

        // worldPosition for <shadowmap_vertex>:
        //
        //   Use the UN-SHIFTED iPosition (= the surface centroid) as
        //   the base, then add a normal-direction bias of one world
        //   radius. The chunk adds the per-light shadowNormalBias on
        //   top.
        //
        //   This is the "50% of the quad size" bias the user asked
        //   for, with a useful symmetry: the visible splat is centred
        //   at (surface - radius * normal), and its outer edge on the
        //   normal side sits at the surface — exactly where the
        //   receiver's shadow coord lands once we add (+radius * normal)
        //   to the un-shifted surface position. So the shadow lookup
        //   happens at the splat's own outer-edge depth, which is
        //   sub-pixel-stable against the caster's depth across
        //   typical viewing distances. Self-shadow acne goes away.
        vec3 normal_world = normalize(mat3(modelMatrix) * iNormal);
        vec3 centre_world = (modelMatrix * vec4(iPosition, 1.0)).xyz;
        vec4 worldPosition = vec4(centre_world + normal_world * world_radius, 1.0);

        #include <logdepthbuf_vertex>
        #include <clipping_planes_vertex>
        #include <shadowmap_vertex>
        #include <fog_vertex>
    }
`;
const shader_fg = `
    #define STANDARD

    uniform vec3 diffuse;
    uniform vec3 emissive;
    uniform float roughness;
    uniform float metalness;
    uniform float opacity;

    flat varying vec3 vAlbedo;
    flat varying vec3 vNormal_OS;
    flat varying vec3 vORM;
    varying vec3 vViewPosition;

    uniform mat3 normalMatrix;

    uniform bool uDiscMask;
    varying vec2 vUv;  // for disc mask via uv interpolation

    #include <common>
    #include <packing>
    #include <dithering_pars_fragment>
    #include <color_pars_fragment>
    #include <bsdfs>
    #include <cube_uv_reflection_fragment>
    #include <envmap_common_pars_fragment>
    #include <envmap_physical_pars_fragment>
    #include <fog_pars_fragment>
    #include <lights_pars_begin>
    #include <lights_physical_pars_fragment>
    #include <shadowmap_pars_fragment>
    #include <logdepthbuf_pars_fragment>
    #include <clipping_planes_pars_fragment>

    void main() {
        #include <clipping_planes_fragment>

        if (uDiscMask) {
            vec2 c = vUv - 0.5;
            if (dot(c, c) > 0.25) {
                discard;
            }
        }

        // sRGB -> linear (voxeliser re-encodes the averaged-linear
        // albedo to sRGB so the on-GPU sample matches the octahedral
        // atlas's "sRGB bytes labelled linear" format).
        vec3 albedo_linear = sRGBToLinear(vec4(vAlbedo, 1.0)).rgb;

        vec4 diffuseColor = vec4(albedo_linear * diffuse, 1.0);
        ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));

        float occlusionFactor  = vORM.r;
        float roughnessFactor  = roughness * vORM.g;
        float metalnessFactor  = metalness * vORM.b;

        vec3 totalEmissiveRadiance = emissive;

        vec3 normal = normalize(normalMatrix * vNormal_OS);
        vec3 geometryNormal = normal;

        #include <logdepthbuf_fragment>

        #include <lights_physical_fragment>
        #include <lights_fragment_begin>
        #include <lights_fragment_maps>
        #include <lights_fragment_end>

        reflectedLight.directDiffuse    *= occlusionFactor;
        reflectedLight.indirectDiffuse  *= occlusionFactor;
        reflectedLight.indirectSpecular *= occlusionFactor;

        vec3 totalDiffuse  = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;
        vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;
        vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;

        gl_FragColor = vec4(outgoingLight, 1.0);

        #include <tonemapping_fragment>
        #include <encodings_fragment>
        #include <fog_fragment>
        #include <premultiplied_alpha_fragment>
        #include <dithering_fragment>
    }
`;


/**
 * PBR-lit voxel impostor material. Each voxel is a world-space-sized
 * camera-facing billboard; lighting follows the baked per-voxel normal,
 * not the quad orientation. Casts and receives shadows.
 *
 * Pair with {@link ShadedGeometry.from}(geometry, material) — default
 * DrawMode.Triangles is correct.
 *
 * Per-instance attributes: iPosition / iNormal / iAlbedo / iORM (set
 * up by make_voxel_impostor_geometry). Per-instance uniforms:
 *   - uPointDiameter: splat diameter in OBJECT space. The shader
 *     multiplies by the entity's modelMatrix scale at draw time.
 *   - diffuse / emissive / roughness / metalness — standard PBR knobs.
 */
export class VoxelImpostorShaderLitV0 extends ShaderMaterial {
    constructor() {
        const uniforms = UniformsUtils.merge([
            UniformsLib.common,
            UniformsLib.fog,
            UniformsLib.lights,
            {
                roughness: { value: 1.0 },
                metalness: { value: 0.0 },
                emissive: { value: new Color(0x000000) }
            },
            {
                uPointDiameter: { value: 0.05 },
                uDiscMask: { value: false }
            }
        ]);

        super({
            uniforms,
            vertexShader: shader_vx,
            fragmentShader: shader_fg,
            lights: true,
            fog: true
        });
    }
}
