using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using System.Runtime.CompilerServices;  // AggressiveInlining

namespace UnityEngine.Rendering.Universal
{
    /// <summary>
    /// Utility class for post processing effects.
    /// </summary>
    public static class PostProcessUtils
    {
        /// <summary>
        /// Creates a post-processing pass material from a shader. Shader is error checked for device support.
        /// </summary>
        /// <param name="shader">Shader used for the material</param>
        /// <param name="passName">Pass name for error messages.</param>
        /// <returns>A new Material instance using the provided shader. Null if the shader is not supported.</returns>
        internal static Material LoadShader(Shader shader, string passName = "")
        {
            if (shader == null)
            {
                Debug.LogError($"Missing shader (in '{passName}'). PostProcessing render passes will not execute. Check for missing reference in the Renderer and/or PostProcessData resources.");
                return null;
            }
            else if (!shader.isSupported)
            {
                Debug.LogWarning($"Shader '{shader.name}' is not supported (in '{passName}'). PostProcessing render passes will not execute.");
                return null;
            }

            return CoreUtils.CreateEngineMaterial(shader);
        }

        /// <summary>
        /// Creates a texture compatible with post-processing effects.
        /// </summary>
        /// <param name="renderGraph">RenderGraph that creates the texture.</param>
        /// <param name="source">Source texture for the texture descriptor.</param>
        /// <param name="name">Texture name.</param>
        /// <param name="clear">Texture needs to be cleared on first use.</param>
        /// <param name="filterMode">Texture filtering mode.</param>
        /// <returns>Texture compatible with post-processing effects.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static TextureHandle CreateCompatibleTexture(RenderGraph renderGraph, in TextureHandle source, string name, bool clear, FilterMode filterMode)
        {
            var desc = source.GetDescriptor(renderGraph);
            MakeCompatible(ref desc);
            desc.name = name;
            desc.clearBuffer = clear;
            desc.filterMode = filterMode;
            return renderGraph.CreateTexture(desc);
        }

        /// <summary>
        /// Creates a texture compatible with post-processing effects.
        /// </summary>
        /// <param name="renderGraph">RenderGraph that creates the texture.</param>
        /// <param name="desc">Texture descriptor.</param>
        /// <param name="name">Texture name.</param>
        /// <param name="clear">Texture needs to be cleared on first use.</param>
        /// <param name="filterMode">Texture filtering mode.</param>
        /// <returns>Texture compatible with post-processing effects.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static TextureHandle CreateCompatibleTexture(RenderGraph renderGraph, in TextureDesc desc, string name, bool clear, FilterMode filterMode)
        {
            var descCompatible = GetCompatibleDescriptor(desc);
            descCompatible.name = name;
            descCompatible.clearBuffer = clear;
            descCompatible.filterMode = filterMode;
            return renderGraph.CreateTexture(descCompatible);
        }

        /// <summary>
        /// Converts existing descriptor into a post-processing compatible descriptor.
        /// </summary>
        /// <param name="desc">Source texture descriptor.</param>
        /// <param name="width">Texture width.</param>
        /// <param name="height">Texture height.</param>
        /// <param name="format">Texture format.</param>
        /// <returns>Texture descriptor compatible with post-processing.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static TextureDesc GetCompatibleDescriptor(TextureDesc desc, int width, int height, GraphicsFormat format)
        {
            desc.width = width;
            desc.height = height;
            desc.format = format;

            MakeCompatible(ref desc);

            return desc;
        }

