using System;
using System.Collections.Generic;
using System.Linq;
using R3;
using UnityEngine;

namespace KitsuneCafe.Extension
{
    public static class R3Extensions
    {

        private readonly struct TimeProviderInfo
        {
            public readonly FrameProvider FrameProvider;
            public readonly TimeKind TimeKind;

            public TimeProviderInfo(FrameProvider frameProvider, TimeKind timeKind)
            {
                FrameProvider = frameProvider;
                TimeKind = timeKind;
            }
        }

        private static readonly Dictionary<TimeProvider, TimeProviderInfo> timeProviderInfo = new()
        {
            { UnityTimeProvider.Initialization, new (UnityFrameProvider.Initialization, TimeKind.Time) },
            { UnityTimeProvider.EarlyUpdate, new (UnityFrameProvider.EarlyUpdate, TimeKind.Time) },
            { UnityTimeProvider.FixedUpdate, new (UnityFrameProvider.FixedUpdate, TimeKind.Time) },
            { UnityTimeProvider.PreUpdate, new (UnityFrameProvider.PreUpdate, TimeKind.Time) },
            { UnityTimeProvider.Update, new (UnityFrameProvider.Update, TimeKind.Time) },
            { UnityTimeProvider.PreLateUpdate, new (UnityFrameProvider.PreLateUpdate, TimeKind.Time) },
            { UnityTimeProvider.PostLateUpdate, new (UnityFrameProvider.PostLateUpdate, TimeKind.Time) },
            { UnityTimeProvider.TimeUpdate, new (UnityFrameProvider.TimeUpdate, TimeKind.Time) },

            { UnityTimeProvider.InitializationIgnoreTimeScale, new (UnityFrameProvider.Initialization, TimeKind.UnscaledTime) },
            { UnityTimeProvider.EarlyUpdateIgnoreTimeScale, new (UnityFrameProvider.EarlyUpdate, TimeKind.UnscaledTime) },
            { UnityTimeProvider.FixedUpdateIgnoreTimeScale, new (UnityFrameProvider.FixedUpdate, TimeKind.UnscaledTime) },
            { UnityTimeProvider.PreUpdateIgnoreTimeScale, new (UnityFrameProvider.PreUpdate, TimeKind.UnscaledTime) },
            { UnityTimeProvider.UpdateIgnoreTimeScale, new (UnityFrameProvider.Update, TimeKind.UnscaledTime) },
            { UnityTimeProvider.PreLateUpdateIgnoreTimeScale, new (UnityFrameProvider.PreLateUpdate, TimeKind.UnscaledTime) },
            { UnityTimeProvider.PostLateUpdateIgnoreTimeScale, new (UnityFrameProvider.PostLateUpdate, TimeKind.UnscaledTime) },
            { UnityTimeProvider.TimeUpdateIgnoreTimeScale, new (UnityFrameProvider.TimeUpdate, TimeKind.UnscaledTime) },

            { UnityTimeProvider.InitializationRealtime, new (UnityFrameProvider.Initialization, TimeKind.Realtime) },
            { UnityTimeProvider.EarlyUpdateRealtime, new (UnityFrameProvider.EarlyUpdate, TimeKind.Realtime) },
            { UnityTimeProvider.FixedUpdateRealtime, new (UnityFrameProvider.FixedUpdate, TimeKind.Realtime) },
            { UnityTimeProvider.PreUpdateRealtime, new (UnityFrameProvider.PreUpdate, TimeKind.Realtime) },
            { UnityTimeProvider.UpdateRealtime, new (UnityFrameProvider.Update, TimeKind.Realtime) },
            { UnityTimeProvider.PreLateUpdateRealtime, new (UnityFrameProvider.PreLateUpdate, TimeKind.Realtime) },
            { UnityTimeProvider.PostLateUpdateRealtime, new (UnityFrameProvider.PostLateUpdate, TimeKind.Realtime) },
            { UnityTimeProvider.TimeUpdateRealtime, new (UnityFrameProvider.TimeUpdate, TimeKind.Realtime) }
        };

        public enum PlayerLoopTiming
        {
            Initialization,
            EarlyUpdate,
            FixedUpdate,
            PreUpdate,
            Update,
            PreLateUpdate,
            PostLateUpdate,
            TimeUpdate,
            PostFixedUpdate
        }

        private static readonly Dictionary<FrameProvider, PlayerLoopTiming> frameProviderTiming = new()
        {
            { UnityFrameProvider.Initialization, PlayerLoopTiming.Initialization },
            { UnityFrameProvider.EarlyUpdate, PlayerLoopTiming.EarlyUpdate },
            { UnityFrameProvider.FixedUpdate, PlayerLoopTiming.FixedUpdate },
            { UnityFrameProvider.PreUpdate, PlayerLoopTiming.PreUpdate },
            { UnityFrameProvider.Update, PlayerLoopTiming.Update },
            { UnityFrameProvider.PreLateUpdate, PlayerLoopTiming.PreLateUpdate },
            { UnityFrameProvider.PostLateUpdate, PlayerLoopTiming.PostLateUpdate },
            { UnityFrameProvider.TimeUpdate, PlayerLoopTiming.TimeUpdate },
            { UnityFrameProvider.PostFixedUpdate, PlayerLoopTiming.PostFixedUpdate },

        };


