using UnityEngine;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering.Universal.Internal;

/// <summary>
/// Renders the on-tile post-processing stack.
/// </summary>
public class OnTilePostProcessPass : ScriptableRenderPass
{
    /// <summary>
    /// The override shader to use.
    /// </summary>
    internal bool m_UseMultisampleShaderResolve = false;
    internal bool m_UseTextureReadFallback = false;
    
    RTHandle m_UserLut;
    Material m_OnTileUberMaterial;
    static readonly int s_BlitScaleBias = Shader.PropertyToID("_BlitScaleBias");
    static readonly int s_BlitTexture = Shader.PropertyToID("_BlitTexture");
    int m_DitheringTextureIndex;
    PostProcessData m_PostProcessData;

    const string m_PassName = "On Tile Post Processing";
    const string m_FallbackPassName = "On Tile Post Processing (sampling fallback) ";

    internal OnTilePostProcessPass(PostProcessData postProcessData)
    {
        m_PostProcessData = postProcessData;
#if ENABLE_VR && ENABLE_XR_MODULE
        m_UseMultisampleShaderResolve = SystemInfo.supportsMultisampledShaderResolve;
#endif
    }

    internal void Setup(ref Material onTileUberMaterial)
    {
        m_OnTileUberMaterial = onTileUberMaterial;
    }

    /// <summary>
    /// Disposes used resources.
    /// </summary>
    public void Dispose()
    {
        m_UserLut?.Release();
        CoreUtils.Destroy(m_OnTileUberMaterial);
    }

    /// <inheritdoc cref="IRenderGraphRecorder.RecordRenderGraph"/>
    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        if (m_OnTileUberMaterial == null) return;

        var resourceData = frameData.Get<UniversalResourceData>();
        var renderingData = frameData.Get<UniversalRenderingData>();
        var cameraData = frameData.Get<UniversalCameraData>();
        var postProcessingData = frameData.Get<UniversalPostProcessingData>();

        if (SystemInfo.graphicsShaderLevel < 30)
        {
            Debug.LogError("DrawProcedural is required for the On-Tile post processing feature but it is not supported by the platform. Pass will not execute.");
            return;
        }

        int lutSize = postProcessingData.lutSize;

        var stack = VolumeManager.instance.stack;
        var vignette = stack.GetComponent<Vignette>();
        var colorLookup = stack.GetComponent<ColorLookup>();
        var colorAdjustments = stack.GetComponent<ColorAdjustments>();
        var tonemapping = stack.GetComponent<Tonemapping>();
        var filmgrain = stack.GetComponent<FilmGrain>();

#if ENABLE_VR && ENABLE_XR_MODULE
        bool useVisibilityMesh = cameraData.xr.enabled && cameraData.xr.hasValidVisibleMesh;
#else
        const bool useVisibilityMesh = false;
#endif

        TextureHandle source = resourceData.activeColorTexture;
        TextureDesc srcDesc = renderGraph.GetTextureDesc(source); ;
        
        TextureHandle destination = resourceData.backBufferColor;

        SetupVignette(m_OnTileUberMaterial, cameraData.xr, srcDesc.width, srcDesc.height, vignette);
        SetupLut(m_OnTileUberMaterial, colorLookup, colorAdjustments, lutSize);
        SetupTonemapping(m_OnTileUberMaterial, tonemapping, isHdrGrading: postProcessingData.gradingMode == ColorGradingMode.HighDynamicRange);
        SetupGrain(m_OnTileUberMaterial, cameraData, filmgrain, m_PostProcessData);
        SetupDithering(m_OnTileUberMaterial, cameraData, m_PostProcessData);

