using UnityEngine;

namespace VLB
{
    [ExecuteInEditMode]
    [DisallowMultipleComponent]
    [RequireComponent(typeof(VolumetricLightBeamHD))]
    [HelpURL(Consts.Help.HD.UrlShadow)]
    public class VolumetricShadowHD : MonoBehaviour
    {
        public const string ClassName = "VolumetricShadowHD";

        /// <summary>
        /// Controls how dark the shadow cast by this Light Beam will be.
        /// The bigger the value, the more the shadow will affect the visual.
        /// </summary>
        public float strength
        {
            get { return m_Strength; }
            set { if (m_Strength != value) { m_Strength = value; SetDirty(); } }
        }

        /// <summary>
        /// How often will the occlusion be processed?
        /// Try to update the occlusion as rarely as possible to keep good performance.
        /// </summary>
        public ShadowUpdateRate updateRate
        {
            get { return m_UpdateRate; }
            set { m_UpdateRate = value; }
        }

        /// <summary>
        /// How many frames we wait between 2 occlusion tests?
        /// If you want your beam to be super responsive to the changes of your environment, update it every frame by setting 1.
        /// If you want to save on performance, we recommend to wait few frames between each update by setting a higher value.
        /// </summary>
        public int waitXFrames
        {
            get { return m_WaitXFrames; }
            set { m_WaitXFrames = value; }
        }

        /// <summary>
        /// The beam can only be occluded by objects located on the layers matching this mask.
        /// It's very important to set it as restrictive as possible (checking only the layers which are necessary)
        /// to perform a more efficient process in order to increase the performance.
        /// It should NOT include the layer on which the beams are generated.
        /// </summary>
        public LayerMask layerMask
        {
            get { return m_LayerMask; }
            set
            {
                m_LayerMask = value;
                UpdateDepthCameraProperties();
            }
        }
        /// <summary>
        /// Whether or not the virtual camera will use occlusion culling during rendering from the beam's POV.
        /// </summary>
        public bool useOcclusionCulling
        {
            get { return m_UseOcclusionCulling; }
            set
            {
                m_UseOcclusionCulling = value;
                UpdateDepthCameraProperties();
            }
        }

        /// <summary>
        /// Controls how large the depth texture captured by the virtual camera is.
        /// The lower the resolution, the better the performance, but the less accurate the rendering.
        /// </summary>
        public int depthMapResolution
        {
            get { return m_DepthMapResolution; }
            set
            {
                if(m_DepthCamera != null && Application.isPlaying) { Debug.LogErrorFormat(Consts.Shadow.GetErrorChangeRuntimeDepthMapResolution(this)); }
                m_DepthMapResolution = value;
            }
        }

        /// <summary>
        /// Manually process the occlusion.
        /// You have to call this function in order to update the occlusion when using ShadowUpdateRate.Never.
        /// </summary>
        public void ProcessOcclusionManually() { ProcessOcclusion(ProcessOcclusionSource.User); }

        [SerializeField] float m_Strength = Consts.Shadow.StrengthDefault;
        [SerializeField] ShadowUpdateRate m_UpdateRate = Consts.Shadow.UpdateRateDefault;
        [SerializeField] int m_WaitXFrames = Consts.Shadow.WaitFramesCountDefault;
        [SerializeField] LayerMask m_LayerMask = Consts.Shadow.LayerMaskDefault;
        [SerializeField] bool m_UseOcclusionCulling = Consts.Shadow.OcclusionCullingDefault;
        [SerializeField] int m_DepthMapResolution = Consts.Shadow.DepthMapResolutionDefault;

        public void UpdateDepthCameraProperties()
        {
            if (m_DepthCamera)
            {
                m_DepthCamera.cullingMask = layerMask;
                m_DepthCamera.useOcclusionCulling = useOcclusionCulling;
            }
        }

        enum ProcessOcclusionSource
        {
            RenderLoop,
            OnEnable,
            EditorUpdate,
            User,
        }

        void ProcessOcclusion(ProcessOcclusionSource source)
        {
            if (!Config.Instance.featureEnabledShadow)
                return;

            if(m_LastFrameRendered == Time.frameCount && Application.isPlaying && source == ProcessOcclusionSource.OnEnable)
                return; // allow to call ProcessOcclusion from OnEnable (when disabling/enabling multiple a beam on the same frame) without generating an error

            Debug.Assert(!Application.isPlaying || m_LastFrameRendered != Time.frameCount, "ProcessOcclusion has been called twice on the same frame, which is forbidden");
            Debug.Assert(m_Master && m_DepthCamera);

            if (SRPHelper.IsUsingCustomRenderPipeline()) // Recursive rendering is not supported on SRP
                m_NeedToUpdateOcclusionNextFrame = true;
            else
                ProcessOcclusionInternal();

            SetDirty(); // refresh material

            if (updateRate.HasFlag(ShadowUpdateRate.OnBeamMove))
                m_TransformPacked = transform.GetWorldPacked();

            bool firstTime = m_LastFrameRendered < 0;
            m_LastFrameRendered = Time.frameCount;

            if (firstTime && _INTERNAL_ApplyRandomFrameOffset)
            {
                m_LastFrameRendered += Random.Range(0, waitXFrames); // add a random offset to prevent from updating texture for all beams having the same wait value
            }
        }

