using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using VContainer.Diagnostics;
using VContainer.Internal;

namespace VContainer.Unity
{
    [DefaultExecutionOrder(-5000)]
    public partial class LifetimeScope : MonoBehaviour, IDisposable
    {
        public readonly struct ParentOverrideScope : IDisposable
        {
            public ParentOverrideScope(LifetimeScope nextParent)
            {
                lock (SyncRoot)
                {
                    GlobalOverrideParents.Push(nextParent);
                }
            }

            public void Dispose()
            {
                lock (SyncRoot)
                {
                    GlobalOverrideParents.Pop();
                }
            }
        }

        public readonly struct ExtraInstallationScope : IDisposable
        {
            public ExtraInstallationScope(IInstaller installer)
            {
                lock (SyncRoot)
                {
                    GlobalExtraInstallers.Push(installer);
                }
            }

            void IDisposable.Dispose()
            {
                lock (SyncRoot)
                {
                    GlobalExtraInstallers.Pop();
                }
            }
        }

        [SerializeField]
        public ParentReference parentReference;

        [SerializeField]
        public bool autoRun = true;

        [SerializeField]
        protected List<GameObject> autoInjectGameObjects;

        string scopeName;

        static readonly Stack<LifetimeScope> GlobalOverrideParents = new Stack<LifetimeScope>();
        static readonly Stack<IInstaller> GlobalExtraInstallers = new Stack<IInstaller>();
        static readonly object SyncRoot = new object();

        public static LifetimeScope Create(IInstaller installer = null, string name = null)
        {
            var gameObject = new GameObject(name ?? "LifetimeScope");
            gameObject.SetActive(false);
            var newScope = gameObject.AddComponent<LifetimeScope>();
            if (installer != null)
            {
                newScope.localExtraInstallers.Add(installer);
            }
            gameObject.SetActive(true);
            return newScope;
        }

        public static LifetimeScope Create(Action<IContainerBuilder> configuration, string name = null)
            => Create(new ActionInstaller(configuration), name);

        public static ParentOverrideScope EnqueueParent(LifetimeScope parent)
            => new ParentOverrideScope(parent);

        public static ExtraInstallationScope Enqueue(Action<IContainerBuilder> installing)
            => new ExtraInstallationScope(new ActionInstaller(installing));

        public static ExtraInstallationScope Enqueue(IInstaller installer)
            => new ExtraInstallationScope(installer);

        [Obsolete("LifetimeScope.PushParent is obsolete. Use LifetimeScope.EnqueueParent instead.", false)]
        public static ParentOverrideScope PushParent(LifetimeScope parent) => new ParentOverrideScope(parent);

        [Obsolete("LifetimeScope.Push is obsolete. Use LifetimeScope.Enqueue instead.", false)]
        public static ExtraInstallationScope Push(Action<IContainerBuilder> installing) => Enqueue(installing);

        [Obsolete("LifetimeScope.Push is obsolete. Use LifetimeScope.Enqueue instead.", false)]
        public static ExtraInstallationScope Push(IInstaller installer) => Enqueue(installer);

        public static LifetimeScope Find<T>(Scene scene) where T : LifetimeScope => Find(typeof(T), scene);
        public static LifetimeScope Find<T>() where T : LifetimeScope => Find(typeof(T));

        static LifetimeScope Find(Type type, Scene scene)
        {
            using (ListPool<GameObject>.Get(out var buffer))
            {
                scene.GetRootGameObjects(buffer);
                foreach (var gameObject in buffer)
                {
                    var found = gameObject.GetComponentInChildren(type) as LifetimeScope;
                    if (found != null)
                        return found;
                }
            }
            return null;
        }

        static LifetimeScope Find(Type type)
        {
#if UNITY_2022_1_OR_NEWER
            return (LifetimeScope)FindAnyObjectByType(type);
#else
            return (LifetimeScope)FindObjectOfType(type);
#endif
        }

        public IObjectResolver Container { get; private set; }
        public LifetimeScope Parent { get; private set; }

        public bool IsRoot => VContainerSettings.Instance != null &&
                              VContainerSettings.Instance.IsRootLifetimeScopeInstance(this);

        readonly List<IInstaller> localExtraInstallers = new List<IInstaller>();

        protected virtual void Awake()
        {
            if (VContainerSettings.DiagnosticsEnabled && string.IsNullOrEmpty(scopeName))
            {
                scopeName = $"{name} ({gameObject.GetInstanceID()})";
            }
            try
            {
                if (autoRun)
                {
                    Build();
                }
            }
            catch (VContainerParentTypeReferenceNotFound) when(!IsRoot)
            {
                if (WaitingList.Contains(this))
                {
                    throw;
                }
                EnqueueAwake(this);
            }
        }

        protected virtual void OnDestroy()
        {
            DisposeCore();
        }

        protected virtual void Configure(IContainerBuilder builder) { }

        public void Dispose()
        {
            DisposeCore();
            if (this != null)
            {
                Destroy(gameObject);
            }
        }

        public void DisposeCore()
        {
            Container?.Dispose();
            Container = null;
            CancelAwake(this);
            if (VContainerSettings.DiagnosticsEnabled)
            {
                DiagnositcsContext.RemoveCollector(scopeName);
            }
        }