        CoreUtils.SetKeyword(m_OnTileUberMaterial, ShaderKeywordStrings.LinearToSRGBConversion, cameraData.requireSrgbConversion);
        CoreUtils.SetKeyword(m_OnTileUberMaterial, ShaderKeywordStrings.UseFastSRGBLinearConversion, postProcessingData.useFastSRGBLinearConversion);
        CoreUtils.SetKeyword(m_OnTileUberMaterial, ShaderKeywordStrings._ENABLE_ALPHA_OUTPUT, cameraData.isAlphaOutputEnabled);

        UberShaderPasses shaderPass = useVisibilityMesh ? UberShaderPasses.NormalVisMesh : UberShaderPasses.Normal;
#if ENABLE_VR && ENABLE_XR_MODULE
        bool setMultisamplesShaderResolveFeatureFlag = false;
#endif
        if (srcDesc.msaaSamples != MSAASamples.None)
        {
            if (srcDesc.msaaSamples == MSAASamples.MSAA8x)
            {
                Debug.LogError("MSAA8x is enabled in Universal Render Pipeline Asset but it is not supported by the on-tile post-processing feature yet. Please use MSAA4x or MSAA2x instead.");
                return;
            }

            var destInfo = renderGraph.GetRenderTargetInfo(destination);

#if ENABLE_VR && ENABLE_XR_MODULE
            if (m_UseMultisampleShaderResolve)
            {
                // If we have support for msaa shader resolve we can do MSAA -> non MSAA in our render pass.
                shaderPass = useVisibilityMesh ? UberShaderPasses.MSAASoftwareResolveVisMesh : UberShaderPasses.MSAASoftwareResolve;

                // When rendering into the backbuffer, we could enable the shader resolve extension to resolve into the msaa1x surface directly on platforms that support auto resolve.
                // For platforms that don't support auto resolve, the backbuffer is a multisampled surface and we don't need to enable the extension. This is to maximize the pass merging because shader resolve enabled pass has to be the last subpass.
                if (SystemInfo.supportsMultisampleAutoResolve)
                {

                    setMultisamplesShaderResolveFeatureFlag = true;
                }
            }
            else
#endif
            {
                if (destInfo.msaaSamples == (int)srcDesc.msaaSamples)
                {
                    // If we have MSAA -> MSAA we can still resolve in the shader running at fragmnet rate
                    // and the hardware resolve will sometimes be optimized as a result.
                    shaderPass = useVisibilityMesh ? UberShaderPasses.MSAASoftwareResolveVisMesh : UberShaderPasses.MSAASoftwareResolve;
                }
                else
                {
                    // We are going MSAA -> Non MSAA without shader resolve support which is not a valid render pass
                    // configuration. So we need to force a resolve before running our shader which will cause us not
                    // to be on tile anymore.
                    shaderPass = useVisibilityMesh ? UberShaderPasses.TextureReadVisMesh : UberShaderPasses.TextureRead;
                }
            }
        }

        // Fallback to texture read mode when requested.
        if (m_UseTextureReadFallback)
        {
            shaderPass = useVisibilityMesh ? UberShaderPasses.TextureReadVisMesh : UberShaderPasses.TextureRead;
#if ENABLE_VR && ENABLE_XR_MODULE
            setMultisamplesShaderResolveFeatureFlag = false;
#endif
        }

        // Fallback logic to handle the case where Frame Buffer Fetch is not compatible with the pass setup.
        TextureDesc destDesc;
        var info = renderGraph.GetRenderTargetInfo(destination);
        destDesc = new TextureDesc(info.width, info.height);
        destDesc.format = info.format;
        destDesc.msaaSamples = (MSAASamples)info.msaaSamples;
        destDesc.bindTextureMS = info.bindMS;
        destDesc.slices = info.volumeDepth;
        destDesc.dimension = info.volumeDepth > 1 ? TextureDimension.Tex2DArray : TextureDimension.Tex2D;

