using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using UnityEngine.Assertions;
using System.Runtime.CompilerServices;

namespace UnityEngine.Rendering.PostProcessing
{
    using SceneManagement;
    using UnityObject = UnityEngine.Object;

    public static class RuntimeUtilities
    {
        #region Textures

        static Texture2D m_WhiteTexture;
        public static Texture2D whiteTexture
        {
            get
            {
                if (m_WhiteTexture == null)
                {
                    m_WhiteTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false);
                    m_WhiteTexture.SetPixel(0, 0, Color.white);
                    m_WhiteTexture.Apply();
                }

                return m_WhiteTexture;
            }
        }

        static Texture2D m_BlackTexture;
        public static Texture2D blackTexture
        {
            get
            {
                if (m_BlackTexture == null)
                {
                    m_BlackTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false);
                    m_BlackTexture.SetPixel(0, 0, Color.black);
                    m_BlackTexture.Apply();
                }

                return m_BlackTexture;
            }
        }

        static Texture2D m_TransparentTexture;
        public static Texture2D transparentTexture
        {
            get
            {
                if (m_TransparentTexture == null)
                {
                    m_TransparentTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false);
                    m_TransparentTexture.SetPixel(0, 0, Color.clear);
                    m_TransparentTexture.Apply();
                }

                return m_TransparentTexture;
            }
        }

        #endregion

        #region Rendering  
        static Mesh s_FullscreenTriangle;
        public static Mesh fullscreenTriangle
        {
            get
            {
                if (s_FullscreenTriangle != null)
                    return s_FullscreenTriangle;

                s_FullscreenTriangle = new Mesh { name = "Fullscreen Triangle" };

                // Because we have to support older platforms (GLES2/3, DX9 etc) we can't do all of
                // this directly in the vertex shader using vertex ids :(
                s_FullscreenTriangle.SetVertices(new List<Vector3>
                {
                    new Vector3(-1f, -1f, 0f),
                    new Vector3(-1f,  3f, 0f),
                    new Vector3( 3f, -1f, 0f)
                });
                s_FullscreenTriangle.SetIndices(new [] { 0, 1, 2 }, MeshTopology.Triangles, 0, false);
                s_FullscreenTriangle.UploadMeshData(false);

                return s_FullscreenTriangle;
            }
        }

        static Material s_CopyStdMaterial;
        public static Shader s_CopyStdShader;
        public static Material copyStdMaterial
        {
            get
            {
                if (s_CopyStdMaterial != null)
                    return s_CopyStdMaterial;

                var shader = s_CopyStdShader;
                if (shader == null)
                    return null;
                s_CopyStdMaterial = new Material(shader)
                {
                    name = "PostProcess - CopyStd",
                    hideFlags = HideFlags.HideAndDontSave
                };

                return s_CopyStdMaterial;
            }
        }

        static Material s_CopyMaterial;
        
        public static Shader s_CopyShader;
        public static Material copyMaterial
        {
            get
            {
                if (s_CopyMaterial != null)
                    return s_CopyMaterial;

                var shader = s_CopyShader;
                s_CopyMaterial = new Material(shader)
                {
                    name = "PostProcess - Copy",
                    hideFlags = HideFlags.HideAndDontSave
                };

                return s_CopyMaterial;
            }
        }
        public static Shader s_CopyStencilShader;
        static Material s_CopyStencilMaterial;
        static int s_CopyStencilShaderID = -1;
        private static int s_BlendWeightTex = -1;
        public static int copyStencilShaderID
        {
            get
            {
                if(s_CopyStencilShaderID == -1)
                    s_CopyStencilShaderID = Shader.PropertyToID("_ExcludeID");
                return s_CopyStencilShaderID;
            }
        }

        public static int blendWeightTexID
        {
            get
            {
                if (s_BlendWeightTex == -1)
                    s_BlendWeightTex = Shader.PropertyToID("_BlendWeightTex");
                return s_BlendWeightTex;
            }
        }



        public static Material copyStencilMaterial
        {
            get
            {
                if(s_CopyStencilMaterial != null)
                    return s_CopyStencilMaterial;

                var shader = s_CopyStencilShader;
                s_CopyStencilMaterial = new Material(shader)
                {
                    name = "PostProcess - CopyStencil",
                    hideFlags = HideFlags.HideAndDontSave
                };
                
                return s_CopyStencilMaterial;
            }
        }
        //static PropertySheet s_CopySheet;
        //public static PropertySheet copySheet
        //{
        //    get
        //    {
        //        if (s_CopySheet == null)
        //            s_CopySheet = new PropertySheet(copyMaterial);

        //        return s_CopySheet;
        //    }
        //}

        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
        public static void SetRenderTargetDontCareColor(CommandBuffer cmd, RenderTargetIdentifier dest, bool useDontCare)
        {
            cmd.SetRenderTarget(dest);
        }
        // Use a custom blit method to draw a fullscreen triangle instead of a fullscreen quad
        // https://michaldrobot.com/2014/04/01/gcn-execution-patterns-in-full-screen-passes/
        public static void BlitFullscreenTriangle(this CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, bool clear = false, bool useDontCare = false)
        {
            cmd.SetGlobalTexture(ShaderIDs.MainTex, source);

            SetRenderTargetDontCareColor(cmd, destination, useDontCare);
            if (clear)
                cmd.ClearRenderTarget(true, true, Color.clear);

            cmd.DrawMesh(fullscreenTriangle, Matrix4x4.identity, copyMaterial, 0, 0);
        }
        public static void BlitFullscreenTriangle(this CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, RenderTargetIdentifier depth, int stencil,bool clear = false, Texture BlurMask = null)
        {
            cmd.SetGlobalTexture(ShaderIDs.MainTex, source);
            cmd.SetRenderTarget(destination, depth);

            if(clear)
                cmd.ClearRenderTarget(true, true, Color.clear);
            

            var pass = BlurMask == null ? 0 : 2;
            var stencil_mask = BlurMask == null ? stencil : 255;

            copyStencilMaterial.SetTexture(blendWeightTexID, BlurMask);
            copyStencilMaterial.SetInt(copyStencilShaderID, stencil_mask);
            cmd.DrawMesh(fullscreenTriangle, Matrix4x4.identity, copyStencilMaterial, 0, pass);
        }

        public static void BlitFullscreenTriangle(this CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, Material material, int pass, bool clear = false, bool useDontCare = false)
        {
            cmd.SetGlobalTexture(ShaderIDs.MainTex, source);
            SetRenderTargetDontCareColor(cmd, destination, useDontCare);

            if (clear)
                cmd.ClearRenderTarget(true, true, Color.clear);

            cmd.DrawMesh(fullscreenTriangle, Matrix4x4.identity, material, 0, pass, null);
        }

        public static void BlitFullscreenTriangle(this CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, RenderTargetIdentifier depth, Material material, int pass, bool clear = false)
        {
            cmd.SetGlobalTexture(ShaderIDs.MainTex, source);
            cmd.SetRenderTarget(destination, depth);

            if (clear)
                cmd.ClearRenderTarget(true, true, Color.clear);

            cmd.DrawMesh(fullscreenTriangle, Matrix4x4.identity, material, 0, pass, null);
        }

        public static void BlitFullscreenTriangle(this CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier[] destinations, RenderTargetIdentifier depth, Material material, int pass, bool clear = false)
        {
            cmd.SetGlobalTexture(ShaderIDs.MainTex, source);
            cmd.SetRenderTarget(destinations, depth);

            if (clear)
                cmd.ClearRenderTarget(true, true, Color.clear);

            cmd.DrawMesh(fullscreenTriangle, Matrix4x4.identity, material, 0, pass, null);
        }

        public static void BlitFullscreenTriangle(Texture source, RenderTexture destination, Material material, int pass)
        {
            var oldRt = RenderTexture.active;

            material.SetPass(pass);
            if (source != null)
                material.SetTexture(ShaderIDs.MainTex, source);

            Graphics.SetRenderTarget(destination);
            Graphics.DrawMeshNow(fullscreenTriangle, Matrix4x4.identity);
            RenderTexture.active = oldRt;
        }

        // Fast basic copy texture if available, falls back to blit copy if not
        // Assumes that both textures have the exact same type and format
        public static void CopyTexture(CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination)
        {
            if (SystemInfo.copyTextureSupport > CopyTextureSupport.None)
            {
                cmd.CopyTexture(source, destination);
                return;
            }

            cmd.BlitFullscreenTriangle(source, destination);
        }

        // TODO: Generalize the GetTemporaryRT and Blit commands in order to support
        // RT Arrays for Stereo Instancing/MultiView