        public static bool TryGetFrameProvider(this TimeProvider provider, out FrameProvider frameProvider)
        {
            if (timeProviderInfo.TryGetValue(provider, out var info))
            {
                frameProvider = info.FrameProvider;
                return true;
            }
            else
            {
                frameProvider = null;
                return false;
            }
        }

        public static FrameProvider GetFrameProvider(this TimeProvider provider)
        {
            return TryGetFrameProvider(provider, out var frameProvider) ? frameProvider : null;
        }

        public static bool TryGetDeltaTime(this TimeProvider provider, out float deltaTime)
        {
            if (
                timeProviderInfo.TryGetValue(provider, out var info)
                && info.FrameProvider.TryGetFrameTiming(out var timing)
                && (info.TimeKind != TimeKind.Realtime)
            )
            {
                deltaTime = (timing, info.TimeKind) switch
                {
                    (PlayerLoopTiming.FixedUpdate, TimeKind.Time) => Time.fixedDeltaTime,
                    (PlayerLoopTiming.FixedUpdate, TimeKind.UnscaledTime) => Time.fixedUnscaledDeltaTime,
                    (_, TimeKind.Time) => Time.deltaTime,
                    (_, TimeKind.UnscaledTime) => Time.unscaledDeltaTime,
                    (_, _) => default
                };

                return true;
            }
            else
            {
                deltaTime = default;
                return false;
            }
        }

        public static float GetDeltaTime(this TimeProvider provider, float defaultValue = default)
        {
            return TryGetDeltaTime(provider, out var deltaTime) ? deltaTime : defaultValue;
        }

        public static bool TryGetFrameTiming(this FrameProvider provider, out PlayerLoopTiming timing)
        {
            return frameProviderTiming.TryGetValue(provider, out timing);
        }

        public static PlayerLoopTiming? GetFrameTiming(this UnityFrameProvider provider)
        {
            return provider.TryGetFrameTiming(out var timing) ? timing : null;
        }

        public static TimeProvider GetTimeProvider(PlayerLoopTiming timing, TimeKind kind)
        {
            return (timing, kind) switch
            {
                (PlayerLoopTiming.Initialization, TimeKind.Time) => UnityTimeProvider.Initialization,
                (PlayerLoopTiming.Initialization, TimeKind.UnscaledTime) => UnityTimeProvider.InitializationIgnoreTimeScale,
                (PlayerLoopTiming.Initialization, TimeKind.Realtime) => UnityTimeProvider.InitializationRealtime,

                (PlayerLoopTiming.EarlyUpdate, TimeKind.Time) => UnityTimeProvider.EarlyUpdate,
                (PlayerLoopTiming.EarlyUpdate, TimeKind.UnscaledTime) => UnityTimeProvider.EarlyUpdate,
                (PlayerLoopTiming.EarlyUpdate, TimeKind.Realtime) => UnityTimeProvider.EarlyUpdate,

                (PlayerLoopTiming.FixedUpdate, TimeKind.Time) => UnityTimeProvider.FixedUpdate,
                (PlayerLoopTiming.FixedUpdate, TimeKind.UnscaledTime) => UnityTimeProvider.FixedUpdate,
                (PlayerLoopTiming.FixedUpdate, TimeKind.Realtime) => UnityTimeProvider.FixedUpdate,

                (PlayerLoopTiming.PreUpdate, TimeKind.Time) => UnityTimeProvider.PreUpdate,
                (PlayerLoopTiming.PreUpdate, TimeKind.UnscaledTime) => UnityTimeProvider.PreUpdate,
                (PlayerLoopTiming.PreUpdate, TimeKind.Realtime) => UnityTimeProvider.PreUpdate,

                (PlayerLoopTiming.Update, TimeKind.Time) => UnityTimeProvider.Update,
                (PlayerLoopTiming.Update, TimeKind.UnscaledTime) => UnityTimeProvider.Update,
                (PlayerLoopTiming.Update, TimeKind.Realtime) => UnityTimeProvider.Update,

                (PlayerLoopTiming.PreLateUpdate, TimeKind.Time) => UnityTimeProvider.PreLateUpdate,
                (PlayerLoopTiming.PreLateUpdate, TimeKind.UnscaledTime) => UnityTimeProvider.PreLateUpdate,
                (PlayerLoopTiming.PreLateUpdate, TimeKind.Realtime) => UnityTimeProvider.PreLateUpdate,

                (PlayerLoopTiming.PostLateUpdate, TimeKind.Time) => UnityTimeProvider.PostLateUpdate,
                (PlayerLoopTiming.PostLateUpdate, TimeKind.UnscaledTime) => UnityTimeProvider.PostLateUpdate,
                (PlayerLoopTiming.PostLateUpdate, TimeKind.Realtime) => UnityTimeProvider.PostLateUpdate,

                (PlayerLoopTiming.TimeUpdate, TimeKind.Time) => UnityTimeProvider.TimeUpdate,
                (PlayerLoopTiming.TimeUpdate, TimeKind.UnscaledTime) => UnityTimeProvider.TimeUpdate,
                (PlayerLoopTiming.TimeUpdate, TimeKind.Realtime) => UnityTimeProvider.TimeUpdate,

                (var t, var k) => throw new ArgumentException("({t}, {k}) are not valid values.")
            };
        }
    }
}