        // Falls back to texture read mode if texture dimension does not match between source and destination (invalid frame buffer fetch setup). 
        if (srcDesc.width != destDesc.width || srcDesc.height != destDesc.height || srcDesc.slices != destDesc.slices)
        {
            shaderPass = useVisibilityMesh ? UberShaderPasses.TextureReadVisMesh : UberShaderPasses.TextureRead;
#if ENABLE_VR && ENABLE_XR_MODULE
            setMultisamplesShaderResolveFeatureFlag = false;
#endif
        }

        var lutTexture = resourceData.internalColorLut;
        var passName = m_UseTextureReadFallback ? m_FallbackPassName : m_PassName;
        using (var builder = renderGraph.AddRasterRenderPass<PassData>(passName, out var passData))
        {
            passData.source = source;
            passData.destination = destination;
            passData.material = m_OnTileUberMaterial;
            passData.shaderPass = shaderPass;

            if (shaderPass == UberShaderPasses.TextureRead || shaderPass == UberShaderPasses.TextureReadVisMesh)
            {
                builder.UseTexture(source, AccessFlags.Read);
            }
            else
            {
                builder.SetInputAttachment(source, 0);
                // MSAA shader resolve keywords require global state modification
                builder.AllowGlobalStateModification(true);
            }

            builder.UseTexture(lutTexture, AccessFlags.Read);
            passData.lutTexture = lutTexture;

            var userLutTexture = TryGetCachedUserLutTextureHandle(colorLookup, renderGraph);
            passData.userLutTexture = userLutTexture;
            if (userLutTexture.IsValid())
            {
                builder.UseTexture(userLutTexture, AccessFlags.Read);
            }

            builder.SetRenderAttachment(destination, 0, AccessFlags.WriteAll);
            builder.SetRenderFunc(static (PassData data, RasterGraphContext context) => ExecuteFBFetchPass(data, context));

            passData.useXRVisibilityMesh = false;
            passData.msaaSamples = (int)srcDesc.msaaSamples;

#if ENABLE_VR && ENABLE_XR_MODULE
            if (cameraData.xr.enabled)
            {
                ExtendedFeatureFlags xrFeatureFlag = ExtendedFeatureFlags.MultiviewRenderRegionsCompatible;
                if (setMultisamplesShaderResolveFeatureFlag)
                {
                    xrFeatureFlag |= ExtendedFeatureFlags.MultisampledShaderResolve;
                }
                builder.SetExtendedFeatureFlags(xrFeatureFlag);

                // We want our foveation logic to match other geometry passes(eg. Opaque, Transparent, Skybox) because we want to merge with previous passes.
                bool passSupportsFoveation = cameraData.xrUniversal.canFoveateIntermediatePasses || resourceData.isActiveTargetBackBuffer;
                builder.EnableFoveatedRasterization(
                    cameraData.xr.supportsFoveatedRendering && passSupportsFoveation);

                passData.useXRVisibilityMesh = useVisibilityMesh;
                passData.xr = cameraData.xr; // Need to pass this down for the method call RenderVisibleMeshCustomMaterial()
            }
#endif
        }