#endregion

#region Unity specifics & misc methods

        public static bool scriptableRenderPipelineActive
        {
            get { return GraphicsSettings.renderPipelineAsset != null; } // 5.6+ only
        }

        public static bool supportsDeferredShading
        {
            get { return scriptableRenderPipelineActive || GraphicsSettings.GetShaderMode(BuiltinShaderType.DeferredShading) != BuiltinShaderMode.Disabled; }
        }

        public static bool supportsDepthNormals
        {
            get { return scriptableRenderPipelineActive || GraphicsSettings.GetShaderMode(BuiltinShaderType.DepthNormals) != BuiltinShaderMode.Disabled; }
        }

#if UNITY_EDITOR
        public static bool isSinglePassStereoSelected
        {
            get
            {
                return UnityEditor.PlayerSettings.virtualRealitySupported
                    && UnityEditor.PlayerSettings.stereoRenderingPath == UnityEditor.StereoRenderingPath.SinglePass;
            }
        }
#endif

        // TODO: Check for SPSR support at runtime
        public static bool isSinglePassStereoEnabled
        {
            get
            {
#if UNITY_EDITOR
                return isSinglePassStereoSelected && Application.isPlaying;
#else
                return false;
#endif
            }
        }

        public static bool isVREnabled
        {
            get
            {
#if UNITY_EDITOR
                return UnityEditor.PlayerSettings.virtualRealitySupported;
#else
                return false;
#endif
            }
        }

        public static bool isAndroidOpenGL
        {
            get { return Application.platform == RuntimePlatform.Android && SystemInfo.graphicsDeviceType != GraphicsDeviceType.Vulkan; }
        }

        public static void Destroy(UnityObject obj)
        {
            if (obj != null)
            {
#if UNITY_EDITOR
                if (Application.isPlaying)
                    Object.DestroyImmediate(obj, false);
                else
#endif
                    Object.Destroy(obj);
                obj = null;
            }
               
        }

        public static bool isLinearColorSpace
        {
            get { return QualitySettings.activeColorSpace == ColorSpace.Linear; }
        }

        public static bool IsResolvedDepthAvailable(Camera camera)
        {
            // AFAIK resolved depth is only available on D3D11/12 via BuiltinRenderTextureType.ResolvedDepth
            // TODO: Is there more proper way to determine this? What about SRPs?
            var gtype = SystemInfo.graphicsDeviceType;
            return camera.actualRenderingPath == RenderingPath.DeferredShading &&
                (gtype == GraphicsDeviceType.Direct3D11 || gtype == GraphicsDeviceType.Direct3D12 || gtype == GraphicsDeviceType.XboxOne);
        }

        //public static void DestroyProfile(PostProcessProfile profile, bool destroyEffects)
        //{
        //    if (destroyEffects)
        //    {
        //        foreach (var effect in profile.settings)
        //            Destroy(effect);
        //    }

        //    Destroy(profile);
        //}

        //public static void DestroyVolume(PostProcessVolume volume, bool destroySharedProfile)
        //{
        //    if (destroySharedProfile)
        //        DestroyProfile(volume.sharedProfile, true);

        //    Destroy(volume);
        //}

        // Returns ALL scene objects in the hierarchy, included inactive objects
        // Beware, this method will be slow for big scenes
        public static IEnumerable<T> GetAllSceneObjects<T>()
            where T : Component
        {
            var queue = new Queue<Transform>();
            var roots = SceneManager.GetActiveScene().GetRootGameObjects();

            foreach (var root in roots)
            {
                queue.Enqueue(root.transform);
                var comp = root.GetComponent<T>();

                if (comp != null)
                    yield return comp;
            }

            while (queue.Count > 0)
            {
                foreach (Transform child in queue.Dequeue())
                {
                    queue.Enqueue(child);
                    var comp = child.GetComponent<T>();

                    if (comp != null)
                        yield return comp;
                }
            }
        }

        public static void CreateIfNull<T>(ref T obj)
            where T : class, new()
        {
            if (obj == null)
                obj = new T();
        }

