namespace UnityStandardAssets.CinematicEffects
{
    using UnityEngine;
    using UnityEngine.Events;
    using System;

    [ExecuteInEditMode]
    [RequireComponent(typeof(Camera))]
    [AddComponentMenu("Image Effects/Cinematic/Tonemapping and Color Grading")]
#if UNITY_5_4_OR_NEWER
    [ImageEffectAllowedInSceneView]
#endif
    public class TonemappingColorGrading : MonoBehaviour
    {
#if UNITY_EDITOR
        // EDITOR ONLY call for allowing the editor to update the histogram
        public UnityAction<RenderTexture> onFrameEndEditorOnly;

        [SerializeField]
        private ComputeShader m_HistogramComputeShader;
        public ComputeShader histogramComputeShader
        {
            get
            {
                if (m_HistogramComputeShader == null)
                    m_HistogramComputeShader = Resources.Load<ComputeShader>("HistogramCompute");

                return m_HistogramComputeShader;
            }
        }

        [SerializeField]
        private Shader m_HistogramShader;
        public Shader histogramShader
        {
            get
            {
                if (m_HistogramShader == null)
                    m_HistogramShader = Shader.Find("Hidden/TonemappingColorGradingHistogram");

                return m_HistogramShader;
            }
        }

        [SerializeField]
        public bool histogramRefreshOnPlay = true;
#endif

        #region Attributes
        [AttributeUsage(AttributeTargets.Field)]
        public class SettingsGroup : Attribute
        {}

        public class IndentedGroup : PropertyAttribute
        {}

        public class ChannelMixer : PropertyAttribute
        {}

        public class ColorWheelGroup : PropertyAttribute
        {
            public int minSizePerWheel = 60;
            public int maxSizePerWheel = 150;

            public ColorWheelGroup()
            {}

            public ColorWheelGroup(int minSizePerWheel, int maxSizePerWheel)
            {
                this.minSizePerWheel = minSizePerWheel;
                this.maxSizePerWheel = maxSizePerWheel;
            }
        }

        public class Curve : PropertyAttribute
        {
            public Color color = Color.white;

            public Curve()
            {}

            public Curve(float r, float g, float b, float a) // Can't pass a struct in an attribute
            {
                color = new Color(r, g, b, a);
            }
        }
        #endregion

        #region Settings
        [Serializable]
        public struct EyeAdaptationSettings
        {
            public bool enabled;

            [Min(0f), Tooltip("Midpoint Adjustment.")]
            public float middleGrey;

            [Tooltip("The lowest possible exposure value; adjust this value to modify the brightest areas of your level.")]
            public float min;

            [Tooltip("The highest possible exposure value; adjust this value to modify the darkest areas of your level.")]
            public float max;

            [Min(0f), Tooltip("Speed of linear adaptation. Higher is faster.")]
            public float speed;

            [Tooltip("Displays a luminosity helper in the GameView.")]
            public bool showDebug;

            public static EyeAdaptationSettings defaultSettings
            {
                get
                {
                    return new EyeAdaptationSettings
                    {
                        enabled = false,
                        showDebug = false,
                        middleGrey = 0.5f,
                        min = -3f,
                        max = 3f,
                        speed = 1.5f
                    };
                }
            }
        }

        public enum Tonemapper
        {
            ACES,
            Curve,
            Hable,
            HejlDawson,
            Photographic,
            Reinhard,
            Neutral
        }

        [Serializable]
        public struct TonemappingSettings
        {
            public bool enabled;

            [Tooltip("Tonemapping technique to use. ACES is the recommended one.")]
            public Tonemapper tonemapper;

            [Min(0f), Tooltip("Adjusts the overall exposure of the scene.")]
            public float exposure;

            [Tooltip("Custom tonemapping curve.")]
            public AnimationCurve curve;

            // Neutral settings
            [Range(-0.1f, 0.1f)]
            public float neutralBlackIn;

            [Range(1f, 20f)]
            public float neutralWhiteIn;

            [Range(-0.09f, 0.1f)]
            public float neutralBlackOut;

            [Range(1f, 19f)]
            public float neutralWhiteOut;

