using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

using RobocraftX.Common;
using RobocraftX.GUI.MyGamesScreen;
using RobocraftX.StateSync;
using Svelto.ECS;

using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Tasks;
using GamecraftModdingAPI.Utility;

namespace GamecraftModdingAPI.App
{
    /// <summary>
    /// An in-game save.
	/// This can be a menu item for a local save or the currently loaded save.
	/// Support for Steam Workshop coming soon (hopefully).
    /// </summary>
    public class Game
    {
        // extensible engines
		protected static GameGameEngine gameEngine = new GameGameEngine();
		protected static GameMenuEngine menuEngine = new GameMenuEngine();
		protected static DebugInterfaceEngine debugOverlayEngine = new DebugInterfaceEngine();
		protected static GameBuildSimEventEngine buildSimEventEngine = new GameBuildSimEventEngine();

        private List<string> debugIds = new List<string>();

		private bool menuMode = true;
		private bool hasId = false;

        /// <summary>
        /// Initializes a new instance of the <see cref="T:GamecraftModdingAPI.App.Game"/> class.
        /// </summary>
        /// <param name="id">Menu identifier.</param>
		public Game(uint id) : this(new EGID(id, MyGamesScreenExclusiveGroups.MyGames))
        {
        }

		/// <summary>
        /// Initializes a new instance of the <see cref="T:GamecraftModdingAPI.App.Game"/> class.
        /// </summary>
        /// <param name="id">Menu identifier.</param>
		public Game(EGID id)
		{
			this.Id = id.entityID;
			this.EGID = id;
			this.hasId = true;
			menuMode = true;
			if (!VerifyMode()) throw new AppStateException("Game cannot be created while not in a game nor in a menu (is the game in a loading screen?)");
		}

        /// <summary>
        /// Initializes a new instance of the <see cref="T:GamecraftModdingAPI.App.Game"/> class without id.
		/// This is assumed to be the current game.
        /// </summary>
        public Game()
		{
			menuMode = false;
			if (!VerifyMode()) throw new AppStateException("Game cannot be created while not in a game nor in a menu (is the game in a loading screen?)");
			if (menuEngine.IsInMenu) throw new GameNotFoundException("Game not found.");
		}

        /// <summary>
        /// Returns the currently loaded game.
		/// If in a menu, manipulating the returned object may not work as intended.
        /// </summary>
        /// <returns>The current game.</returns>
        public static Game CurrentGame()
		{
			return new Game();
		}

        /// <summary>
        /// Creates a new game and adds it to the menu.
		/// If not in a menu, this will throw AppStateException.
        /// </summary>
        /// <returns>The new game.</returns>
		public static Game NewGame()
		{
			if (!menuEngine.IsInMenu) throw new AppStateException("New Game cannot be created while not in a menu.");
			uint nextId = menuEngine.HighestID() + 1;
			EGID egid = new EGID(nextId, MyGamesScreenExclusiveGroups.MyGames);
			menuEngine.CreateMyGame(egid);
			return new Game(egid);
		}

        /// <summary>
        /// An event that fires whenever a game is switched to simulation mode (time running mode).
        /// </summary>
		public static event EventHandler<GameEventArgs> Simulate
		{
			add => buildSimEventEngine.SimulationMode += value;
			remove => buildSimEventEngine.SimulationMode -= value;
		}

        /// <summary>
        /// An event that fires whenever a game is switched to edit mode (time stopped mode).
		/// This does not fire when a game is loaded.
        /// </summary>
		public static event EventHandler<GameEventArgs> Edit
        {
            add => buildSimEventEngine.BuildMode += value;
            remove => buildSimEventEngine.BuildMode -= value;
        }

        /// <summary>
        /// An event that fires right after a game is completely loaded.
        /// </summary>
		public static event EventHandler<GameEventArgs> Enter
        {
			add => gameEngine.EnterGame += value;
			remove => gameEngine.EnterGame -= value;
        }

        /// <summary>
        /// An event that fires right before a game returns to the main menu.
		/// At this point, Gamecraft is transitioning state so many things are invalid/unstable here.
        /// </summary>
		public static event EventHandler<GameEventArgs> Exit
        {
			add => gameEngine.ExitGame += value;
			remove => gameEngine.ExitGame -= value;
        }

        /// <summary>
        /// The game's unique menu identifier.
        /// </summary>
        /// <value>The identifier.</value>
        public uint Id
		{
			get;
			private set;
		}

        /// <summary>
        /// The game's unique menu EGID.
        /// </summary>
        /// <value>The egid.</value>
		public EGID EGID
		{
			get;
			private set;
		}

