import {
    AddEquation,
    CustomBlending,
    GLSL3,
    Matrix4,
    OneFactor,
    OneMinusSrcAlphaFactor,
    ShaderMaterial,
    UniformsLib,
    UniformsUtils,
    Vector2 as ThreeVector2
} from "three";

const CHUNK_LAMBERT_LIGHTING = `
vec3 diffuse = vec3( 1.0 );

GeometricContext geometry;
geometry.position = -vViewPosition;
geometry.normal = normalize( transformedNormal );
geometry.viewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( -vViewPosition.xyz );

GeometricContext backGeometry;
backGeometry.position = geometry.position;
backGeometry.normal = -geometry.normal;
backGeometry.viewDir = geometry.viewDir;

vLightFront = vec3( 0.0 );
vIndirectFront = vec3( 0.0 );

IncidentLight directLight;
float dotNL;
vec3 directLightColor_Diffuse;

#if NUM_POINT_LIGHTS > 0

	#pragma unroll_loop_start
	for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {

		getPointLightInfo( pointLights[ i ], geometry, directLight );

		RE_Direct( directLight, geometry, material, reflectedLight );
		
	}
	#pragma unroll_loop_end

#endif

#if NUM_SPOT_LIGHTS > 0

	#pragma unroll_loop_start
	for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {

		getSpotLightInfo( spotLights[ i ], geometry, directLight );

		RE_Direct( directLight, geometry, material, reflectedLight );
	}
	#pragma unroll_loop_end

#endif

#if NUM_DIR_LIGHTS > 0

	#pragma unroll_loop_start
	for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {

		getDirectionalLightInfo( directionalLights[ i ], geometry, directLight );

		RE_Direct( directLight, geometry, material, reflectedLight );

	}
	#pragma unroll_loop_end

#endif



#if defined( RE_IndirectDiffuse )
    vec3 iblIrradiance = vec3( 0.0 );
    vec3 irradiance = getAmbientLightIrradiance( ambientLightColor );
    irradiance += getLightProbeIrradiance( lightProbe, geometry.normal );
    #if NUM_HEMI_LIGHTS > 0

	#pragma unroll_loop_start
        for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {
    
            vIndirectFront += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry.normal );
    
        }
        #pragma unroll_loop_end
    
    #endif

	#if defined( USE_ENVMAP ) && defined( ENVMAP_TYPE_CUBE_UV )
		iblIrradiance += getIBLIrradiance( geometry.normal );
	#endif
#endif

#if defined( RE_IndirectSpecular )
    vec3 radiance = vec3( 0.0 );
    vec3 clearcoatRadiance = vec3( 0.0 );
#endif

#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular )
	radiance += getIBLRadiance( geometry.viewDir, geometry.normal, material.roughness );
#endif

#if defined( RE_IndirectDiffuse )
	RE_IndirectDiffuse( irradiance, geometry, material, reflectedLight );
#endif
#if defined( RE_IndirectSpecular )
	RE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometry, material, reflectedLight );
#endif
`;

function make_vertex_shader() {
    //language=GLSL
    return `
                uniform float cameraNear;
                uniform float cameraFar;
                uniform vec2 resolution;
                
                attribute float size;
                attribute vec4 color;
                attribute vec4 atlasPatch;
                attribute float rotation;
                attribute vec3 velocity;
                attribute float blending;
                
                varying vec4 vPatch;
                varying float vSize;
                varying float vFadeDistance;
                varying vec4 vColor;
                varying vec2 vRotationMultiplier;
                varying float vBlending;
                
                #if defined(LIGHTS_ENABLED)
                    varying vec3 vViewPosition;
                #endif
                    
                
                
                void main() {
                   
                    vColor = color;
                    vSize = size;
                    vPatch = atlasPatch;
                    vBlending = blending;
                    
                    mat4 modelViewProjectionMatrix = projectionMatrix * modelViewMatrix;
                                                    
                    vec3 transformed = position;
                    
                    vec4 position4 = vec4( position, 1.0 );
                    
                    #if defined(LIGHTS_ENABLED)
                        vec4 mvPosition = modelViewMatrix * position4;
                        vViewPosition = -mvPosition.xyz;
                        
                        vec3 transformedNormal = vec3(0.0,0.0,1.0);
                        
                        #include <worldpos_vertex>
                    #endif
                    
                    gl_Position = modelViewProjectionMatrix * position4;
                                                    
                    
                    float rotationGlobal = rotation;
                    
                    //
                    #ifdef ALIGN_DIRECTION_ENABLED 
                        if(velocity != vec3(0.0)){
                    
                            vec4 vecNext = modelViewProjectionMatrix * (position4 + vec4(velocity,0.0) );
                            
                            vec2 p_2 = gl_Position.xy / gl_Position.w;
                            vec2 vecNext_2 = vecNext.xy / vecNext.w;
                            
                            vec2 direction = vecNext_2 - p_2;
                            
                            float upAngle = atan(direction.y, direction.x) - 1.57079632679;
                            
                            rotationGlobal = rotationGlobal + upAngle;
                            
                        }
                    #endif
                    
                    vRotationMultiplier = vec2( cos(rotationGlobal),sin(rotationGlobal) );
                    
                    float radius = vSize * 0.5;
                    
                    float projected_radius = resolution.y * projectionMatrix[1][1] * radius / gl_Position.w;
                                    
                    #ifdef DEPTH_SOFT_ENABLED
                    
                        vFadeDistance = 1.0 / radius;
                    
                    #endif
                    
                    gl_PointSize = projected_radius;
                
                    
                }`;
}

