using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Experimental.TerrainAPI;
using UnityEngine.Experimental.TerrainAPI;
using UnityEngine;
using UObject = UnityEngine.Object;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;

namespace UnityEditor.Experimental.TerrainAPI
{
    public struct ActiveRenderTextureScope : System.IDisposable
    {
        RenderTexture m_Prev;

        public ActiveRenderTextureScope(RenderTexture rt)
        {
            m_Prev = RenderTexture.active;
            RenderTexture.active = rt;
        }

        public void Dispose()
        {
            // restore prev active RT
            RenderTexture.active = m_Prev;
        }
    }

    
    /// <summary>
    /// Utility class for safely managing the lifetime of a RenderTexture
    /// </summary>
    public class RTHandle
    {
        private RenderTexture   m_RT;
        private bool           m_IsTemp;
        
        /// <summary>
        /// The RenderTexture for this RTHandle
        /// </summary>
        public RenderTexture                RT      => m_RT;

        /// <summary>
        /// The descriptor for the RTHandle and RenderTexture
        /// </summary>
        public RenderTextureDescriptor     Desc    => m_RT?.descriptor ?? default;

        internal bool IsTemp => m_IsTemp;

        /// <summary>
        /// The name for the RTHandle and RenderTexture
        /// </summary>
        public string Name
        {
            get => m_RT?.name ?? default;
            set => m_RT.name = value;
        }

        internal RTHandle()
        {

        }

        /// <summary>
        /// Sets the name of the RenderTexture and returns the reference to the RTHandle
        /// </summary>
        /// <param name="name">The name of the underlying RenderTexture</param>
        /// <returns>Reference to the RTHandle</returns>
        public RTHandle WithName(string name)
        {
            Name = name;
            return this;
        }

        public static implicit operator RenderTexture(RTHandle handle)
        {
            return handle.RT;
        }

        public static implicit operator Texture(RTHandle handle)
        {
            return handle.RT;
        }

        internal void SetRenderTexture(RenderTexture rt, bool isTemp)
        {
            m_RT = rt;
            m_IsTemp = isTemp;
        }

        /// <summary>
        /// Structure for handling the lifetime of a RTHandle within a using block. RenderTexture is released when this structure is disposed
        /// </summary>
        public struct RTHandleScope : System.IDisposable
        {
            RTHandle m_Handle;

            internal RTHandleScope(RTHandle handle)
            {
                m_Handle = handle;
            }

            public void Dispose()
            {
                RTUtils.Release(m_Handle);
            }
        }

        /// <summary>
        /// Get a new disposable RTHandleScope instance to use in using blocks
        /// </summary>
        public RTHandleScope Scoped() => new RTHandleScope(this);
    }

    /// <summary>
    /// Utility class for getting and releasing RenderTextures handles.
    /// Lifetimes of these RenderTextures are tracked and any that have not been released within several frames are
    /// regarded as leaked RenderTexture resources and will generate warnings in the Console.
    /// </summary>
    public static class RTUtils
    {
        class Log
        {
            public int Frames;
            public string StackTrace;
        }
        
        internal static bool s_EnableStackTrace = false;
        private static Stack<Log> s_LogPool = new Stack<Log>();
        private static Dictionary<RTHandle, Log> s_Logs = new Dictionary<RTHandle, Log>();

        internal static int s_CreatedHandleCount;
        internal static int s_TempHandleCount;

        private static bool m_AgeCheckAdded;

        private static void AgeCheck()
        {
            if(!m_AgeCheckAdded)
            {
                Debug.LogError("Checking lifetime of RenderTextures but m_AgeCheckAdded = false");
            }

            foreach (var kvp in s_Logs)
            {
                var log = kvp.Value;

                if (log.Frames >= 4)
                {
                    var trace = !s_EnableStackTrace ? string.Empty : "\n" + log.StackTrace;
                    Debug.LogWarning($"RTHandle \"{kvp.Key.Name}\" has existed for more than 4 frames. Possible memory leak.{trace}");
                }
                
                log.Frames++;
            }
        }

        private static void CheckAgeCheck()
        {
            if(s_TempHandleCount != 0 || s_CreatedHandleCount != 0) return;

            Debug.Assert(s_Logs.Count == 0, "Internal RTHandle type counts for temporary and non-temporary RTHandles are both 0 but the containers for tracking leaked RTHandles have counts that are not 0");

            if(!m_AgeCheckAdded)
            {
                EditorApplication.update += AgeCheck;
                m_AgeCheckAdded = true;
            }
            else
            {
                EditorApplication.update -= AgeCheck;
                m_AgeCheckAdded = false;
            }
        }

