using System;
using System.Collections.Generic;
using UnityEngine.Playables;

namespace UnityEngine.Timeline
{
    /// <summary>
    /// Use this PlayableBehaviour to send notifications at a given time.
    /// </summary>
    /// <seealso cref="UnityEngine.Timeline.NotificationFlags"/>
    public class TimeNotificationBehaviour : PlayableBehaviour
    {
        struct NotificationEntry
        {
            public double time;
            public INotification payload;
            public bool notificationFired;
            public NotificationFlags flags;

            public bool triggerInEditor
            {
                get { return (flags & NotificationFlags.TriggerInEditMode) != 0; }
            }
            public bool prewarm
            {
                get { return (flags & NotificationFlags.Retroactive) != 0; }
            }
            public bool triggerOnce
            {
                get { return (flags & NotificationFlags.TriggerOnce) != 0; }
            }
        }

        readonly List<NotificationEntry> m_Notifications = new List<NotificationEntry>();
        double m_PreviousTime;
        bool m_NeedSortNotifications;

        Playable m_TimeSource;

        /// <summary>
        /// Sets an optional Playable that provides duration and Wrap mode information.
        /// </summary>
        /// <remarks>
        /// timeSource is optional. By default, the duration and Wrap mode will come from the current Playable.
        /// </remarks>
        public Playable timeSource
        {
            set { m_TimeSource = value; }
        }

        /// <summary>
        /// Creates and initializes a ScriptPlayable with a TimeNotificationBehaviour.
        /// </summary>
        /// <param name="graph">The playable graph.</param>
        /// <param name="duration">The duration of the playable.</param>
        /// <param name="loopMode">The loop mode of the playable.</param>
        /// <returns>A new TimeNotificationBehaviour linked to the PlayableGraph.</returns>
        public static ScriptPlayable<TimeNotificationBehaviour> Create(PlayableGraph graph, double duration, DirectorWrapMode loopMode)
        {
            var notificationsPlayable = ScriptPlayable<TimeNotificationBehaviour>.Create(graph);
            notificationsPlayable.SetDuration(duration);
            notificationsPlayable.SetTimeWrapMode(loopMode);
            notificationsPlayable.SetPropagateSetTime(true);
            return notificationsPlayable;
        }

        /// <summary>
        /// Adds a notification to be sent with flags, at a specific time.
        /// </summary>
        /// <param name="time">The time to send the notification.</param>
        /// <param name="payload">The notification.</param>
        /// <param name="flags">The notification flags that determine the notification behaviour. This parameter is set to Retroactive by default.</param>
        /// <seealso cref="UnityEngine.Timeline.NotificationFlags"/>
        public void AddNotification(double time, INotification payload, NotificationFlags flags = NotificationFlags.Retroactive)
        {
            m_Notifications.Add(new NotificationEntry
            {
                time = time,
                payload = payload,
                flags = flags
            });
            m_NeedSortNotifications = true;
        }

        /// <summary>
        /// This method is called when the PlayableGraph that owns this PlayableBehaviour starts.
        /// </summary>
        /// <param name="playable">The reference to the playable associated with this PlayableBehaviour.</param>
        public override void OnGraphStart(Playable playable)
        {
            SortNotifications();
            var currentTime = playable.GetTime();
            for (var i = 0; i < m_Notifications.Count; i++)
            {
                // case 1257208 - when a timeline is _resumed_, only reset notifications after the resumed time
                if (m_Notifications[i].time > currentTime && !m_Notifications[i].triggerOnce)
                {
                    var notification = m_Notifications[i];
                    notification.notificationFired = false;
                    m_Notifications[i] = notification;
                }
            }
            m_PreviousTime = playable.GetTime();
        }

