using UnityEngine.Rendering;

namespace UnityEngine.PostProcessing
{
    using DebugMode = BuiltinDebugViewsModel.Mode;

    public sealed class AmbientOcclusionComponent : PostProcessingComponentCommandBuffer<AmbientOcclusionModel>
    {
        static class Uniforms
        {
            internal static readonly int _Intensity         = Shader.PropertyToID("_Intensity");
            internal static readonly int _Radius            = Shader.PropertyToID("_Radius");
            internal static readonly int _FogParams         = Shader.PropertyToID("_FogParams");
            internal static readonly int _Downsample        = Shader.PropertyToID("_Downsample");
            internal static readonly int _SampleCount       = Shader.PropertyToID("_SampleCount");
            internal static readonly int _OcclusionTexture1 = Shader.PropertyToID("_OcclusionTexture1");
            internal static readonly int _OcclusionTexture2 = Shader.PropertyToID("_OcclusionTexture2");
            internal static readonly int _OcclusionTexture  = Shader.PropertyToID("_OcclusionTexture");
            internal static readonly int _MainTex           = Shader.PropertyToID("_MainTex");
            internal static readonly int _TempRT            = Shader.PropertyToID("_TempRT");
        }

        const string k_BlitShaderString = "Hidden/Post FX/Blit";
        const string k_ShaderString = "Hidden/Post FX/Ambient Occlusion";

        readonly RenderTargetIdentifier[] m_MRT =
        {
            BuiltinRenderTextureType.GBuffer0, // Albedo, Occ
            BuiltinRenderTextureType.CameraTarget // Ambient
        };

        enum OcclusionSource
        {
            DepthTexture,
            DepthNormalsTexture,
            GBuffer
        }

        OcclusionSource occlusionSource
        {
            get
            {
                if (context.isGBufferAvailable && !model.settings.forceForwardCompatibility)
                    return OcclusionSource.GBuffer;

                if (model.settings.highPrecision && (!context.isGBufferAvailable || model.settings.forceForwardCompatibility))
                    return OcclusionSource.DepthTexture;

                return OcclusionSource.DepthNormalsTexture;
            }
        }

        bool ambientOnlySupported
        {
            get { return context.isHdr && model.settings.ambientOnly && context.isGBufferAvailable && !model.settings.forceForwardCompatibility; }
        }

        public override bool active
        {
            get
            {
                return model.enabled
                       && model.settings.intensity > 0f
                       && !context.interrupted;
            }
        }

        public override DepthTextureMode GetCameraFlags()
        {
            var flags = DepthTextureMode.None;

            if (occlusionSource == OcclusionSource.DepthTexture)
                flags |= DepthTextureMode.Depth;

            if (occlusionSource != OcclusionSource.GBuffer)
                flags |= DepthTextureMode.DepthNormals;

            return flags;
        }

        public override string GetName()
        {
            return "Ambient Occlusion";
        }

        public override CameraEvent GetCameraEvent()
        {
            return ambientOnlySupported && !context.profile.debugViews.IsModeActive(DebugMode.AmbientOcclusion)
                   ? CameraEvent.BeforeReflections
                   : CameraEvent.BeforeImageEffectsOpaque;
        }

        public override void PopulateCommandBuffer(CommandBuffer cb)
        {
            var settings = model.settings;

            // Material setup
            var blitMaterial = context.materialFactory.Get(k_BlitShaderString);

            var material = context.materialFactory.Get(k_ShaderString);
            material.shaderKeywords = null;
            material.SetFloat(Uniforms._Intensity, settings.intensity);
            material.SetFloat(Uniforms._Radius, settings.radius);
            material.SetFloat(Uniforms._Downsample, settings.downsampling ? 0.5f : 1f);
            material.SetInt(Uniforms._SampleCount, (int)settings.sampleCount);

            if (!context.isGBufferAvailable && RenderSettings.fog)
            {
                material.SetVector(Uniforms._FogParams, new Vector3(RenderSettings.fogDensity, RenderSettings.fogStartDistance, RenderSettings.fogEndDistance));

                switch (RenderSettings.fogMode)
                {
                    case FogMode.Linear:
                        material.EnableKeyword("FOG_LINEAR");
                        break;
                    case FogMode.Exponential:
                        material.EnableKeyword("FOG_EXP");
                        break;
                    case FogMode.ExponentialSquared:
                        material.EnableKeyword("FOG_EXP2");
                        break;
                }
            }
            else
            {
                material.EnableKeyword("FOG_OFF");
            }

            int tw = context.width;
            int th = context.height;
            int ts = settings.downsampling ? 2 : 1;
            const RenderTextureFormat kFormat = RenderTextureFormat.ARGB32;
            const RenderTextureReadWrite kRWMode = RenderTextureReadWrite.Linear;
            const FilterMode kFilter = FilterMode.Bilinear;

            // AO buffer
            var rtMask = Uniforms._OcclusionTexture1;
            cb.GetTemporaryRT(rtMask, tw / ts, th / ts, 0, kFilter, kFormat, kRWMode);

            // AO estimation
            cb.Blit((Texture)null, rtMask, material, (int)occlusionSource);

            // Blur buffer
            var rtBlur = Uniforms._OcclusionTexture2;

            // Separable blur (horizontal pass)
            cb.GetTemporaryRT(rtBlur, tw, th, 0, kFilter, kFormat, kRWMode);
            cb.SetGlobalTexture(Uniforms._MainTex, rtMask);
            cb.Blit(rtMask, rtBlur, material, occlusionSource == OcclusionSource.GBuffer ? 4 : 3);
            cb.ReleaseTemporaryRT(rtMask);

            // Separable blur (vertical pass)
            rtMask = Uniforms._OcclusionTexture;
            cb.GetTemporaryRT(rtMask, tw, th, 0, kFilter, kFormat, kRWMode);
            cb.SetGlobalTexture(Uniforms._MainTex, rtBlur);
            cb.Blit(rtBlur, rtMask, material, 5);
            cb.ReleaseTemporaryRT(rtBlur);

            if (context.profile.debugViews.IsModeActive(DebugMode.AmbientOcclusion))
            {
                cb.SetGlobalTexture(Uniforms._MainTex, rtMask);
                cb.Blit(rtMask, BuiltinRenderTextureType.CameraTarget, material, 8);
                context.Interrupt();
            }
            else if (ambientOnlySupported)
            {
                cb.SetRenderTarget(m_MRT, BuiltinRenderTextureType.CameraTarget);
                cb.DrawMesh(GraphicsUtils.quad, Matrix4x4.identity, material, 0, 7);
            }
            else
            {
                var fbFormat = context.isHdr ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default;

                int tempRT = Uniforms._TempRT;
                cb.GetTemporaryRT(tempRT, context.width, context.height, 0, FilterMode.Bilinear, fbFormat);
                cb.Blit(BuiltinRenderTextureType.CameraTarget, tempRT, blitMaterial, 0);
                cb.SetGlobalTexture(Uniforms._MainTex, tempRT);
                cb.Blit(tempRT, BuiltinRenderTextureType.CameraTarget, material, 6);
                cb.ReleaseTemporaryRT(tempRT);
            }

            cb.ReleaseTemporaryRT(rtMask);
        }
    }
}
