import { GLSL3, NoBlending, RawShaderMaterial, Vector3 } from "three";
import chunk_preamble from './glsl/common.glsl';

const shader_vx = `
    
    in vec2 uv;
    in vec3 position;
    
    out vec2 vUv;
    out vec4 plane0;
    out vec4 plane1;
    out vec4 plane2;
    
    ${chunk_preamble}
    
    void ImposterVertex( inout ImposterData imp )
    {
        //incoming vertex, object space
        vec4 vertex = imp.vertex;
        
        //camera in object space
        vec3 objectSpaceCameraPos = mul( unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz,1) ).xyz;
        vec2 texcoord = imp.uv;
        float4x4 objectToWorld = unity_ObjectToWorld;
        float4x4 worldToObject = unity_WorldToObject;
    
        vec3 imposterPivotOffset = _ImposterOffset.xyz;
        half framesMinusOne = _ImposterFrames-1;
        
        float3 objectScale = float3(length(float3(objectToWorld[0].x, objectToWorld[1].x, objectToWorld[2].x)),
                                    length(float3(objectToWorld[0].y, objectToWorld[1].y, objectToWorld[2].y)),
                                    length(float3(objectToWorld[0].z, objectToWorld[1].z, objectToWorld[2].z)));
                 
        //pivot to camera ray
        float3 pivotToCameraRay = normalize(objectSpaceCameraPos.xyz-imposterPivotOffset.xyz);
    
        //scale uv to single frame
        texcoord = vec2(texcoord.x,texcoord.y)*(1.0/_ImposterFrames.x);  
        
        //radius * 2 * unity scaling
        vec2 size = _ImposterSize.xx * 2.0; // * objectScale.xx; //unity_BillboardSize.xy                 
        
        vec3 projected = SpriteProjection( pivotToCameraRay, _ImposterFrames, size, texcoord.xy );
    
        //this creates the proper offset for vertices to camera facing billboard
        vec3 vertexOffset = projected + imposterPivotOffset;
        //subtract from camera pos 
        vertexOffset = normalize(objectSpaceCameraPos-vertexOffset);
        //then add the original projected world
        vertexOffset += projected;
        //remove position of vertex
        vertexOffset -= vertex.xyz;
        //add pivot
        vertexOffset += imposterPivotOffset;
    
        //camera to projection vector
        vec3 rayDirectionLocal = (imposterPivotOffset + projected) - objectSpaceCameraPos;
                     
        //projected position to camera ray
        vec3 projInterpolated = normalize( objectSpaceCameraPos - (projected + imposterPivotOffset) ); 
        
        Ray rayLocal;
        rayLocal.Origin = objectSpaceCameraPos-imposterPivotOffset; 
        rayLocal.Direction = rayDirectionLocal; 
        
        vec2 grid = VectorToGrid( pivotToCameraRay );
        vec2 gridRaw = grid;
        grid = saturate((grid+1.0)*0.5); //bias and scale to 0 to 1 
        grid *= framesMinusOne;
        
        vec2 gridFrac = frac(grid);
        
        vec2 gridFloor = floor(grid);
        
        vec4 weights = TriangleInterpolate( gridFrac ); 
        
        //3 nearest frames
        vec2 frame0 = gridFloor;
        vec2 frame1 = gridFloor + lerp(vec2(0,1),vec2(1,0),weights.w);
        vec2 frame2 = gridFloor + vec2(1,1);
        
        //convert frame coordinate to octahedron direction
        vec3 frame0ray = FrameXYToRay(frame0, framesMinusOne.xx);
        vec3 frame1ray = FrameXYToRay(frame1, framesMinusOne.xx);
        vec3 frame2ray = FrameXYToRay(frame2, framesMinusOne.xx);
        
        vec3 planeCenter = vec3(0,0,0);
        
        vec3 plane0x;
        vec3 plane0normal = frame0ray;
        vec3 plane0z;
        vec3 frame0local = FrameTransform( projInterpolated, frame0ray, plane0x, plane0z );
        frame0local.xz = frame0local.xz/_ImposterFrames.xx; //for displacement
        
        //virtual plane UV coordinates
        vec2 vUv0 = VirtualPlaneUV( plane0normal, plane0x, plane0z, planeCenter, size, rayLocal );
        vUv0 /= _ImposterFrames.xx;   
        
        vec3 plane1x; 
        vec3 plane1normal = frame1ray;
        vec3 plane1z;
        vec3 frame1local = FrameTransform( projInterpolated, frame1ray, plane1x, plane1z);
        frame1local.xz = frame1local.xz/_ImposterFrames.xx; //for displacement
        
        //virtual plane UV coordinates
        vec2 vUv1 = VirtualPlaneUV( plane1normal, plane1x, plane1z, planeCenter, size, rayLocal );
        vUv1 /= _ImposterFrames.xx;
        
        vec3 plane2x;
        vec3 plane2normal = frame2ray;
        vec3 plane2z;
        vec3 frame2local = FrameTransform( projInterpolated, frame2ray, plane2x, plane2z );
        frame2local.xz = frame2local.xz/_ImposterFrames.xx; //for displacement
        
        //virtual plane UV coordinates
        vec2 vUv2 = VirtualPlaneUV( plane2normal, plane2x, plane2z, planeCenter, size, rayLocal );
        vUv2 /= _ImposterFrames.xx;
        
        //add offset here
        imp.vertex.xyz += vertexOffset;
        //overwrite others
        imp.uv = texcoord;
        imp.grid = grid;
        imp.frame0 = vec4(vUv0.xy,frame0local.xz);
        imp.frame1 = vec4(vUv1.xy,frame1local.xz);
        imp.frame2 = vec4(vUv2.xy,frame2local.xz);
    }
    
    void main() {
        vUv = uv;
        
        ImposterData imp;
        imp.vertex = vec4(position, 1.0);
        imp.uv = uv;
        
        ImposterVertex(imp);
        
        gl_Position = imp.vertex;
        
        float3 normalWorld = UnityObjectToWorldDir(v.normal.xyz);
        float3 tangentWorld = UnityObjectToWorldDir(v.tangent.xyz);
        float3x3 tangentToWorld = CreateTangentToWorldPerVertex(normalWorld, tangentWorld, v.tangent.w);
        o.tangentWorld = tangentToWorld[0];
        o.bitangentWorld = tangentToWorld[1];
        o.normalWorld = tangentToWorld[2];
        
        //surface
        o.texCoord.xy = imp.uv;
        o.texCoord.zw = imp.grid;
        o.plane0 = imp.frame0;
        o.plane1 = imp.frame1;
        o.plane2 = imp.frame2;
    }
`;
const shader_fg = `
    
    in vec2 vUv;
    
    in vec4 plane0;
    in vec4 plane1;
    in vec4 plane2;
    
    struct Material{
        vec3 diffuse;
        vec3 normal;
        float depth;
        float occlusion;
        float roughness;
        float metalness;
    };
    
    void ImposterSample( in ImposterData imp, out vec4 baseTex, out vec4 worldNormal )//, out half depth )
    {
        vec2 fracGrid = frac(imp.grid);
        
        vec4 weights = TriangleInterpolate( fracGrid );
          
        vec2 gridSnap = floor(imp.grid) / _ImposterFrames.xx;
            
        vec2 frame0 = gridSnap;
        vec2 frame1 = gridSnap + (lerp(vec2(0,1),vec2(1,0),weights.w)/_ImposterFrames.xx);
        vec2 frame2 = gridSnap + (vec2(1,1)/_ImposterFrames.xx);
        
        vec2 vp0uv = frame0 + imp.frame0.xy;
        vec2 vp1uv = frame1 + imp.frame1.xy; 
        vec2 vp2uv = frame2 + imp.frame2.xy;
       
        //resolution of atlas (Square)
        float textureDims = _ImposterBaseTex_TexelSize.z;
        //fractional frame size, ex 2048/12 = 170.6
        float frameSize = textureDims/_ImposterFrames; 
        //actual atlas resolution used, ex 170*12 = 2040
        float actualDims = floor(frameSize) * _ImposterFrames; 
        //the scale factor to apply to UV coordinate, ex 2048/2040 = 0.99609375
        float scaleFactor = actualDims / textureDims;
       
        vp0uv *= scaleFactor;
        vp1uv *= scaleFactor;
        vp2uv *= scaleFactor;
       
        //clamp out neighboring frames TODO maybe discard instead?
        vec2 gridSize = 1.0/_ImposterFrames.xx;
        gridSize *= _ImposterBaseTex_TexelSize.zw;
        gridSize *= _ImposterBaseTex_TexelSize.xy;
        float2 border = _ImposterBaseTex_TexelSize.xy*_ImposterBorderClamp;
        
        //vp0uv = clamp(vp0uv,frame0+border,frame0+gridSize-border);
        //vp1uv = clamp(vp1uv,frame1+border,frame1+gridSize-border);
        //vp2uv = clamp(vp2uv,frame2+border,frame2+gridSize-border);
       
        //for parallax modify
        vec4 n0 = tex2Dlod( _ImposterWorldNormalDepthTex, vec4(vp0uv, 0, 1 ) );
        vec4 n1 = tex2Dlod( _ImposterWorldNormalDepthTex, vec4(vp1uv, 0, 1 ) );
        vec4 n2 = tex2Dlod( _ImposterWorldNormalDepthTex, vec4(vp2uv, 0, 1 ) );
            
        half n0s = 0.5-n0.a;    
        half n1s = 0.5-n1.a;
        half n2s = 0.5-n2.a;
        
        vec2 n0p = imp.frame0.zw * n0s;
        vec2 n1p = imp.frame1.zw * n1s;
        vec2 n2p = imp.frame2.zw * n2s;
        
        //add parallax shift 
        vp0uv += n0p;
        vp1uv += n1p;
        vp2uv += n2p;
        
        //clamp out neighboring frames TODO maybe discard instead?
        vp0uv = clamp(vp0uv,frame0+border,frame0+gridSize-border);
        vp1uv = clamp(vp1uv,frame1+border,frame1+gridSize-border);
        vp2uv = clamp(vp2uv,frame2+border,frame2+gridSize-border);
        
        vec2 ddxy = vec2( ddx(imp.uv.x), ddy(imp.uv.y) );
        
        worldNormal = ImposterBlendWeights( _ImposterWorldNormalDepthTex, imp.uv, vp0uv, vp1uv, vp2uv, weights, ddxy );
        baseTex = ImposterBlendWeights( _ImposterBaseTex, imp.uv, vp0uv, vp1uv, vp2uv, weights, ddxy );
            
        //pixel depth offset
        //half pdo = 1-baseTex.a;
        //float3 objectScale = float3(length(float3(unity_ObjectToWorld[0].x, unity_ObjectToWorld[1].x, unity_ObjectToWorld[2].x)),
        //                        length(float3(unity_ObjectToWorld[0].y, unity_ObjectToWorld[1].y, unity_ObjectToWorld[2].y)),
        //                        length(float3(unity_ObjectToWorld[0].z, unity_ObjectToWorld[1].z, unity_ObjectToWorld[2].z)));
        //vec2 size = _ImposterSize.xx * 2.0;// * objectScale.xx;  
        //vec3 viewWorld = mul( UNITY_MATRIX_VP, float4(0,0,1,0) ).xyz;
        //pdo *= size * abs(dot(normalize(imp.viewDirWorld.xyz),viewWorld));
        //depth = pdo;
    }
    
    void main(){
    
    }
`;

export class ImpostorShaderStandard 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)
                }
            },
            glslVersion: GLSL3
        });

        // Save some effort by disabling blending
        this.blending = NoBlending;

    }
}
