// Stylized Water 3 by Staggart Creations (http://staggart.xyz)
// COPYRIGHT PROTECTED UNDER THE UNITY ASSET STORE EULA (https://unity.com/legal/as-terms)
//    • Copying or referencing source code for the production of new asset store, or public, content is strictly prohibited!
//    • Uploading this file to a public repository will subject it to an automated DMCA takedown request.

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.XR;

namespace StylizedWater3
{
    [CustomEditor(typeof(PlanarReflectionRenderer))]
    public class PlanarReflectionRendererInspector : Editor
    {
        private PlanarReflectionRenderer renderer;
        
        //Rendering
        private SerializedProperty rotatable;
        private SerializedProperty cullingMask;
        private SerializedProperty rendererIndex;
        private SerializedProperty offset;
        private SerializedProperty includeSkybox;
        private SerializedProperty enableFog;
        private SerializedProperty enableInSceneView;
        
        //Quality
        private SerializedProperty renderShadows;
        private SerializedProperty renderScale;
        private SerializedProperty maximumLODLevel;
        
        private SerializedProperty waterObjects;
        private SerializedProperty moveWithTransform;

        private Bounds curBounds;
        private bool waterLayerError;
        private bool gpuOcclusionCulling;

        private bool previewReflection
        {
            get => EditorPrefs.GetBool("SWS2_PREVIEW_REFLECTION_ENABLED", true);
            set => EditorPrefs.SetBool("SWS2_PREVIEW_REFLECTION_ENABLED", value);
        }
        private RenderTexture previewTexture;

#if URP
        private void OnEnable()
        {
            PipelineUtilities.RefreshRendererList();
            
            renderer = (PlanarReflectionRenderer)target;

            rotatable = serializedObject.FindProperty("rotatable");
            cullingMask = serializedObject.FindProperty("cullingMask");
            rendererIndex = serializedObject.FindProperty("rendererIndex");
            offset = serializedObject.FindProperty("offset");
            includeSkybox = serializedObject.FindProperty("includeSkybox");
            enableFog = serializedObject.FindProperty("enableFog");
            enableInSceneView = serializedObject.FindProperty("enableInSceneView");
            renderShadows = serializedObject.FindProperty("renderShadows");
            renderScale = serializedObject.FindProperty("renderScale");
            maximumLODLevel = serializedObject.FindProperty("maximumLODLevel");
            waterObjects = serializedObject.FindProperty("waterObjects");
            moveWithTransform = serializedObject.FindProperty("moveWithTransform");
            
            if (renderer.waterObjects.Count == 0 && WaterObject.Instances.Count == 1)
            {
                renderer.waterObjects.Add(WaterObject.Instances[0]);
                renderer.RecalculateBounds();
                renderer.EnableMaterialReflectionSampling();
                
                EditorUtility.SetDirty(target);
                serializedObject.ApplyModifiedPropertiesWithoutUndo();
            }

            ValidateWaterObjectLayer();

            curBounds = renderer.CalculateBounds();
            gpuOcclusionCulling = UniversalRenderPipeline.asset.gpuResidentDrawerEnableOcclusionCullingInCameras;

            RenderPipelineManager.endCameraRendering += OnEndCameraRendering;
        }

        private Camera currentCamera;
        private string currentCameraName;
        private bool waterObjectsVisible;
        
        private void OnEndCameraRendering(ScriptableRenderContext context, Camera camera)
        {
            if (!previewReflection) return;

            if (renderer.InvalidContext(camera)) return;

            currentCamera = camera;
            
            waterObjectsVisible = renderer.WaterObjectsVisible(currentCamera);
            
            previewTexture = renderer.TryGetReflectionTexture(currentCamera);
            currentCameraName = currentCamera.name;
        }
        
        private void OnDisable()
        {
            RenderPipelineManager.endCameraRendering -= OnEndCameraRendering;
        }
#endif