        /// <summary>
        /// Whether the game is a (valid) menu item.
        /// </summary>
        /// <value><c>true</c> if menu item; otherwise, <c>false</c>.</value>
        public bool MenuItem
		{
			get => menuMode && hasId;
		}

        /// <summary>
        /// The game's name.
        /// </summary>
        /// <value>The name.</value>
        public string Name
		{
			get
			{
				if (!VerifyMode()) return null;
				if (menuMode) return menuEngine.GetGameInfo(EGID).GameName;
				return GameMode.SaveGameDetails.Name;
			}

			set
			{
				if (!VerifyMode()) return;
                if (menuMode)
				{
					menuEngine.SetGameName(EGID, value);
				}
				else 
				{
					GameMode.SaveGameDetails.Name = value;
				}
			}
		}

        /// <summary>
        /// The game's description.
        /// </summary>
        /// <value>The description.</value>
		public string Description
        {
            get
            {
				if (!VerifyMode()) return null;
				if (menuMode) return menuEngine.GetGameInfo(EGID).GameDescription;
                return "";
            }

            set
            {
				if (!VerifyMode()) return;
                if (menuMode)
                {
                    menuEngine.SetGameDescription(EGID, value);
                }
                else
                {
                    // No description exists in-game
                }
            }
        }

        /// <summary>
        /// The path to the game's save folder.
        /// </summary>
        /// <value>The path.</value>
		public string Path
        {
            get
            {
				if (!VerifyMode()) return null;
				if (menuMode) return menuEngine.GetGameInfo(EGID).SavedGamePath;
				return GameMode.SaveGameDetails.Folder;
            }

            set
            {
				if (!VerifyMode()) return;
                if (menuMode)
                {
					menuEngine.GetGameInfo(EGID).SavedGamePath.Set(value);
                }
                else
                {
					// this likely breaks things
					GameMode.SaveGameDetails = new SaveGameDetails(GameMode.SaveGameDetails.Name, value, GameMode.SaveGameDetails.WorkshopId);
                }
            }
        }

        /// <summary>
        /// The Steam Workshop Id of the game save.
		/// In most cases this is invalid and returns 0, so this can be ignored.
        /// </summary>
        /// <value>The workshop identifier.</value>
		public ulong WorkshopId
        {
            get
            {
				if (!VerifyMode()) return 0uL;
                if (menuMode) return 0uL; // MyGames don't have workshop IDs
				return GameMode.SaveGameDetails.WorkshopId;
            }

            set
            {
                VerifyMode();
                if (menuMode)
                {
					// MyGames don't have workshop IDs
                    // menuEngine.GetGameInfo(EGID).GameName.Set(value);
                }
                else
                {
					// this likely breaks things
					GameMode.SaveGameDetails = new SaveGameDetails(GameMode.SaveGameDetails.Name, GameMode.SaveGameDetails.Folder, value);
                }
            }
        }

        /// <summary>
        /// Whether the game is in simulation mode.
        /// </summary>
        /// <value><c>true</c> if is simulating; otherwise, <c>false</c>.</value>
        public bool IsSimulating
		{
			get
			{
				if (!VerifyMode()) return false;
				return !menuMode && gameEngine.IsTimeRunningMode();
			} 

			set
			{
				if (!VerifyMode()) return;
				if (!menuMode && gameEngine.IsTimeRunningMode() != value)
					gameEngine.ToggleTimeMode();
			}
		}

        /// <summary>
		/// Whether the game is in time-running mode.
        /// Alias of IsSimulating.
        /// </summary>
        /// <value><c>true</c> if is time running; otherwise, <c>false</c>.</value>
		public bool IsTimeRunning
        {
			get => IsSimulating;

            set
            {
				IsSimulating = value;
            }
        }

        /// <summary>
        /// Whether the game is in time-stopped mode.
        /// </summary>
        /// <value><c>true</c> if is time stopped; otherwise, <c>false</c>.</value>
        public bool IsTimeStopped
		{
			get
			{
				if (!VerifyMode()) return false;
				return !menuMode && gameEngine.IsTimeStoppedMode();
			}

            set
            {
				if (!VerifyMode()) return;
                if (!menuMode && gameEngine.IsTimeStoppedMode() != value)
                    gameEngine.ToggleTimeMode();
            }
		}

        /// <summary>
        /// Toggles the time mode.
        /// </summary>
        public void ToggleTimeMode()
		{
			if (!VerifyMode()) return;
			if (menuMode || !gameEngine.IsInGame)
			{
				throw new AppStateException("Game menu item cannot toggle it's time mode");
			}
			gameEngine.ToggleTimeMode();
		}

