using System;

namespace UnityEngine.Rendering.PostProcessing
{
    // Scalable ambient obscurance
    [UnityEngine.Scripting.Preserve]
    [Serializable]
    internal sealed class ScalableAO : IAmbientOcclusionMethod
    {
        RenderTexture m_Result;
        PropertySheet m_PropertySheet;
        AmbientOcclusion m_Settings;

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

        readonly int[] m_SampleCount = { 4, 6, 10, 8, 12 };

        enum Pass
        {
            OcclusionEstimationForward,
            OcclusionEstimationDeferred,
            HorizontalBlurForward,
            HorizontalBlurDeferred,
            VerticalBlur,
            CompositionForward,
            CompositionDeferred,
            DebugOverlay
        }

        public ScalableAO(AmbientOcclusion settings)
        {
            m_Settings = settings;
        }

        public DepthTextureMode GetCameraFlags()
        {
            return DepthTextureMode.Depth | DepthTextureMode.DepthNormals;
        }

        void DoLazyInitialization(PostProcessRenderContext context)
        {
            m_PropertySheet = context.propertySheets.Get(context.resources.shaders.scalableAO);

            bool reset = false;

            if (m_Result == null || !m_Result.IsCreated())
            {
                // Initial allocation
                m_Result = context.GetScreenSpaceTemporaryRT(0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
                m_Result.hideFlags = HideFlags.DontSave;
                m_Result.filterMode = FilterMode.Bilinear;

                reset = true;
            }
            else if (m_Result.width != context.width || m_Result.height != context.height)
            {
                // Release and reallocate
                m_Result.Release();
                m_Result.width = context.width;
                m_Result.height = context.height;
                reset = true;
            }

            if (reset)
                m_Result.Create();
        }

        void Render(PostProcessRenderContext context, CommandBuffer cmd, int occlusionSource)
        {
            DoLazyInitialization(context);
            m_Settings.radius.value = Mathf.Max(m_Settings.radius.value, 1e-4f);

            // Material setup
            // Always use a quater-res AO buffer unless High/Ultra quality is set.
            bool downsampling = (int)m_Settings.quality.value < (int)AmbientOcclusionQuality.High;
            float px = m_Settings.intensity.value;
            float py = m_Settings.radius.value;
            float pz = downsampling ? 0.5f : 1f;
            float pw = m_SampleCount[(int)m_Settings.quality.value];

            var sheet = m_PropertySheet;
            sheet.ClearKeywords();
            sheet.properties.SetVector(ShaderIDs.AOParams, new Vector4(px, py, pz, pw));
            sheet.properties.SetVector(ShaderIDs.AOColor, Color.white - m_Settings.color.value);

            // In forward fog is applied at the object level in the grometry pass so we need to
            // apply it to AO as well or it'll drawn on top of the fog effect.
            // Not needed in Deferred.
            if (context.camera.actualRenderingPath == RenderingPath.Forward && RenderSettings.fog)
            {
                sheet.EnableKeyword("APPLY_FORWARD_FOG");
                sheet.properties.SetVector(
                    ShaderIDs.FogParams,
                    new Vector3(RenderSettings.fogDensity, RenderSettings.fogStartDistance, RenderSettings.fogEndDistance)
                );
            }

            // Texture setup
            int ts = downsampling ? 2 : 1;
            const RenderTextureFormat kFormat = RenderTextureFormat.ARGB32;
            const RenderTextureReadWrite kRWMode = RenderTextureReadWrite.Linear;
            const FilterMode kFilter = FilterMode.Bilinear;

            // AO buffer
            var rtMask = ShaderIDs.OcclusionTexture1;
            int scaledWidth = context.width / ts;
            int scaledHeight = context.height / ts;
            context.GetScreenSpaceTemporaryRT(cmd, rtMask, 0, kFormat, kRWMode, kFilter, scaledWidth, scaledHeight);

            // AO estimation
            cmd.BlitFullscreenTriangle(BuiltinRenderTextureType.None, rtMask, sheet, (int)Pass.OcclusionEstimationForward + occlusionSource);

            // Blur buffer
            var rtBlur = ShaderIDs.OcclusionTexture2;
            context.GetScreenSpaceTemporaryRT(cmd, rtBlur, 0, kFormat, kRWMode, kFilter);

            // Separable blur (horizontal pass)
            cmd.BlitFullscreenTriangle(rtMask, rtBlur, sheet, (int)Pass.HorizontalBlurForward + occlusionSource);
            cmd.ReleaseTemporaryRT(rtMask);

            // Separable blur (vertical pass)
            cmd.BlitFullscreenTriangle(rtBlur, m_Result, sheet, (int)Pass.VerticalBlur);
            cmd.ReleaseTemporaryRT(rtBlur);

            if (context.IsDebugOverlayEnabled(DebugOverlay.AmbientOcclusion))
                context.PushDebugOverlay(cmd, m_Result, sheet, (int)Pass.DebugOverlay);
        }

        public void RenderAfterOpaque(PostProcessRenderContext context)
        {
            var cmd = context.command;
            cmd.BeginSample("Ambient Occlusion");
            Render(context, cmd, 0);
            cmd.SetGlobalTexture(ShaderIDs.SAOcclusionTexture, m_Result);
            cmd.BlitFullscreenTriangle(BuiltinRenderTextureType.None, BuiltinRenderTextureType.CameraTarget, m_PropertySheet, (int)Pass.CompositionForward, RenderBufferLoadAction.Load);
            cmd.EndSample("Ambient Occlusion");
        }

        public void RenderAmbientOnly(PostProcessRenderContext context)
        {
            var cmd = context.command;
            cmd.BeginSample("Ambient Occlusion Render");
            Render(context, cmd, 1);
            cmd.EndSample("Ambient Occlusion Render");
        }

        public void CompositeAmbientOnly(PostProcessRenderContext context)
        {
            var cmd = context.command;
            cmd.BeginSample("Ambient Occlusion Composite");
            cmd.SetGlobalTexture(ShaderIDs.SAOcclusionTexture, m_Result);
            cmd.BlitFullscreenTriangle(BuiltinRenderTextureType.None, m_MRT, BuiltinRenderTextureType.CameraTarget, m_PropertySheet, (int)Pass.CompositionDeferred);
            cmd.EndSample("Ambient Occlusion Composite");
        }

        public void Release()
        {
            RuntimeUtilities.Destroy(m_Result);
            m_Result = null;
        }
    }
}
