// Stylized Water 2
// Staggart Creations (http://staggart.xyz)
// Copyright protected under Unity Asset Store EULA
// Copying or referencing source code for the production of new asset store content is strictly prohibited.

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

#if URP
using UnityEngine.Rendering.Universal;
#endif

namespace StylizedWater2
{
    [ExecuteInEditMode]
    [AddComponentMenu("Stylized Water 2/Planar Reflection Renderer")]
    [HelpURL("https://staggart.xyz/unity/stylized-water-2/sws-2-docs/?section=planar-reflections")]
    public class PlanarReflectionRenderer : MonoBehaviour
    {
#if URP
        public static List<PlanarReflectionRenderer> Instances = new List<PlanarReflectionRenderer>();
        public Dictionary<Camera, Camera> reflectionCameras = new Dictionary<Camera, Camera>();
        
        //Rendering
        [Tooltip("If enabled, the reflection plane will be based on this transform's up vector (green arrow).\n\nOtherwise the world's upwards direction is assumed")]
        public bool rotatable = false;
        [Tooltip("Set the layers that should be rendered into the reflection. The \"Water\" layer is always excluded")]
        public LayerMask cullingMask = -1;
        [Tooltip("The renderer used by the reflection camera. It's recommend to create a separate renderer, so any custom render features aren't executed for the reflection")]
        public int rendererIndex = -1;

        [Min(0f)]
        public float offset = 0.05f;
        [Tooltip("When disabled, the skybox reflection comes from a Reflection Probe. This has the benefit of being omni-directional rather than flat/planar. Enabled this to render the skybox into the planar reflection anyway." +
                 "\n\nNote that enabling this will override Screen Space Reflections completely!")]
        public bool includeSkybox;
        [Tooltip("Render Unity's default scene fog in the reflection. Note that this doesn't strictly work correctly on large triangles, as it is incompatible with oblique camera projections." +
                 "\n\n" +
                 "This does not include to post-processing fog effects!")]
        public bool enableFog;

        //Quality
        public bool renderShadows;
        [Tooltip("Objects beyond this range aren't rendered into the reflection. Note that this may causes popping for large/tall objects.")]
		public float renderRange = 500f;
        [Range(0.25f, 1f)] 
        [Tooltip("A multiplier for the rendering resolution, based on the current screen resolution. The render scale, as configured in the pipeline settings is multiplied over this.")]
		public float renderScale = 0.75f;

        [Range(0, 4)]
        [Tooltip("Do not render LOD objects lower than this value. Example: With a value of 1, LOD0 for LOD Groups will not be used")]
        public int maximumLODLevel = 0;
        
        [SerializeField]
        public List<WaterObject> waterObjects = new List<WaterObject>();
        [Tooltip("If enabled, the center of the rendering bounds (that wraps around the water objects) moves with the Transform position" +
                 "\n\nYou must however ensure you are only moving on the XZ axis")]
        public bool moveWithTransform;
        [HideInInspector]
        public Bounds bounds = new Bounds();

        private float m_renderScale = 1f;
        private float m_renderRange;

        /// <summary>
        /// Reflections will only render if this is true. Value can be set through the static SetQuality function
        /// </summary>
        public static bool AllowReflections { get; private set; } = true;

        private static readonly int _PlanarReflectionsEnabledID = Shader.PropertyToID("_PlanarReflectionsEnabled");
        private static readonly int _PlanarReflectionID = Shader.PropertyToID("_PlanarReflection");
		
        #if UNITY_2023_1_OR_NEWER
        private UniversalRenderPipeline.SingleCameraRequest requestData;
        #endif
        
        [NonSerialized]
        public bool isRendering;

        private Camera m_reflectionCamera;
		private static UniversalAdditionalCameraData m_cameraData;
        
        private void Reset()
        {
            this.gameObject.name = "Planar Reflection Renderer";
        }
        