            [Range(0.1f, 20f)]
            public float neutralWhiteLevel;

            [Range(1f, 10f)]
            public float neutralWhiteClip;

            public static TonemappingSettings defaultSettings
            {
                get
                {
                    return new TonemappingSettings
                    {
                        enabled = false,
                        tonemapper = Tonemapper.Neutral,
                        exposure = 1f,
                        curve = CurvesSettings.defaultCurve,
                        neutralBlackIn = 0.02f,
                        neutralWhiteIn = 10f,
                        neutralBlackOut = 0f,
                        neutralWhiteOut = 10f,
                        neutralWhiteLevel = 5.3f,
                        neutralWhiteClip = 10f
                    };
                }
            }
        }

        [Serializable]
        public struct LUTSettings
        {
            public bool enabled;

            [Tooltip("Custom lookup texture (strip format, e.g. 256x16).")]
            public Texture texture;

            [Range(0f, 1f), Tooltip("Blending factor.")]
            public float contribution;

            public static LUTSettings defaultSettings
            {
                get
                {
                    return new LUTSettings
                    {
                        enabled = false,
                        texture = null,
                        contribution = 1f
                    };
                }
            }
        }

        [Serializable]
        public struct ColorWheelsSettings
        {
            [ColorUsage(false)]
            public Color shadows;

            [ColorUsage(false)]
            public Color midtones;

            [ColorUsage(false)]
            public Color highlights;

            public static ColorWheelsSettings defaultSettings
            {
                get
                {
                    return new ColorWheelsSettings
                    {
                        shadows = Color.white,
                        midtones = Color.white,
                        highlights = Color.white
                    };
                }
            }
        }

        [Serializable]
        public struct BasicsSettings
        {
            [Range(-2f, 2f), Tooltip("Sets the white balance to a custom color temperature.")]
            public float temperatureShift;

            [Range(-2f, 2f), Tooltip("Sets the white balance to compensate for a green or magenta tint.")]
            public float tint;

            [Space, Range(-0.5f, 0.5f), Tooltip("Shift the hue of all colors.")]
            public float hue;

            [Range(0f, 2f), Tooltip("Pushes the intensity of all colors.")]
            public float saturation;

            [Range(-1f, 1f), Tooltip("Adjusts the saturation so that clipping is minimized as colors approach full saturation.")]
            public float vibrance;

            [Range(0f, 10f), Tooltip("Brightens or darkens all colors.")]
            public float value;

            [Space, Range(0f, 2f), Tooltip("Expands or shrinks the overall range of tonal values.")]
            public float contrast;

            [Range(0.01f, 5f), Tooltip("Contrast gain curve. Controls the steepness of the curve.")]
            public float gain;

            [Range(0.01f, 5f), Tooltip("Applies a pow function to the source.")]
            public float gamma;

            public static BasicsSettings defaultSettings
            {
                get
                {
                    return new BasicsSettings
                    {
                        temperatureShift = 0f,
                        tint = 0f,
                        contrast = 1f,
                        hue = 0f,
                        saturation = 1f,
                        value = 1f,
                        vibrance = 0f,
                        gain = 1f,
                        gamma = 1f
                    };
                }
            }
        }

        [Serializable]
        public struct ChannelMixerSettings
        {
            public int currentChannel;
            public Vector3[] channels;

            public static ChannelMixerSettings defaultSettings
            {
                get
                {
                    return new ChannelMixerSettings
                    {
                        currentChannel = 0,
                        channels = new[]
                        {
                            new Vector3(1f, 0f, 0f),
                            new Vector3(0f, 1f, 0f),
                            new Vector3(0f, 0f, 1f)
                        }
                    };
                }
            }
        }

        [Serializable]
        public struct CurvesSettings
        {
            [Curve]
            public AnimationCurve master;

            [Curve(1f, 0f, 0f, 1f)]
            public AnimationCurve red;

            [Curve(0f, 1f, 0f, 1f)]
            public AnimationCurve green;

            [Curve(0f, 1f, 1f, 1f)]
            public AnimationCurve blue;

