using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Playables;
namespace UnityEngine.Timeline
{
    /// 
    /// Playable Asset that generates playables for controlling time-related elements on a GameObject.
    /// 
    [Serializable]
    [NotKeyable]
    public class ControlPlayableAsset : PlayableAsset, IPropertyPreview, ITimelineClipAsset
    {
        const int k_MaxRandInt = 10000;
        static readonly List k_EmptyDirectorsList = new List(0);
        static readonly List k_EmptyParticlesList = new List(0);
        static readonly HashSet s_SubEmitterCollector = new HashSet();
        /// 
        /// GameObject in the scene to control, or the parent of the instantiated prefab.
        /// 
        [SerializeField] public ExposedReference sourceGameObject;
        /// 
        /// Prefab object that will be instantiated.
        /// 
        [SerializeField] public GameObject prefabGameObject;
        /// 
        /// Indicates whether Particle Systems will be controlled.
        /// 
        [SerializeField] public bool updateParticle = true;
        /// 
        /// Random seed to supply particle systems that are set to use autoRandomSeed
        /// 
        /// 
        /// This is used to maintain determinism when playing back in timeline. Sub emitters will be assigned incrementing random seeds to maintain determinism and distinction.
        /// 
        [SerializeField] public uint particleRandomSeed;
        /// 
        /// Indicates whether playableDirectors are controlled.
        /// 
        [SerializeField] public bool updateDirector = true;
        /// 
        /// Indicates whether Monobehaviours implementing ITimeControl will be controlled.
        /// 
        [SerializeField] public bool updateITimeControl = true;
        /// 
        /// Indicates whether to search the entire hierarchy for controllable components.
        /// 
        [SerializeField] public bool searchHierarchy;
        /// 
        /// Indicate whether GameObject activation is controlled
        /// 
        [SerializeField] public bool active = true;
        /// 
        /// Indicates the active state of the GameObject when Timeline is stopped.
        /// 
        [SerializeField]
        public ActivationControlPlayable.PostPlaybackState postPlayback = ActivationControlPlayable.PostPlaybackState.Revert;
        /// 
        /// Which action to apply to the  at the end of the control clip.
        /// 
        /// 
        [SerializeField]
        public DirectorControlPlayable.PauseAction directorOnClipEnd;
        PlayableAsset m_ControlDirectorAsset;
        double m_Duration = PlayableBinding.DefaultDuration;
        bool m_SupportLoop;
        private static HashSet s_ProcessedDirectors = new HashSet();
        private static HashSet s_CreatedPrefabs = new HashSet();
        // does the last instance created control directors and/or particles
        internal bool controllingDirectors { get; private set; }
        internal bool controllingParticles { get; private set; }
        /// 
        /// This function is called when the object is loaded.
        /// 
        public void OnEnable()
        {
            // can't be set in a constructor
            if (particleRandomSeed == 0)
                particleRandomSeed = (uint)Random.Range(1, k_MaxRandInt);
        }
        /// 
        /// Returns the duration in seconds needed to play the underlying director or particle system exactly once.
        /// 
        public override double duration { get { return m_Duration; } }
        /// 
        /// Returns the capabilities of TimelineClips that contain a ControlPlayableAsset
        /// 
        public ClipCaps clipCaps
        {
            get { return ClipCaps.ClipIn | ClipCaps.SpeedMultiplier | (m_SupportLoop ? ClipCaps.Looping : ClipCaps.None); }
        }
        /// 
        /// Creates the root of a Playable subgraph to control the contents of the game object.
        /// 
        /// PlayableGraph that will own the playable
        /// The GameObject that triggered the graph build
        /// The root playable of the subgraph
        public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
        {
            // case 989856
            if (prefabGameObject != null)
            {
                if (s_CreatedPrefabs.Contains(prefabGameObject))
                {
                    Debug.LogWarningFormat("Control Track Clip ({0}) is causing a prefab to instantiate itself recursively. Aborting further instances.", name);
                    return Playable.Create(graph);
                }
                s_CreatedPrefabs.Add(prefabGameObject);
            }
            Playable root = Playable.Null;
            var playables = new List();
            GameObject sourceObject = sourceGameObject.Resolve(graph.GetResolver());
            if (prefabGameObject != null)
            {
                Transform parenTransform = sourceObject != null ? sourceObject.transform : null;
                var controlPlayable = PrefabControlPlayable.Create(graph, prefabGameObject, parenTransform);
                sourceObject = controlPlayable.GetBehaviour().prefabInstance;
                playables.Add(controlPlayable);
            }
            m_Duration = PlayableBinding.DefaultDuration;
            m_SupportLoop = false;
            controllingParticles = false;
            controllingDirectors = false;
            if (sourceObject != null)
            {
                var directors = updateDirector ? GetComponent(sourceObject) : k_EmptyDirectorsList;
                var particleSystems = updateParticle ? GetControllableParticleSystems(sourceObject) : k_EmptyParticlesList;
                // update the duration and loop values (used for UI purposes) here
                // so they are tied to the latest gameObject bound
                UpdateDurationAndLoopFlag(directors, particleSystems);
                var director = go.GetComponent();
                if (director != null)
                    m_ControlDirectorAsset = director.playableAsset;
                if (go == sourceObject && prefabGameObject == null)
                {
                    Debug.LogWarningFormat("Control Playable ({0}) is referencing the same PlayableDirector component than the one in which it is playing.", name);
                    active = false;
                    if (!searchHierarchy)
                        updateDirector = false;
                }
                if (active)
                    CreateActivationPlayable(sourceObject, graph, playables);
                if (updateDirector)
                    SearchHierarchyAndConnectDirector(directors, graph, playables, prefabGameObject != null);
                if (updateParticle)
                    SearchHierarchyAndConnectParticleSystem(particleSystems, graph, playables);
                if (updateITimeControl)
                    SearchHierarchyAndConnectControlableScripts(GetControlableScripts(sourceObject), graph, playables);
                // Connect Playables to Generic to Mixer
                root = ConnectPlayablesToMixer(graph, playables);
            }
            if (prefabGameObject != null)
                s_CreatedPrefabs.Remove(prefabGameObject);
            if (!root.IsValid())
                root = Playable.Create(graph);
            return root;
        }
        static Playable ConnectPlayablesToMixer(PlayableGraph graph, List playables)
        {
            var mixer = Playable.Create(graph, playables.Count);
            for (int i = 0; i != playables.Count; ++i)
            {
                ConnectMixerAndPlayable(graph, mixer, playables[i], i);
            }
            mixer.SetPropagateSetTime(true);
            return mixer;
        }
        void CreateActivationPlayable(GameObject root, PlayableGraph graph,
            List outplayables)
        {
            var activation = ActivationControlPlayable.Create(graph, root, postPlayback);
            if (activation.IsValid())
                outplayables.Add(activation);
        }
        void SearchHierarchyAndConnectParticleSystem(IEnumerable particleSystems, PlayableGraph graph,
            List outplayables)
        {
            foreach (var particleSystem in particleSystems)
            {
                if (particleSystem != null)
                {
                    controllingParticles = true;
                    outplayables.Add(ParticleControlPlayable.Create(graph, particleSystem, particleRandomSeed));
                }
            }
        }
        void SearchHierarchyAndConnectDirector(IEnumerable directors, PlayableGraph graph,
            List outplayables, bool disableSelfReferences)
        {
            foreach (var director in directors)
            {
                if (director != null)
                {
                    if (director.playableAsset != m_ControlDirectorAsset)
                    {
                        ScriptPlayable directorControlPlayable = DirectorControlPlayable.Create(graph, director);
                        directorControlPlayable.GetBehaviour().pauseAction = directorOnClipEnd;
                        outplayables.Add(directorControlPlayable);
                        controllingDirectors = true;
                    }
                    // if this self references, disable the director.
                    else if (disableSelfReferences)
                    {
                        director.enabled = false;
                    }
                }
            }
        }
        static void SearchHierarchyAndConnectControlableScripts(IEnumerable controlableScripts, PlayableGraph graph, List outplayables)
        {
            foreach (var script in controlableScripts)
            {
                outplayables.Add(TimeControlPlayable.Create(graph, (ITimeControl)script));
            }
        }
        static void ConnectMixerAndPlayable(PlayableGraph graph, Playable mixer, Playable playable,
            int portIndex)
        {
            graph.Connect(playable, 0, mixer, portIndex);
            mixer.SetInputWeight(playable, 1.0f);
        }
        internal IList GetComponent(GameObject gameObject)
        {
            var components = new List();
            if (gameObject != null)
            {
                if (searchHierarchy)
                {
                    gameObject.GetComponentsInChildren(true, components);
                }
                else
                {
                    gameObject.GetComponents(components);
                }
            }
            return components;
        }
        internal static IEnumerable GetControlableScripts(GameObject root)
        {
            if (root == null)
                yield break;
            foreach (var script in root.GetComponentsInChildren())
            {
                if (script is ITimeControl)
                    yield return script;
            }
        }
        internal void UpdateDurationAndLoopFlag(IList directors, IList particleSystems)
        {
            if (directors.Count == 0 && particleSystems.Count == 0)
                return;
            const double invalidDuration = double.NegativeInfinity;
            var maxDuration = invalidDuration;
            var supportsLoop = false;
            foreach (var director in directors)
            {
                if (director.playableAsset != null)
                {
                    var assetDuration = director.playableAsset.duration;
                    if (director.playableAsset is TimelineAsset && assetDuration > 0.0)
                        // Timeline assets report being one tick shorter than they actually are, unless they are empty
                        assetDuration = (double)((DiscreteTime)assetDuration).OneTickAfter();
                    maxDuration = Math.Max(maxDuration, assetDuration);
                    supportsLoop = supportsLoop || director.extrapolationMode == DirectorWrapMode.Loop;
                }
            }
            foreach (var particleSystem in particleSystems)
            {
                maxDuration = Math.Max(maxDuration, particleSystem.main.duration);
                supportsLoop = supportsLoop || particleSystem.main.loop;
            }
            m_Duration = double.IsNegativeInfinity(maxDuration) ? PlayableBinding.DefaultDuration : maxDuration;
            m_SupportLoop = supportsLoop;
        }
        IList GetControllableParticleSystems(GameObject go)
        {
            var roots = new List();
            // searchHierarchy will look for particle systems on child objects.
            // once a particle system is found, all child particle systems are controlled with playables
            // unless they are subemitters
            if (searchHierarchy || go.GetComponent() != null)
            {
                GetControllableParticleSystems(go.transform, roots, s_SubEmitterCollector);
                s_SubEmitterCollector.Clear();
            }
            return roots;
        }
        static void GetControllableParticleSystems(Transform t, ICollection roots, HashSet subEmitters)
        {
            var ps = t.GetComponent();
            if (ps != null)
            {
                if (!subEmitters.Contains(ps))
                {
                    roots.Add(ps);
                    CacheSubEmitters(ps, subEmitters);
                }
            }
            for (int i = 0; i < t.childCount; ++i)
            {
                GetControllableParticleSystems(t.GetChild(i), roots, subEmitters);
            }
        }
        static void CacheSubEmitters(ParticleSystem ps, HashSet subEmitters)
        {
            if (ps == null)
                return;
            for (int i = 0; i < ps.subEmitters.subEmittersCount; i++)
            {
                subEmitters.Add(ps.subEmitters.GetSubEmitterSystem(i));
                // don't call this recursively. subEmitters are only simulated one level deep.
            }
        }
        /// 
        public void GatherProperties(PlayableDirector director, IPropertyCollector driver)
        {
            // This method is no longer called by Control Tracks.
            if (director == null)
                return;
            // prevent infinite recursion
            if (s_ProcessedDirectors.Contains(director))
                return;
            s_ProcessedDirectors.Add(director);
            var gameObject = sourceGameObject.Resolve(director);
            if (gameObject != null)
            {
                if (updateParticle)// case 1076850 -- drive all emitters, not just roots.
                    PreviewParticles(driver, gameObject.GetComponentsInChildren(true));
                if (active)
                    PreviewActivation(driver, new[] { gameObject });
                if (updateITimeControl)
                    PreviewTimeControl(driver, director, GetControlableScripts(gameObject));
                if (updateDirector)
                    PreviewDirectors(driver, GetComponent(gameObject));
            }
            s_ProcessedDirectors.Remove(director);
        }
        internal static void PreviewParticles(IPropertyCollector driver, IEnumerable particles)
        {
            foreach (var ps in particles)
            {
                driver.AddFromName(ps.gameObject, "randomSeed");
                driver.AddFromName(ps.gameObject, "autoRandomSeed");
            }
        }
        internal static void PreviewActivation(IPropertyCollector driver, IEnumerable objects)
        {
            foreach (var gameObject in objects)
                driver.AddFromName(gameObject, "m_IsActive");
        }
        internal static void PreviewTimeControl(IPropertyCollector driver, PlayableDirector director, IEnumerable scripts)
        {
            foreach (var script in scripts)
            {
                var propertyPreview = script as IPropertyPreview;
                if (propertyPreview != null)
                    propertyPreview.GatherProperties(director, driver);
                else
                    driver.AddFromComponent(script.gameObject, script);
            }
        }
        internal static void PreviewDirectors(IPropertyCollector driver, IEnumerable directors)
        {
            foreach (var childDirector in directors)
            {
                if (childDirector == null)
                    continue;
                var timeline = childDirector.playableAsset as TimelineAsset;
                if (timeline == null)
                    continue;
                timeline.GatherProperties(childDirector, driver);
            }
        }
    }
}