        private void OnEnable()
        {
            InitializeValues();

            Instances.Add(this);
            EnableReflections();
        }

        private void OnDisable()
        {
            Instances.Remove(this);
            DisableReflections();
        }

        public void InitializeValues()
        {
            m_renderScale = renderScale;
            m_renderRange = renderRange;
        }

        /// <summary>
        /// Assigns all Water Objects in the WaterObject.Instances list and enables reflection for them
        /// </summary>
        public void ApplyToAllWaterInstances()
        {
            waterObjects = new List<WaterObject>(WaterObject.Instances);
            RecalculateBounds();
            EnableMaterialReflectionSampling();
        }

        /// <summary>
        /// Toggle reflections or set the render scale for all reflection renderers. This can be tied into performance scaling or graphics settings in menus
        /// </summary>
        /// <param name="enableReflections">Toggles rendering of reflections, and toggles it on all the assigned water objects</param>
        /// <param name="renderScale">A multiplier for the current screen resolution. Note that the render scale configured in URP is also taken into account</param>
        /// <param name="renderRange">Objects beyond this range aren't rendered into the reflection</param>
        public static void SetQuality(bool enableReflections, float renderScale = -1f, float renderRange = -1f, int maxLodLevel = -1)
        {
            AllowReflections = enableReflections;
            
            foreach (PlanarReflectionRenderer renderer in Instances)
            {
                if (renderScale > 0) renderer.renderScale = renderScale;
                if (renderRange > 0) renderer.renderRange = renderRange;
                if (maxLodLevel >= 0) renderer.maximumLODLevel = maxLodLevel;
                renderer.InitializeValues();

                if (enableReflections) renderer.EnableReflections();
                if (!enableReflections) renderer.DisableReflections();
            }
        }

        public void EnableReflections()
        {
            if (!AllowReflections || PipelineUtilities.VREnabled()) return;

            RenderPipelineManager.beginCameraRendering += OnWillRenderCamera;
            ToggleMaterialReflectionSampling(true);
        }

        public void DisableReflections()
        {
            RenderPipelineManager.beginCameraRendering -= OnWillRenderCamera;
            ToggleMaterialReflectionSampling(false);

            //Clear cameras
            foreach (var kvp in reflectionCameras)
            {
                if (kvp.Value == null) continue;

                if (kvp.Value)
                {
                    RenderTexture.ReleaseTemporary(kvp.Value.targetTexture);
                    
                    DestroyImmediate(kvp.Value.gameObject);
                }
            }

            reflectionCameras.Clear();
        }

        private void OnDrawGizmosSelected()
        {
            Gizmos.color = bounds.size.y > 0.01f ? Color.yellow : Color.white;
            Gizmos.DrawWireCube(bounds.center, bounds.size);
        }

        public Bounds CalculateBounds()
        {
            Bounds m_bounds = new Bounds(Vector3.zero, Vector3.zero);
            
            if (waterObjects == null) return m_bounds;
            if (waterObjects.Count == 0) return m_bounds;

            Vector3 minSum = Vector3.one * Mathf.Infinity;
            Vector3 maxSum = Vector3.one * Mathf.NegativeInfinity;
            
            for (int i = 0; i < waterObjects.Count; i++)
            {
                if (!waterObjects[i]) continue;
                    
                minSum = Vector3.Min(waterObjects[i].meshRenderer.bounds.min, minSum);
                maxSum = Vector3.Max(waterObjects[i].meshRenderer.bounds.max, maxSum);
            }
            
            m_bounds.SetMinMax(minSum, maxSum);

            //Flatten to center
            m_bounds.size = new Vector3(m_bounds.size.x, 0f, m_bounds.size.z);

            return m_bounds;
        }

        public void RecalculateBounds()
        {
            bounds = CalculateBounds();
        }

