using System.Collections.Generic;
using UnityEngine.Assertions;

namespace UnityEngine.Rendering.PostProcessing
{
    class TextureLerper
    {
        static TextureLerper m_Instance;
        internal static TextureLerper instance
        {
            get
            {
                if (m_Instance == null)
                    m_Instance = new TextureLerper();

                return m_Instance;
            }
        }

        CommandBuffer m_Command;
        PropertySheetFactory m_PropertySheets;
        PostProcessResources m_Resources;

        List<RenderTexture> m_Recycled;
        List<RenderTexture> m_Actives;

        TextureLerper()
        {
            m_Recycled = new List<RenderTexture>();
            m_Actives = new List<RenderTexture>();
        }

        internal void BeginFrame(PostProcessRenderContext context)
        {
            m_Command = context.command;
            m_PropertySheets = context.propertySheets;
            m_Resources = context.resources;
        }

        internal void EndFrame()
        {
            // Release any remaining RT in the recycled list
            if (m_Recycled.Count > 0)
            {
                foreach (var rt in m_Recycled)
                    RuntimeUtilities.Destroy(rt);

                m_Recycled.Clear();
            }

            // There's a high probability that RTs will be requested in the same order on next
            // frame so keep them in the same order
            if (m_Actives.Count > 0)
            {
                m_Recycled.AddRange(m_Actives);

                m_Actives.Clear();
            }
        }

        RenderTexture Get(RenderTextureFormat format, int w, int h, int d = 1, bool enableRandomWrite = false, bool force3D = false)
        {
            RenderTexture rt = null;
            int i, len = m_Recycled.Count;

            for (i = 0; i < len; i++)
            {
                var r = m_Recycled[i];
                if (r.width == w && r.height == h && r.volumeDepth == d && r.format == format && r.enableRandomWrite == enableRandomWrite && (!force3D || (r.dimension == TextureDimension.Tex3D)))
                {
                    rt = r;
                    break;
                }
            }

            if (rt == null)
            {
                var dimension = (d > 1) || force3D
                    ? TextureDimension.Tex3D
                    : TextureDimension.Tex2D;

                rt = new RenderTexture(w, h, 0, format)
                {
                    dimension = dimension,
                    filterMode = FilterMode.Bilinear,
                    wrapMode = TextureWrapMode.Clamp,
                    anisoLevel = 0,
                    volumeDepth = d,
                    enableRandomWrite = enableRandomWrite
                };
                rt.Create();
            }
            else m_Recycled.RemoveAt(i);

            m_Actives.Add(rt);
            return rt;
        }

        internal Texture Lerp(Texture from, Texture to, float t)
        {
            Assert.IsNotNull(from);
            Assert.IsNotNull(to);
            Assert.AreEqual(from.width, to.width);
            Assert.AreEqual(from.height, to.height);

            // Saves a potentially expensive fullscreen blit when using dirt textures & the likes
            if (from == to)
                return from;

            // Don't need to lerp boundary conditions
            if (t <= 0f) return from;
            if (t >= 1f) return to;

            bool is3D = from is Texture3D
                || (from is RenderTexture && ((RenderTexture)from).volumeDepth > 1);

            RenderTexture rt;

            // 3D texture blending is a special case and only works on compute enabled platforms
            if (is3D)
            {
                int dpth = @from is Texture3D ? ((Texture3D)@from).depth : ((RenderTexture)@from).volumeDepth;
                int size = Mathf.Max(from.width, from.height);
                size = Mathf.Max(size, dpth);

                rt = Get(RenderTextureFormat.ARGBHalf, from.width, from.height, dpth, true, true);

                var compute = m_Resources.computeShaders.texture3dLerp;
                int kernel = compute.FindKernel("KTexture3DLerp");
                m_Command.SetComputeVectorParam(compute, "_DimensionsAndLerp", new Vector4(from.width, from.height, dpth, t));
                m_Command.SetComputeTextureParam(compute, kernel, "_Output", rt);
                m_Command.SetComputeTextureParam(compute, kernel, "_From", from);
                m_Command.SetComputeTextureParam(compute, kernel, "_To", to);

                uint tgsX, tgsY, tgsZ;
                compute.GetKernelThreadGroupSizes(kernel, out tgsX, out tgsY, out tgsZ);
                Assert.AreEqual(tgsX, tgsY);
                int groupSizeXY = Mathf.CeilToInt(size / (float)tgsX);
                int groupSizeZ = Mathf.CeilToInt(size / (float)tgsZ);

                m_Command.DispatchCompute(compute, kernel, groupSizeXY, groupSizeXY, groupSizeZ);
                return rt;
            }

            // 2D texture blending
            // We could handle textures with different sizes by picking the biggest one to avoid
            // popping effects. This would work in most cases but will still pop if one texture is
            // wider but shorter than the other. Generally speaking you're expected to use same-size
            // textures anyway so we decided not to handle this case at the moment, especially since
            // it would waste a lot of texture memory as soon as you start using bigger textures
            // (snow ball effect).
            var format = TextureFormatUtilities.GetUncompressedRenderTextureFormat(to);
            rt = Get(format, to.width, to.height);

            var sheet = m_PropertySheets.Get(m_Resources.shaders.texture2dLerp);
            sheet.properties.SetTexture(ShaderIDs.To, to);
            sheet.properties.SetFloat(ShaderIDs.Interp, t);

            m_Command.BlitFullscreenTriangle(from, rt, sheet, 0);

            return rt;
        }

