using System;
using System.Threading;
#if UNITY_2019_3_OR_NEWER
using UnityEngine.LowLevel;
using UnityEngine.PlayerLoop;
#else
using UnityEngine.Experimental.LowLevel;
using UnityEngine.Experimental.PlayerLoop;
#endif

namespace VContainer.Unity
{
    public struct VContainerInitialization {}
    public struct VContainerPostInitialization {}
    public struct VContainerStartup {}
    public struct VContainerPostStartup {}
    public struct VContainerFixedUpdate {}
    public struct VContainerPostFixedUpdate {}
    public struct VContainerUpdate {}
    public struct VContainerPostUpdate {}
    public struct VContainerLateUpdate {}
    public struct VContainerPostLateUpdate {}

    enum PlayerLoopTiming
    {
        Initialization = 0,
        PostInitialization = 1,

        Startup = 2,
        PostStartup = 3,

        FixedUpdate = 4,
        PostFixedUpdate = 5,

        Update = 6,
        PostUpdate = 7,

        LateUpdate = 8,
        PostLateUpdate = 9,
    }

    static class PlayerLoopHelper
    {
        static readonly PlayerLoopRunner[] Runners = new PlayerLoopRunner[10];
        static long initialized;

        // [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        public static void EnsureInitialized()
        {
            if (Interlocked.CompareExchange(ref initialized, 1, 0) != 0)
                return;

            for (var i = 0; i < Runners.Length; i++)
            {
                Runners[i] = new PlayerLoopRunner();
            }

            var playerLoop =
#if UNITY_2019_3_OR_NEWER
                PlayerLoop.GetCurrentPlayerLoop();
#else
                PlayerLoop.GetDefaultPlayerLoop();
#endif

            var copyList = playerLoop.subSystemList;

            ref var initializeSystem = ref FindSubSystem(typeof(Initialization), copyList);
            InsertSubsystem(
                ref initializeSystem,
                null,
                new PlayerLoopSystem
                {
                    type = typeof(VContainerInitialization),
                    updateDelegate = Runners[(int)PlayerLoopTiming.Initialization].Run
                },
                new PlayerLoopSystem
                {
                    type = typeof(VContainerPostInitialization),
                    updateDelegate = Runners[(int)PlayerLoopTiming.PostInitialization].Run
                });


            ref var earlyUpdateSystem = ref FindSubSystem(typeof(EarlyUpdate), copyList);
            InsertSubsystem(
                ref earlyUpdateSystem,
                typeof(EarlyUpdate.ScriptRunDelayedStartupFrame),
                new PlayerLoopSystem
                {
                    type = typeof(VContainerStartup),
                    updateDelegate = Runners[(int)PlayerLoopTiming.Startup].Run
                },
                new PlayerLoopSystem
                {
                    type = typeof(VContainerPostStartup),
                    updateDelegate = Runners[(int)PlayerLoopTiming.PostStartup].Run
                });

            ref var fixedUpdateSystem = ref FindSubSystem(typeof(FixedUpdate), copyList);
            InsertSubsystem(
                ref fixedUpdateSystem,
                typeof(FixedUpdate.ScriptRunBehaviourFixedUpdate),
                new PlayerLoopSystem
                {
                    type = typeof(VContainerFixedUpdate),
                    updateDelegate = Runners[(int)PlayerLoopTiming.FixedUpdate].Run
                },
                new PlayerLoopSystem
                {
                    type = typeof(VContainerPostFixedUpdate),
                    updateDelegate = Runners[(int)PlayerLoopTiming.PostFixedUpdate].Run
                });

            ref var updateSystem = ref FindSubSystem(typeof(Update), copyList);
            InsertSubsystem(
                ref updateSystem,
                typeof(Update.ScriptRunBehaviourUpdate),
                new PlayerLoopSystem
                {
                    type = typeof(VContainerUpdate),
                    updateDelegate = Runners[(int)PlayerLoopTiming.Update].Run
                },
                new PlayerLoopSystem
                {
                    type = typeof(VContainerPostUpdate),
                    updateDelegate = Runners[(int)PlayerLoopTiming.PostUpdate].Run
                });

            ref var lateUpdateSystem = ref FindSubSystem(typeof(PreLateUpdate), copyList);
            InsertSubsystem(
                ref lateUpdateSystem,
                typeof(PreLateUpdate.ScriptRunBehaviourLateUpdate),
                new PlayerLoopSystem
                {
                    type = typeof(VContainerLateUpdate),
                    updateDelegate = Runners[(int)PlayerLoopTiming.LateUpdate].Run
                },
                new PlayerLoopSystem
                {
                    type = typeof(VContainerPostLateUpdate),
                    updateDelegate = Runners[(int)PlayerLoopTiming.PostLateUpdate].Run
                });

            playerLoop.subSystemList = copyList;
            PlayerLoop.SetPlayerLoop(playerLoop);
        }

        public static void Dispatch(PlayerLoopTiming timing, IPlayerLoopItem item)
        {
            EnsureInitialized();
            Runners[(int)timing].Dispatch(item);
        }

        static ref PlayerLoopSystem FindSubSystem(Type targetType, PlayerLoopSystem[] systems)
        {
            for (var i = 0; i < systems.Length; i++)
            {
                if (systems[i].type == targetType)
                    return ref systems[i];
            }
            throw new InvalidOperationException($"{targetType.FullName} not in systems");
        }

        static void InsertSubsystem(
            ref PlayerLoopSystem parentSystem,
            Type beforeType,
            PlayerLoopSystem newSystem,
            PlayerLoopSystem newPostSystem)
        {
            var source = parentSystem.subSystemList;
            var insertIndex = -1;
            if (beforeType == null)
            {
                insertIndex = 0;
            }
            for (var i = 0; i < source.Length; i++)
            {
                if (source[i].type == beforeType)
                {
                    insertIndex = i;
                }
            }

            if (insertIndex < 0)
            {
                throw new ArgumentException($"{beforeType.FullName} not in system {parentSystem} {parentSystem.type.FullName}");
            }

            var dest = new PlayerLoopSystem[source.Length + 2];
            for (var i = 0; i < dest.Length; i++)
            {
                if (i == insertIndex)
                {
                    dest[i] = newSystem;
                }
                else if (i == dest.Length - 1)
                {
                    dest[i] = newPostSystem;
                }
                else if (i < insertIndex)
                {
                    dest[i] = source[i];
                }
                else
                {
                    dest[i] = source[i - 1];
                }
            }

            parentSystem.subSystemList = dest;
        }
    }
}