            public static CurvesSettings defaultSettings
            {
                get
                {
                    return new CurvesSettings
                    {
                        master = defaultCurve,
                        red = defaultCurve,
                        green = defaultCurve,
                        blue = defaultCurve
                    };
                }
            }

            public static AnimationCurve defaultCurve
            {
                get { return new AnimationCurve(new Keyframe(0f, 0f, 1f, 1f), new Keyframe(1f, 1f, 1f, 1f)); }
            }
        }

        public enum ColorGradingPrecision
        {
            Normal = 16,
            High = 32
        }

        [Serializable]
        public struct ColorGradingSettings
        {
            public bool enabled;

            [Tooltip("Internal LUT precision. \"Normal\" is 256x16, \"High\" is 1024x32. Prefer \"Normal\" on mobile devices.")]
            public ColorGradingPrecision precision;

            [Space, ColorWheelGroup]
            public ColorWheelsSettings colorWheels;

            [Space, IndentedGroup]
            public BasicsSettings basics;

            [Space, ChannelMixer]
            public ChannelMixerSettings channelMixer;

            [Space, IndentedGroup]
            public CurvesSettings curves;

            [Space, Tooltip("Use dithering to try and minimize color banding in dark areas.")]
            public bool useDithering;

            [Tooltip("Displays the generated LUT in the top left corner of the GameView.")]
            public bool showDebug;

            public static ColorGradingSettings defaultSettings
            {
                get
                {
                    return new ColorGradingSettings
                    {
                        enabled = false,
                        useDithering = false,
                        showDebug = false,
                        precision = ColorGradingPrecision.Normal,
                        colorWheels = ColorWheelsSettings.defaultSettings,
                        basics = BasicsSettings.defaultSettings,
                        channelMixer = ChannelMixerSettings.defaultSettings,
                        curves = CurvesSettings.defaultSettings
                    };
                }
            }

            internal void Reset()
            {
                curves = CurvesSettings.defaultSettings;
            }
        }

        [SerializeField, SettingsGroup]
        private EyeAdaptationSettings m_EyeAdaptation = EyeAdaptationSettings.defaultSettings;
        public EyeAdaptationSettings eyeAdaptation
        {
            get { return m_EyeAdaptation; }
            set { m_EyeAdaptation = value; }
        }

        [SerializeField, SettingsGroup]
        private TonemappingSettings m_Tonemapping = TonemappingSettings.defaultSettings;
        public TonemappingSettings tonemapping
        {
            get { return m_Tonemapping; }
            set
            {
                m_Tonemapping = value;
                SetTonemapperDirty();
            }
        }

        [SerializeField, SettingsGroup]
        private ColorGradingSettings m_ColorGrading = ColorGradingSettings.defaultSettings;
        public ColorGradingSettings colorGrading
        {
            get { return m_ColorGrading; }
            set
            {
                m_ColorGrading = value;
                SetDirty();
            }
        }

        [SerializeField, SettingsGroup]
        private LUTSettings m_Lut = LUTSettings.defaultSettings;
        public LUTSettings lut
        {
            get { return m_Lut; }
            set { m_Lut = value; }
        }
        #endregion

        private Texture2D m_IdentityLut;
        private RenderTexture m_InternalLut;
        private Texture2D m_CurveTexture;
        private Texture2D m_TonemapperCurve;
        private float m_TonemapperCurveRange;

        private Texture2D identityLut
        {
            get
            {
                if (m_IdentityLut == null || m_IdentityLut.height != lutSize)
                {
                    DestroyImmediate(m_IdentityLut);
                    m_IdentityLut = GenerateIdentityLut(lutSize);
                }

                return m_IdentityLut;
            }
        }

        private RenderTexture internalLutRt
        {
            get
            {
                if (m_InternalLut == null || !m_InternalLut.IsCreated() || m_InternalLut.height != lutSize)
                {
                    DestroyImmediate(m_InternalLut);
                    m_InternalLut = new RenderTexture(lutSize * lutSize, lutSize, 0, RenderTextureFormat.ARGB32)
                    {
                        name = "Internal LUT",
                        filterMode = FilterMode.Bilinear,
                        anisoLevel = 0,
                        hideFlags = HideFlags.DontSave
                    };
                }

                return m_InternalLut;
            }
        }

