using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;

namespace StardewModdingAPI.Framework
{
    /// <summary>SMAPI's extension of the game's core <see cref="GameRunner"/>, used to inject SMAPI components.</summary>
    internal class SGameRunner : GameRunner
    {
        /*********
        ** Fields
        *********/
        /// <summary>Encapsulates monitoring and logging for SMAPI.</summary>
        private readonly Monitor Monitor;

        /// <summary>Manages SMAPI events for mods.</summary>
        private readonly EventManager Events;

        /// <summary>Simplifies access to private game code.</summary>
        private readonly Reflector Reflection;

        /// <summary>Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
        private readonly Action<string> ExitGameImmediately;

        /// <summary>The core SMAPI mod hooks.</summary>
        private readonly SModHooks ModHooks;

        /// <summary>The core multiplayer logic.</summary>
        private readonly SMultiplayer Multiplayer;

        /// <summary>Raised after the game finishes loading its initial content.</summary>
        private readonly Action OnGameContentLoaded;

        /// <summary>Raised when XNA is updating (roughly 60 times per second).</summary>
        private readonly Action<GameTime, Action> OnGameUpdating;

        /// <summary>Raised when the game instance for a local split-screen player is updating (once per <see cref="OnGameUpdating"/> per player).</summary>
        private readonly Action<SGame, GameTime, Action> OnPlayerInstanceUpdating;

        /// <summary>Raised before the game exits.</summary>
        private readonly Action OnGameExiting;


        /*********
        ** Public methods
        *********/
        /// <summary>The singleton instance.</summary>
        public static SGameRunner Instance => (SGameRunner)GameRunner.instance;


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="monitor">Encapsulates monitoring and logging for SMAPI.</param>
        /// <param name="reflection">Simplifies access to private game code.</param>
        /// <param name="eventManager">Manages SMAPI events for mods.</param>
        /// <param name="modHooks">Handles mod hooks provided by the game.</param>
        /// <param name="multiplayer">The core multiplayer logic.</param>
        /// <param name="exitGameImmediately">Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</param>
        /// <param name="onGameContentLoaded">Raised after the game finishes loading its initial content.</param>
        /// <param name="onGameUpdating">Raised when XNA is updating its state (roughly 60 times per second).</param>
        /// <param name="onPlayerInstanceUpdating">Raised when the game instance for a local split-screen player is updating (once per <see cref="OnGameUpdating"/> per player).</param>
        /// <param name="onGameExiting">Raised before the game exits.</param>
        public SGameRunner(Monitor monitor, Reflector reflection, EventManager eventManager, SModHooks modHooks, SMultiplayer multiplayer, Action<string> exitGameImmediately, Action onGameContentLoaded, Action<GameTime, Action> onGameUpdating, Action<SGame, GameTime, Action> onPlayerInstanceUpdating, Action onGameExiting)
        {
            // init XNA
            Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef;

            // hook into game
            this.ModHooks = modHooks;

            // init SMAPI
            this.Monitor = monitor;
            this.Events = eventManager;
            this.Reflection = reflection;
            this.Multiplayer = multiplayer;
            this.ExitGameImmediately = exitGameImmediately;
            this.OnGameContentLoaded = onGameContentLoaded;
            this.OnGameUpdating = onGameUpdating;
            this.OnPlayerInstanceUpdating = onPlayerInstanceUpdating;
            this.OnGameExiting = onGameExiting;
        }

        /// <summary>Create a game instance for a local player.</summary>
        /// <param name="playerIndex">The player index.</param>
        /// <param name="instanceIndex">The instance index.</param>
        public override Game1 CreateGameInstance(PlayerIndex playerIndex = PlayerIndex.One, int instanceIndex = 0)
        {
            SInputState inputState = new SInputState();
            return new SGame(playerIndex, instanceIndex, this.Monitor, this.Reflection, this.Events, inputState, this.ModHooks, this.Multiplayer, this.ExitGameImmediately, this.OnPlayerInstanceUpdating, this.OnGameContentLoaded);
        }

        /// <inheritdoc />
        public override void AddGameInstance(PlayerIndex playerIndex)
        {
            base.AddGameInstance(playerIndex);

            EarlyConstants.LogScreenId = Context.ScreenId;
            this.UpdateForSplitScreenChanges();
        }

        /// <inheritdoc />
        public override void RemoveGameInstance(Game1 gameInstance)
        {
            base.RemoveGameInstance(gameInstance);

            if (this.gameInstances.Count <= 1)
                EarlyConstants.LogScreenId = null;
            this.UpdateForSplitScreenChanges();
        }

        /// <summary>Get the screen ID for a given player ID, if the player is local.</summary>
        /// <param name="playerId">The player ID to check.</param>
        public int? GetScreenId(long playerId)
        {
            return this.gameInstances
                .FirstOrDefault(p => ((SGame)p).PlayerId == playerId)
                ?.instanceId;
        }


        /*********
        ** Protected methods
        *********/
        /// <summary>Perform cleanup logic when the game exits.</summary>
        /// <param name="sender">The event sender.</param>
        /// <param name="args">The event args.</param>
        /// <remarks>This overrides the logic in <see cref="Game1.exitEvent"/> to let SMAPI clean up before exit.</remarks>
        protected override void OnExiting(object sender, EventArgs args)
        {
            this.OnGameExiting();
        }

        /// <summary>The method called when the game is updating its state (roughly 60 times per second).</summary>
        /// <param name="gameTime">A snapshot of the game timing state.</param>
        protected override void Update(GameTime gameTime)
        {
            this.OnGameUpdating(gameTime, () => base.Update(gameTime));
        }

        /// <summary>Update metadata when a split screen is added or removed.</summary>
        private void UpdateForSplitScreenChanges()
        {
            HashSet<int> oldScreenIds = new HashSet<int>(Context.ActiveScreenIds);

            // track active screens
            Context.ActiveScreenIds.Clear();
            foreach (var screen in this.gameInstances)
                Context.ActiveScreenIds.Add(screen.instanceId);

            // remember last removed screen
            foreach (int id in oldScreenIds)
            {
                if (!Context.ActiveScreenIds.Contains(id))
                    Context.LastRemovedScreenId = id;
            }
        }
    }
}