using System;
using System.Collections.Generic;
using System.Linq;
using ModestTree;
using ModestTree.Util;
#if !NOT_UNITY3D
using UnityEngine.SceneManagement;
using UnityEngine;
#endif

namespace Zenject.Internal
{
    public static class ZenUtilInternal
    {
#if UNITY_EDITOR
        static GameObject _disabledIndestructibleGameObject;
#endif

        // Due to the way that Unity overrides the Equals operator,
        // normal null checks such as (x == null) do not always work as
        // expected
        // In those cases you can use this function which will also
        // work with non-unity objects
        public static bool IsNull(System.Object obj)
        {
            return obj == null || obj.Equals(null);
        }

#if UNITY_EDITOR
        // This can be useful if you are running code outside unity
        // since in that case you have to make sure to avoid calling anything
        // inside Unity DLLs
        public static bool IsOutsideUnity()
        {
            return AppDomain.CurrentDomain.FriendlyName != "Unity Child Domain";
        }
#endif

        public static bool AreFunctionsEqual(Delegate left, Delegate right)
        {
            return left.Target == right.Target && left.Method() == right.Method();
        }

        // Taken from here:
        // http://stackoverflow.com/questions/28937324/in-c-how-could-i-get-a-classs-inheritance-distance-to-base-class/28937542#28937542
        public static int GetInheritanceDelta(Type derived, Type parent)
        {
            Assert.That(derived.DerivesFromOrEqual(parent));

            if (parent.IsInterface())
            {
                // Not sure if we can calculate this so just return 1
                return 1;
            }

            if (derived == parent)
            {
                return 0;
            }

            int distance = 1;

            Type child = derived;

            while ((child = child.BaseType()) != parent)
            {
                distance++;
            }

            return distance;
        }

#if !NOT_UNITY3D
        public static IEnumerable<SceneContext> GetAllSceneContexts()
        {
            foreach (var scene in UnityUtil.AllLoadedScenes)
            {
                var contexts = scene.GetRootGameObjects()
                    .SelectMany(root => root.GetComponentsInChildren<SceneContext>()).ToList();

                if (contexts.IsEmpty())
                {
                    continue;
                }

                Assert.That(contexts.Count == 1,
                    "Found multiple scene contexts in scene '{0}'", scene.name);

                yield return contexts[0];
            }
        }

        public static void AddStateMachineBehaviourAutoInjectersInScene(Scene scene)
        {
            foreach (var rootObj in GetRootGameObjects(scene))
            {
                if (rootObj != null)
                {
                    AddStateMachineBehaviourAutoInjectersUnderGameObject(rootObj);
                }
            }
        }

        // Call this before calling GetInjectableMonoBehavioursUnderGameObject to ensure that the StateMachineBehaviour's
        // also get injected properly
        // The StateMachineBehaviour's cannot be retrieved until after the Start() method so we
        // need to use ZenjectStateMachineBehaviourAutoInjecter to do the injection at that
        // time for us
        public static void AddStateMachineBehaviourAutoInjectersUnderGameObject(GameObject root)
        {
#if ZEN_INTERNAL_PROFILING
            using (ProfileTimers.CreateTimedBlock("Searching Hierarchy"))
#endif
            {
                var animators = root.GetComponentsInChildren<Animator>(true);

                foreach (var animator in animators)
                {
                    if (animator.gameObject.GetComponent<ZenjectStateMachineBehaviourAutoInjecter>() == null)
                    {
                        animator.gameObject.AddComponent<ZenjectStateMachineBehaviourAutoInjecter>();
                    }
                }
            }
        }

        public static void GetInjectableMonoBehavioursInScene(
            Scene scene, List<MonoBehaviour> monoBehaviours)
        {
#if ZEN_INTERNAL_PROFILING
            using (ProfileTimers.CreateTimedBlock("Searching Hierarchy"))
#endif
            {
                foreach (var rootObj in GetRootGameObjects(scene))
                {
                    if (rootObj != null)
                    {
                        GetInjectableMonoBehavioursUnderGameObjectInternal(rootObj, monoBehaviours);
                    }
                }
            }
        }

        // NOTE: This method will not return components that are within a GameObjectContext
        // It returns monobehaviours in a bottom-up order
        public static void GetInjectableMonoBehavioursUnderGameObject(
            GameObject gameObject, List<MonoBehaviour> injectableComponents)
        {
#if ZEN_INTERNAL_PROFILING
            using (ProfileTimers.CreateTimedBlock("Searching Hierarchy"))
#endif
            {
                GetInjectableMonoBehavioursUnderGameObjectInternal(gameObject, injectableComponents);
            }
        }