        private Texture2D curveTexture
        {
            get
            {
                if (m_CurveTexture == null)
                {
                    m_CurveTexture = new Texture2D(256, 1, TextureFormat.ARGB32, false, true)
                    {
                        name = "Curve texture",
                        wrapMode = TextureWrapMode.Clamp,
                        filterMode = FilterMode.Bilinear,
                        anisoLevel = 0,
                        hideFlags = HideFlags.DontSave
                    };
                }

                return m_CurveTexture;
            }
        }

        private Texture2D tonemapperCurve
        {
            get
            {
                if (m_TonemapperCurve == null)
                {
                    TextureFormat format = TextureFormat.RGB24;
                    if (SystemInfo.SupportsTextureFormat(TextureFormat.RFloat))
                        format = TextureFormat.RFloat;
                    else if (SystemInfo.SupportsTextureFormat(TextureFormat.RHalf))
                        format = TextureFormat.RHalf;

                    m_TonemapperCurve = new Texture2D(256, 1, format, false, true)
                    {
                        name = "Tonemapper curve texture",
                        wrapMode = TextureWrapMode.Clamp,
                        filterMode = FilterMode.Bilinear,
                        anisoLevel = 0,
                        hideFlags = HideFlags.DontSave
                    };
                }

                return m_TonemapperCurve;
            }
        }

        [SerializeField]
        private Shader m_Shader;
        public Shader shader
        {
            get
            {
                if (m_Shader == null)
                    m_Shader = Shader.Find("Hidden/TonemappingColorGrading");

                return m_Shader;
            }
        }

        private Material m_Material;
        public Material material
        {
            get
            {
                if (m_Material == null)
                    m_Material = ImageEffectHelper.CheckShaderAndCreateMaterial(shader);

                return m_Material;
            }
        }

        public bool isGammaColorSpace
        {
            get { return QualitySettings.activeColorSpace == ColorSpace.Gamma; }
        }

        public int lutSize
        {
            get { return (int)colorGrading.precision; }
        }

        private enum Pass
        {
            LutGen,
            AdaptationLog,
            AdaptationExpBlend,
            AdaptationExp,
            TonemappingOff,
            TonemappingACES,
            TonemappingCurve,
            TonemappingHable,
            TonemappingHejlDawson,
            TonemappingPhotographic,
            TonemappingReinhard,
            TonemappingNeutral,
            AdaptationDebug
        }

        public bool validRenderTextureFormat { get; private set; }
        public bool validUserLutSize { get; private set; }

        private bool m_Dirty = true;
        private bool m_TonemapperDirty = true;

        private RenderTexture m_SmallAdaptiveRt;
        private RenderTextureFormat m_AdaptiveRtFormat;

        public void SetDirty()
        {
            m_Dirty = true;
        }

        public void SetTonemapperDirty()
        {
            m_TonemapperDirty = true;
        }

        private void OnEnable()
        {
            if (!ImageEffectHelper.IsSupported(shader, false, true, this))
            {
                enabled = false;
                return;
            }

            SetDirty();
            SetTonemapperDirty();
        }

        private void OnDisable()
        {
            if (m_Material != null)
                DestroyImmediate(m_Material);

            if (m_IdentityLut != null)
                DestroyImmediate(m_IdentityLut);

            if (m_InternalLut != null)
                DestroyImmediate(internalLutRt);

            if (m_SmallAdaptiveRt != null)
                DestroyImmediate(m_SmallAdaptiveRt);

            if (m_CurveTexture != null)
                DestroyImmediate(m_CurveTexture);

            if (m_TonemapperCurve != null)
                DestroyImmediate(m_TonemapperCurve);

            m_Material = null;
            m_IdentityLut = null;
            m_InternalLut = null;
            m_SmallAdaptiveRt = null;
            m_CurveTexture = null;
            m_TonemapperCurve = null;
        }

        private void OnValidate()
        {
            SetDirty();
            SetTonemapperDirty();
        }

