#include "/lib/misc/reprojection.glsl"

#ifdef OVERWORLD
    #include "/lib/atmospherics/sky.glsl"
#endif
#if defined END && defined COMPOSITE
    #include "/lib/atmospherics/volumetricLight/enderBeams.glsl"
#endif

#ifdef ATM_COLOR_MULTS
    #include "/lib/colors/colorMultipliers.glsl"
#endif
#ifdef MOON_PHASE_INF_ATMOSPHERE
    #include "/lib/colors/moonPhaseInfluence.glsl"
#endif

#if WORLD_SPACE_REFLECTIONS_INTERNAL > 0 && defined COMPOSITE
    #include "/lib/voxelization/lightVoxelization.glsl"
    #include "/lib/materials/materialMethods/worldSpaceRef.glsl"
#endif

float GetApproxDistance(float depth) {
    return near * far / (far - depth * far);
}

vec3 nvec3(vec4 pos) {
    return pos.xyz/pos.w;
}

float refDist = far;

#include "/lib/materials/materialMethods/reflectionBackground.glsl"

vec4 GetReflection(inout vec3 normalM, vec3 viewPos, vec3 nViewPos, vec3 playerPos, float lViewPos, float z0,
                   sampler2D depthtex, float dither, float skyLightFactor, float fresnel,
                   float smoothness, vec3 geoNormal, vec3 color, vec3 shadowMult, float highlightMult) {
    // ============================== Step 1: Prepare ============================== //
    #if WORLD_SPACE_REFLECTIONS_INTERNAL == -1
        vec2 rEdge = vec2(0.70, 0.65);
    #else
        vec2 rEdge = vec2(0.65, 0.65);
    #endif
    vec3 normalMR = normalM;

    #if defined GBUFFERS_WATER && WATER_STYLE == 1 && defined GENERATED_NORMALS
        normalMR = normalize(mix(geoNormal, normalM, 0.05));
    #endif

    vec3 nViewPosR = normalize(reflect(nViewPos, normalMR));
    float RVdotU = dot(nViewPosR, upVec);
    float RVdotS = dot(nViewPosR, sunVec);

    // ("bug refleksi air waktu kamera nunduk ke bawah" fix): when the
    // camera looks straight down (or straight up while underwater) at a
    // near-horizontal water surface, the reflection vector points almost
    // directly back through the camera (nViewPosR.z >= 0 in view space,
    // i.e. behind the near plane instead of into the screen). Both
    // screen-space methods below project nViewPosR with gbufferProjection
    // and divide by .w - for a behind-camera direction that .w is zero or
    // negative, which flips/garbles the resulting screen UV instead of
    // safely failing, and that garbage UV can still pass the rEdge screen
    // bounds check. That's what reads as glitchy/incorrect reflections
    // when looking down. Skip both screen-space methods entirely for
    // behind-camera directions and fall back to the clean sky/background
    // reflection (Step 3 below) instead of sampling garbage.
    bool validRefDir = nViewPosR.z < -0.001;

    #if defined GBUFFERS_WATER && WATER_STYLE >= 2
        normalMR = normalize(mix(geoNormal, normalM, 0.8));
    #endif
    // ============================== End of Step 1 ============================== //

    // ============================== Step 2: Calculate Terrain Reflection and Alpha ============================== //
    #if WORLD_SPACE_REFLECTIONS_INTERNAL > 0 && defined COMPOSITE && WATER_REFLECT_QUALITY >= 1
        // In COMPOSITE for translucents we just need to return WSR and that's it
        if (z0 != z1) {
            vec4 reflection = getWSR(playerPos, normalMR, nViewPosR, RVdotU, RVdotS, z0, dither, refDist);
            return reflection;
        }
    #endif

    vec4 reflection = vec4(0.0);
    #if (defined COMPOSITE || WATER_REFLECT_QUALITY >= 1) && (WORLD_SPACE_REFLECTIONS_INTERNAL == -1 || WORLD_SPACE_REF_MODE == 2)
        // Method 1: Ray Marched Reflection //
        #if defined COMPOSITE || WATER_REFLECT_QUALITY >= 2
          if (validRefDir) { // behind-camera guard, see validRefDir above
            // Ray Marching
            vec3 start = viewPos + normalMR * (lViewPos * 0.025 * (1.0 - fresnel) + 0.05);
            #if defined GBUFFERS_WATER && WATER_STYLE >= 2
                vec3 vector = normalize(reflect(nViewPos, normalMR)); // Not using nViewPosR because normalMR changed
            #else
                vec3 vector = nViewPosR;
            #endif
            vector *= 0.5;

            // ---- Adaptive Close-Range Precision ----
            // Semakin dekat kamera ke permukaan reflektif (lViewPos kecil), step awal dibuat lebih mikro
            // dan jumlah iterasi refinement ditambah, supaya pantulan menempel presisi tanpa gap saat close-up.
            float closeRangeFactor = clamp(1.0 - lViewPos / 4.0, 0.0, 1.0); // 1.0 di <1 block, 0.0 di atas 4 block
            vector *= mix(1.0, 0.4, closeRangeFactor);

            vec3 vectorBase = vector;
            vec3 viewPosRT = viewPos + vector;
            vec3 tvector = vector;

            #if WORLD_SPACE_REFLECTIONS_INTERNAL == -1
                int sampleCount = 52;
                int refinementCount = 10;
            #else
                int sampleCount = 64;
                int refinementCount = 16;
            #endif
            refinementCount += int(closeRangeFactor * 8.0); // iterasi biner tambahan khusus jarak dekat

            int sr = 0;
            float dist = 0.0;
            vec3 refPos = vec3(0.0);
            vec3 rfragpos = vec3(0.0);
            float err = 9999999.0;
            float sampleDepth = 1.0;
            for (int i = 0; i < sampleCount; i++) {
                refPos = nvec3(gbufferProjection * vec4(viewPosRT, 1.0)) * 0.5 + 0.5;
                if (abs(refPos.x - 0.5) > rEdge.x || abs(refPos.y - 0.5) > rEdge.y) break;

                sampleDepth = texture2D(depthtex, refPos.xy).r;
                rfragpos = nvec3(gbufferProjectionInverse * vec4(vec3(refPos.xy, sampleDepth) * 2.0 - 1.0, 1.0));

                #if defined GBUFFERS_WATER && (defined DISTANT_HORIZONS || defined VOXY)
                    if (sampleDepth >= 1.0) {
                        #ifdef VOXY
                            sampleDepth = texture2D(vxDepthTexOpaque, refPos.xy).r;
                            if (sampleDepth < 1.0) {
                                rfragpos = nvec3(vxProjInv * vec4(vec3(refPos.xy, sampleDepth) * 2.0 - 1.0, 1.0));
                            }
                        #endif
                        #ifdef DISTANT_HORIZONS
                            sampleDepth = texture2D(dhDepthTex1, refPos.xy).r;
                            if (sampleDepth < 1.0) {
                                rfragpos = nvec3(dhProjectionInverse * vec4(vec3(refPos.xy, sampleDepth) * 2.0 - 1.0, 1.0));
                            }
                        #endif
                    }
                #endif

                dist = length(start - rfragpos);

                err = length(viewPosRT - rfragpos);
                if (err * 0.33333 < length(vector)) {
                    sr++;
                    if (sr >= refinementCount) break;
                    tvector -= vector;
                    vector *= 0.1;
                }
                vector *= 2.0;
                tvector += vector * (0.95 + 0.1 * dither);
                viewPosRT = start + tvector;
            }

            float lViewPosRT = length(rfragpos);
            refPos.z = sampleDepth;

            #ifdef COMPOSITE
                bool possibleReflection = true;
            #else
                vec3 viewRefPos = ScreenToView(refPos);
                // #ifdef VOXY
                //     vec3 viewRefPosVx = ScreenToViewVoxy(refPos);
                //     if (length(viewRefPos) < length(viewRefPosVx)) viewRefPos = viewRefPosVx;
                // #endif
                bool possibleReflection = dot(normalize(viewRefPos - start), geoNormal) > 0.0;
            #endif

            #ifdef VOXY_PATCH
                if (refPos.x > 0.0 && refPos.x < 1.0 && refPos.y > 0.0 && refPos.y < 1.0) 
                {
                    // Previous frame reprojection from Chocapic13
                    // Voxy water needs reflection reprojection due to rendering before deferred
                    vec4 viewPosPrev = vxProjInv * vec4(refPos * 2.0 - 1.0, 1.0);
                    viewPosPrev /= viewPosPrev.w;
                    
                    viewPosPrev = vxModelViewInv * viewPosPrev;

                    vec4 previousPosition = viewPosPrev + vec4(cameraPosition - previousCameraPosition, 0.0);
                    previousPosition = vxModelViewPrev * previousPosition;
                    previousPosition = vxProjPrev * previousPosition;
                    refPos.xy = previousPosition.xy / previousPosition.w * 0.5 + 0.5;
                }
            #endif

            // Finalizing Terrain Reflection and Alpha
            if (
                refPos.z < 1.0
                && possibleReflection
                && (err * (1.0 - fresnel) < mix(20.0, 100.0, clamp(lViewPosRT / 4.0, 0.0, 1.0)) + lViewPosRT * 0.2)
                #if WORLD_SPACE_REFLECTIONS_INTERNAL > 0 && COLORED_LIGHTING_INTERNAL > 0
                    && (err < 2.0 + pow2(lViewPosRT) * 0.001 || lViewPosRT > 0.25 * COLORED_LIGHTING_INTERNAL)
                #endif
            ) {
                vec2 absPos = abs(refPos.xy - 0.5);
                vec2 cdist = absPos / rEdge;
                float border = clamp(1.0 - pow(max(cdist.x, cdist.y), 50.0), 0.0, 1.0);
                reflection.a = border;

                if (reflection.a > 0.001) {
                    vec2 edgeFactor = pow2(pow2(pow2(cdist)));
                    #if WORLD_SPACE_REFLECTIONS_INTERNAL == -1
                        refPos.y += (dither - 0.5) * (0.05 * (edgeFactor.x + edgeFactor.y));
                    #endif

                    #if defined GBUFFERS_WATER || defined DH_WATER || defined VOXY_PATCH
                        #ifndef VOXY_PATCH
                            reflection = vec4(texture2D(gaux2, refPos.xy).rgb, 1.0);
                        #else
                            reflection = vec4(texture2D(colortex19, refPos.xy).rgb, 1.0);
                        #endif
                        reflection.rgb = pow2(reflection.rgb * 2.0);
                    #else
                        float smoothnessDM = pow2(smoothness);
                        float lodFactor = 1.0 - exp(-0.125 * (1.0 - smoothnessDM) * dist);
                        float lod = log2(viewHeight / 8.0 * (1.0 - smoothnessDM) * lodFactor) * 0.30;
                        if (z0 <= 0.56) lod *= 2.22; // Using more lod to compensate for less roughness noise on held items
                        lod = max(lod - 1.5, 0.0);

                        reflection.rgb = texture2DLod(colortex0, refPos.xy, lod).rgb;
                    #endif

                    float skyFade = 0.0;

                    #if defined GBUFFERS_WATER || defined DH_WATER || defined VOXY_PATCH
                        float reflectionPrevAlpha = reflection.a;
                        DoFog(reflection, skyFade, lViewPosRT, ViewToPlayer(rfragpos.xyz), RVdotU, RVdotS, dither, true, lViewPos);
                        reflection.a = reflectionPrevAlpha;
                        //reflection.a *= 1.0 - skyFade;
                    #endif

                    edgeFactor.x = pow2(edgeFactor.x);
                    edgeFactor = 1.0 - edgeFactor;
                    float refFactor = pow(edgeFactor.x * edgeFactor.y, 2.0 + 3.0 * GetLuminance(reflection.rgb));
                    #if WORLD_SPACE_REFLECTIONS_INTERNAL > 0 && defined GBUFFERS_WATER
                        refFactor = min(refFactor, 0.1) * 10.0;
                    #endif
                    reflection.a *= refFactor;
                    refDist = dist;
                }

                //float posDif = lViewPosRT - lViewPos;
                //reflection.a *= clamp(posDif + 3.0, 0.0, 1.0);
            }
          } // end validRefDir (Method 1)
        #endif

        // Method 2: Mirorred Image Reflection //
        #if !defined COMPOSITE && WATER_REFLECT_QUALITY < 2
          if (validRefDir) { // behind-camera guard, see validRefDir above
            #if WATER_REFLECT_QUALITY < 2 && !defined DISTANT_HORIZONS
                float verticalStretch = 0.013;
            #else
                float verticalStretch = 0.0025;
            #endif

            vec4 clipPosR = gbufferProjection * vec4(nViewPosR + verticalStretch * viewPos, 1.0);
            vec3 screenPosR = clipPosR.xyz / clipPosR.w * 0.5 + 0.5;
            vec2 screenPosRM = abs(screenPosR.xy - 0.5);

            if (screenPosRM.x < rEdge.x && screenPosRM.y < rEdge.y) {
                vec2 edgeFactor = pow2(pow2(pow2(screenPosRM / rEdge)));
                screenPosR.y += (dither - 0.5) * (0.03 * (edgeFactor.x + edgeFactor.y) + 0.004);
                float z1R = texture2D(depthtex1, screenPosR.xy).x;
                screenPosR.z = z1R;
                vec3 viewPosR = ScreenToView(screenPosR);
                float lViewPosR = length(viewPosR);

                #ifdef DISTANT_HORIZONS
                    float z1RDH = texture2D(dhDepthTex, screenPosR.xy).x;
                    vec4 screenPos1DH = vec4(screenPosR.xy, z1RDH, 1.0);
                    vec4 viewPos1DH = dhProjectionInverse * (screenPos1DH * 2.0 - 1.0);
                    viewPos1DH /= viewPos1DH.w;
                    lViewPosR = min(lViewPosR, length(viewPos1DH.xyz));
                    
                    z1R = min(z1R, z1RDH);
                #endif

                if (z1R < 0.9997) {
                    reflection.rgb = texture2D(gaux2, screenPosR.xy).rgb;
                    reflection.rgb = pow2(reflection.rgb * 2.0);

                    edgeFactor = 1.0 - edgeFactor;
                    reflection.a = edgeFactor.x * pow2(edgeFactor.y);
                    reflection.a *= clamp01((dot(nViewPos, nViewPosR) - 0.45) * 10.0); // Fixes perpendicular ref bug

                    #ifdef BORDER_FOG
                        float fog = lViewPosR / renderDistance;
                        fog = pow2(pow2(fog));
                        #ifndef DISTANT_HORIZONS
                            fog = pow2(pow2(fog));
                        #endif
                        reflection.a *= exp(-3.0 * fog);
                    #endif
                }
            }
          } // end validRefDir (Method 2)
        #endif
    #endif
    // ============================== End of Step 2 ============================== //

    // ============================== Step 3: Add Sky or WSR Reflection ============================== //
    #if defined COMPOSITE || WATER_REFLECT_QUALITY >= 1
        if (reflection.a < 1.0)
    #endif
    {
        AddBackgroundReflection(reflection, color, playerPos, normalM, normalMR, nViewPos, nViewPosR,
                                shadowMult, RVdotU, RVdotS, z0, dither, skyLightFactor, smoothness, highlightMult);
    } 
    // ============================== End of Step 3 ============================== //

    // Tweak for wsr water ref normals to look correct
    #if WORLD_SPACE_REFLECTIONS_INTERNAL > 0 && WATER_REFLECT_QUALITY >= 1
        normalM = normalMR;
    #endif

    return reflection;
}