//${CHUNK_LAMBERT_LIGHTING}
//#include <lights_lambert_vertex>
function make_fragment_shader() {
    return `
                uniform sampler2D tSpriteAtlas;
                uniform sampler2D tDepth;
                uniform float cameraNear;
                uniform float cameraFar;
                uniform mat4 modelViewMatrix;
                uniform mat4 projectionMatrix;
                      
                uniform mat4 uProjectionInverse;
                uniform mat4 uViewInverse;
                
                uniform vec2 resolution;
                
                varying float vFadeDistance;
                varying vec4 vPatch;
                varying float vSize;
                varying vec4 vColor;
                varying vec2 vRotationMultiplier;
                varying float vBlending;
                
                out highp vec4 pc_fragColor;
                
                float depthToLinear( float z ) {
                    float f = cameraFar;
                    float n = cameraNear;
                    
                    float d_n = 2.0*z - 1.0;
                    
                    float z_diff = f - n;
                    
                    float denominator = (f + n - d_n * z_diff );
                    
                    float z_view = (2.0*f*n) / denominator;
                
                    return z_view-n;
                }
                
                float depthToLinear_v2(float z){
                    float far = cameraFar;
                    float near = cameraNear;
                    
                    // perspectiveDepthToViewZ
                    float view_z = ( near * far ) / ( ( near-far  ) * z + far );
                    
                    return  view_z;
                }
                
                
                float calculateFade(in float particleDepth, in float sceneDepth){
                    float zFade;
                                                            
                    float linearParticleDepth = depthToLinear_v2(particleDepth);
                    
                    float linearSceneDepth = depthToLinear_v2(sceneDepth);
                                    
                    float depthDelta = (linearSceneDepth - linearParticleDepth);
                                                    
                    float inputDepth = depthDelta* vFadeDistance;
                    
                    if(inputDepth <= 0.0){
                        return 0.0;
                    }
                    
                    if(inputDepth >= 1.0){
                        return 1.0;
                    }
                    
                    if(inputDepth <= 0.5){
                        zFade = 2.0 * pow( clamp(inputDepth, 0.0, 1.0), 2.0);
                    }else{
                        zFade = 1.0 - 2.0 * pow( clamp(1.0 - inputDepth, 0.0, 1.0), 2.0);
                    }
                    
                    return zFade;
                }
                
                
                #if defined(LIGHTS_ENABLED)
                
                    vec3 computeWorldPositionFromFragmentPosition(vec3 coord){
                         vec4 ndc = vec4(
                        (coord.x - 0.5) * 2.0,
                        (coord.y - 0.5) * 2.0,
                        (coord.z - 0.5) * 2.0,
                        1.0);
                    
                        vec4 clip = uProjectionInverse * ndc;
                        return (uViewInverse* (clip / clip.w) ).xyz;
                    }
                
                    vec3 construct_spherical_normal_from_point_coord(vec2 coord){
                        float x = coord.x * 2.0 - 1.0;
                        float y = coord.y * 2.0 - 1.0; // note we flip Y
                    
                        float z = sqrt(1.0 - clamp(x*x + y*y, 0.0, 1.0));
                    
                        
                        vec3 position = vec3(x, -y, z);
                    
                        vec3 normal = normalize(position);
                        
                        return normal;
                    }
                
                    #include <common>
                    #include <bsdfs>
                    #include <lights_pars_begin>
                    
                    struct LambertMaterial {
                        vec3 diffuseColor;
                        float opacity;
                    };
                    
                    void RE_Direct_Lambert( const in IncidentLight directLight, const in GeometricContext geometry, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {
                        float unsaturated_direction_dot = dot( geometry.normal, directLight.direction );
                        
                        float dotNL = saturate( unsaturated_direction_dot );
                        
                        vec3 irradiance =  dotNL * directLight.color;
                        
                        reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );
                    }
                    
                    void RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in GeometricContext geometry, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {
                        reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );
                    }
                    
                    #define RE_Direct				RE_Direct_Lambert
                    #define RE_IndirectDiffuse		RE_IndirectDiffuse_Lambert
                    
                    varying vec3 vViewPosition;
                #endif
                
                void main() {
                    float fragmentDepth = gl_FragCoord.z;
                    
                    vec2 pixelPosition = gl_FragCoord.xy / resolution;
                    
                    float sceneDepth =  texture2D( tDepth, pixelPosition ).x;
                    
                    #ifdef DEPTH_READ_ENABLED
                    
                        if(fragmentDepth > sceneDepth){
                            discard;
                        }
                    
                    #endif
                   
                    vec2 centeredUv = gl_PointCoord.xy - 0.5;
                    
                    vec2 texelCoordinate = vec2( centeredUv.x* vRotationMultiplier.x - centeredUv.y* vRotationMultiplier.y, centeredUv.x * vRotationMultiplier.y + centeredUv.y * vRotationMultiplier.x) + 0.5;
                    
                    texelCoordinate = clamp(texelCoordinate, vec2(0.0), vec2(1.0) );
                                                   
                    vec2 uv = texelCoordinate*vPatch.zw + vPatch.xy;
                    
                    vec4 spriteColor = texture2D( tSpriteAtlas, uv );
                    
                    if(spriteColor.a == 0.0){
                        discard;
                    }
                    
                    vec4 texel = spriteColor * vColor;
                    
                    #if defined(LIGHTS_ENABLED)
                        // compute world position
                        vec3 v_world_position = computeWorldPositionFromFragmentPosition(gl_FragCoord.xyz);
               
                
                        vec3 vLightFront;
                        vec3 vIndirectFront;
                        
                        vec3 transformedNormal = construct_spherical_normal_from_point_coord(gl_PointCoord.xy);
                        
                        LambertMaterial material = LambertMaterial(texel.rgb, texel.a);
                        ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
                        
                        ${CHUNK_LAMBERT_LIGHTING}
                       
                        vec3 totalEmissiveRadiance = vec3(0.0);
                        
                        //@injection_point FORWARD_PLUS:accumulation
                        
                        vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;
                        
                        texel.rgb = outgoingLight;
                    #endif
                                    
                    
                    // premultiply alpha
                    texel.xyz *= texel.a;
                    
                    if(vBlending > 0.1){
                        // additive blending
                        texel.a = 0.0;
                    }
                    
                    #ifdef DEPTH_SOFT_ENABLED
                    
                        float zFade = calculateFade(fragmentDepth, sceneDepth);
                        texel *= zFade;
                    
                    #endif
                    
                    pc_fragColor = texel;
                }`;
}