        private static Texture2D GenerateIdentityLut(int dim)
        {
            Color[] newC = new Color[dim * dim * dim];
            float oneOverDim = 1f / ((float)dim - 1f);

            for (int i = 0; i < dim; i++)
                for (int j = 0; j < dim; j++)
                    for (int k = 0; k < dim; k++)
                        newC[i + (j * dim) + (k * dim * dim)] = new Color((float)i * oneOverDim, Mathf.Abs((float)k * oneOverDim), (float)j * oneOverDim, 1f);

            Texture2D tex2D = new Texture2D(dim * dim, dim, TextureFormat.RGB24, false, true)
            {
                name = "Identity LUT",
                filterMode = FilterMode.Bilinear,
                anisoLevel = 0,
                hideFlags = HideFlags.DontSave
            };
            tex2D.SetPixels(newC);
            tex2D.Apply();

            return tex2D;
        }

        // An analytical model of chromaticity of the standard illuminant, by Judd et al.
        // http://en.wikipedia.org/wiki/Standard_illuminant#Illuminant_series_D
        // Slightly modifed to adjust it with the D65 white point (x=0.31271, y=0.32902).
        private float StandardIlluminantY(float x)
        {
            return 2.87f * x - 3f * x * x - 0.27509507f;
        }

        // CIE xy chromaticity to CAT02 LMS.
        // http://en.wikipedia.org/wiki/LMS_color_space#CAT02
        private Vector3 CIExyToLMS(float x, float y)
        {
            float Y = 1f;
            float X = Y * x / y;
            float Z = Y * (1f - x - y) / y;

            float L =  0.7328f * X + 0.4296f * Y - 0.1624f * Z;
            float M = -0.7036f * X + 1.6975f * Y + 0.0061f * Z;
            float S =  0.0030f * X + 0.0136f * Y + 0.9834f * Z;

            return new Vector3(L, M, S);
        }

        private Vector3 GetWhiteBalance()
        {
            float t1 = colorGrading.basics.temperatureShift;
            float t2 = colorGrading.basics.tint;

            // Get the CIE xy chromaticity of the reference white point.
            // Note: 0.31271 = x value on the D65 white point
            float x = 0.31271f - t1 * (t1 < 0f ? 0.1f : 0.05f);
            float y = StandardIlluminantY(x) + t2 * 0.05f;

            // Calculate the coefficients in the LMS space.
            Vector3 w1 = new Vector3(0.949237f, 1.03542f, 1.08728f); // D65 white point
            Vector3 w2 = CIExyToLMS(x, y);
            return new Vector3(w1.x / w2.x, w1.y / w2.y, w1.z / w2.z);
        }

        private static Color NormalizeColor(Color c)
        {
            float sum = (c.r + c.g + c.b) / 3f;

            if (Mathf.Approximately(sum, 0f))
                return new Color(1f, 1f, 1f, 1f);

            return new Color
            {
                r = c.r / sum,
                g = c.g / sum,
                b = c.b / sum,
                a = 1f
            };
        }

        private void GenerateLiftGammaGain(out Color lift, out Color gamma, out Color gain)
        {
            Color nLift = NormalizeColor(colorGrading.colorWheels.shadows);
            Color nGamma = NormalizeColor(colorGrading.colorWheels.midtones);
            Color nGain = NormalizeColor(colorGrading.colorWheels.highlights);

            float avgLift = (nLift.r + nLift.g + nLift.b) / 3f;
            float avgGamma = (nGamma.r + nGamma.g + nGamma.b) / 3f;
            float avgGain = (nGain.r + nGain.g + nGain.b) / 3f;

            // Magic numbers
            const float liftScale = 0.1f;
            const float gammaScale = 0.5f;
            const float gainScale = 0.5f;

            float liftR = (nLift.r - avgLift) * liftScale;
            float liftG = (nLift.g - avgLift) * liftScale;
            float liftB = (nLift.b - avgLift) * liftScale;

            float gammaR = Mathf.Pow(2f, (nGamma.r - avgGamma) * gammaScale);
            float gammaG = Mathf.Pow(2f, (nGamma.g - avgGamma) * gammaScale);
            float gammaB = Mathf.Pow(2f, (nGamma.b - avgGamma) * gammaScale);

            float gainR = Mathf.Pow(2f, (nGain.r - avgGain) * gainScale);
            float gainG = Mathf.Pow(2f, (nGain.g - avgGain) * gainScale);
            float gainB = Mathf.Pow(2f, (nGain.b - avgGain) * gainScale);

            const float minGamma = 0.01f;
            float invGammaR = 1f / Mathf.Max(minGamma, gammaR);
            float invGammaG = 1f / Mathf.Max(minGamma, gammaG);
            float invGammaB = 1f / Mathf.Max(minGamma, gammaB);

            lift = new Color(liftR, liftG, liftB);
            gamma = new Color(invGammaR, invGammaG, invGammaB);
            gain = new Color(gainR, gainG, gainB);
        }

