using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class UniqueShadowFeature : ScriptableRendererFeature
{
    class UniqueShadowPass : ScriptableRenderPass
    {
        static int spWorldToShadow;
        static int spShadowSize;
        const string profilerTag = "UniqueShadowMap";
        const float normalBiasForCharacter = 0.05f;
        const int shadowMapRes = 512;
        const int shadowMapBit = 16;
        RenderTargetHandle uniqueShadowHandle;
        RenderTexture uniqueShadowRT;

        public UniqueShadowPass()
        {
            uniqueShadowHandle.Init("_UniqueShadowmapTexture");

            spWorldToShadow = Shader.PropertyToID("_WorldToUniqueShadow");
            spShadowSize = Shader.PropertyToID("_UniqueShadowParams");
        }

        // This method is called before executing the render pass.
        // It can be used to configure render targets and their clear state. Also to create temporary render target textures.
        // When empty this render pass will render to the active camera render target.
        // You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
        // The render pipeline will ensure target setup and clearing happens in an performance manner.
        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            uniqueShadowRT = ShadowUtils.GetTemporaryShadowTexture(shadowMapRes, shadowMapRes, shadowMapBit);
            ConfigureTarget(new RenderTargetIdentifier(uniqueShadowRT));
            ConfigureClear(ClearFlag.All, Color.black);
        }

        // Here you can implement the rendering logic.
        // Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
        // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
        // You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get(profilerTag);

            int shadowLightIndex = renderingData.lightData.mainLightIndex;
            if (shadowLightIndex == -1)
                return;
            VisibleLight shadowLight = renderingData.lightData.visibleLights[shadowLightIndex];

            UniqueShadowManager.Instance.ExtractLightMatrix(shadowLight.light, out Matrix4x4 view, out Matrix4x4 proj, out Matrix4x4 shadowTransform);
            //check if we have work to do
            if (UniqueShadowManager.Instance.HasNoRendererJob())
            {
                CoreUtils.SetKeyword(cmd, "_UNIQUE_SHADOW", false);
                context.ExecuteCommandBuffer(cmd);
                CommandBufferPool.Release(cmd);
                return;
            }
            CoreUtils.SetKeyword(cmd, "_UNIQUE_SHADOW", true);

            Vector4 shadowBias = GetShadowBias(ref shadowLight, shadowLightIndex, ref renderingData.shadowData, proj, shadowMapRes);

            //set shader CBs
            ShadowUtils.SetupShadowCasterConstantBuffer(cmd, ref shadowLight, shadowBias);

            cmd.SetViewport(new Rect(0, 0, shadowMapRes, shadowMapRes));
            cmd.EnableScissorRect(new Rect(4, 4, shadowMapRes - 8, shadowMapRes - 8));
            cmd.SetViewProjectionMatrices(view, proj);
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            //draw all parts
            UniqueShadowManager.Instance.PopulateCommondListForUniqueShadowmap(cmd);

            cmd.DisableScissorRect();

            SetUniqueShadowReceiverConstants(cmd, ref shadowTransform);

            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }

        /// Cleanup any allocated resources that were created during the execution of this render pass.
        public override void FrameCleanup(CommandBuffer cmd)
        {
            if (cmd == null)
                throw new System.ArgumentNullException("");
            if (uniqueShadowRT)
            {
                RenderTexture.ReleaseTemporary(uniqueShadowRT);
                uniqueShadowRT = null;
            }
        }

        void SetUniqueShadowReceiverConstants(CommandBuffer cmd, ref Matrix4x4 shadowTransform)
        {
            cmd.SetGlobalTexture(uniqueShadowHandle.id, uniqueShadowRT);
            cmd.SetGlobalMatrix(spWorldToShadow, shadowTransform);
            float texelWidth = 1.0f / shadowMapRes;
            cmd.SetGlobalVector(spShadowSize, new Vector4(texelWidth, texelWidth, shadowMapRes, shadowMapRes));
        }

        Vector4 GetShadowBias(ref VisibleLight shadowLight, int shadowLightIndex, ref ShadowData shadowData, Matrix4x4 lightProjectionMatrix, float shadowResolution)
        {
            if (shadowLightIndex < 0 || shadowLightIndex >= shadowData.bias.Count)
            {
                Debug.LogWarning(string.Format("{0} is not a valid light index.", shadowLightIndex));
                return Vector4.zero;
            }

            float frustumSize;
            if (shadowLight.lightType == LightType.Directional)
            {
                // Frustum size is guaranteed to be a cube as we wrap shadow frustum around a sphere
                frustumSize = 2.0f / lightProjectionMatrix.m00;
            }
            else if (shadowLight.lightType == LightType.Spot)
            {
                // For perspective projections, shadow texel size varies with depth
                // It will only work well if done in receiver side in the pixel shader. Currently UniversalRP
                // do bias on caster side in vertex shader. When we add shader quality tiers we can properly
                // handle this. For now, as a poor approximation we do a constant bias and compute the size of
                // the frustum as if it was orthogonal considering the size at mid point between near and far planes.
                // Depending on how big the light range is, it will be good enough with some tweaks in bias
                frustumSize = Mathf.Tan(shadowLight.spotAngle * 0.5f * Mathf.Deg2Rad) * shadowLight.range;
            }
            else
            {
                Debug.LogWarning("Only spot and directional shadow casters are supported in universal pipeline");
                frustumSize = 0.0f;
            }

            // depth and normal bias scale is in shadowmap texel size in world space
            float texelSize = frustumSize / shadowResolution;
            float depthBias = -shadowData.bias[shadowLightIndex].x * texelSize;
            float normalBias = -normalBiasForCharacter * texelSize;

            if (shadowData.supportsSoftShadows)
            {
                // TODO: depth and normal bias assume sample is no more than 1 texel away from shadowmap
                // This is not true with PCF. Ideally we need to do either
                // cone base bias (based on distance to center sample)
                // or receiver place bias based on derivatives.
                // For now we scale it by the PCF kernel size (5x5)
                const float kernelRadius = 2.5f;
                depthBias *= kernelRadius;
                normalBias *= kernelRadius;
            }

            return new Vector4(depthBias, normalBias, 0.0f, 0.0f);
        }
    }

    UniqueShadowPass m_ScriptablePass;

    public override void Create()
    {
        m_ScriptablePass = new UniqueShadowPass();

        // Configures where the render pass should be injected.
        m_ScriptablePass.renderPassEvent = RenderPassEvent.BeforeRenderingShadows;
    }

    // Here you can inject one or multiple render passes in the renderer.
    // This method is called when setting up the renderer once per-camera.
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(m_ScriptablePass);
    }
}