        //This will prevent the final blit pass from being added/needed (still internal API in trunk)
        resourceData.activeColorID = UniversalResourceData.ActiveID.BackBuffer;
        resourceData.activeDepthID = UniversalResourceData.ActiveID.BackBuffer;
    }

    enum UberShaderPasses
    {
        Normal,
        MSAASoftwareResolve,
        TextureRead,
        NormalVisMesh,
        MSAASoftwareResolveVisMesh,
        TextureReadVisMesh,
    };

    // This static method is used to execute the pass and passed as the RenderFunc delegate to the RenderGraph render pass
    static void ExecuteFBFetchPass(PassData data, RasterGraphContext context)
    {
        var cmd = context.cmd;

        data.material.SetTexture(ShaderConstants._InternalLut, data.lutTexture);
        if (data.userLutTexture.IsValid())
            data.material.SetTexture(ShaderConstants._UserLut, data.userLutTexture);

        bool IsHandleYFlipped = RenderingUtils.IsHandleYFlipped(context, in data.destination); 
        // We need to set the "_BlitScaleBias" uniform for user materials with shaders relying on core Blit.hlsl to work
        data.material.SetVector(s_BlitScaleBias, !IsHandleYFlipped ? new Vector4(1, -1, 0, 1) : new Vector4(1, 1, 0, 0));

        if (data.shaderPass == UberShaderPasses.TextureRead || data.shaderPass == UberShaderPasses.TextureReadVisMesh)
        {
            data.material.SetTexture(s_BlitTexture, data.source);
        }
        else if (data.shaderPass == UberShaderPasses.MSAASoftwareResolve || data.shaderPass == UberShaderPasses.MSAASoftwareResolveVisMesh)
        {
            // Setup MSAA samples
            switch (data.msaaSamples)
            {
                case 4:
                    CoreUtils.SetKeyword(data.material, ShaderKeywordStrings.Msaa2, false);
                    CoreUtils.SetKeyword(data.material, ShaderKeywordStrings.Msaa4, true);
                    break;

                case 2:
                    CoreUtils.SetKeyword(data.material, ShaderKeywordStrings.Msaa2, true);
                    CoreUtils.SetKeyword(data.material, ShaderKeywordStrings.Msaa4, false);
                    break;

                // MSAA disabled, auto resolve supported, resolve texture requested, or ms textures not supported
                default:
                    CoreUtils.SetKeyword(data.material, ShaderKeywordStrings.Msaa2, false);
                    CoreUtils.SetKeyword(data.material, ShaderKeywordStrings.Msaa4, false);
                    break;
            }
        }

#if ENABLE_VR && ENABLE_XR_MODULE
        if (data.useXRVisibilityMesh)
        {
            MaterialPropertyBlock xrPropertyBlock = XRSystemUniversal.GetMaterialPropertyBlock();
            data.xr.RenderVisibleMeshCustomMaterial(cmd, data.xr.occlusionMeshScale, data.material, xrPropertyBlock, (int)(data.shaderPass), false);
        }
        else
#endif
        {
            cmd.DrawProcedural(Matrix4x4.identity, data.material, (int)(data.shaderPass),
                MeshTopology.Triangles, 3, 1);
        }
    }

    private class PassData
    {
        internal TextureHandle source;
        internal TextureHandle destination;
        internal TextureHandle lutTexture;
        internal TextureHandle userLutTexture;
        internal Material material;
        internal UberShaderPasses shaderPass;
        internal Vector4 scaleBias;
        internal bool useXRVisibilityMesh;
        internal XRPass xr;
        internal int msaaSamples;
    }

    TextureHandle TryGetCachedUserLutTextureHandle(ColorLookup colorLookup, RenderGraph renderGraph)
    {
        if (colorLookup.texture.value == null)
        {
            if (m_UserLut != null)
            {
                m_UserLut.Release();
                m_UserLut = null;
            } 
        }
        else
        {
            if (m_UserLut == null || m_UserLut.externalTexture != colorLookup.texture.value)
            {
                m_UserLut?.Release();
                m_UserLut = RTHandles.Alloc(colorLookup.texture.value);
            }
        }
        return m_UserLut != null ? renderGraph.ImportTexture(m_UserLut) : TextureHandle.nullHandle;
    }

    void SetupLut(Material material, ColorLookup colorLookup, ColorAdjustments colorAdjustments, int lutSize)
    {
        int lutHeight = lutSize;
        int lutWidth = lutHeight * lutHeight;

        float postExposureLinear = Mathf.Pow(2f, colorAdjustments.postExposure.value);
        Vector4 lutParams = new Vector4(1f / lutWidth, 1f / lutHeight, lutHeight - 1f, postExposureLinear);

        Vector4 userLutParams = !colorLookup.IsActive()
            ? Vector4.zero
            : new Vector4(1f / colorLookup.texture.value.width,
                1f / colorLookup.texture.value.height,
                colorLookup.texture.value.height - 1f,
                colorLookup.contribution.value);

        material.SetVector(ShaderConstants._Lut_Params, lutParams);
        material.SetVector(ShaderConstants._UserLut_Params, userLutParams);
    }