        private void GenCurveTexture()
        {
            AnimationCurve master = colorGrading.curves.master;
            AnimationCurve red = colorGrading.curves.red;
            AnimationCurve green = colorGrading.curves.green;
            AnimationCurve blue = colorGrading.curves.blue;

            Color[] pixels = new Color[256];

            for (float i = 0f; i <= 1f; i += 1f / 255f)
            {
                float m = Mathf.Clamp(master.Evaluate(i), 0f, 1f);
                float r = Mathf.Clamp(red.Evaluate(i), 0f, 1f);
                float g = Mathf.Clamp(green.Evaluate(i), 0f, 1f);
                float b = Mathf.Clamp(blue.Evaluate(i), 0f, 1f);
                pixels[(int)Mathf.Floor(i * 255f)] = new Color(r, g, b, m);
            }

            curveTexture.SetPixels(pixels);
            curveTexture.Apply();
        }

        private bool CheckUserLut()
        {
            validUserLutSize = (lut.texture.height == (int)Mathf.Sqrt(lut.texture.width));
            return validUserLutSize;
        }

        private bool CheckSmallAdaptiveRt()
        {
            if (m_SmallAdaptiveRt != null)
                return false;

            m_AdaptiveRtFormat = RenderTextureFormat.ARGBHalf;

            if (SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.RGHalf))
                m_AdaptiveRtFormat = RenderTextureFormat.RGHalf;

            m_SmallAdaptiveRt = new RenderTexture(1, 1, 0, m_AdaptiveRtFormat);
            m_SmallAdaptiveRt.hideFlags = HideFlags.DontSave;

            return true;
        }

        private void OnGUI()
        {
            if (Event.current.type != EventType.Repaint)
                return;

            int yoffset = 0;

            // Color grading debug
            if (m_InternalLut != null && colorGrading.enabled && colorGrading.showDebug)
            {
                Graphics.DrawTexture(new Rect(0f, yoffset, lutSize * lutSize, lutSize), internalLutRt);
                yoffset += lutSize;
            }

            // Eye Adaptation debug
            if (m_SmallAdaptiveRt != null && eyeAdaptation.enabled && eyeAdaptation.showDebug)
            {
                m_Material.SetPass((int)Pass.AdaptationDebug);
                Graphics.DrawTexture(new Rect(0f, yoffset, 256, 16), m_SmallAdaptiveRt, m_Material);
            }
        }

        [ImageEffectTransformsToLDR]
        private void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
#if UNITY_EDITOR
            validRenderTextureFormat = true;

            if (source.format != RenderTextureFormat.ARGBHalf && source.format != RenderTextureFormat.ARGBFloat)
                validRenderTextureFormat = false;
#endif

            material.shaderKeywords = null;

            RenderTexture rtSquared = null;
            RenderTexture[] rts = null;