        public static bool InvalidContext(Camera camera)
        {
            #if UNITY_EDITOR
            //Avoid the "Screen position outside of frustrum" error
            if (camera.orthographic && Vector3.Dot(Vector3.up, camera.transform.up) > 0.9999f) return true;
            
            #if UNITY_2021_2_OR_NEWER
            //Causes an internal error in URP's rendering code in the CopyColorPass
            if (camera.cameraType == CameraType.SceneView && UnityEditor.SceneView.lastActiveSceneView && UnityEditor.SceneView.lastActiveSceneView.isUsingSceneFiltering) return true;
            #endif
            #endif
            
            //Skip for any special use camera's (except scene view camera)
            //Note: Scene camera still rendering even if window not focused!
            return (camera.cameraType != CameraType.SceneView && (camera.cameraType == CameraType.Reflection || camera.cameraType == CameraType.Preview || camera.hideFlags != HideFlags.None));
        }

        private void OnWillRenderCamera(ScriptableRenderContext context, Camera camera)
        {
            if (InvalidContext(camera))
            {
                isRendering = false;
                return;
            }

            isRendering = WaterObjectsVisible(camera);
            
            if (isRendering == false) return;
            
            
            if (moveWithTransform) bounds.center = this.transform.position;

            m_cameraData = camera.GetComponent<UniversalAdditionalCameraData>();
            if (m_cameraData && m_cameraData.renderType == CameraRenderType.Overlay) return;

            reflectionCameras.TryGetValue(camera, out m_reflectionCamera);
            if (m_reflectionCamera == null) CreateReflectionCamera(camera);
            
            //It's possible it is destroyed at this point when disabling reflections
            if (!m_reflectionCamera) return;
            
            UnityEngine.Profiling.Profiler.BeginSample("Planar Water Reflections", camera);

            //Render scale changed
            if (Math.Abs(renderScale - m_renderScale) > 0.001f)
            {
                RenderTexture.ReleaseTemporary(m_reflectionCamera.targetTexture);
                CreateRenderTexture(m_reflectionCamera, camera);

                m_renderScale = renderScale;
            }

            UpdateWaterProperties(m_reflectionCamera);
            
            UpdateCameraProperties(camera, m_reflectionCamera);
            UpdatePerspective(camera, m_reflectionCamera);
            
            bool fogEnabled = RenderSettings.fog && !enableFog;
            //Fog is based on clip-space z-distance and doesn't work with oblique projections
            if (fogEnabled) SetFogState(false);
            int maxLODLevel = QualitySettings.maximumLODLevel;
            QualitySettings.maximumLODLevel = maximumLODLevel;
            GL.invertCulling = true;

            RenderReflection(context, m_reflectionCamera);

            if (fogEnabled) SetFogState(true);
            QualitySettings.maximumLODLevel = maxLODLevel;
            GL.invertCulling = false;

            UnityEngine.Profiling.Profiler.EndSample();
        }

        private void RenderReflection(ScriptableRenderContext context, Camera target)
        {
            /* Uncomment to render NR's vegetation in the reflection
            // Register the reflection camera for Nature Renderer
            var cameraId = VisualDesignCafe.Rendering.Instancing.RendererPool.RegisterCamera(target);

            // Render the instanced objects (details and trees)
            VisualDesignCafe.Rendering.Instancing.RendererPool.GetCamera(cameraId).Render();
            */
            
#pragma warning disable 0618
#if UNITY_2023_1_OR_NEWER
            /*
            requestData = new UniversalRenderPipeline.SingleCameraRequest();
            requestData.destination = target.targetTexture;
            requestData.slice = -1;

            //Throws the 'Recursive rendering is not supported in SRP (are you calling Camera.Render from within a render pipeline?).' error.
            if (RenderPipeline.SupportsRenderRequest(target, requestData))
            {
                RenderPipeline.SubmitRenderRequest(target, requestData);
            }
            */
            
            //Instead, Unity will whine about using an obsolete API.
            UniversalRenderPipeline.RenderSingleCamera(context, target);
            
            //So then what?
#else
            UniversalRenderPipeline.RenderSingleCamera(context, target);
#endif
#pragma warning restore 0618
        }