        static void GetInjectableMonoBehavioursUnderGameObjectInternal(
            GameObject gameObject, List<MonoBehaviour> injectableComponents)
        {
            if (gameObject == null)
            {
                return;
            }

            var monoBehaviours = gameObject.GetComponents<MonoBehaviour>();

            for (int i = 0; i < monoBehaviours.Length; i++)
            {
                var monoBehaviour = monoBehaviours[i];

                // Can be null for broken component references
                if (monoBehaviour != null
                        && monoBehaviour.GetType().DerivesFromOrEqual<GameObjectContext>())
                {
                    // Need to make sure we don't inject on any MonoBehaviour's that are below a GameObjectContext
                    // Since that is the responsibility of the GameObjectContext
                    // BUT we do want to inject on the GameObjectContext itself
                    injectableComponents.Add(monoBehaviour);
                    return;
                }
            }

            // Recurse first so it adds components bottom up though it shouldn't really matter much
            // because it should always inject in the dependency order
            for (int i = 0; i < gameObject.transform.childCount; i++)
            {
                var child = gameObject.transform.GetChild(i);

                if (child != null)
                {
                    GetInjectableMonoBehavioursUnderGameObjectInternal(child.gameObject, injectableComponents);
                }
            }

            for (int i = 0; i < monoBehaviours.Length; i++)
            {
                var monoBehaviour = monoBehaviours[i];

                // Can be null for broken component references
                if (monoBehaviour != null
                    && IsInjectableMonoBehaviourType(monoBehaviour.GetType()))
                {
                    injectableComponents.Add(monoBehaviour);
                }
            }
        }

        public static bool IsInjectableMonoBehaviourType(Type type)
        {
            // Do not inject on installers since these are always injected before they are installed
            return type != null && !type.DerivesFrom<MonoInstaller>() && TypeAnalyzer.HasInfo(type);
        }

        public static IEnumerable<GameObject> GetRootGameObjects(Scene scene)
        {
#if ZEN_INTERNAL_PROFILING
            using (ProfileTimers.CreateTimedBlock("Searching Hierarchy"))
#endif
            {
                if (scene.isLoaded)
                {
                    return scene.GetRootGameObjects()
                        .Where(x => x.GetComponent<ProjectContext>() == null);
                }

                // Note: We can't use scene.GetRootObjects() here because that apparently fails with an exception
                // about the scene not being loaded yet when executed in Awake
                // We also can't use GameObject.FindObjectsOfType<Transform>() because that does not include inactive game objects
                // So we use Resources.FindObjectsOfTypeAll, even though that may include prefabs.  However, our assumption here
                // is that prefabs do not have their "scene" property set correctly so this should work
                //
                // It's important here that we only inject into root objects that are part of our scene, to properly support
                // multi-scene editing features of Unity 5.x
                //
                // Also, even with older Unity versions, if there is an object that is marked with DontDestroyOnLoad, then it will
                // be injected multiple times when another scene is loaded
                //
                // We also make sure not to inject into the project root objects which are injected by ProjectContext.
                return Resources.FindObjectsOfTypeAll<GameObject>()
                    .Where(x => x.transform.parent == null
                            && x.GetComponent<ProjectContext>() == null
                            && x.scene == scene);
            }
        }

#if UNITY_EDITOR
        // Returns a Transform in the DontDestroyOnLoad scene (or, if we're not in play mode, within the current active scene)
        // whose GameObject is inactive, and whose hide flags are set to HideAndDontSave. We can instantiate prefabs in here
        // without any of their Awake() methods firing.
        public static Transform GetOrCreateInactivePrefabParent()
        {
            if(_disabledIndestructibleGameObject == null || (!Application.isPlaying && _disabledIndestructibleGameObject.scene != SceneManager.GetActiveScene()))
            {
                var go = new GameObject("ZenUtilInternal_PrefabParent");
                go.hideFlags = HideFlags.HideAndDontSave;
                go.SetActive(false);

                if(Application.isPlaying)
                {
                    UnityEngine.Object.DontDestroyOnLoad(go);
                }

                _disabledIndestructibleGameObject = go;
            }

            return _disabledIndestructibleGameObject.transform;
        }
#endif

#endif
    }
}
