using UnityEngine;
using UnityEngine.Serialization;

namespace VLB
{
    [AddComponentMenu("")] // hide it from Component search
    [DisallowMultipleComponent]
    [RequireComponent(typeof(VolumetricLightBeamSD))]
    public abstract class DynamicOcclusionAbstractBase : MonoBehaviour
    {
        public const string ClassName = "DynamicOcclusionAbstractBase";

        /// <summary>
        /// How often will the occlusion be processed?
        /// Try to update the occlusion as rarely as possible to keep good performance.
        /// </summary>
        public DynamicOcclusionUpdateRate updateRate = Consts.DynOcclusion.UpdateRateDefault;

        /// <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>
        [FormerlySerializedAs("waitFrameCount")]
        public int waitXFrames = Consts.DynOcclusion.WaitFramesCountDefault;

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

        public event System.Action onOcclusionProcessed;

        public static bool _INTERNAL_ApplyRandomFrameOffset = true;


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

        protected void ProcessOcclusion(ProcessOcclusionSource source)
        {
            if (!Config.Instance.featureEnabledDynamicOcclusion)
                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);

            bool occlusionSuccess = OnProcessOcclusion(source);

            if(onOcclusionProcessed != null)
                onOcclusionProcessed();

            if (m_Master)
            {
                Debug.Assert(m_MaterialModifierCallbackCached != null);
                m_Master._INTERNAL_SetDynamicOcclusionCallback(GetShaderKeyword(), occlusionSuccess ? m_MaterialModifierCallbackCached : (MaterialModifier.Callback)(null));
            }

            if (updateRate.HasFlag(DynamicOcclusionUpdateRate.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
            }
        }

        TransformUtils.Packed m_TransformPacked;
        int m_LastFrameRendered = int.MinValue;
        public int _INTERNAL_LastFrameRendered { get { return m_LastFrameRendered; } } // for unit tests
        protected VolumetricLightBeamSD m_Master = null;
        protected MaterialModifier.Callback m_MaterialModifierCallbackCached = null;

        protected abstract string GetShaderKeyword();
        protected abstract MaterialManager.SD.DynamicOcclusion GetDynamicOcclusionMode();

        protected abstract bool OnProcessOcclusion(ProcessOcclusionSource source);
        protected abstract void OnModifyMaterialCallback(MaterialModifier.Interface owner);
        protected abstract void OnEnablePostValidate();


        protected virtual void OnValidateProperties()
        {
            waitXFrames = Mathf.Clamp(waitXFrames, 1, 60);
        }

        protected virtual void Awake()
        {
            m_Master = GetComponent<VolumetricLightBeamSD>();
            Debug.Assert(m_Master);

            m_Master._INTERNAL_DynamicOcclusionMode = GetDynamicOcclusionMode();
        }

        protected virtual void OnDestroy()
        {
            m_Master._INTERNAL_DynamicOcclusionMode = MaterialManager.SD.DynamicOcclusion.Off;
            DisableOcclusion();
        }

        protected virtual void OnEnable()
        {
            // cache the delegate to prevent from being inlined as '() => OnModifyMaterialCallback' when calling _INTERNAL_SetDynamicOcclusionCallback and from generating GC garbage
            m_MaterialModifierCallbackCached = OnModifyMaterialCallback;

            OnValidateProperties();

            OnEnablePostValidate();

#if UNITY_EDITOR
            if (Application.isPlaying)
#endif
            {
                m_Master.onWillCameraRenderThisBeam += OnWillCameraRender;

                if (!updateRate.HasFlag(DynamicOcclusionUpdateRate.Never))
                    m_Master.RegisterOnBeamGeometryInitializedCallback(() => ProcessOcclusion(ProcessOcclusionSource.OnEnable));
            }
        }

        protected virtual void OnDisable()
        {
#if UNITY_EDITOR
            if (Application.isPlaying)
#endif
            {
                m_Master.onWillCameraRenderThisBeam -= OnWillCameraRender;
            }

            DisableOcclusion();
        }

#if UNITY_EDITOR
        protected virtual void OnValidate()
        {
            OnValidateProperties();
        }
#endif


        void OnWillCameraRender(Camera cam)
        {
            Debug.Assert(Application.isPlaying);

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

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

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

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

        void DisableOcclusion()
        {
            m_Master._INTERNAL_SetDynamicOcclusionCallback(GetShaderKeyword(), null);
        }
    }
}