        internal Texture Lerp(Texture from, Color to, float t)
        {
            Assert.IsNotNull(from);

            if (t < 0.00001)
                return from;

            bool is3D = from is Texture3D
                || (from is RenderTexture && ((RenderTexture)from).volumeDepth > 1);

            RenderTexture rt;

            // 3D texture blending is a special case and only works on compute enabled platforms
            if (is3D)
            {
                int dpth = @from is Texture3D ? ((Texture3D)@from).depth : ((RenderTexture)@from).volumeDepth;
                int size = Mathf.Max(from.width, from.height);
                size = Mathf.Max(size, dpth);

                rt = Get(RenderTextureFormat.ARGBHalf, from.width, from.height, dpth, true, true);

                var compute = m_Resources.computeShaders.texture3dLerp;
                int kernel = compute.FindKernel("KTexture3DLerpToColor");
                m_Command.SetComputeVectorParam(compute, "_DimensionsAndLerp", new Vector4(from.width, from.height, dpth, t));
                m_Command.SetComputeVectorParam(compute, "_TargetColor", new Vector4(to.r, to.g, to.b, to.a));
                m_Command.SetComputeTextureParam(compute, kernel, "_Output", rt);
                m_Command.SetComputeTextureParam(compute, kernel, "_From", from);

                int groupSize = Mathf.CeilToInt(size / 4f);
                m_Command.DispatchCompute(compute, kernel, groupSize, groupSize, groupSize);
                return rt;
            }

            // 2D texture blending
            // We could handle textures with different sizes by picking the biggest one to avoid
            // popping effects. This would work in most cases but will still pop if one texture is
            // wider but shorter than the other. Generally speaking you're expected to use same-size
            // textures anyway so we decided not to handle this case at the moment, especially since
            // it would waste a lot of texture memory as soon as you start using bigger textures
            // (snow ball effect).
            var format = TextureFormatUtilities.GetUncompressedRenderTextureFormat(from);
            rt = Get(format, from.width, from.height);

            var sheet = m_PropertySheets.Get(m_Resources.shaders.texture2dLerp);
            sheet.properties.SetVector(ShaderIDs.TargetColor, new Vector4(to.r, to.g, to.b, to.a));
            sheet.properties.SetFloat(ShaderIDs.Interp, t);

            m_Command.BlitFullscreenTriangle(from, rt, sheet, 1);

            return rt;
        }

        internal void Clear()
        {
            foreach (var rt in m_Actives)
                RuntimeUtilities.Destroy(rt);

            foreach (var rt in m_Recycled)
                RuntimeUtilities.Destroy(rt);

            m_Actives.Clear();
            m_Recycled.Clear();
        }
    }
}