        private static void AddLogForHandle(RTHandle handle)
        {
            var log = s_LogPool.Any() ? s_LogPool.Pop() : new Log();
            if(s_EnableStackTrace) log.StackTrace = System.Environment.StackTrace;
            s_Logs.Add(handle, log);
        }
        
        /// <summary>
        /// Get a RenderTextureDescriptor set up for RenderTexture operations on GPU
        /// <param name="width">Width of the RenderTexture</param>
        /// <param name="height">Height of the RenderTexture</param>
        /// <param name="format">RenderTextureFormat of the RenderTexture</param>
        /// <param name="depth">Depth of the RenderTexture</param>
        /// <param name="mipCount">MipCount of the RenderTexture. Default is 0</param>
        /// <param name="srgb">Flag determining whether RenderTextures created using this descriptor should be sRGB or Linear space</param>
        /// </summary>
        public static RenderTextureDescriptor GetDescriptor(int width, int height, int depth, RenderTextureFormat format, int mipCount = 0, bool srgb = false)
        {
            return GetDescriptor(width, height, depth, GraphicsFormatUtility.GetGraphicsFormat(format, srgb), mipCount, srgb);
        }

        /// <summary>
        /// Get a RenderTextureDescriptor set up for RenderTexture operations on GPU with the enableRandomWrite flag set to true
        /// <param name="width">Width of the RenderTexture</param>
        /// <param name="height">Height of the RenderTexture</param>
        /// <param name="format">RenderTextureFormat of the RenderTexture</param>
        /// <param name="depth">Depth of the RenderTexture</param>
        /// <param name="mipCount">MipCount of the RenderTexture. Default is 0</param>
        /// <param name="srgb">Flag determining whether RenderTextures created using this descriptor should be sRGB or Linear space</param>
        /// </summary>
        public static RenderTextureDescriptor GetDescriptorRW(int width, int height, int depth, RenderTextureFormat format, int mipCount = 0, bool srgb = false)
        {
            return GetDescriptorRW(width, height, depth, GraphicsFormatUtility.GetGraphicsFormat(format, srgb), mipCount, srgb);
        }
        
        /// <summary>
        /// Get a RenderTextureDescriptor set up for RenderTexture operations on GPU with the enableRandomWrite flag set to true
        /// <param name="width">Width of the RenderTexture</param>
        /// <param name="height">Height of the RenderTexture</param>
        /// <param name="format">GraphicsFormat of the RenderTexture</param>
        /// <param name="depth">Depth of the RenderTexture</param>
        /// <param name="mipCount">MipCount of the RenderTexture. Default is 0</param>
        /// <param name="srgb">Flag determining whether RenderTextures created using this descriptor should be sRGB or Linear space</param>
        /// </summary>
        public static RenderTextureDescriptor GetDescriptorRW(int width, int height, int depth, GraphicsFormat format, int mipCount = 0, bool srgb = false)
        {
            var desc = GetDescriptor(width, height, depth, format, mipCount, srgb);
            desc.enableRandomWrite = true;
            return desc;
        }

        /// <summary>
        /// Get a RenderTextureDescriptor set up for RenderTexture operations on GPU
        /// <param name="width">Width of the RenderTexture</param>
        /// <param name="height">Height of the RenderTexture</param>
        /// <param name="format">RenderTextureFormat of the RenderTexture</param>
        /// <param name="depth">Depth of the RenderTexture</param>
        /// <param name="mipCount">MipCount of the RenderTexture. Default is 0</param>
        /// <param name="srgb">Flag determining whether RenderTextures created using this descriptor should be sRGB or Linear space</param>
        /// </summary>
        public static RenderTextureDescriptor GetDescriptor(int width, int height, int depth, GraphicsFormat format, int mipCount = 0, bool srgb = false)
        {
            var desc = new RenderTextureDescriptor(width, height, format, depth)
            {
                sRGB = srgb,
                mipCount = mipCount,
                useMipMap = mipCount != 0,
            };

            return desc;
        }

        private static RTHandle GetHandle(RenderTextureDescriptor desc, bool isTemp)
        {
            CheckAgeCheck();

            if (isTemp) s_TempHandleCount++;
            else s_CreatedHandleCount++;
            
            var handle = new RTHandle();
            handle.SetRenderTexture(isTemp ? RenderTexture.GetTemporary(desc) : new RenderTexture(desc), isTemp);
            AddLogForHandle(handle);
            
            return handle;
        }
        