/**
 * NOTES:
 * - To implement per-particle blend modes, we need to have access to what we're blending on to, that is - the color buffer
 *   Having the color buffer - we can do just about any kind of a blend
 *
 * - Support animation by adding animation sprite sheet to the main sprite atlas, record resolution of the sheet (in frames X,Y) and interpolate over tiles based on some parameter
 * @param {boolean} [lights]
 */
export function make_particle_billboard_shader({ lights = false } = {}) {
    let uniforms = {
        tSpriteAtlas: {
            type: 't',
            value: null
        },
        cameraNear: {
            type: 'f',
            value: 1,
        },
        cameraFar: {
            type: 'f',
            value: 1,
        },
        tDepth: {
            type: 't',
            value: null
        },
        tColor: {
            type: 't',
            value: null
        },
        modelViewMatrix: {
            type: 'm4',
            value: new Matrix4()
        },
        projectionMatrix: {
            type: 'm4',
            value: new Matrix4()
        },
        resolution: {
            type: 'v2',
            value: new ThreeVector2()
        },
        uViewInverse: { type: 'm4', value: new Matrix4() },
        uProjectionInverse: { type: 'm4', value: new Matrix4() },
    };

    if (lights) {
        // mix in light uniforms
        uniforms = Object.assign(uniforms, UniformsUtils.clone(UniformsLib.lights));
    }

    const shader = new ShaderMaterial({
        defines: {
            DEPTH_READ_ENABLED: true,
            DEPTH_SOFT_ENABLED: true,
            ALIGN_DIRECTION_ENABLED: false,
            LIGHTS_ENABLED: lights
        },
        uniforms: uniforms,
        vertexShader: make_vertex_shader(),
        fragmentShader: make_fragment_shader(),
        blending: CustomBlending,
        lights: lights,
        /*
         forcing transparent pass in Graphics Engine
         three.js and indeed WebGL doesn't provide a way to pass depth buffer into the main draw pass (to canvas), so this is a workaround
         */
        depthTest: false,
        depthWrite: false,
        transparent: true,
        vertexColors: false,
        glslVersion: GLSL3
    });

    shader.blending = CustomBlending;
    shader.blendEquation = AddEquation;
    shader.blendSrc = OneFactor;
    shader.blendDst = OneMinusSrcAlphaFactor;


    return shader;
}