        public override void OnInspectorGUI()
        {
#if !URP
            UI.DrawNotification("The Universal Render Pipeline package v" + AssetInfo.MIN_URP_VERSION + " or newer is not installed", MessageType.Error);
#else
            UI.DrawHeader();

            if (ShaderUtil.anythingCompiling)
            {
                EditorGUILayout.HelpBox("Rendering is temporarily paused, while the editor finishes compiling shaders", MessageType.Info);
            }
            
            UI.DrawNotification(gpuOcclusionCulling, "GPU Occlusion Culling is enabled, this can cause some objects to disappear in the reflection.", MessageType.Warning);
            
            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.Space(EditorGUIUtility.labelWidth);
                previewReflection =
                    GUILayout.Toggle(previewReflection, new GUIContent("  Preview reflection", EditorGUIUtility.IconContent(
                        (previewReflection ? "animationvisibilitytoggleon" : "animationvisibilitytoggleoff")).image), "Button");
            }
            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.Space(EditorGUIUtility.labelWidth);
                EditorGUILayout.LabelField("Status: " + (waterObjectsVisible && currentCamera ? $"Rendering (camera: {currentCamera.name})" : "Not rendering (water not in view for any camera)"), EditorStyles.miniLabel);
            }
            
            UI.DrawNotification(PipelineUtilities.VREnabled(), "Not supported with VR rendering", MessageType.Error);
            
            UI.DrawNotification(PlanarReflectionRenderer.AllowReflections == false, "Reflections have been globally disabled by an external script", MessageType.Warning);
            
            serializedObject.Update();
            EditorGUI.BeginChangeCheck();
            
            EditorGUILayout.LabelField("Rendering", EditorStyles.boldLabel);

            EditorGUI.BeginChangeCheck();
            UI.DrawRendererProperty(rendererIndex);
            if (EditorGUI.EndChangeCheck())
            {
                renderer.SetRendererIndex(rendererIndex.intValue);
            }
            
            //Default renderer
            if (rendererIndex.intValue == 0)
            {
                UI.DrawNotification("\n" +
                                        "Using the default renderer for reflections is strongly discouraged." +
                                        "\n\nMost (if not all) render features, such as third-party post processing effects, will also render for the reflection." +
                                        "\n\nThis can lead to rendering artefacts and negatively impacts overall performance." +
                                        "\n", MessageType.Warning);
                
                //If there are no other renderers to assign, suggest to auto-create one
                UI.DrawNotification(PipelineUtilities.rendererIndexList.Length <= 2, "It is highly recommend to create a separate empty renderer", "Create and assign", CreateRenderer, MessageType.None);
                
                EditorGUILayout.Space();
            }
            
            EditorGUILayout.PropertyField(cullingMask);
            
            EditorGUILayout.PropertyField(includeSkybox);
            EditorGUILayout.PropertyField(enableFog);
			if(enableFog.boolValue) EditorGUILayout.HelpBox("Unity's built-in fog does not support oblique projections. Expect incorrect fog shading on tall objects or large triangles", MessageType.Warning);
            EditorGUILayout.PropertyField(enableInSceneView);
            
            EditorGUILayout.Space();

            EditorGUILayout.PropertyField(rotatable);
            EditorGUILayout.PropertyField(offset);
            
            EditorGUILayout.Space();
            
            EditorGUILayout.LabelField("Quality", EditorStyles.boldLabel);
            EditorGUI.BeginChangeCheck();
            EditorGUILayout.PropertyField(renderShadows);
            if (EditorGUI.EndChangeCheck())
            {
                renderer.ToggleShadows(renderShadows.boolValue);
            }
            EditorGUILayout.PropertyField(renderScale);
            EditorGUILayout.PropertyField(maximumLODLevel);
            
            EditorGUILayout.Space();

            EditorGUILayout.LabelField("Target water objects", EditorStyles.boldLabel);
            EditorGUILayout.PropertyField(moveWithTransform, new GUIContent("Move bounds with transform", moveWithTransform.tooltip));

            EditorGUI.BeginChangeCheck();
            EditorGUILayout.PropertyField(waterObjects);
            if (EditorGUI.EndChangeCheck())
            {
                curBounds = renderer.CalculateBounds();
            }
            