        /// <summary>
        /// Get a RTHandle for a RenderTexture acquired from RenderTexture.GetTemporary. Free using RTUtils.Release
        /// <param name="desc">RenderTextureDescriptor for the RenderTexture</param>
        /// </summary>
        public static RTHandle GetTempHandle(RenderTextureDescriptor desc)
        {
            return GetHandle(desc, true);
        }
        
        /// <summary>
        /// Get a RTHandle for a RenderTexture acquired with RenderTexture.GetTemporary. Free using RTUtils.Release
        /// </summary>
        /// <param name="width">Width of the RenderTexture</param>
        /// <param name="height">Height of the RenderTexture</param>
        /// <param name="depth">Depth of the RenderTexture</param>
        /// <param name="format">Format of the RenderTexture</param>
        public static RTHandle GetTempHandle(int width, int height, int depth, GraphicsFormat format)
        {
            return GetHandle(GetDescriptor(width, height, depth, format), true);
        }

        /// <summary>
        /// Get a RTHandle for a RenderTexture acquired with 'new RenderTexture(desc)'. Free using RTUtils.Release
        /// <param name="desc">RenderTextureDescriptor for the RenderTexture</param>
        /// </summary>
        public static RTHandle GetNewHandle(RenderTextureDescriptor desc)
        {
            return GetHandle(desc, false);
        }
        
        /// <summary>
        /// Get a RTHandle for a RenderTexture acquired with 'new RenderTexture(desc)'. Free using RTUtils.Release
        /// </summary>
        /// <param name="width">Width of the RenderTexture</param>
        /// <param name="height">Height of the RenderTexture</param>
        /// <param name="depth">Depth of the RenderTexture</param>
        /// <param name="format">Format of the RenderTexture</param>
        /// <returns></returns>
        public static RTHandle GetNewHandle(int width, int height, int depth, GraphicsFormat format)
        {
            return GetHandle(GetDescriptor(width, height, depth, format), false);
        }

        /// <summary>
        /// Release the RenderTexture resource associated with the specified RTHandle
        /// <param name="handle">RTHandle from which RenderTexture resources will be released</param>
        /// </summary>
        public static void Release(RTHandle handle)
        {
            if(handle.RT == null) return;
            
            if(!s_Logs.ContainsKey(handle)) throw new InvalidOperationException("Attemping to release a RTHandle that is not currently tracked by the system. This should never happen");

            var log = s_Logs[handle];
            s_Logs.Remove(handle);
            log.Frames = 0;
            log.StackTrace = null;
            
            s_LogPool.Push(log);

            if(handle.IsTemp)
            {
                --s_TempHandleCount;
                RenderTexture.ReleaseTemporary(handle.RT);
            }
            else
            {
                --s_CreatedHandleCount;
                handle.RT.Release();
                Destroy(handle.RT);
            }
            
            CheckAgeCheck();
        }

        /// <summary>
        /// Destroy a RenderTexture created using 'new RenderTexture()'
        /// <param name="rt">RenderTexture to destroy</param>
        /// </summary>
        public static void Destroy(RenderTexture rt)
        {
            if(rt == null) return;

#if UNITY_EDITOR
            if (Application.isPlaying)
                UObject.Destroy(rt);
            else
                UObject.DestroyImmediate(rt);
#else
            UObject.Destroy(rt);
#endif
        }

        /// <summary>
        /// Get the number of RTHandles that have been requested and not released yet.
        /// </summary>
        /// <returns>Number of RTHandles that have been requested and not released</returns>
        public static int GetHandleCount() => s_Logs.Count;
    }

    public static class Utility
    {
        static Material m_DefaultPreviewMat = null;
        public static Material GetDefaultPreviewMaterial()
        {
            if (m_DefaultPreviewMat == null)
            {
                m_DefaultPreviewMat = new Material(Shader.Find("Hidden/TerrainTools/BrushPreview"));
            }
            return m_DefaultPreviewMat;
        }

        private static Material m_TexelValidityMaterial;
        private static Material GetTexelValidityMaterial()
        {
            if(m_TexelValidityMaterial == null)
            {
                m_TexelValidityMaterial = new Material(Shader.Find("Hidden/TerrainTools/TexelValidityBlit"));
            }

            return m_TexelValidityMaterial;
        }