        private void SetFogState(bool value)
        {
            #if UNITY_EDITOR
            UnityEditor.Unsupported.SetRenderSettingsUseFogNoDirty(value);
            #else
            RenderSettings.fog = value;
            #endif
        }

        private float GetRenderScale()
        {
            return Mathf.Clamp(renderScale * UniversalRenderPipeline.asset.renderScale, 0.25f, 1f);
        }

        /// <summary>
        /// Should the renderer index be changed at runtime, this function must be called to update any reflection cameras
        /// </summary>
        /// <param name="index"></param>
        public void SetRendererIndex(int index)
        {
            index = PipelineUtilities.ValidateRenderer(index);

            foreach (var kvp in reflectionCameras)
            {
                if (kvp.Value == null) continue;
                
                m_cameraData = kvp.Value.GetComponent<UniversalAdditionalCameraData>();
                m_cameraData.SetRenderer(index);
            }
        }

        public void ToggleShadows(bool state)
        {
            foreach (var kvp in reflectionCameras)
            {
                if (kvp.Value == null) continue;
                
                m_cameraData = kvp.Value.GetComponent<UniversalAdditionalCameraData>();
                m_cameraData.renderShadows = state;
            }
        }
        
        /// <summary>
        /// Add the WaterObject, and recalculates the rendering bounds.
        /// </summary>
        /// <param name="waterObject"></param>
        public void AddWaterObject(WaterObject waterObject)
        {
            ToggleMaterialReflectionSampling(waterObject, true);
            waterObjects.Add(waterObject);

            RecalculateBounds();
        }
        
        /// <summary>
        /// Remove the WaterObject, and recalculates the rendering bounds.
        /// </summary>
        /// <param name="waterObject"></param>
        public void RemoveWaterObject(WaterObject waterObject)
        {
            ToggleMaterialReflectionSampling(waterObject, false);
            waterObjects.Remove(waterObject);
            
            RecalculateBounds();
        }
        
        /// <summary>
        /// Enables planar reflections on the MeshRenderers of the assigned water objects
        /// </summary>
        public void EnableMaterialReflectionSampling()
        {
            ToggleMaterialReflectionSampling(AllowReflections);
        }
        
        /// <summary>
        /// Toggles the sampling of the planar reflections texture in the water shader.
        /// </summary>
        /// <param name="state"></param>
        public void ToggleMaterialReflectionSampling(bool state)
        {
            if (waterObjects == null) return;

            for (int i = 0; i < waterObjects.Count; i++)
            {
                if (waterObjects[i] == null) continue;
                
                ToggleMaterialReflectionSampling(waterObjects[i], state);
            }
        }

        private void ToggleMaterialReflectionSampling(WaterObject waterObject, bool state)
        {
            waterObject.props.SetFloat(_PlanarReflectionsEnabledID, state ? 1f : 0f);
            waterObject.ApplyInstancedProperties();
        }

        private void CreateReflectionCamera(Camera source)
        {
            //Object creation
            GameObject go = new GameObject($"{source.name} Planar Reflection");
            go.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy;
            
            Camera newCamera = go.AddComponent<Camera>();
            newCamera.hideFlags = HideFlags.DontSave;
            //For the scene-view camera this also copies unwanted properties. Such as the camera type and background color!
            newCamera.CopyFrom(source);
            
            //Always exclude water layer
            newCamera.cullingMask = ~(1 << 4) & cullingMask;
            //Must always be set to Game, otherwise shadows render anyway
            newCamera.cameraType = CameraType.Game;
            newCamera.depth = source.depth-1f;
            newCamera.rect = new Rect(0,0,1,1);
            newCamera.enabled = false;
            newCamera.clearFlags = includeSkybox ? CameraClearFlags.Skybox : CameraClearFlags.Depth;
            //Required to maintain the alpha channel for the scene view
            newCamera.backgroundColor = Color.clear;
            
            //Occlusion culling has to be disabled, otherwise objects culled by the main camera will be culled for the reflection camera
            //Setting the culling matrix for the camera doesn't appear to have any effect
            newCamera.useOcclusionCulling = false;

            //Component required for the UniversalRenderPipeline.RenderSingleCamera call
            UniversalAdditionalCameraData data = newCamera.gameObject.AddComponent<UniversalAdditionalCameraData>();
            data.requiresDepthTexture = false;
            data.requiresColorTexture = false;
            data.renderShadows = renderShadows;
            rendererIndex = PipelineUtilities.ValidateRenderer(rendererIndex);
            data.SetRenderer(rendererIndex);

            CreateRenderTexture(newCamera, source);
            
            reflectionCameras[source] = newCamera;
        }