            if (eyeAdaptation.enabled)
            {
                bool freshlyBrewedSmallRt = CheckSmallAdaptiveRt();
                int srcSize = source.width < source.height ? source.width : source.height;

                // Fast lower or equal power of 2
                int adaptiveSize = srcSize;
                adaptiveSize |= (adaptiveSize >> 1);
                adaptiveSize |= (adaptiveSize >> 2);
                adaptiveSize |= (adaptiveSize >> 4);
                adaptiveSize |= (adaptiveSize >> 8);
                adaptiveSize |= (adaptiveSize >> 16);
                adaptiveSize -= (adaptiveSize >> 1);

                rtSquared = RenderTexture.GetTemporary(adaptiveSize, adaptiveSize, 0, m_AdaptiveRtFormat);
                Graphics.Blit(source, rtSquared);

                int downsample = (int)Mathf.Log(rtSquared.width, 2f);

                int div = 2;
                rts = new RenderTexture[downsample];
                for (int i = 0; i < downsample; i++)
                {
                    rts[i] = RenderTexture.GetTemporary(rtSquared.width / div, rtSquared.width / div, 0, m_AdaptiveRtFormat);
                    div <<= 1;
                }

                // Downsample pyramid
                var lumRt = rts[downsample - 1];
                Graphics.Blit(rtSquared, rts[0], material, (int)Pass.AdaptationLog);
                for (int i = 0; i < downsample - 1; i++)
                {
                    Graphics.Blit(rts[i], rts[i + 1]);
                    lumRt = rts[i + 1];
                }

                // Keeping luminance values between frames, RT restore expected
                m_SmallAdaptiveRt.MarkRestoreExpected();

                material.SetFloat("_AdaptationSpeed", Mathf.Max(eyeAdaptation.speed, 0.001f));

#if UNITY_EDITOR
                if (Application.isPlaying && !freshlyBrewedSmallRt)
                    Graphics.Blit(lumRt, m_SmallAdaptiveRt, material, (int)Pass.AdaptationExpBlend);
                else
                    Graphics.Blit(lumRt, m_SmallAdaptiveRt, material, (int)Pass.AdaptationExp);
#else
                Graphics.Blit(lumRt, m_SmallAdaptiveRt, material, freshlyBrewedSmallRt ? (int)Pass.AdaptationExp : (int)Pass.AdaptationExpBlend);
#endif

                material.SetFloat("_MiddleGrey", eyeAdaptation.middleGrey);
                material.SetFloat("_AdaptationMin", Mathf.Pow(2f, eyeAdaptation.min));
                material.SetFloat("_AdaptationMax", Mathf.Pow(2f, eyeAdaptation.max));
                material.SetTexture("_LumTex", m_SmallAdaptiveRt);
                material.EnableKeyword("ENABLE_EYE_ADAPTATION");
            }

            int renderPass = (int)Pass.TonemappingOff;

            if (tonemapping.enabled)
            {
                if (tonemapping.tonemapper == Tonemapper.Curve)
                {
                    if (m_TonemapperDirty)
                    {
                        float range = 1f;

                        if (tonemapping.curve.length > 0)
                        {
                            range = tonemapping.curve[tonemapping.curve.length - 1].time;

                            for (float i = 0f; i <= 1f; i += 1f / 255f)
                            {
                                float c = tonemapping.curve.Evaluate(i * range);
                                tonemapperCurve.SetPixel(Mathf.FloorToInt(i * 255f), 0, new Color(c, c, c));
                            }

                            tonemapperCurve.Apply();
                        }

                        m_TonemapperCurveRange = 1f / range;
                        m_TonemapperDirty = false;
                    }

                    material.SetFloat("_ToneCurveRange", m_TonemapperCurveRange);
                    material.SetTexture("_ToneCurve", tonemapperCurve);
                }
                else if (tonemapping.tonemapper == Tonemapper.Neutral)
                {
                    const float scaleFactor = 20f;
                    const float scaleFactorHalf = scaleFactor * 0.5f;

                    float inBlack = tonemapping.neutralBlackIn * scaleFactor + 1f;
                    float outBlack = tonemapping.neutralBlackOut * scaleFactorHalf + 1f;
                    float inWhite = tonemapping.neutralWhiteIn / scaleFactor;
                    float outWhite = 1f - tonemapping.neutralWhiteOut / scaleFactor;
                    float blackRatio = inBlack / outBlack;
                    float whiteRatio = inWhite / outWhite;

                    const float a = 0.2f;
                    float b = Mathf.Max(0f, Mathf.LerpUnclamped(0.57f, 0.37f, blackRatio));
                    float c = Mathf.LerpUnclamped(0.01f, 0.24f, whiteRatio);
                    float d = Mathf.Max(0f, Mathf.LerpUnclamped(0.02f, 0.20f, blackRatio));
                    const float e = 0.02f;
                    const float f = 0.30f;

                    material.SetVector("_NeutralTonemapperParams1", new Vector4(a, b, c, d));
                    material.SetVector("_NeutralTonemapperParams2", new Vector4(e, f, tonemapping.neutralWhiteLevel, tonemapping.neutralWhiteClip / scaleFactorHalf));
                }

                material.SetFloat("_Exposure", tonemapping.exposure);
                renderPass += (int)tonemapping.tonemapper + 1;
            }