        public static void SetupMaterialForPainting(PaintContext paintContext, BrushTransform brushTransform, Material material)
        {
            TerrainPaintUtility.SetupTerrainToolMaterialProperties(paintContext, brushTransform, material);
            
            material.SetVector("_Heightmap_Tex",
	            new Vector4(
		            1f / paintContext.targetTextureWidth,
		            1f / paintContext.targetTextureHeight,
		            paintContext.targetTextureWidth,
		            paintContext.targetTextureHeight)
            );

            material.SetVector("_PcPixelRect",
	            new Vector4(paintContext.pixelRect.x,
		            paintContext.pixelRect.y,
		            paintContext.pixelRect.width,
		            paintContext.pixelRect.height)
            );

            material.SetVector("_PcUvVectors",
	            new Vector4(paintContext.pixelRect.x / (float)paintContext.targetTextureWidth,
				            paintContext.pixelRect.y / (float)paintContext.targetTextureHeight,
				            paintContext.pixelRect.width / (float)paintContext.targetTextureWidth,
				            paintContext.pixelRect.height / (float)paintContext.targetTextureHeight)
            );
        }

        public static void SetupMaterialForPaintingWithTexelValidityContext(PaintContext paintContext, PaintContext texelCtx, BrushTransform brushTransform, Material material)
        {
            SetupMaterialForPainting(paintContext, brushTransform, material);
            material.SetTexture("_PCValidityTex", texelCtx.sourceRenderTexture);
        }

        public static PaintContext CollectTexelValidity(Terrain terrain, Rect boundsInTerrainSpace, int extraBorderPixels = 0)
        {
            var res = terrain.terrainData.heightmapResolution;
            // use holes format because we really only need to know if the texel value is 0 or 1
            var ctx = PaintContext.CreateFromBounds(terrain, boundsInTerrainSpace, res, res, extraBorderPixels);
            ctx.CreateRenderTargets(Terrain.holesRenderTextureFormat);
            ctx.Gather(
                t => t.terrain.terrainData.heightmapTexture, // just provide heightmap texture. no need to create a temp one
                new Color(0, 0, 0, 0),
                blitMaterial : GetTexelValidityMaterial()
            );

            return ctx;
        }
        
        //assume this a 1D texture that has already been created
        public static Vector2 AnimationCurveToRenderTexture(AnimationCurve curve, ref Texture2D tex) {

            tex.wrapMode = TextureWrapMode.Clamp;
            tex.filterMode = FilterMode.Bilinear;

            float val = curve.Evaluate(0.0f);
            Vector2 range = new Vector2(val, val);

            Color[] pixels = new Color[tex.width * tex.height];
            pixels[0].r = val;
            for (int i = 1; i < tex.width; i++) {
                float pct = (float)i / (float)tex.width;
                pixels[i].r = curve.Evaluate(pct);
                range[0] = Mathf.Min(range[0], pixels[i].r);
                range[1] = Mathf.Max(range[1], pixels[i].r);
            }
            tex.SetPixels(pixels);
            tex.Apply();

            return range;
        }

        /// <summary>
        /// Set the filter render texture for transformation brushes
        /// </summary>
        public static void SetFilterRT(IBrushUIGroup commonUI, RenderTexture sourceRenderTexture, RenderTexture destinationRenderTexture, Material mat)
        {
            commonUI.GetBrushMask(sourceRenderTexture, destinationRenderTexture);
            mat.SetTexture("_FilterTex", destinationRenderTexture);
        }
    }

    public static class MeshUtils
    {
        public enum ShaderPass
        {
            Height = 0,
            Mask = 1,
        }

        private static Material m_defaultProjectionMaterial;
        public static Material defaultProjectionMaterial
        {
            get
            {
                if( m_defaultProjectionMaterial == null )
                {
                    m_defaultProjectionMaterial = new Material( Shader.Find( "Hidden/TerrainTools/MeshUtility" ) );
                }

                return m_defaultProjectionMaterial;
            }
        }

        public static Quaternion QuaternionFromMatrix(Matrix4x4 m)
        {
            // Adapted from: http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm
            Quaternion q = new Quaternion();
            q.w = Mathf.Sqrt( Mathf.Max( 0, 1 + m[0,0] + m[1,1] + m[2,2] ) ) / 2; 
            q.x = Mathf.Sqrt( Mathf.Max( 0, 1 + m[0,0] - m[1,1] - m[2,2] ) ) / 2; 
            q.y = Mathf.Sqrt( Mathf.Max( 0, 1 - m[0,0] + m[1,1] - m[2,2] ) ) / 2; 
            q.z = Mathf.Sqrt( Mathf.Max( 0, 1 - m[0,0] - m[1,1] + m[2,2] ) ) / 2; 
            q.x *= Mathf.Sign( q.x * ( m[2,1] - m[1,2] ) );
            q.y *= Mathf.Sign( q.y * ( m[0,2] - m[2,0] ) );
            q.z *= Mathf.Sign( q.z * ( m[1,0] - m[0,1] ) );
            return q;
        }

