using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
using Object = UnityEngine.Object;

namespace UnityEditor.Timeline
{
    class PropertyCollector : IPropertyCollector
    {
        readonly Stack<GameObject> m_ObjectStack = new Stack<GameObject>();

        // Call immediately before use
        public void Reset()
        {
            m_ObjectStack.Clear();
        }

        // call to reset caches. should be called when switching master timelines
        public void Clear()
        {
            m_ObjectStack.Clear();
            AnimationPreviewUtilities.ClearCaches();
        }

        public void PushActiveGameObject(GameObject gameObject)
        {
            m_ObjectStack.Push(gameObject);
        }

        public void PopActiveGameObject()
        {
            m_ObjectStack.Pop();
        }

        public void AddFromClip(AnimationClip clip)
        {
            var go = m_ObjectStack.Peek(); // allow it to throw if empty
            if (go != null && clip != null) // null game object is allowed for calls to be ignored
                AddFromClip(go, clip);
        }

        public void AddFromClips(IEnumerable<AnimationClip> clips)
        {
            var go = m_ObjectStack.Peek();
            if (go != null)
                AddFromClips(go, clips);
        }

        public void AddFromName<T>(string name) where T : Component
        {
            var go = m_ObjectStack.Peek(); // allow it to throw if empty
            if (go != null) // null game object is allowed for calls to be ignored
                AddFromName<T>(go, name);
        }

        public void AddFromName(string name)
        {
            var go = m_ObjectStack.Peek(); // allow it to throw if empty
            if (go != null) // null game object is allowed for calls to be ignored
                AddFromName(go, name);
        }

        public void AddFromClip(GameObject obj, AnimationClip clip)
        {
            if (!Application.isPlaying)
                AddPropertiesFromClip(obj, clip);
        }

        public void AddFromClips(GameObject animatorRoot, IEnumerable<AnimationClip> clips)
        {
            if (Application.isPlaying)
                return;

            AnimationPreviewUtilities.PreviewFromCurves(animatorRoot, AnimationPreviewUtilities.GetBindings(animatorRoot, clips));
        }

        public void AddFromName<T>(GameObject obj, string name) where T : Component
        {
            if (!Application.isPlaying)
                AddPropertiesFromName(obj, typeof(T), name);
        }

        public void AddFromName(GameObject obj, string name)
        {
            if (!Application.isPlaying)
                AddPropertiesFromName(obj, name);
        }

        public void AddFromName(Component component, string name)
        {
            if (!Application.isPlaying)
                AddPropertyModification(component, name);
        }

        public void AddFromComponent(GameObject obj, Component component)
        {
            if (Application.isPlaying)
                return;

            if (obj == null || component == null)
                return;

            var serializedObject = new SerializedObject(component);
            SerializedProperty property = serializedObject.GetIterator();

            while (property.NextVisible(true))
            {
                if (property.hasVisibleChildren || !AnimatedParameterUtility.IsTypeAnimatable(property.propertyType))
                    continue;

                AddPropertyModification(component, property.propertyPath);
            }
        }

        void AddPropertiesFromClip(GameObject go, AnimationClip clip)
        {
            if (go != null && clip != null)
            {
                AnimationMode.InitializePropertyModificationForGameObject(go, clip);
            }
        }

        static void AddPropertiesFromName(GameObject go, string property)
        {
            if (go == null)
                return;

            AddPropertyModification(go, property);
        }

        static void AddPropertiesFromName(GameObject go, Type compType, string property)
        {
            if (go == null)
                return;
            var comp = go.GetComponent(compType);
            if (comp == null)
                return;

            AddPropertyModification(comp, property);
        }

        public void AddObjectProperties(Object obj, AnimationClip clip)
        {
            if (obj == null || clip == null)
                return;

            IPlayableAsset asset = obj as IPlayableAsset;
            IPlayableBehaviour playable = obj as IPlayableBehaviour;

            // special case for assets that contain animated script playables.
            // The paths in the clip start from the field with the templated playable
            if (asset != null)
            {
                if (playable == null)
                {
                    AddSerializedPlayableModifications(asset, clip);
                }
                else
                {
                    // in this case the asset is the playable. The clip applies directly
                    AnimationMode.InitializePropertyModificationForObject(obj, clip);
                }
            }
        }

        void AddSerializedPlayableModifications(IPlayableAsset asset, AnimationClip clip)
        {
            var obj = asset as Object;
            if (obj == null)
                return;

            var driver = WindowState.previewDriver;
            if (driver == null || !AnimationMode.InAnimationMode(driver))
                return;

            var serializedObj = new SerializedObject(obj);
            var bindings = AnimationClipCurveCache.Instance.GetCurveInfo(clip).bindings;
            var fields = AnimatedParameterUtility.GetScriptPlayableFields(asset);

            // go through each binding and offset using the field name
            //  so the modification system can find the particle object using the asset as a root
            foreach (var b in bindings)
            {
                foreach (var f in fields)
                {
                    var propertyPath = f.Name + "." + b.propertyName;
                    if (serializedObj.FindProperty(propertyPath) != null)
                    {
                        DrivenPropertyManager.RegisterProperty(driver, obj, propertyPath);
                        break;
                    }
                }
            }
        }

        private static void AddPropertyModification(GameObject obj, string propertyName)
        {
            var driver = WindowState.previewDriver;
            if (driver == null || !AnimationMode.InAnimationMode(driver))
                return;

            DrivenPropertyManager.RegisterProperty(driver, obj, propertyName);
        }

        private static void AddPropertyModification(Component comp, string name)
        {
            if (comp == null)
                return;

            var driver = WindowState.previewDriver;
            if (driver == null || !AnimationMode.InAnimationMode(driver))
                return;

            // Register Property will display an error if a property doesn't exist (wanted behaviour)
            // However, it also displays an error on Monobehaviour m_Script property, since it can't be driven. (not wanted behaviour)
            // case 967026
            if (name == "m_Script" && (comp as MonoBehaviour) != null)
                return;

            DrivenPropertyManager.RegisterProperty(driver, comp, name);
        }
    }
}