        /// <summary>
        /// Converts existing descriptor into a post-processing compatible descriptor.
        /// Disables MSAA, mipmaps etc.
        /// </summary>
        /// <param name="desc">Source texture descriptor.</param>
        /// <returns>Texture descriptor compatible with post-processing.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static TextureDesc GetCompatibleDescriptor(TextureDesc desc)
        {
            MakeCompatible(ref desc);

            return desc;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void MakeCompatible(ref TextureDesc desc)
        {
            desc.msaaSamples = MSAASamples.None;
            desc.useMipMap = false;
            desc.autoGenerateMips = false;
            desc.anisoLevel = 0;
            desc.discardBuffer = false;
        }

        /// <summary>
        /// Configures the blue noise dithering used.
        /// </summary>
        /// <param name="data">The <c>PostProcessData</c> resources to use.</param>
        /// <param name="index">The current array index to the Blue noise textures.</param>
        /// <param name="camera">The camera using the dithering effect.</param>
        /// <param name="material">The material used with the dithering effect.</param>
        /// <returns>The new array index to the Blue noise textures.</returns>
        [System.Obsolete("This method is obsolete. Use ConfigureDithering override that takes camera pixel width and height instead. #from(2021.1)")]
        public static int ConfigureDithering(PostProcessData data, int index, Camera camera, Material material)
        {
            return ConfigureDithering(data, index, camera.pixelWidth, camera.pixelHeight, material);
        }

        /// <summary>
        /// Configures the blue noise dithering used.
        /// </summary>
        /// <param name="data">The <c>PostProcessData</c> resources to use.</param>
        /// <param name="index">The current array index to the Blue noise textures.</param>
        /// <param name="cameraPixelWidth">The camera pixel width.</param>
        /// <param name="cameraPixelHeight">The camera pixel height.</param>
        /// <param name="material">The material used with the dithering effect.</param>
        /// <returns>The new array index to the Blue noise textures.</returns>
        public static int ConfigureDithering(PostProcessData data, int index, int cameraPixelWidth, int cameraPixelHeight, Material material)
        {
            var blueNoise = data.textures.blueNoise16LTex;

            if (blueNoise == null || blueNoise.Length == 0)
                return 0; // Safe guard

#if LWRP_DEBUG_STATIC_POSTFX // Used by QA for automated testing
            index = 0;
#else
            if (++index >= blueNoise.Length)
                index = 0;
#endif
            // Ideally we would be sending a texture array once and an index to the slice to use
            // on every frame but these aren't supported on all Universal targets
            var noiseTex = blueNoise[index];

            var tilingParams = CalcNoiseTextureTilingParams(noiseTex, cameraPixelWidth, cameraPixelHeight, GetRandomOffset2D());
            ConfigureDitheringMaterial(material, noiseTex, tilingParams);

            return index;
        }

        /// <summary>
        /// Configures the Film grain shader parameters.
        /// </summary>
        /// <param name="data">The <c>PostProcessData</c> resources to use.</param>
        /// <param name="settings">The Film Grain settings. </param>
        /// <param name="camera">The camera using the dithering effect.</param>
        /// <param name="material">The material used with the dithering effect.</param>
        [System.Obsolete("This method is obsolete. Use ConfigureFilmGrain override that takes camera pixel width and height instead. #from(2021.1)")]
        public static void ConfigureFilmGrain(PostProcessData data, FilmGrain settings, Camera camera, Material material)
        {
            ConfigureFilmGrain(data, settings, camera.pixelWidth, camera.pixelHeight, material);
        }

        /// <summary>
        /// Configures the Film grain shader parameters.
        /// </summary>
        /// <param name="data">The <c>PostProcessData</c> resources to use.</param>
        /// <param name="settings">The Film Grain settings. </param>
        /// <param name="cameraPixelWidth">The camera pixel width.</param>
        /// <param name="cameraPixelHeight">The camera pixel height.</param>
        /// <param name="material">The material used with the dithering effect.</param>
        public static void ConfigureFilmGrain(PostProcessData data, FilmGrain settings, int cameraPixelWidth, int cameraPixelHeight, Material material)
        {
            var texture = settings.texture.value;

            if (settings.type.value != FilmGrainLookup.Custom)
                texture = data.textures.filmGrainTex[(int)settings.type.value];

            var tilingParams = CalcNoiseTextureTilingParams(texture, cameraPixelWidth, cameraPixelHeight, GetRandomOffset2D());
            ConfigureFilmGrainMaterial(material, texture, new Vector2(settings.intensity.value * 4f, settings.response.value), tilingParams);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static Vector2 GetRandomOffset2D()
        {
#if LWRP_DEBUG_STATIC_POSTFX
            return Vector2.zero;
#else
            var oldState = Random.state;
            Random.InitState(Time.frameCount);
            float rndOffsetX = Random.value;
            float rndOffsetY = Random.value;
            Random.state = oldState;
            return new Vector2(rndOffsetX, rndOffsetY);
#endif
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static Vector4 CalcNoiseTextureTilingParams(Texture noiseTexture, int cameraPixelWidth, int cameraPixelHeight, Vector2 offset)
        {
            if (noiseTexture == null)
                return Vector4.zero;

            return new Vector4(cameraPixelWidth / (float)noiseTexture.width, cameraPixelHeight / (float)noiseTexture.height, offset.x, offset.y);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void ConfigureDitheringMaterial(Material material, Texture noiseTexture, Vector4 tilingParams)
        {
            material.SetTexture(ShaderConstants._BlueNoise_Texture, noiseTexture);
            material.SetVector(ShaderConstants._Dithering_Params, tilingParams);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void ConfigureFilmGrainMaterial(Material material, Texture grainTexture, Vector2 grainParams, Vector4 tilingParams)
        {
            material.SetTexture(ShaderConstants._Grain_Texture, grainTexture);
            material.SetVector(ShaderConstants._Grain_Params, grainParams);
            material.SetVector(ShaderConstants._Grain_TilingParams, tilingParams);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static bool IsFxaaEnabled(UniversalCameraData cameraData)
        {
            return (cameraData.antialiasing == AntialiasingMode.FastApproximateAntialiasing);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static bool IsFsrEnabled(UniversalCameraData cameraData)
        {
            return ((cameraData.imageScalingMode == ImageScalingMode.Upscaling) && (cameraData.upscalingFilter == ImageUpscalingFilter.FSR));
        }

        // TODO: Should these methods be in UniversalCameraData?
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static bool IsTaaSharpeningEnabled(UniversalCameraData cameraData)
        {
            return (cameraData.IsTemporalAAEnabled() && cameraData.taaSettings.contrastAdaptiveSharpening > 0.0f) && !IsFsrEnabled(cameraData) && !cameraData.IsSTPEnabled() &&
#if ENABLE_UPSCALER_FRAMEWORK
                cameraData.upscalingFilter != ImageUpscalingFilter.IUpscaler
#else
                true
#endif
                ;
        }

#region HDR Output

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static bool RequireHDROutput(UniversalCameraData cameraData)
        {
            // If capturing, don't convert to HDR.
            // If not last in the stack, don't convert to HDR.
            return cameraData.isHDROutputActive && cameraData.captureActions == null;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void SetupHDROutput(Material material, HDROutputUtils.HDRDisplayInformation hdrDisplayInformation, ColorGamut hdrDisplayColorGamut, Tonemapping tonemapping, HDROutputUtils.Operation hdrOperations, bool rendersOverlayUI)
        {
            Vector4 hdrOutputLuminanceParams;
            UniversalRenderPipeline.GetHDROutputLuminanceParameters(hdrDisplayInformation, hdrDisplayColorGamut, tonemapping, out hdrOutputLuminanceParams);
            material.SetVector(ShaderPropertyId.hdrOutputLuminanceParams, hdrOutputLuminanceParams);

            HDROutputUtils.ConfigureHDROutput(material, hdrDisplayColorGamut, hdrOperations);
            CoreUtils.SetKeyword(material, ShaderKeywordStrings.HDROverlay, rendersOverlayUI);
        }
#endregion

#region Blit

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static Vector4 CalcShaderSourceSize(float width, float height, RenderTexture rt)
        {
            if (rt != null && rt.useDynamicScale)
            {
                width *= ScalableBufferManager.widthScaleFactor;
                height *= ScalableBufferManager.heightScaleFactor;
            }
            return new Vector4(width, height, 1.0f / width, 1.0f / height);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static Vector4 CalcShaderSourceSize(RTHandle source)
        {
            return CalcShaderSourceSize(source.rt.width, source.rt.height, source.rt);
        }

        internal static void SetGlobalShaderSourceSize(RasterCommandBuffer cmd, float width, float height, RenderTexture rt)
        {
            cmd.SetGlobalVector(ShaderConstants._SourceSize, CalcShaderSourceSize(width, height, rt));
        }

        internal static void SetGlobalShaderSourceSize(CommandBuffer cmd, float width, float height, RenderTexture rt)
        {
            SetGlobalShaderSourceSize(CommandBufferHelpers.GetRasterCommandBuffer(cmd), width, height, rt);
        }

        internal static void SetGlobalShaderSourceSize(RasterCommandBuffer cmd, RTHandle source)
        {
            SetGlobalShaderSourceSize(cmd, source.rt.width, source.rt.height, source.rt);
        }

        internal static void SetGlobalShaderSourceSize(CommandBuffer cmd, RTHandle source)
        {
            SetGlobalShaderSourceSize(CommandBufferHelpers.GetRasterCommandBuffer(cmd), source);
        }

        internal static void ScaleViewport(RasterCommandBuffer cmd, RTHandle dest, UniversalCameraData cameraData, bool isFinalPass)
        {
            RenderTargetIdentifier cameraTarget = BuiltinRenderTextureType.CameraTarget;
#if ENABLE_VR && ENABLE_XR_MODULE
            if (cameraData.xr.enabled)
                cameraTarget = cameraData.xr.renderTarget;
#endif
            if (dest.nameID == cameraTarget || cameraData.targetTexture != null)
            {
                if (!isFinalPass || !cameraData.resolveFinalTarget)
                {
                    // Inside the camera stack the target is the shared intermediate target, which can be scaled with render scale.
                    // camera.pixelRect is the viewport of the final target in pixels, so it cannot be used for the intermediate target.
                    // On intermediate target allocation the viewport size is baked into the target size.
                    // Which means the intermediate target does not have a viewport rect. Its offset is always 0 and its size matches viewport size.
                    // The overlay cameras inherit the base viewport, so they cannot have a different viewport,
                    // a necessary limitation since the target covers only the base viewport area.
                    // The offsetting is finally done by the final output viewport-rect to the final target.
                    // Note: effectively this is setting a fullscreen viewport for the intermediate target.
                    var targetWidth = cameraData.cameraTargetDescriptor.width;
                    var targetHeight = cameraData.cameraTargetDescriptor.height;
                    var targetViewportInPixels = new Rect(
                        0,
                        0,
                        targetWidth,
                        targetHeight);
                    cmd.SetViewport(targetViewportInPixels);
                }
                else
                    cmd.SetViewport(cameraData.pixelRect);
            }
        }

        internal static void ScaleViewportAndBlit(RasterGraphContext context, in TextureHandle sourceTexture, in TextureHandle destTexture, UniversalCameraData cameraData, Material material, bool isFinalPass)
        {
            Vector4 scaleBias = RenderingUtils.GetFinalBlitScaleBias(context, sourceTexture, destTexture);
            ScaleViewport(context.cmd, destTexture, cameraData, isFinalPass);

            Blitter.BlitTexture(context.cmd, sourceTexture, scaleBias, material, 0);
        }

        internal static void ScaleViewportAndDrawVisibilityMesh(RasterGraphContext context, in TextureHandle sourceTexture, in TextureHandle destTexture, UniversalCameraData cameraData, Material material, bool isFinalPass)
        {
#if ENABLE_VR && ENABLE_XR_MODULE
            Vector4 scaleBias = RenderingUtils.GetFinalBlitScaleBias(context, sourceTexture, destTexture);
            ScaleViewport(context.cmd, destTexture, cameraData, isFinalPass);

            // Set property block for blit shader
            MaterialPropertyBlock xrPropertyBlock = XRSystemUniversal.GetMaterialPropertyBlock();
            xrPropertyBlock.SetVector(Shader.PropertyToID("_BlitScaleBias"), scaleBias);
            xrPropertyBlock.SetTexture(Shader.PropertyToID("_BlitTexture"), sourceTexture);
            cameraData.xr.RenderVisibleMeshCustomMaterial(context.cmd, cameraData.xr.occlusionMeshScale, material, xrPropertyBlock, 1, context.GetTextureUVOrigin(in sourceTexture) == context.GetTextureUVOrigin(in destTexture));
#endif
        }
#endregion

        // Precomputed shader ids to same some CPU cycles (mostly affects mobile)
        static class ShaderConstants
        {
            public static readonly int _Grain_Texture = Shader.PropertyToID("_Grain_Texture");
            public static readonly int _Grain_Params = Shader.PropertyToID("_Grain_Params");
            public static readonly int _Grain_TilingParams = Shader.PropertyToID("_Grain_TilingParams");

            public static readonly int _BlueNoise_Texture = Shader.PropertyToID("_BlueNoise_Texture");
            public static readonly int _Dithering_Params = Shader.PropertyToID("_Dithering_Params");

            public static readonly int _SourceSize = Shader.PropertyToID("_SourceSize");
        }
    }
}