        public static Bounds TransformBounds( Matrix4x4 m, Bounds bounds )
        {
            Vector3[] points = new Vector3[ 8 ];

            // get points for each corner of the bounding box
            points[ 0 ] = new Vector3( bounds.max.x, bounds.max.y, bounds.max.z );
            points[ 1 ] = new Vector3( bounds.min.x, bounds.max.y, bounds.max.z );
            points[ 2 ] = new Vector3( bounds.max.x, bounds.min.y, bounds.max.z );
            points[ 3 ] = new Vector3( bounds.max.x, bounds.max.y, bounds.min.z );
            points[ 4 ] = new Vector3( bounds.min.x, bounds.min.y, bounds.max.z );
            points[ 5 ] = new Vector3( bounds.min.x, bounds.min.y, bounds.min.z );
            points[ 6 ] = new Vector3( bounds.max.x, bounds.min.y, bounds.min.z );
            points[ 7 ] = new Vector3( bounds.min.x, bounds.max.y, bounds.min.z );

            Vector3 min = Vector3.one * float.PositiveInfinity;
            Vector3 max = Vector3.one * float.NegativeInfinity;

            for( int i = 0; i < points.Length; ++i )
            {
                Vector3 p = m.MultiplyPoint( points[ i ] );

                // update min values
                if( p.x < min.x )
                {
                    min.x = p.x;
                }

                if( p.y < min.y )
                {
                    min.y = p.y;
                }

                if( p.z < min.z )
                {
                    min.z = p.z;
                }

                // update max values
                if( p.x > max.x )
                {
                    max.x = p.x;
                }

                if( p.y > max.y )
                {
                    max.y = p.y;
                }

                if( p.z > max.z )
                {
                    max.z = p.z;
                }
            }

            return new Bounds() { max = max, min = min };
        }

        private static string GetPrettyVectorString( Vector3 v )
        {
            return string.Format( "( {0}, {1}, {2} )", v.x, v.y, v.z );
        }

        public static void RenderTopdownProjection( Mesh mesh, Matrix4x4 model, RenderTexture destination, Material mat, ShaderPass pass )
        {
            RenderTexture prev = RenderTexture.active;
            RenderTexture.active = destination;
            
            Bounds modelBounds = TransformBounds( model, mesh.bounds );

            float nearPlane = ( modelBounds.max.y - modelBounds.center.y ) * 4;
            float farPlane =  ( modelBounds.min.y - modelBounds.center.y );

            Vector3 viewFrom = new Vector3( modelBounds.center.x, modelBounds.center.z, -modelBounds.center.y );
            Vector3 viewTo = viewFrom + Vector3.down;
            Vector3 viewUp = Vector3.forward;
            
//             Debug.Log(
// $@"Bounds =
// [
//     center: { modelBounds.center }
//     max: { modelBounds.max }
//     extents: { modelBounds.extents }
// ]
// nearPlane: { nearPlane }
// farPlane: { farPlane }
// diff: { nearPlane - farPlane }
// view: [ from = { GetPrettyVectorString( viewFrom ) }, to = { GetPrettyVectorString( viewTo ) }, up = { GetPrettyVectorString( viewUp ) } ]"
//             );

            // reset the view to accomodate for the transformed bounds
            Matrix4x4 view = Matrix4x4.LookAt( viewFrom, viewTo, viewUp );
            Matrix4x4 proj = Matrix4x4.Ortho( -1, 1, -1, 1, nearPlane, farPlane );
            Matrix4x4 mvp = proj * view * model;

            GL.Clear( true, true, Color.black );

            mat.SetMatrix( "_Matrix_M", model );
            mat.SetMatrix( "_Matrix_MV", view * model );
            mat.SetMatrix( "_Matrix_MVP", mvp );

            mat.SetPass( ( int )pass );
            GL.PushMatrix();
            {
                Graphics.DrawMeshNow( mesh, Matrix4x4.identity );
            }
            GL.PopMatrix();

            RenderTexture.active = prev;
        }
    }
}