import {
    AddEquation,
    CustomBlending,
    GLSL3,
    OneFactor,
    OneMinusSrcAlphaFactor,
    RawShaderMaterial,
    Vector3
} from "three";

// The wireframe must trace the SAME card geometry that ImpostorShaderV0
// produces, otherwise the red outline lies about where the impostor surface
// actually is. So this vertex shader is the geometry half of the main V0
// vertex shader, minus everything the fragment shader doesn't read (no
// vUv, vViewPos, vTangent/vBinormal/vNormal, no per-frame xforms, no
// atlas lookup).
const shader_vx = `

    in vec3 position;

    uniform mat4 modelViewMatrix;
    uniform mat4 projectionMatrix;

    uniform vec3 uOffset;
    uniform float uRadius;
    uniform float uFrames;
    uniform bool uIsFullSphere;

    // ----- direction -> octahedral grid coord (range -1..+1) -----

    vec2 VecToSphereOct(vec3 pivotToCamera)
    {
        vec3 octant = sign(pivotToCamera);
        float sum = dot(pivotToCamera, octant);
        vec3 octahedron = pivotToCamera / sum;
        if (octahedron.y < 0.0) {
            vec3 absolute = abs(octahedron);
            octahedron.xz = octant.xz * vec2(1.0 - absolute.z, 1.0 - absolute.x);
        }
        return octahedron.xz;
    }

    vec2 VecToHemiSphereOct(vec3 v)
    {
        v.y = max(v.y, 0.001);
        v = normalize(v);
        vec3 octant = sign(v);
        float sum = dot(v, octant);
        vec3 octahedron = v / sum;
        return vec2(
            octahedron.x + octahedron.z,
            octahedron.z - octahedron.x
        );
    }

    vec2 VectorToGrid(vec3 v)
    {
        return uIsFullSphere ? VecToSphereOct(v) : VecToHemiSphereOct(v);
    }

    // ----- octahedral grid coord (range 0..1) -> direction -----

    vec3 OctaSphereDec(vec2 coord)
    {
        coord = (coord - 0.5) * 2.0;
        vec3 p = vec3(coord.x, 0.0, coord.y);
        vec2 a = abs(p.xz);
        p.y = 1.0 - a.x - a.y;
        if (p.y < 0.0) {
            p.xz = sign(p.xz) * vec2(1.0 - a.y, 1.0 - a.x);
        }
        return p;
    }

    vec3 OctaHemiSphereDec(vec2 coord)
    {
        vec3 p = vec3(coord.x - coord.y, 0.0, -1.0 + coord.x + coord.y);
        vec2 a = abs(p.xz);
        p.y = 1.0 - a.x - a.y;
        return p;
    }

    vec3 GridToVector(vec2 coord)
    {
        return uIsFullSphere ? OctaSphereDec(coord) : OctaHemiSphereDec(coord);
    }

    vec3 FrameToRay(vec2 frame, vec2 framesMinusOne)
    {
        vec2 f = clamp(frame / framesMinusOne, 0.0, 1.0);
        return normalize(GridToVector(f));
    }

    vec4 BilinearWeights(vec2 frac_uv)
    {
        vec2 omuv = vec2(1.0) - frac_uv;
        return vec4(
            omuv.x * omuv.y,
            frac_uv.x * omuv.y,
            omuv.x * frac_uv.y,
            frac_uv.x * frac_uv.y
        );
    }

    void main() {
        // Pivot-to-camera direction in object-local space; same as V0.
        vec3 cameraPos_OS = (inverse(modelViewMatrix) * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
        // Atlas lookup must be relative to the bounding-sphere centre
        // (uOffset), since the bake's D_frame is measured from the centre,
        // not from the object origin. Mirrors V0.
        vec3 pivotToCameraRay = normalize(cameraPos_OS - uOffset);

        // Pick the cell and its 4-corner bilinear weights, then blend the
        // four corner rays into the "effective" bake direction the card is
        // showing. Mirrors V0.
        vec2 framesMinusOne = vec2(uFrames - 1.0);
        vec2 octahedral_uv = clamp(VectorToGrid(pivotToCameraRay) * 0.5 + 0.5, 0.0, 1.0);
        vec2 grid = octahedral_uv * framesMinusOne;
        vec2 gridFloor = min(floor(grid), framesMinusOne - 1.0);
        vec4 weights = BilinearWeights(grid - gridFloor);

        vec3 ray00 = FrameToRay(gridFloor + vec2(0.0, 0.0), framesMinusOne);
        vec3 ray10 = FrameToRay(gridFloor + vec2(1.0, 0.0), framesMinusOne);
        vec3 ray01 = FrameToRay(gridFloor + vec2(0.0, 1.0), framesMinusOne);
        vec3 ray11 = FrameToRay(gridFloor + vec2(1.0, 1.0), framesMinusOne);
        vec3 projectedRay = normalize(
            ray00 * weights.x +
            ray10 * weights.y +
            ray01 * weights.z +
            ray11 * weights.w
        );

        // TBN construction identical to V0 so the card spans the same plane.
        vec3 normal_OS = projectedRay;
        vec3 up_OS = abs(normal_OS.y) > 0.999
                     ? vec3(0.0, 0.0, -1.0)
                     : vec3(0.0, 1.0,  0.0);
        vec3 tangent_OS  = normalize(cross(up_OS, normal_OS));
        vec3 binormal_OS = cross(normal_OS, tangent_OS);

        float card_diameter = uRadius * 2.0;
        vec3 pos_OS = uOffset
                    + position.x * card_diameter * tangent_OS
                    + position.y * card_diameter * binormal_OS;

        gl_Position = projectionMatrix * modelViewMatrix * vec4(pos_OS, 1.0);
    }
`;
const shader_fg = `
    precision highp float;
    precision highp int;
    
    out vec4 color_out;

    void main(){
        
        color_out = vec4(1.0, .0, .0, 1.0);
    }
`;

export class ImpostorShaderWireframeV0 extends RawShaderMaterial {
    constructor() {
        super({
            fragmentShader: shader_fg,
            vertexShader: shader_vx,
            uniforms: {
                /**
                 * RGB + Alpha
                 */
                tBase: {
                    value: null
                },
                /**
                 * Normal+Depth
                 */
                tGeometry: {
                    value: null
                },
                /**
                 * Material properties: Occlusion, Roughness, Metalness
                 * Alpha unused
                 */
                tMaterial: {
                    value: null
                },
                /**
                 * Number of frames
                 */
                uFrames: {
                    value: 0
                },
                /**
                 * Radius of bounding sphere of the impostor
                 */
                uRadius: {
                    value: 0
                },
                /**
                 * Impostor offset
                 */
                uOffset: {
                    value: new Vector3(0, 0, 0)
                },
                uIsFullSphere: {
                    value: false
                },
                uDepthScale: {
                    // value should be in range between 0 and 1
                    value: 1
                }
            },
            glslVersion: GLSL3
        });

        // Save some effort by disabling blending
        this.blending = CustomBlending;
        this.blendEquation = AddEquation;
        this.blendSrc = OneFactor;
        this.blendDst = OneMinusSrcAlphaFactor;

    }
}