#endregion

#region Maths

        public static float Exp2(float x)
        {
            return Mathf.Exp(x * 0.69314718055994530941723212145818f);
        }

        // Adapted heavily from PlayDead's TAA code
        // https://github.com/playdeadgames/temporal/blob/master/Assets/Scripts/Extensions.cs
        public static Matrix4x4 GetJitteredPerspectiveProjectionMatrix(Camera camera, Vector2 offset)
        {
            float vertical = Mathf.Tan(0.5f * Mathf.Deg2Rad * camera.fieldOfView);
            float horizontal = vertical * camera.aspect;
            float near = camera.nearClipPlane;
            float far = camera.farClipPlane;

            offset.x *= horizontal / (0.5f * camera.pixelWidth);
            offset.y *= vertical / (0.5f * camera.pixelHeight);

            float left = (offset.x - horizontal) * near;
            float right = (offset.x + horizontal) * near;
            float top = (offset.y + vertical) * near;
            float bottom = (offset.y - vertical) * near;

            var matrix = new Matrix4x4();

            matrix[0, 0] = (2f * near) / (right - left);
            matrix[0, 1] = 0f;
            matrix[0, 2] = (right + left) / (right - left);
            matrix[0, 3] = 0f;

            matrix[1, 0] = 0f;
            matrix[1, 1] = (2f * near) / (top - bottom);
            matrix[1, 2] = (top + bottom) / (top - bottom);
            matrix[1, 3] = 0f;

            matrix[2, 0] = 0f;
            matrix[2, 1] = 0f;
            matrix[2, 2] = -(far + near) / (far - near);
            matrix[2, 3] = -(2f * far * near) / (far - near);

            matrix[3, 0] = 0f;
            matrix[3, 1] = 0f;
            matrix[3, 2] = -1f;
            matrix[3, 3] = 0f;

            return matrix;
        }

        public static Matrix4x4 GetJitteredOrthographicProjectionMatrix(Camera camera, Vector2 offset)
        {
            float vertical = camera.orthographicSize;
            float horizontal = vertical * camera.aspect;

            offset.x *= horizontal / (0.5f * camera.pixelWidth);
            offset.y *= vertical / (0.5f * camera.pixelHeight);

            float left = offset.x - horizontal;
            float right = offset.x + horizontal;
            float top = offset.y + vertical;
            float bottom = offset.y - vertical;

            return Matrix4x4.Ortho(left, right, bottom, top, camera.nearClipPlane, camera.farClipPlane);
        }