        /// <summary>
        /// This method is called when the Playable play state is changed to PlayState.Paused
        /// </summary>
        /// <param name="playable">The reference to the playable associated with this PlayableBehaviour.</param>
        /// <param name="info">Playable context information such as weight, evaluationType, and so on.</param>
        public override void OnBehaviourPause(Playable playable, FrameData info)
        {
            if (playable.IsDone())
            {
                SortNotifications();
                for (var i = 0; i < m_Notifications.Count; i++)
                {
                    var e = m_Notifications[i];
                    if (!e.notificationFired)
                    {
                        var duration = playable.GetDuration();
                        var canTrigger = m_PreviousTime <= e.time && e.time <= duration;
                        if (canTrigger)
                        {
                            Trigger_internal(playable, info.output, ref e);
                            m_Notifications[i] = e;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// This method is called during the PrepareFrame phase of the PlayableGraph.
        /// </summary>
        /// <remarks>
        /// Called once before processing starts.
        /// </remarks>
        /// <param name="playable">The reference to the playable associated with this PlayableBehaviour.</param>
        /// <param name="info">Playable context information such as weight, evaluationType, and so on.</param>
        public override void PrepareFrame(Playable playable, FrameData info)
        {
            // Never trigger on scrub
            if (info.evaluationType == FrameData.EvaluationType.Evaluate)
            {
                return;
            }

            SyncDurationWithExternalSource(playable);
            SortNotifications();
            var currentTime = playable.GetTime();

            // Fire notifications from previousTime till the end
            if (info.timeLooped)
            {
                var duration = playable.GetDuration();
                TriggerNotificationsInRange(m_PreviousTime, duration, info, playable, true);
                var dx = playable.GetDuration() - m_PreviousTime;
                var nFullTimelines = (int)((info.deltaTime * info.effectiveSpeed - dx) / playable.GetDuration());
                for (var i = 0; i < nFullTimelines; i++)
                {
                    TriggerNotificationsInRange(0, duration, info, playable, false);
                }
                TriggerNotificationsInRange(0, currentTime, info, playable, false);
            }
            else
            {
                var pt = playable.GetTime();
                TriggerNotificationsInRange(m_PreviousTime, pt, info,
                    playable, true);
            }

            for (var i = 0; i < m_Notifications.Count; ++i)
            {
                var e = m_Notifications[i];
                if (e.notificationFired && CanRestoreNotification(e, info, currentTime, m_PreviousTime))
                {
                    Restore_internal(ref e);
                    m_Notifications[i] = e;
                }
            }

            m_PreviousTime = playable.GetTime();
        }

        void SortNotifications()
        {
            if (m_NeedSortNotifications)
            {
                m_Notifications.Sort((x, y) => x.time.CompareTo(y.time));
                m_NeedSortNotifications = false;
            }
        }

        static bool CanRestoreNotification(NotificationEntry e, FrameData info, double currentTime, double previousTime)
        {
            if (e.triggerOnce)
                return false;
            if (info.timeLooped)
                return true;

            //case 1111595: restore the notification if the time is manually set before it
            return previousTime > currentTime && currentTime <= e.time;
        }

        void TriggerNotificationsInRange(double start, double end, FrameData info, Playable playable, bool checkState)
        {
            if (start <= end)
            {
                var playMode = Application.isPlaying;
                for (var i = 0; i < m_Notifications.Count; i++)
                {
                    var e = m_Notifications[i];
                    if (e.notificationFired && (checkState || e.triggerOnce))
                        continue;

                    var notificationTime = e.time;
                    if (e.prewarm && notificationTime < end && (e.triggerInEditor || playMode))
                    {
                        Trigger_internal(playable, info.output, ref e);
                        m_Notifications[i] = e;
                    }
                    else
                    {
                        if (notificationTime < start || notificationTime > end)
                            continue;

                        if (e.triggerInEditor || playMode)
                        {
                            Trigger_internal(playable, info.output, ref e);
                            m_Notifications[i] = e;
                        }
                    }
                }
            }
        }

        void SyncDurationWithExternalSource(Playable playable)
        {
            if (m_TimeSource.IsValid())
            {
                playable.SetDuration(m_TimeSource.GetDuration());
                playable.SetTimeWrapMode(m_TimeSource.GetTimeWrapMode());
            }
        }

        static void Trigger_internal(Playable playable, PlayableOutput output, ref NotificationEntry e)
        {
            output.PushNotification(playable, e.payload);
            e.notificationFired = true;
        }

        static void Restore_internal(ref NotificationEntry e)
        {
            e.notificationFired = false;
        }
    }
}