        public static void ApplyMaterialProperties(VolumetricShadowHD instance, BeamGeometryHD geom)
        {
            Debug.Assert(geom != null);

            if (instance && instance.enabled)
            {
                Debug.Assert(instance.m_DepthCamera);
                geom.SetMaterialProp(ShaderProperties.HD.ShadowDepthTexture, instance.m_DepthCamera.targetTexture);

                var scale = instance.m_Master.scalable ? instance.m_Master.GetLossyScale() : Vector3.one;
                geom.SetMaterialProp(ShaderProperties.HD.ShadowProps, new Vector4(Mathf.Sign(scale.x) * Mathf.Sign(scale.z), Mathf.Sign(scale.y), instance.m_Strength, instance.m_DepthCamera.orthographic ? 0f : 1f));
            }
            else
            {
                geom.SetMaterialProp(ShaderProperties.HD.ShadowDepthTexture, BeamGeometryHD.InvalidTexture.NoDepth);
            }
        }

        void Awake()
        {
            m_Master = GetComponent<VolumetricLightBeamHD>();
            Debug.Assert(m_Master);

#if UNITY_EDITOR
            MarkMaterialAsDirty();
#endif
        }

        void OnEnable()
        {
            OnValidateProperties();
            InstantiateOrActivateDepthCamera();
            OnBeamEnabled();
        }

        void OnDisable()
        {
            if (m_DepthCamera) m_DepthCamera.gameObject.SetActive(false);
            SetDirty(); // refresh material with empty depth texture
        }

        void OnDestroy()
        {
            DestroyDepthCamera();

#if UNITY_EDITOR
            MarkMaterialAsDirty();
#endif
        }

        void ProcessOcclusionInternal()
        {
            UpdateDepthCameraPropertiesAccordingToBeam();
            m_DepthCamera.Render();
        }

        void OnBeamEnabled()
        {
#if UNITY_EDITOR
            if (!Application.isPlaying) { return; }
#endif
            if (!enabled) { return; }

            if (!updateRate.HasFlag(ShadowUpdateRate.Never))
                ProcessOcclusion(ProcessOcclusionSource.OnEnable);
        }

        public void OnWillCameraRenderThisBeam(Camera cam, BeamGeometryHD beamGeom)
        {
#if UNITY_EDITOR
            if (!Application.isPlaying) { return; }
#endif
            if (!enabled) { return; }

            if(cam != null
            && cam.enabled
            && Time.frameCount != m_LastFrameRendered // prevent from updating multiple times if there are more than 1 camera
            && updateRate != ShadowUpdateRate.Never)
            {
                bool shouldUpdate = false;

                if (!shouldUpdate && updateRate.HasFlag(ShadowUpdateRate.OnBeamMove))
                {
                    if (!m_TransformPacked.IsSame(transform))
                        shouldUpdate = true;
                }

                if (!shouldUpdate && updateRate.HasFlag(ShadowUpdateRate.EveryXFrames))
                {
                    if (Time.frameCount >= m_LastFrameRendered + waitXFrames)
                        shouldUpdate = true;
                }

                if (shouldUpdate)
                    ProcessOcclusion(ProcessOcclusionSource.RenderLoop);
            }
        }

        void Update()
        {
            if (m_NeedToUpdateOcclusionNextFrame && m_Master && m_DepthCamera
                && Time.frameCount > 1)  // fix NullReferenceException in UnityEngine.Rendering.Universal.Internal.CopyDepthPass.Execute when using SRP
            {
                ProcessOcclusionInternal();
                m_NeedToUpdateOcclusionNextFrame = false;
            }
        }

        void UpdateDepthCameraPropertiesAccordingToBeam()
        {
            Debug.Assert(m_Master);

            Utils.SetupDepthCamera(m_DepthCamera
                , m_Master.GetConeApexOffsetZ(true), m_Master.maxGeometryDistance, m_Master.coneRadiusStart, m_Master.coneRadiusEnd
                , m_Master.beamLocalForward, m_Master.GetLossyScale(), m_Master.scalable, m_Master.beamInternalLocalRotation
                , false);
        }