//        public static Matrix4x4 GenerateJitteredProjectionMatrixFromOriginal(PostProcessRenderContext context, Matrix4x4 origProj, Vector2 jitter)
//        {
//#if UNITY_2017_2_OR_NEWER
//            var planes = origProj.decomposeProjection;

//            float vertFov = Math.Abs(planes.top) + Math.Abs(planes.bottom);
//            float horizFov = Math.Abs(planes.left) + Math.Abs(planes.right);

//            var planeJitter = new Vector2(jitter.x * horizFov / context.screenWidth,
//                                          jitter.y * vertFov / context.screenHeight);

//            planes.left += planeJitter.x;
//            planes.right += planeJitter.x;
//            planes.top += planeJitter.y;
//            planes.bottom += planeJitter.y;

//            var jitteredMatrix = Matrix4x4.Frustum(planes);

//            return jitteredMatrix;
//#else
//            var rTan = (1.0f + origProj[0, 2]) / origProj[0, 0];
//            var lTan = (-1.0f + origProj[0, 2]) / origProj[0, 0];

//            var tTan = (1.0f + origProj[1, 2]) / origProj[1, 1];
//            var bTan = (-1.0f + origProj[1, 2]) / origProj[1, 1];

//            float tanVertFov = Math.Abs(tTan) + Math.Abs(bTan);
//            float tanHorizFov = Math.Abs(lTan) + Math.Abs(rTan);

