/*
 * Author: Nikolay Dvurechensky
 * Site: https://www.dvurechensky.pro/
 * Gmail: dvurechenskysoft@gmail.com
 * Last Updated: 12 мая 2025 08:07:05
 * Version: 1.0.1
 */

using System;
using System.Collections.Generic;
using ModestTree;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Zenject.Internal
{
    [InitializeOnLoad]
    public static class SceneParentAutomaticLoader
    {
        static SceneParentAutomaticLoader()
        {
            EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
        }

        static void OnPlayModeStateChanged(PlayModeStateChange state)
        {
            if (state == PlayModeStateChange.ExitingEditMode)
            {
                try
                {
                    ValidateMultiSceneSetupAndLoadDefaultSceneParents();
                }
                catch (Exception e)
                {
                    EditorApplication.isPlaying = false;
                    throw new ZenjectException(
                        "Failure occurred when attempting to load default scene parent contracts!", e);
                }
            }
            else if (state == PlayModeStateChange.EnteredEditMode)
            {
                // It would be cool to restore the initial scene set up here but in order to do this
                // we would have to make sure that the user saves the scene before running which
                // would be too annoying, so just leave any changes we've made alone
            }
        }

        public static void ValidateMultiSceneSetupAndLoadDefaultSceneParents()
        {
            var defaultContractsMap = LoadDefaultContractsMap();

            // NOTE: Even if configs is empty we still want to do the below logic to validate the
            // multi scene setup

            var sceneInfos = GetLoadedZenjectSceneInfos();
            var contractMap = GetCurrentSceneContractsMap(sceneInfos);

            foreach (var sceneInfo in sceneInfos)
            {
                ProcessScene(sceneInfo, contractMap, defaultContractsMap);
            }
        }

        static Dictionary<string, LoadedSceneInfo> GetCurrentSceneContractsMap(
            List<LoadedSceneInfo> sceneInfos)
        {
            var contractMap = new Dictionary<string, LoadedSceneInfo>();

            foreach (var info in sceneInfos)
            {
                AddToContractMap(contractMap, info);
            }

            return contractMap;
        }

        static void ProcessScene(
            LoadedSceneInfo sceneInfo,
            Dictionary<string, LoadedSceneInfo> contractMap,
            Dictionary<string, string> defaultContractsMap)
        {
            if (sceneInfo.SceneContext != null)
            {
                Assert.IsNull(sceneInfo.DecoratorContext);
                ProcessSceneParents(sceneInfo, contractMap, defaultContractsMap);
            }
            else
            {
                Assert.IsNotNull(sceneInfo.DecoratorContext);
                ProcessSceneDecorators(sceneInfo, contractMap, defaultContractsMap);
            }
        }

        static void ProcessSceneDecorators(
            LoadedSceneInfo sceneInfo,
            Dictionary<string, LoadedSceneInfo> contractMap,
            Dictionary<string, string> defaultContractsMap)
        {
            var decoratedContractName = sceneInfo.DecoratorContext.DecoratedContractName;

            LoadedSceneInfo decoratedSceneInfo;

            if (contractMap.TryGetValue(decoratedContractName, out decoratedSceneInfo))
            {
                ValidateDecoratedSceneMatch(sceneInfo, decoratedSceneInfo);
                return;
            }

            decoratedSceneInfo = LoadDefaultSceneForContract(
                sceneInfo, decoratedContractName, defaultContractsMap);

            EditorSceneManager.MoveSceneAfter(decoratedSceneInfo.Scene, sceneInfo.Scene);

            ValidateDecoratedSceneMatch(sceneInfo, decoratedSceneInfo);

            ProcessScene(decoratedSceneInfo, contractMap, defaultContractsMap);
        }

        static void ProcessSceneParents(
            LoadedSceneInfo sceneInfo,
            Dictionary<string, LoadedSceneInfo> contractMap,
            Dictionary<string, string> defaultContractsMap)
        {
            foreach (var parentContractName in sceneInfo.SceneContext.ParentContractNames)
            {
                LoadedSceneInfo parentInfo;

                if (contractMap.TryGetValue(parentContractName, out parentInfo))
                {
                    ValidateParentChildMatch(parentInfo, sceneInfo);
                    continue;
                }

                parentInfo = LoadDefaultSceneForContract(sceneInfo, parentContractName, defaultContractsMap);

                AddToContractMap(contractMap, parentInfo);

                EditorSceneManager.MoveSceneBefore(parentInfo.Scene, sceneInfo.Scene);

                ValidateParentChildMatch(parentInfo, sceneInfo);

                ProcessScene(parentInfo, contractMap, defaultContractsMap);
            }
        }

        static LoadedSceneInfo LoadDefaultSceneForContract(
            LoadedSceneInfo sceneInfo, string contractName, Dictionary<string, string> defaultContractsMap)
        {
            string scenePath;

            if (!defaultContractsMap.TryGetValue(contractName, out scenePath))
            {
                throw Assert.CreateException(
                    "Could not fill contract '{0}' for scene '{1}'.  No scenes with that contract name are loaded, and could not find a match in any default scene contract configs to auto load one either."
                    .Fmt(contractName, sceneInfo.Scene.name));
            }

            Scene scene;

            try
            {
                scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive);
            }
            catch (Exception e)
            {
                throw new ZenjectException(
                    "Error while attempting to load contracts for scene '{0}'".Fmt(sceneInfo.Scene.name), e);
            }

            return CreateLoadedSceneInfo(scene);
        }

        static void ValidateDecoratedSceneMatch(
            LoadedSceneInfo decoratorInfo, LoadedSceneInfo decoratedInfo)
        {
            var decoratorIndex = GetSceneIndex(decoratorInfo.Scene);
            var decoratedIndex = GetSceneIndex(decoratedInfo.Scene);
            var activeIndex = GetSceneIndex(EditorSceneManager.GetActiveScene());

            Assert.That(decoratorIndex < decoratedIndex,
                "Decorator scene '{0}' must be loaded before decorated scene '{1}'.  Please drag the decorator scene to be placed above the other scene in the scene hierarchy.",
                decoratorInfo.Scene.name, decoratedInfo.Scene.name);

            if (activeIndex > decoratorIndex)
            {
                EditorSceneManager.SetActiveScene(decoratorInfo.Scene);
            }
        }

        static void ValidateParentChildMatch(
            LoadedSceneInfo parentSceneInfo, LoadedSceneInfo sceneInfo)
        {
            var parentIndex = GetSceneIndex(parentSceneInfo.Scene);
            var childIndex = GetSceneIndex(sceneInfo.Scene);
            var activeIndex = GetSceneIndex(EditorSceneManager.GetActiveScene());

            Assert.That(parentIndex < childIndex,
                "Parent scene '{0}' must be loaded before child scene '{1}'.  Please drag it to be placed above its child in the scene hierarchy.", parentSceneInfo.Scene.name, sceneInfo.Scene.name);

            if (activeIndex > parentIndex)
            {
                EditorSceneManager.SetActiveScene(parentSceneInfo.Scene);
            }
        }

        static int GetSceneIndex(Scene scene)
        {
            for (int i = 0; i < EditorSceneManager.sceneCount; i++)
            {
                if (EditorSceneManager.GetSceneAt(i) == scene)
                {
                    return i;
                }
            }

            throw Assert.CreateException();
        }

        static Dictionary<string, string> LoadDefaultContractsMap()
        {
            var configs = Resources.LoadAll<DefaultSceneContractConfig>(DefaultSceneContractConfig.ResourcePath);

            var map = new Dictionary<string, string>();

            foreach (var config in configs)
            {
                foreach (var info in config.DefaultContracts)
                {
                    if (info.ContractName.Trim().IsEmpty())
                    {
                        Log.Warn("Found empty contract name in default scene contract config at path '{0}'", AssetDatabase.GetAssetPath(config));
                        continue;
                    }

                    Assert.That(!map.ContainsKey(info.ContractName),
                        "Found duplicate contract '{0}' in default scene contract config at '{1}'!  Default contract already specified", info.ContractName, AssetDatabase.GetAssetPath(config));

                    map.Add(info.ContractName, AssetDatabase.GetAssetPath(info.Scene));
                }
            }

            return map;
        }

        static LoadedSceneInfo CreateLoadedSceneInfo(Scene scene)
        {
            var info = TryCreateLoadedSceneInfo(scene);
            Assert.IsNotNull(info, "Expected scene '{0}' to be a zenject scene", scene.name);
            return info;
        }

        static LoadedSceneInfo TryCreateLoadedSceneInfo(Scene scene)
        {
            var sceneContext = ZenUnityEditorUtil.TryGetSceneContextForScene(scene);
            var decoratorContext = ZenUnityEditorUtil.TryGetDecoratorContextForScene(scene);

            if (sceneContext == null && decoratorContext == null)
            {
                return null;
            }

            var info = new LoadedSceneInfo
            {
                Scene = scene
            };

            if (sceneContext != null)
            {
                Assert.IsNull(decoratorContext,
                "Found both SceneContext and SceneDecoratorContext in scene '{0}'", scene.name);

                info.SceneContext = sceneContext;
            }
            else
            {
                Assert.IsNotNull(decoratorContext);

                info.DecoratorContext = decoratorContext;
            }

            return info;
        }

        static List<LoadedSceneInfo> GetLoadedZenjectSceneInfos()
        {
            var result = new List<LoadedSceneInfo>();

            for (int i = 0; i < EditorSceneManager.sceneCount; i++)
            {
                var scene = EditorSceneManager.GetSceneAt(i);
                var info = TryCreateLoadedSceneInfo(scene);

                if (info != null)
                {
                    result.Add(info);
                }
            }

            return result;
        }

        static void AddToContractMap(
            Dictionary<string, LoadedSceneInfo> contractMap, LoadedSceneInfo info)
        {
            if (info.SceneContext == null)
            {
                return;
            }

            foreach (var contractName in info.SceneContext.ContractNames)
            {
                LoadedSceneInfo currentInfo;

                if (contractMap.TryGetValue(contractName, out currentInfo))
                {
                    throw Assert.CreateException(
                        "Found multiple scene contracts with name '{0}'. Scene '{1}' and scene '{2}'",
                        contractName, currentInfo.Scene.name, info.Scene.name);
                }

                contractMap.Add(contractName, info);
            }
        }

        public class LoadedSceneInfo
        {
            public SceneContext SceneContext;
            public SceneDecoratorContext DecoratorContext;
            public Scene Scene;
        }
    }
}