#region Vignette


    //these methods should be publicly available for user features
    void SetupVignette(Material material, XRPass xrPass, int width, int height, Vignette vignette)
    {
        var color = vignette.color.value;
        var center = vignette.center.value;
        var aspectRatio = width / (float)height;

#if ENABLE_VR
        if (xrPass != null && xrPass.enabled)
        {
            if (xrPass.singlePassEnabled)
                material.SetVector(ShaderConstants._Vignette_ParamsXR, xrPass.ApplyXRViewCenterOffset(center));
            else
                // In multi-pass mode we need to modify the eye center with the values from .xy of the corrected
                // center since the version of the shader that is not single-pass will use the value in _Vignette_Params2
                center = xrPass.ApplyXRViewCenterOffset(center);
        }
#endif

        var v1 = new Vector4(
            color.r, color.g, color.b,
            vignette.rounded.value ? aspectRatio : 1f
        );
        var v2 = new Vector4(
            center.x, center.y,
            vignette.intensity.value * 3f,
            vignette.smoothness.value * 5f
        );

        material.SetVector(ShaderConstants._Vignette_Params1, v1);
        material.SetVector(ShaderConstants._Vignette_Params2, v2);
    }

#endregion

    private void SetupTonemapping(Material onTileUberMaterial, Tonemapping tonemapping, bool isHdrGrading)
    {
        if (isHdrGrading)
        {
            CoreUtils.SetKeyword(m_OnTileUberMaterial, ShaderKeywordStrings.HDRGrading, isHdrGrading);
        }
        else
        {
            CoreUtils.SetKeyword(m_OnTileUberMaterial, ShaderKeywordStrings.TonemapNeutral,
                tonemapping.mode.value == TonemappingMode.Neutral);
            CoreUtils.SetKeyword(m_OnTileUberMaterial, ShaderKeywordStrings.TonemapACES,
                tonemapping.mode.value == TonemappingMode.ACES);
        }
    }

    void SetupGrain(Material onTileUberMaterial, UniversalCameraData cameraData, FilmGrain filmgrain, PostProcessData data)
    {
        if (filmgrain.IsActive())
        {
            onTileUberMaterial.EnableKeyword(ShaderKeywordStrings.FilmGrain);
            PostProcessUtils.ConfigureFilmGrain(
                data,
                filmgrain,
                cameraData.pixelWidth, cameraData.pixelHeight,
                onTileUberMaterial
            );
        }
    }

    void SetupDithering(Material onTileUberMaterial, UniversalCameraData cameraData, PostProcessData data)
    {
        if (cameraData.isDitheringEnabled)
        {
            onTileUberMaterial.EnableKeyword(ShaderKeywordStrings.Dithering);
            m_DitheringTextureIndex = PostProcessUtils.ConfigureDithering(
                data,
                m_DitheringTextureIndex,
                cameraData.pixelWidth, cameraData.pixelHeight,
                onTileUberMaterial
            );
        }
    }

    static class ShaderConstants
    {
        public static readonly int _Vignette_Params1 = Shader.PropertyToID("_Vignette_Params1");
        public static readonly int _Vignette_Params2 = Shader.PropertyToID("_Vignette_Params2");
        public static readonly int _Vignette_ParamsXR = Shader.PropertyToID("_Vignette_ParamsXR");
        public static readonly int _Lut_Params = Shader.PropertyToID("_Lut_Params");
        public static readonly int _UserLut_Params = Shader.PropertyToID("_UserLut_Params");
        public static readonly int _InternalLut = Shader.PropertyToID("_InternalLut");
        public static readonly int _UserLut = Shader.PropertyToID("_UserLut");
    }
}