        /// <summary>
        /// Load the game save.
		/// This happens asynchronously, so when this method returns the game not loaded yet.
		/// Use the Game.Enter event to perform operations after the game has completely loaded.
        /// </summary>
        public void EnterGame()
		{
			if (!VerifyMode()) return;
			if (!hasId)
			{
				throw new GameNotFoundException("Game has an invalid ID");
			}
			ISchedulable task = new Once(() => { menuEngine.EnterGame(EGID); this.menuMode = false; });
			Scheduler.Schedule(task);
		}

        /// <summary>
        /// Return to the menu.
		/// Part of this always happens asynchronously, so when this method returns the game has not exited yet.
		/// Use the Client.EnterMenu event to perform operations after the game has completely exited.
        /// </summary>
        /// <param name="async">If set to <c>true</c>, do this async.</param>
        public void ExitGame(bool async = false)
		{
			if (!VerifyMode()) return;
			if (menuMode)
			{
				throw new GameNotFoundException("Cannot exit game using menu ID");
			}
			gameEngine.ExitCurrentGame(async);
			this.menuMode = true;
		}

        /// <summary>
        /// Saves the game.
		/// Part of this happens asynchronously, so when this method returns the game has not been saved yet.
        /// </summary>
		public void SaveGame()
		{
			if (!VerifyMode()) return;
            if (menuMode)
            {
                throw new GameNotFoundException("Cannot save game using menu ID");
            }
			gameEngine.SaveCurrentGame();
		}

        /// <summary>
        /// Add information to the in-game debug display.
		/// When this object is garbage collected, this debug info is automatically removed.
        /// </summary>
        /// <param name="id">Debug info identifier.</param>
        /// <param name="contentGetter">Content getter.</param>
		public void AddDebugInfo(string id, Func<string> contentGetter)
        {
			if (!VerifyMode()) return;
			if (menuMode)
			{
				throw new GameNotFoundException("Game object references a menu item but AddDebugInfo only works on the currently-loaded game");
			}
            debugOverlayEngine.SetInfo(id, contentGetter);
            debugIds.Add(id);
        }

        /// <summary>
        /// Remove information from the in-game debug display.
        /// </summary>
        /// <returns><c>true</c>, if debug info was removed, <c>false</c> otherwise.</returns>
        /// <param name="id">Debug info identifier.</param>
        public bool RemoveDebugInfo(string id)
        {
			if (!VerifyMode()) return false;
            if (menuMode)
            {
				throw new GameNotFoundException("Game object references a menu item but RemoveDebugInfo only works on the currently-loaded game");
            }
            if (!debugIds.Contains(id)) return false;
            debugOverlayEngine.RemoveInfo(id);
            return debugIds.Remove(id);
        }

        /// <summary>
        /// Gets the blocks in the game.
		/// This returns null when in a loading state, and throws AppStateException when in menu.
        /// </summary>
        /// <returns>The blocks in game.</returns>
        /// <param name="filter">The block to search for. BlockIDs.Invalid will return all blocks.</param>
		public Block[] GetBlocksInGame(BlockIDs filter = BlockIDs.Invalid)
		{
			if (!VerifyMode()) return null;
            if (menuMode)
            {
                throw new AppStateException("Game object references a menu item but GetBlocksInGame only works on the currently-loaded game");
            }
			EGID[] blockEGIDs = gameEngine.GetAllBlocksInGame(filter);
			Block[] blocks = new Block[blockEGIDs.Length];
			for (int b = 0; b < blockEGIDs.Length; b++)
			{
				blocks[b] = new Block(blockEGIDs[b]);
			}
			return blocks;
		}

		~Game()
        {
            foreach (string id in debugIds)
            {
                debugOverlayEngine.RemoveInfo(id);
            }
        }

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
        private bool VerifyMode()
		{
			if (menuMode && (!menuEngine.IsInMenu || gameEngine.IsInGame))
			{
                // either game loading or API is broken
				return false;
			}
			if (!menuMode && (menuEngine.IsInMenu || !gameEngine.IsInGame))
			{
                // either game loading or API is broken
				return false;
			}
			return true;
		}

        internal static void Init()
		{
			GameEngineManager.AddGameEngine(gameEngine);
			GameEngineManager.AddGameEngine(debugOverlayEngine);
			MenuEngineManager.AddMenuEngine(menuEngine);
		}

		internal static void InitDeterministic(StateSyncRegistrationHelper stateSyncReg)
		{
			stateSyncReg.AddDeterministicEngine(buildSimEventEngine);
		}
    }
}