        void InstantiateOrActivateDepthCamera()
        {
            if (m_DepthCamera != null)
            {
                m_DepthCamera.gameObject.SetActive(true); // active it in case it has been disabled by OnDisable()
            }
            else
            {
                // delete old depth cameras when duplicating the GAO
                gameObject.ForeachComponentsInDirectChildrenOnly<Camera>(cam => DestroyImmediate(cam.gameObject), true);

                m_DepthCamera = Utils.NewWithComponent<Camera>("Depth Camera");

                if (m_DepthCamera && m_Master)
                {
                    m_DepthCamera.enabled = false;

                    UpdateDepthCameraProperties(); // set layerMask & useOcclusionCulling
                    m_DepthCamera.clearFlags = CameraClearFlags.Depth;
                    m_DepthCamera.depthTextureMode = DepthTextureMode.Depth;
                    m_DepthCamera.renderingPath = RenderingPath.Forward; // RenderingPath.VertexLit is faster, but RenderingPath.Forward allows to catch alpha cutout
                    m_DepthCamera.gameObject.hideFlags = Consts.Internal.ProceduralObjectsHideFlags;
                    m_DepthCamera.transform.SetParent(transform, false);
                    Config.Instance.SetURPScriptableRendererIndexToDepthCamera(m_DepthCamera);

                    var rt = new RenderTexture(depthMapResolution, depthMapResolution, 16, RenderTextureFormat.Depth);
                    m_DepthCamera.targetTexture = rt;

                    UpdateDepthCameraPropertiesAccordingToBeam();

#if UNITY_EDITOR
                    UnityEditor.GameObjectUtility.SetStaticEditorFlags(m_DepthCamera.gameObject, m_Master.GetStaticEditorFlagsForSubObjects());
                    m_DepthCamera.gameObject.SetSameSceneVisibilityStatesThan(m_Master.gameObject);
#endif
                }
            }
        }

        void DestroyDepthCamera()
        {
            if (m_DepthCamera)
            {
                if (m_DepthCamera.targetTexture)
                {
                    m_DepthCamera.targetTexture.Release();
                    DestroyImmediate(m_DepthCamera.targetTexture);
                    m_DepthCamera.targetTexture = null;
                }

                DestroyImmediate(m_DepthCamera.gameObject); // Make sure to delete the GAO
                m_DepthCamera = null;
            }
        }

        void OnValidateProperties()
        {
            m_WaitXFrames = Mathf.Clamp(m_WaitXFrames, 1, 60);
            m_DepthMapResolution = Mathf.Clamp(Mathf.NextPowerOfTwo(m_DepthMapResolution), 8, 2048);
        }

        void SetDirty()
        {
            if (m_Master)
                m_Master.SetPropertyDirty(DirtyProps.ShadowProps);
        }

        VolumetricLightBeamHD m_Master = null;
        TransformUtils.Packed m_TransformPacked;
        int m_LastFrameRendered = int.MinValue;
        public int _INTERNAL_LastFrameRendered { get { return m_LastFrameRendered; } } // for unit tests

        Camera m_DepthCamera = null;
        bool m_NeedToUpdateOcclusionNextFrame = false;
        public static bool _INTERNAL_ApplyRandomFrameOffset = true;

#if UNITY_EDITOR
        bool m_NeedToReinstantiateDepthCamera = false;

        void MarkMaterialAsDirty()
        {
            // when adding/removing this component in editor, we might need to switch from a GPU Instanced material to a custom one,
            // since this feature doesn't support GPU Instancing
            if (!Application.isPlaying)
                m_Master._EditorSetBeamGeomDirty();
        }

        void OnValidate()
        {
            OnValidateProperties();
            m_NeedToReinstantiateDepthCamera = true;
        }

        void LateUpdate()
        {
            if (!Application.isPlaying)
            {
                if (m_NeedToReinstantiateDepthCamera)
                {
                    DestroyDepthCamera();
                    InstantiateOrActivateDepthCamera();
                    m_NeedToReinstantiateDepthCamera = false;
                }

                if (m_Master && m_Master.enabled)
                    ProcessOcclusion(ProcessOcclusionSource.EditorUpdate);
            }
        }

        public bool HasLayerMaskIssues()
        {
            if (Config.Instance.geometryOverrideLayer)
            {
                int layerBit = 1 << Config.Instance.geometryLayerID;
                return ((layerMask.value & layerBit) == layerBit);
            }
            return false;
        }
#endif // UNITY_EDITOR
    }
}