            if (EditorGUI.EndChangeCheck())
            {
                serializedObject.ApplyModifiedProperties();
            }
            
            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.FlexibleSpace();
                if(GUILayout.Button(new GUIContent("Auto-find", "Assigns all active water objects currently in the scene"), EditorStyles.miniButton))
                {
                    renderer.waterObjects = new List<WaterObject>(WaterObject.Instances);
 
                    renderer.RecalculateBounds();
                    curBounds = renderer.bounds;
                    renderer.EnableMaterialReflectionSampling();

                    ValidateWaterObjectLayer();
                    
                    EditorUtility.SetDirty(target);
                }
                if(GUILayout.Button("Clear", EditorStyles.miniButton))
                {
                    renderer.ToggleMaterialReflectionSampling(false);
                    renderer.waterObjects.Clear();
                    renderer.RecalculateBounds();
                    
                    EditorUtility.SetDirty(target);
                }
            }
            
            if (renderer.waterObjects != null)
            {
                UI.DrawNotification(renderer.waterObjects.Count == 0, "Assign at least one Water Object", MessageType.Info);
                
                if (renderer.waterObjects.Count > 0)
                {
                    UI.DrawNotification(curBounds.size != renderer.bounds.size || (moveWithTransform.boolValue == false && curBounds.center != renderer.bounds.center), "Water objects have changed or moved, bounds needs to be recalculated", "Recalculate",() => RecalculateBounds(), MessageType.Error);
                }
                
                UI.DrawNotification(waterLayerError, "One or more Water Objects aren't on the \"Water\" layer.\n\nThis causes recursive reflections", "Fix", () => SetObjectsOnWaterLayer(), MessageType.Error);
            }

#endif
            
            UI.DrawFooter();
        }
        
#if URP

        private void CreateRenderer()
        {
            int index = -1;
            string path = "";

            PipelineUtilities.CreateAndAssignNewRenderer(out index, out path);

            if (index >= 0)
            {
                rendererIndex.intValue = index;

                serializedObject.ApplyModifiedProperties();
                serializedObject.Update();
                
                renderer.SetRendererIndex(rendererIndex.intValue);

                if (path != string.Empty)
                {
                    Debug.Log("New renderer created at path <i>" + path + "</i>");
                }
            }
        }
        
        public override bool HasPreviewGUI()
        {
            return previewReflection && previewTexture;
        }
        
        public override bool RequiresConstantRepaint()
        {
            return HasPreviewGUI();
        }

        public override GUIContent GetPreviewTitle()
        {
            return currentCamera ? new GUIContent(currentCameraName + " reflection") : new GUIContent("Reflection");
        }

        public override void OnPreviewSettings()
        {
            if (HasPreviewGUI() == false) return;

            GUILayout.Label($"Resolution ({previewTexture.width}x{previewTexture.height})");
        }

        private bool drawAlpha;

        public override void OnPreviewGUI(Rect r, GUIStyle background)
        {
            if (drawAlpha)
            {
                EditorGUI.DrawTextureAlpha(r, previewTexture, ScaleMode.ScaleToFit);
            }
            else
            {
                GUI.DrawTexture(r, previewTexture, ScaleMode.ScaleToFit, false);
            }
            
            Rect btnRect = r;
            btnRect.x += 10f;
            btnRect.y += 10f;
            btnRect.width = 150f;
            btnRect.height = 20f;

            drawAlpha = GUI.Toggle(btnRect, drawAlpha, new GUIContent(" Alpha channel"));
        }

        private void ValidateWaterObjectLayer()
        {
            if (renderer.waterObjects == null) return;

            waterLayerError = false;
            int layerID = LayerMask.NameToLayer("Water");

            foreach (WaterObject obj in renderer.waterObjects)
            {
                //Is not on "Water" layer?
                if (obj.gameObject.layer != layerID)
                {
                    waterLayerError = true;
                    return;
                }
            }
        }

        private void SetObjectsOnWaterLayer()
        {
            int layerID = LayerMask.NameToLayer("Water");

            foreach (WaterObject obj in renderer.waterObjects)
            {
                //Is not on "Water" layer?
                if (obj.gameObject.layer != layerID)
                {
                    obj.gameObject.layer = layerID;
                    EditorUtility.SetDirty(obj);
                }
            }
            
            waterLayerError = false;
        }
    #endif

        private void RecalculateBounds()
        {
#if URP
            renderer.RecalculateBounds();
            curBounds = renderer.bounds;
            EditorUtility.SetDirty(target);
#endif
        }
    }
}