        private void CreateRenderTexture(Camera targetCamera, Camera source)
        {
            //Note: Do not use RenderTextureFormat.Default or HDR, as these may be without an alpha channel on some platforms
            RenderTextureFormat colorFormat = UniversalRenderPipeline.asset.supportsHDR && SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.ARGBHalf) ? RenderTextureFormat.ARGBHalf : RenderTextureFormat.ARGB32;

            float scale = GetRenderScale();

            RenderTextureDescriptor rtDsc = new RenderTextureDescriptor(
                (int)((float)source.scaledPixelWidth * scale),
                (int)((float)source.scaledPixelHeight * scale), 
                colorFormat);

            rtDsc.depthBufferBits = 16;
            //rtDsc.msaaSamples = UniversalRenderPipeline.asset.msaaSampleCount; //Waste of resources, water distortion makes it virtually unnoticeable.
            
            targetCamera.targetTexture = RenderTexture.GetTemporary(rtDsc);
            targetCamera.targetTexture.filterMode = scale < 1f ? FilterMode.Bilinear : FilterMode.Point;
            targetCamera.targetTexture.name = $"{source.name}_Reflection {rtDsc.width}x{rtDsc.height}";
        }
        
        private static readonly Plane[] frustrumPlanes = new Plane[6];
        
        public bool WaterObjectsVisible(Camera targetCamera)
        {
            GeometryUtility.CalculateFrustumPlanes(targetCamera.projectionMatrix * targetCamera.worldToCameraMatrix, frustrumPlanes);

            return GeometryUtility.TestPlanesAABB(frustrumPlanes, bounds);
        }

        //Assigns the render target of the current reflection camera
        private void UpdateWaterProperties(Camera cam)
        {
            for (int i = 0; i < waterObjects.Count; i++)
            {
                if (waterObjects[i] == null) continue;
                
                waterObjects[i].props.SetTexture(_PlanarReflectionID, cam.targetTexture);
                waterObjects[i].ApplyInstancedProperties();
            }
        }

        private static Vector4 reflectionPlane;
        private static Matrix4x4 reflectionBase;
        private static Vector3 oldCamPos;

        private static Matrix4x4 worldToCamera;
        private static Matrix4x4 viewMatrix;
        private static Matrix4x4 projectionMatrix;
        private static Vector4 clipPlane;
        private static readonly float[] layerCullDistances = new float[32];

        private void UpdateCameraProperties(Camera source, Camera reflectionCam)
        {
            reflectionCam.fieldOfView = source.fieldOfView;
            reflectionCam.orthographic = source.orthographic;
            reflectionCam.orthographicSize = source.orthographicSize;
        }

        private void UpdatePerspective(Camera source, Camera reflectionCam)
        {
            if (!source || !reflectionCam) return;

            Vector3 normal = rotatable ? this.transform.up : Vector3.up;
            
            Vector3 position = bounds.center + (normal * offset);

            var d = -Vector3.Dot(normal, position);
            reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);

            reflectionBase = Matrix4x4.identity;
            reflectionBase *= Matrix4x4.Scale(new Vector3(1, -1, 1));