        public void Build()
        {
            if (Parent == null)
                Parent = GetRuntimeParent();

            if (Parent != null)
            {
                if (VContainerSettings.Instance != null && Parent.IsRoot)
                {
                    if (Parent.Container == null)
                        Parent.Build();
                }

                // ReSharper disable once PossibleNullReferenceException
                Parent.Container.CreateScope(builder =>
                {
                    builder.RegisterBuildCallback(SetContainer);
                    builder.ApplicationOrigin = this;
                    builder.Diagnostics = VContainerSettings.DiagnosticsEnabled ? DiagnositcsContext.GetCollector(scopeName) : null;
                    InstallTo(builder);
                });
            }
            else
            {
                var builder = new ContainerBuilder
                {
                    ApplicationOrigin = this,
                    Diagnostics = VContainerSettings.DiagnosticsEnabled ? DiagnositcsContext.GetCollector(scopeName) : null,
                };
                builder.RegisterBuildCallback(SetContainer);
                InstallTo(builder);
                builder.Build();
            }

            AwakeWaitingChildren(this);
        }

        void SetContainer(IObjectResolver container)
        {
            Container = container;
            AutoInjectAll();
        }

        public TScope CreateChild<TScope>(IInstaller installer = null, string childScopeName = null)
            where TScope : LifetimeScope
        {
            var childGameObject = new GameObject(childScopeName ?? "LifetimeScope (Child)");
            childGameObject.SetActive(false);
            childGameObject.transform.SetParent(transform, false);

            var child = childGameObject.AddComponent<TScope>();
            if (installer != null)
            {
                child.localExtraInstallers.Add(installer);
            }
            child.parentReference.Object = this;
            childGameObject.SetActive(true);
            return child;
        }

        public LifetimeScope CreateChild(IInstaller installer = null, string childScopeName = null)
            => CreateChild<LifetimeScope>(installer, childScopeName);

        public TScope CreateChild<TScope>(Action<IContainerBuilder> installation, string childScopeName = null)
            where TScope : LifetimeScope
            => CreateChild<TScope>(new ActionInstaller(installation), childScopeName);

        public LifetimeScope CreateChild(Action<IContainerBuilder> installation, string childScopeName = null)
            => CreateChild<LifetimeScope>(new ActionInstaller(installation), childScopeName);

        public TScope CreateChildFromPrefab<TScope>(TScope prefab, IInstaller installer = null)
            where TScope : LifetimeScope
        {
            var wasActive = prefab.gameObject.activeSelf;
            using (new ObjectResolverUnityExtensions.PrefabDirtyScope(prefab.gameObject))
            {
                if (wasActive)
                {
                    prefab.gameObject.SetActive(false);
                }
                var child = Instantiate(prefab, transform, false);
                if (installer != null)
                {
                    child.localExtraInstallers.Add(installer);
                }
                child.parentReference.Object = this;
                if (wasActive)
                {
                    prefab.gameObject.SetActive(true);
                    child.gameObject.SetActive(true);
                }
                return child;
            }
        }

        public TScope CreateChildFromPrefab<TScope>(TScope prefab, Action<IContainerBuilder> installation)
            where TScope : LifetimeScope
            => CreateChildFromPrefab(prefab, new ActionInstaller(installation));

        void InstallTo(IContainerBuilder builder)
        {
            Configure(builder);

            foreach (var installer in localExtraInstallers)
            {
                installer.Install(builder);
            }
            localExtraInstallers.Clear();

            lock (SyncRoot)
            {
                foreach (var installer in GlobalExtraInstallers)
                {
                    installer.Install(builder);
                }
            }

            builder.RegisterInstance<LifetimeScope>(this).AsSelf();
            EntryPointsBuilder.EnsureDispatcherRegistered(builder);
        }

        protected virtual LifetimeScope FindParent() => null;

        LifetimeScope GetRuntimeParent()
        {
            if (IsRoot) return null;

            if (parentReference.Object != null)
                return parentReference.Object;

            // Find via implementation
            var implParent = FindParent();
            if (implParent != null)
            {
                if (parentReference.Type != null && parentReference.Type != implParent.GetType()) {
                    UnityEngine.Debug.LogWarning($"FindParent returned {implParent.GetType()} but parent reference type is {parentReference.Type}. This may be unintentional.");
                }
                return implParent;
            }

            // Find in scene via type
            if (parentReference.Type != null && parentReference.Type != GetType())
            {
                var found = Find(parentReference.Type);
                if (found != null && found.Container != null)
                {
                    return found;
                }
                throw new VContainerParentTypeReferenceNotFound(
                    parentReference.Type,
                    $"{name} could not found parent reference of type : {parentReference.Type}");
            }

            lock (SyncRoot)
            {
                if (GlobalOverrideParents.Count > 0)
                {
                    return GlobalOverrideParents.Peek();
                }
            }

            // Find root from settings
            if (VContainerSettings.Instance != null)
            {
                return VContainerSettings.Instance.GetOrCreateRootLifetimeScopeInstance();
            }

            return null;
        }

        void AutoInjectAll()
        {
            if (autoInjectGameObjects == null)
                return;

            foreach (var target in autoInjectGameObjects)
            {
                if (target != null) // Check missing reference
                {
                    Container.InjectGameObject(target);
                }
            }
        }
    }
}
