using System;
using System.Runtime.InteropServices;
using AOT;
using UnityEngine;

namespace JamRoulette
{
    public enum GameOverCondition
    {
        Win,
        Lose,
        Error
    }

    /// <summary>
    /// Talks to the Jam Roulette wrapper from a Unity WebGL build. Usage:
    /// <code>
    /// Microgame.GameStarted += gameTime => BeginPlay();
    /// Microgame.GameEnded += condition => Freeze();   // condition is null if skipped
    /// Microgame.Register("Dodge!", "arrows", 5, winOnTimeOut: true);
    /// Microgame.SetGameState(GameOverCondition.Lose, immediatelyEnd: true);
    /// </code>
    /// In the editor, or when the build isn't inside the wrapper, it runs the
    /// round itself so the game is still testable.
    /// </summary>
    public static class Microgame
    {
        public static event Action<SplashInfo> SplashShown;
        public static event Action LoadingEnded;
        public static event Action CountdownStarted;
        public static event Action CountdownEnded;
        /// <summary>Start gameplay here. Argument: round length in seconds.</summary>
        public static event Action<float> GameStarted;
        /// <summary>Every frame of the round. Argument: seconds left.</summary>
        public static event Action<float> GameUpdated;
        /// <summary>Stop gameplay. Null means the player skipped.</summary>
        public static event Action<GameOverCondition?> GameEnded;

        public static bool IsWrapped { get; private set; }
        public static bool IsPlaying { get; private set; }

        private const float HelloTimeout = 1f;
        private const float MinTime = 5f, MaxTime = 10f;

        private static bool _registered;
        private static float _gameTime;
        private static bool _winOnTimeOut;
        private static float _helloTimer;
        private static bool _standalone;
        private static float _timeLeft;
        private static GameOverCondition? _localCondition;

        [Serializable]
        public class SplashInfo
        {
            public string title;
            public string prompt;
            public string interaction;
            public float gameTime;
            public bool winOnTimeOut;
        }

        // Flat on purpose — JsonUtility can't do nesting or optional fields.
#pragma warning disable 0649 // filled in by JsonUtility
        [Serializable]
        private class Message
        {
            public string channel = "microgame";
            public string type;
            public string title;
            public string prompt;
            public string interaction;
            public float gameTime;
            public bool winOnTimeOut;
            public float timeLeft;
            public string condition;
            public bool immediatelyEnd;
            public string message;
        }
#pragma warning restore 0649

        private delegate void MessageCallback(string json);

#if UNITY_WEBGL && !UNITY_EDITOR
        [DllImport("__Internal")] private static extern void JamRoulette_Listen(MessageCallback callback);
        [DllImport("__Internal")] private static extern void JamRoulette_Post(string json);
#else
        private static void JamRoulette_Listen(MessageCallback callback) { }
        private static void JamRoulette_Post(string json) { }
#endif

        // Statics survive play mode when domain reload is disabled.
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
        private static void ResetStatics()
        {
            SplashShown = null;
            LoadingEnded = null;
            CountdownStarted = null;
            CountdownEnded = null;
            GameStarted = null;
            GameUpdated = null;
            GameEnded = null;
            IsWrapped = IsPlaying = _registered = _standalone = false;
            _localCondition = null;
        }

        /// <summary>
        /// Call once your game has booted. Don't start gameplay until GameStarted —
        /// the wrapper boots you hidden, ahead of time.
        /// </summary>
        public static void Register(string prompt, string interaction = null, float gameTime = 5f,
            bool winOnTimeOut = false)
        {
            if (_registered) return;
            _registered = true;
            _gameTime = Mathf.Clamp(gameTime, MinTime, MaxTime);
            _winOnTimeOut = winOnTimeOut;

            var driver = new GameObject("[Microgame]") { hideFlags = HideFlags.HideAndDontSave };
            UnityEngine.Object.DontDestroyOnLoad(driver);
            driver.AddComponent<Driver>();
            Application.logMessageReceived += OnLog;

#if UNITY_WEBGL && !UNITY_EDITOR
            JamRoulette_Listen(OnMessage);
            Post(new Message
            {
                type = "register", prompt = prompt, interaction = interaction,
                gameTime = _gameTime, winOnTimeOut = winOnTimeOut
            });
            _helloTimer = HelloTimeout;
#else
            // Next frame, so listeners subscribed right after Register still hear GameStarted.
            _helloTimer = 0f;
#endif
        }