            if (colorGrading.enabled)
            {
                if (m_Dirty || !m_InternalLut.IsCreated())
                {
                    Color lift, gamma, gain;
                    GenerateLiftGammaGain(out lift, out gamma, out gain);
                    GenCurveTexture();

                    material.SetVector("_WhiteBalance", GetWhiteBalance());
                    material.SetVector("_Lift", lift);
                    material.SetVector("_Gamma", gamma);
                    material.SetVector("_Gain", gain);
                    material.SetVector("_ContrastGainGamma", new Vector3(colorGrading.basics.contrast, colorGrading.basics.gain, 1f / colorGrading.basics.gamma));
                    material.SetFloat("_Vibrance", colorGrading.basics.vibrance);
                    material.SetVector("_HSV", new Vector4(colorGrading.basics.hue, colorGrading.basics.saturation, colorGrading.basics.value));
                    material.SetVector("_ChannelMixerRed", colorGrading.channelMixer.channels[0]);
                    material.SetVector("_ChannelMixerGreen", colorGrading.channelMixer.channels[1]);
                    material.SetVector("_ChannelMixerBlue", colorGrading.channelMixer.channels[2]);
                    material.SetTexture("_CurveTex", curveTexture);
                    internalLutRt.MarkRestoreExpected();
                    Graphics.Blit(identityLut, internalLutRt, material, (int)Pass.LutGen);
                    m_Dirty = false;
                }
                
                material.EnableKeyword("ENABLE_COLOR_GRADING");

                if (colorGrading.useDithering)
                    material.EnableKeyword("ENABLE_DITHERING");
                
                material.SetTexture("_InternalLutTex", internalLutRt);
                material.SetVector("_InternalLutParams", new Vector3(1f / internalLutRt.width, 1f / internalLutRt.height, internalLutRt.height - 1f));
            }

            if (lut.enabled && lut.texture != null && CheckUserLut())
            {
                material.SetTexture("_UserLutTex", lut.texture);
                material.SetVector("_UserLutParams", new Vector4(1f / lut.texture.width, 1f / lut.texture.height, lut.texture.height - 1f, lut.contribution));
                material.EnableKeyword("ENABLE_USER_LUT");
            }

            Graphics.Blit(source, destination, material, renderPass);

            // Cleanup for eye adaptation
            if (eyeAdaptation.enabled)
            {
                for (int i = 0; i < rts.Length; i++)
                    RenderTexture.ReleaseTemporary(rts[i]);

                RenderTexture.ReleaseTemporary(rtSquared);
            }

#if UNITY_EDITOR
            // If we have an on frame end callabck we need to pass a valid result texture
            // if destination is null we wrote to the backbuffer so we need to copy that out.
            // It's slow and not amazing, but editor only
            if (onFrameEndEditorOnly != null)
            {
                if (destination == null)
                {
                    RenderTexture rt = RenderTexture.GetTemporary(source.width, source.height, 0);
                    Graphics.Blit(source, rt, material, renderPass);
                    onFrameEndEditorOnly(rt);
                    RenderTexture.ReleaseTemporary(rt);
                    RenderTexture.active = null;
                }
                else
                {
                    onFrameEndEditorOnly(destination);
                }
            }
#endif
        }

        public Texture2D BakeLUT()
        {
            Texture2D lut = new Texture2D(internalLutRt.width, internalLutRt.height, TextureFormat.RGB24, false, true);
            RenderTexture.active = internalLutRt;
            lut.ReadPixels(new Rect(0f, 0f, lut.width, lut.height), 0, 0);
            RenderTexture.active = null;
            return lut;
        }
    }
}