            // View
            CalculateReflectionMatrix(ref reflectionBase, reflectionPlane);
            oldCamPos = source.transform.position - new Vector3(0, position.y * 2, 0);
            reflectionCam.transform.forward = Vector3.Scale(source.transform.forward, new Vector3(1, -1, 1));

            worldToCamera = source.worldToCameraMatrix;
            viewMatrix = worldToCamera * reflectionBase;

            //Reflect position
            oldCamPos.y = -oldCamPos.y;
            reflectionCam.transform.position = oldCamPos;

            clipPlane = CameraSpacePlane(reflectionCam.worldToCameraMatrix, position - normal * 0.1f, normal, 1.0f);
            projectionMatrix = source.CalculateObliqueMatrix(clipPlane);
            
            //Settings
            reflectionCam.cullingMask = ~(1 << 4) & cullingMask;;
            m_reflectionCamera.clearFlags = includeSkybox ? CameraClearFlags.Skybox : CameraClearFlags.Depth;
            
            #if !UNITY_2023_3_OR_NEWER
            //Only re-apply on value change
            if (m_renderRange != renderRange)
            {
                m_renderRange = renderRange;
                
                for (int i = 0; i < layerCullDistances.Length; i++)
                {
                    layerCullDistances[i] = renderRange;
                }
            }
            reflectionCam.layerCullDistances = layerCullDistances;
            reflectionCam.layerCullSpherical = true;
            #endif

            reflectionCam.projectionMatrix = projectionMatrix;
            reflectionCam.worldToCameraMatrix = viewMatrix;

            //Unfortunately has to effect, camera appears ti use the culling matrix from the source camera anyway
            //reflectionCam.cullingMatrix = projectionMatrix * viewMatrix;
        }

        // Calculates reflection matrix around the given plane
        private void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane)
        {
            reflectionMat.m00 = (1F - 2F * plane[0] * plane[0]);
            reflectionMat.m01 = (-2F * plane[0] * plane[1]);
            reflectionMat.m02 = (-2F * plane[0] * plane[2]);
            reflectionMat.m03 = (-2F * plane[3] * plane[0]);

            reflectionMat.m10 = (-2F * plane[1] * plane[0]);
            reflectionMat.m11 = (1F - 2F * plane[1] * plane[1]);
            reflectionMat.m12 = (-2F * plane[1] * plane[2]);
            reflectionMat.m13 = (-2F * plane[3] * plane[1]);

            reflectionMat.m20 = (-2F * plane[2] * plane[0]);
            reflectionMat.m21 = (-2F * plane[2] * plane[1]);
            reflectionMat.m22 = (1F - 2F * plane[2] * plane[2]);
            reflectionMat.m23 = (-2F * plane[3] * plane[2]);

            reflectionMat.m30 = 0F;
            reflectionMat.m31 = 0F;
            reflectionMat.m32 = 0F;
            reflectionMat.m33 = 1F;
        }
        
        // Given position/normal of the plane, calculates plane in camera space.
        private Vector4 CameraSpacePlane(Matrix4x4 worldToCameraMatrix, Vector3 pos, Vector3 normal, float sideSign)
        {
            var offsetPos = pos + normal * offset;
            var cameraPosition = worldToCameraMatrix.MultiplyPoint(offsetPos);
            var cameraNormal = worldToCameraMatrix.MultiplyVector(normal).normalized * sideSign;
            return new Vector4(cameraNormal.x, cameraNormal.y, cameraNormal.z,
                -Vector3.Dot(cameraPosition, cameraNormal));
        }

        public RenderTexture TryGetReflectionTexture(Camera targetCamera)
        {
            if (targetCamera)
            {
                reflectionCameras.TryGetValue(targetCamera, out m_reflectionCamera);
                if (m_reflectionCamera)
                {
                    return m_reflectionCamera.targetTexture;
                }
            }

            return null;
        }
#endif
    }
}