        /// <summary>With immediatelyEnd false the round keeps running until the timer.</summary>
        public static void SetGameState(GameOverCondition condition, bool immediatelyEnd)
        {
            Post(new Message { type = "setGameState", condition = ToWire(condition), immediatelyEnd = immediatelyEnd });
            if (!_standalone || !IsPlaying) return;
            _localCondition = condition;
            if (immediatelyEnd) EndRound(condition);
        }

        private static void Post(Message msg) => JamRoulette_Post(JsonUtility.ToJson(msg));

        [MonoPInvokeCallback(typeof(MessageCallback))]
        private static void OnMessage(string json)
        {
            var msg = JsonUtility.FromJson<Message>(json);
            switch (msg.type)
            {
                case "hello":
                    IsWrapped = true;
                    break;
                case "showGameSplash":
                    SplashShown?.Invoke(new SplashInfo
                    {
                        title = msg.title, prompt = msg.prompt, interaction = msg.interaction,
                        gameTime = msg.gameTime, winOnTimeOut = msg.winOnTimeOut
                    });
                    break;
                case "loadingEnd":
                    LoadingEnded?.Invoke();
                    break;
                case "countdownStart":
                    CountdownStarted?.Invoke();
                    break;
                case "countdownEnd":
                    CountdownEnded?.Invoke();
                    break;
                case "gameStart":
                    IsPlaying = true;
                    GameStarted?.Invoke(msg.gameTime);
                    break;
                case "gameUpdate":
                    GameUpdated?.Invoke(msg.timeLeft);
                    break;
                case "gameEnd":
                    IsPlaying = false;
                    GameEnded?.Invoke(FromWire(msg.condition));
                    break;
            }
        }

        private static void OnLog(string condition, string stacktrace, LogType type)
        {
            if (type != LogType.Error && type != LogType.Exception && type != LogType.Assert) return;
            Post(new Message { type = "error", message = condition });
            if (_standalone && IsPlaying) EndRound(GameOverCondition.Error);
        }

        private static void StartStandalone()
        {
            _standalone = true;
            _timeLeft = _gameTime;
            _localCondition = null;
            IsPlaying = true;
            GameStarted?.Invoke(_gameTime);
        }

        private static void EndRound(GameOverCondition? condition)
        {
            IsPlaying = false;
            GameEnded?.Invoke(condition);
        }

        private static void Tick(float deltaTime)
        {
            if (_registered && !IsWrapped && !_standalone)
            {
                // Nobody answered: we're on our own itch page, not in the wrapper.
                _helloTimer -= deltaTime;
                if (_helloTimer <= 0) StartStandalone();
                return;
            }
            if (!_standalone || !IsPlaying) return;
            _timeLeft = Mathf.Max(0, _timeLeft - deltaTime);
            GameUpdated?.Invoke(_timeLeft);
            if (_timeLeft <= 0)
                EndRound(_localCondition ?? (_winOnTimeOut ? GameOverCondition.Win : GameOverCondition.Lose));
        }

        private static string ToWire(GameOverCondition condition) => condition.ToString().ToLowerInvariant();

        private static GameOverCondition? FromWire(string condition)
        {
            switch (condition)
            {
                case "win": return GameOverCondition.Win;
                case "lose": return GameOverCondition.Lose;
                case "error": return GameOverCondition.Error;
                default: return null;
            }
        }

        private class Driver : MonoBehaviour
        {
            private void Update() => Tick(Time.unscaledDeltaTime);
        }
    }
}