//            jitter.x *= tanHorizFov / context.screenWidth;
//            jitter.y *= tanVertFov / context.screenHeight;

//            float left = jitter.x + lTan;
//            float right = jitter.x + rTan;
//            float top = jitter.y + tTan;
//            float bottom = jitter.y + bTan;

//            var jitteredMatrix = new Matrix4x4();

//            jitteredMatrix[0, 0] = 2f / (right - left);
//            jitteredMatrix[0, 1] = 0f;
//            jitteredMatrix[0, 2] = (right + left) / (right - left);
//            jitteredMatrix[0, 3] = 0f;

//            jitteredMatrix[1, 0] = 0f;
//            jitteredMatrix[1, 1] = 2f / (top - bottom);
//            jitteredMatrix[1, 2] = (top + bottom) / (top - bottom);
//            jitteredMatrix[1, 3] = 0f;

//            jitteredMatrix[2, 0] = 0f;
//            jitteredMatrix[2, 1] = 0f;
//            jitteredMatrix[2, 2] = origProj[2, 2];
//            jitteredMatrix[2, 3] = origProj[2, 3];

//            jitteredMatrix[3, 0] = 0f;
//            jitteredMatrix[3, 1] = 0f;
//            jitteredMatrix[3, 2] = -1f;
//            jitteredMatrix[3, 3] = 0f;

//            return jitteredMatrix;
//#endif
//        }

#endregion

#region Reflection

        public static IEnumerable<Type> GetAllAssemblyTypes()
        {
            return AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(t =>
                {
                    // Ugly hack to handle mis-versioned dlls
                    var innerTypes = new Type[0];
                    try
                    {
                        innerTypes = t.GetTypes();
                    }
                    catch {}
                    return innerTypes;
                });
        }

        // Quick extension method to get the first attribute of type T on a given Type
        public static T GetAttribute<T>(this Type type) where T : Attribute
        {
            Assert.IsTrue(type.IsDefined(typeof(T), false), "Attribute not found");
            return (T)type.GetCustomAttributes(typeof(T), false)[0];
        }

        // Returns all attributes set on a specific member
        // Note: doesn't include inherited attributes, only explicit ones
        public static Attribute[] GetMemberAttributes<TType, TValue>(Expression<Func<TType, TValue>> expr)
        {
            Expression body = expr;

            if (body is LambdaExpression)
                body = ((LambdaExpression)body).Body;

            switch (body.NodeType)
            {
                case ExpressionType.MemberAccess:
                    var fi = (FieldInfo)((MemberExpression)body).Member;
                    return fi.GetCustomAttributes(false).Cast<Attribute>().ToArray();
                default:
                    throw new InvalidOperationException();
            }
        }

        // Returns a string path from an expression - mostly used to retrieve serialized properties
        // without hardcoding the field path. Safer, and allows for proper refactoring.
        public static string GetFieldPath<TType, TValue>(Expression<Func<TType, TValue>> expr)
        {
            MemberExpression me;
            switch (expr.Body.NodeType)
            {
                case ExpressionType.MemberAccess:
                    me = expr.Body as MemberExpression;
                    break;
                default:
                    throw new InvalidOperationException();
            }

            var members = new List<string>();
            while (me != null)
            {
                members.Add(me.Member.Name);
                me = me.Expression as MemberExpression;
            }

            var sb = new StringBuilder();
            for (int i = members.Count - 1; i >= 0; i--)
            {
                sb.Append(members[i]);
                if (i > 0) sb.Append('.');
            }

            return sb.ToString();
        }

        public static object GetParentObject(string path, object obj)
        {
            var fields = path.Split('.');

            if (fields.Length == 1)
                return obj;

            var info = obj.GetType().GetField(fields[0], BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            obj = info.GetValue(obj);

            return GetParentObject(string.Join(".", fields, 1, fields.Length - 1), obj);
        }

#endregion
    }
}
