#region Using Statements
using System;
using System.Collections.Generic;
using System.IO;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.Utilities;
using Microsoft.Xna.Framework.Windows;
using Microsoft.Xna.Framework.Design;
//using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Media;
#endregion

namespace PickupTheCrew
{
	public enum GameState
	{
		Attract,
		Playing,
		LevelUp,
		GameOver
	}
	/// <summary>
	/// This is the main type for your game
	/// </summary>
	public class PickupTheCrew : Game
	{
		// Graphics
		GraphicsDeviceManager graphics;
		SpriteBatch spriteBatch;
		public static Dictionary<string, Texture2D> textures = new Dictionary<string, Texture2D>();
		// Note: The SoundEffect doesn't work in MonoGame at the moment.
		// No sound is played when using the SoundEffect class.
		//public SoundEffect Sfx_PickupCrewMember;

		// We use the Song class as a workaround to this.
		public Dictionary<string, Song> Sounds = new Dictionary<string, Song>();

		SpriteFont largefont;
		SpriteFont smallfont;

		// Misc Utilities
		public Random randGenerator = new Random();
		public Vector2 ScreenCentre;

		// Objects
		public Player PlayerObject = null; // the player
		public List<Shark> SharkObjects = new List<Shark>(); // list of sharks
		public List<CrewMember> CrewMemberObjects = new List<CrewMember>(); // list of all crew members

		public CrewMember LastRescuedCrewMember = null;

		//public List<Sprite> visibleSprites = new List<Sprite>(); // ?


		// Settings
		public GameState State = GameState.Attract; // The current game state
		public string SaveFileName = "PickUpTheCrew.savegame"; // The filename to save the game to
		public string DisplayMessage = ""; // The message that should be displayed to the user in the bottom corner

		public int Level = 1; // The current 'level' that we are on
		public int Score = 0; // Our current score
		public int HighScore = 0; // The best highscore so far
		public bool IsNewHighScore = false; // Whether the last game was a new HighScore

		public static int initialSharkCount = 3; // the initial number of sharks
		public int SharkCount = initialSharkCount; // the current number of sharks

		public PickupTheCrew()
			: base()
		{
			graphics = new GraphicsDeviceManager(this);
			Content.RootDirectory = "Content";
		}

		/// <summary>
		/// Allows the game to perform any initialization it needs to before starting to run.
		/// This is where it can query for any required services and load any non-graphic
		/// related content.  Calling base.Initialize will enumerate through any components
		/// and initialize them as well.
		/// </summary>
		protected override void Initialize()
		{
			// TODO: Add your initialization logic here

			base.Initialize();
		}

		/// <summary>
		/// LoadContent will be called once per game and is the place to load
		/// all of your content.
		/// </summary>
		protected override void LoadContent()
		{
			// Create a new SpriteBatch, which can be used to draw textures.
			spriteBatch = new SpriteBatch(GraphicsDevice);
			ScreenCentre = new Vector2(GraphicsDevice.Viewport.Width / 2, GraphicsDevice.Viewport.Height / 2);

			List<string> textureList = new List<string>(){
				"background",

				"player",
				"shark",

				"LieutenantSky",
				"MidshipmanVerde",
				"StokerOrange",
				"AbleSeamanShockingPink",
				"CabinboyCrimson",
				"CommanderCustard"
			};

			foreach(string textureName in textureList)
			{
				textures[textureName] = Content.Load<Texture2D>(textureName);
			}

			largefont = Content.Load<SpriteFont>("largefont");
			smallfont = Content.Load<SpriteFont>("smallfont");

			Sounds["LevelUp"] = Content.Load<Song>(@"LevelUp");
			Sounds["Pickup"] = Content.Load<Song>(@"Pickup");
			Sounds["GameOver"] = Content.Load<Song>(@"GameOver");

			// todo Load the last saved game / session if such a thing exists
			if(File.Exists(SaveFileName))
			{
				Load(SaveFileName);
			}
			else
			{
				CreateObjects();
			}
		}

		/// <summary>
		/// UnloadContent will be called once per game and is the place to unload
		/// all content.
		/// </summary>
		protected override void UnloadContent()
		{
			// TODO: Unload any non ContentManager content here
		}

		/// <summary>
		/// Allows the game to run logic such as updating the world,
		/// checking for collisions, gathering input, and playing audio.
		/// </summary>
		/// <param name="gameTime">Provides a snapshot of timing values.</param>
		protected override void Update(GameTime gameTime)
		{
			KeyboardState keyboard = Keyboard.GetState();
			if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || keyboard.IsKeyDown(Keys.Escape))
				Exit();

			// Reset the game in the 'R' key is pressed
			if (keyboard.IsKeyDown(Keys.R))
				HardReset();

			// Save the game if the 'S' key is pressed
			if(keyboard.IsKeyDown(Keys.S))
			{
				Save(SaveFileName);
			}
			
			switch(State)
			{
				case GameState.Attract:
					PlayStates.UpdateStateAttract(this, gameTime, keyboard);
					goto case GameState.Playing;

				case GameState.Playing:
					PlayStates.UpdateStatePlaying(this, gameTime, keyboard);
					break;

				case GameState.LevelUp:
					PlayStates.UpdateStateLevelUp(this, gameTime, keyboard);
					break;

				case GameState.GameOver:
					PlayStates.UpdateStateGameOver(this, gameTime, keyboard);
					break;
			}

			base.Update(gameTime);
		}

		/// <summary>
		/// This is called when the game should draw itself.
		/// </summary>
		/// <param name="gameTime">Provides a snapshot of timing values.</param>
		protected override void Draw(GameTime gameTime)
		{
			//GraphicsDevice.Clear(Color.CornflowerBlue);

			spriteBatch.Begin();

			//-----------------------

			// Draw the background
			spriteBatch.Draw(textures["background"], new Rectangle(
					0, 0,
					textures["background"].Width, textures["background"].Height
				),
				Color.White
			);
			// Draw all the Sharks
			foreach (Sprite visibleSprite in SharkObjects)
			{
				visibleSprite.Draw(gameTime, spriteBatch);
			}
			// Draw all the CrewMembers
			foreach (Sprite visibleSprite in CrewMemberObjects)
			{
				visibleSprite.Draw(gameTime, spriteBatch);
			}
			PlayerObject.Draw(gameTime, spriteBatch); // Draw the Player last so they are on top of everything else

			//-----------------------

			switch(State)
			{
				case GameState.Attract:
					string attractDisplayText = String.Format("Welcome to Pickup The Crew!\nCurrent High Score: {0}\nPress the up arrow to see if you can do better...", HighScore);
					Utilities.DrawStringCentred(spriteBatch, largefont, attractDisplayText, ScreenCentre, Color.White);
					break;

				case GameState.LevelUp:
					Utilities.DrawStringCentred(spriteBatch, largefont, "Press <space> to start the next level...", ScreenCentre, Color.White);
					goto case GameState.Playing;

				case GameState.Playing:
					DrawHUD(spriteBatch, gameTime);
					break;

				case GameState.GameOver:
					// todo pick out the crew member that was eaten by a shark
					string gameOverDisplayText = "Game Over! A Shark got you.\n";
					gameOverDisplayText += String.Format("You got to level {0} and scored {1} points.\n", Level, Score);
					if(IsNewHighScore)
					{
						gameOverDisplayText += "That is a new highscore!\n";
					}
					gameOverDisplayText += "Press '<space>' to return to the main menu...";

					Utilities.DrawStringCentred(spriteBatch, largefont, gameOverDisplayText, ScreenCentre, Color.White);
					break;
			}

			spriteBatch.End();

			base.Draw(gameTime);
		}

		/// <summary>
		/// Draws the heads up display to the screen.
		/// </summary>
		/// <param name="spriteBatch">The SpriteBatch to use to draw the HUD.</param>
		/// <param name="gameTime">The time since the last frame was drawn.</param>
		public void DrawHUD(SpriteBatch spriteBatch, GameTime gameTime)
		{
			string displayText = String.Format("Level: {0} Score: {1}", Level, Score);
			spriteBatch.DrawString(largefont, displayText, new Vector2(10, 10), Color.White);

			spriteBatch.DrawString(smallfont, DisplayMessage, new Vector2(10, GraphicsDevice.Viewport.Height - 34), Color.White);
		}

		/// <summary>
		/// Saves the current game to the given filename.
		/// </summary>
		/// <param name="filename">The filename to save to.</param>
		public void Save(string outFilename)
		{
			StreamWriter destination = null;
			try
			{
				destination = new StreamWriter(outFilename);
				Save(destination);
				destination.Close();
			}
			catch(Exception error)
			{
				throw error;
			}
			finally
			{
				if(destination != null)
					destination.Close();
			}
		}

		/// <summary>
		/// Saves the current game using the given StreamWriter.
		/// </summary>
		/// <param name="destination">The StreamWriter to use to save the game.</param>
		protected void Save(StreamWriter destination)
		{
			// Save the global settings
			destination.WriteLine("type:globalsettings");
			destination.Write("State:{0}|SharkCount:{1}|Level:{2}|", State.ToString(), SharkCount, Level);
			destination.WriteLine("Score:{0}|HighScore:{1}", Score, HighScore);

			// Save all the Sharks
			foreach (Sprite visibleSprite in SharkObjects)
			{
				visibleSprite.Save(destination);
			}
			// Save all the CrewMembers
			foreach (Sprite visibleSprite in CrewMemberObjects)
			{
				visibleSprite.Save(destination);
			}
			PlayerObject.Save(destination); // Save the player
			destination.WriteLine("type:FollowingData");
			// Save who is following who
			foreach(CrewMember visibleSprite in CrewMemberObjects)
			{
				string targetID = "null";
				if (visibleSprite.targetSprite != null)
					targetID = visibleSprite.targetSprite.ID;
				destination.WriteLine("crewMemberID:{0}|targetID:{1}", visibleSprite.ID, targetID);
			}
			destination.WriteLine("FollowingDataEnd");

			// Save the ID of the last rescued crew member
			// Note that null means that the player will be used instead
			string lastRescuedID = "null";
			if(LastRescuedCrewMember != null)
				lastRescuedID = LastRescuedCrewMember.ID;

			destination.WriteLine("type:LastRescued");
			destination.WriteLine("id:{0}", lastRescuedID);


			// Tell the user that we saved the game successfully
			DisplayMessage = "Game saved successfully.";
		}

		/// <summary>
		/// Create all the sprites, sharks, players, crewmembers, etc. ready for a new game.
		/// </summary>
		protected void CreateObjects()
		{
			PlayerObject = new Player(Vector2.Zero);
			PlayerObject.Reset(this);

			for (int i = 0; i < SharkCount; i++)
			{
				Shark nextShark = new Shark(Vector2.Zero);
				nextShark.Reset(this);

				// add the new shark to the appripriate lists to make sure it gets a) drawn and updated b) is referenceable
				SharkObjects.Add(nextShark);
			}

			// Generate all the crew members
			foreach (CrewMemberColour crewColour in Enum.GetValues(typeof(CrewMemberColour)))
			{
				CrewMember nextCrewMember = new CrewMember(crewColour, Vector2.Zero);
				nextCrewMember.Reset(this);

				CrewMemberObjects.Add(nextCrewMember);
			}

			// Tell the user that we have started a new session
			DisplayMessage = "Started New Session";
		}

		/// <summary>
		/// Function to load the game state from the specified filename.
		/// </summary>
		/// <param name="filename">The filename to load from.</param>
		protected void Load(string filename)
		{
			StreamReader source = null;
			try
			{
				source = new StreamReader(filename);
			}
			catch
			{
				Load(source);
			}
			finally
			{
				source.Close();
			}
			
		}

		protected void Load(StreamReader source)
		{
			while(!source.EndOfStream)
			{
				// Read in and parse the next line of save data
				string nextLine = source.ReadLine();
				Dictionary<string, string> nextLineData = Utilities.DecodeSaveString(nextLine);
				// If the line contains the type key then it means that we've hit a new section in the save data
				if(nextLineData.ContainsKey("type"))
				{
					// Handlers for each different type of save data. Note that Sprites are never actually used directly here.
					switch(nextLineData["type"])
					{
						// Load in and set the global settings.
						case "globalsettings":
							Dictionary<string, string> saveData = Utilities.DecodeSaveString(source.ReadLine());

							//State:GameOver|SharkCount:4|Level:2|Score:240|HighScore:240
							State = (GameState)Enum.Parse(typeof(GameState), saveData["State"]);
							SharkCount = int.Parse(saveData["SharkCount"]);
							Level = int.Parse(saveData["Level"]);
							Score = int.Parse(saveData["Score"]);
							HighScore = int.Parse(saveData["HighScore"]);
							break;

						// Load in a Shark
						case "Shark":
							SharkObjects.Add(new Shark(source));
							break;

						// Load in a crew member
						case "CrewMember":
							CrewMemberObjects.Add(new CrewMember(source));
							break;

						// Load in a player
						case "Player":
							PlayerObject = new Player(source);
							break;

						// Load in who is following who
						case "FollowingData":
							while(true)
							{
								// Read in and decode the save data
								string followingDataString = source.ReadLine();
								// But make sure we break out if we have hit the end of the following data
								if (followingDataString == "FollowingDataEnd") break;
								Dictionary<string, string> followingData = Utilities.DecodeSaveString(followingDataString);

								// If this crew member isn't following anyone then we don't need to do anything
								if(followingData["targetID"] == "null") continue; 

								CrewMember currentCrewMember = null;
								Sprite targetSprite = null;
								for (int i = 0; i < CrewMemberObjects.Count; i++ )
								{
									if (CrewMemberObjects[i].ID == followingData["crewMemberID"])
										currentCrewMember = CrewMemberObjects[i];
									if (CrewMemberObjects[i].ID == followingData["targetID"])
										targetSprite = CrewMemberObjects[i];
								}
								if (targetSprite == null) targetSprite = PlayerObject; // if we didn't find the targetSprite then this CrewMember must have been following this player
								currentCrewMember.targetSprite = targetSprite;


							}
							break;

						// Set the last rescued crew member reference
						case "LastRescued":
							string lastRescuedID = Utilities.DecodeSaveString(source.ReadLine())["id"];

							if (PlayerObject.ID == lastRescuedID)
								LastRescuedCrewMember = null;
							else
							{
								for(int i = 0; i < CrewMemberObjects.Count; i++)
								{
									if(CrewMemberObjects[i].ID == lastRescuedID)
									{
										LastRescuedCrewMember = CrewMemberObjects[i];
										break;
									}
								}
								if(LastRescuedCrewMember == null)
								{
									DisplayMessage = "A problem occurred when trying to load the last rescued crew member. The crew snake may be broken!";
								}
							}
							break;

						default:
							throw new InvalidDataException(String.Format("Unknown type found in save file: '{0}'", nextLineData["type"]));
					}
				}
			}

			DisplayMessage = "Loaded session from save file";
		}

		/// <summary>
		/// Ends a game of PickupTheCrew by flipping the GameState and updating the highscore.
		/// </summary>
		public void GameOver()
		{
			// Play the game over sound
			MediaPlayer.Play(Sounds["GameOver"]);
			// Flip the game state
			State = GameState.GameOver;
			// Update the highscore if necessary
			if(Score > HighScore)
			{
				HighScore = Score;
				IsNewHighScore = true;
			}
			// Update the DisplayMessage
			DisplayMessage = "Someone got caught by a shark!";
		}

		/// <summary>
		/// Resets the game completely ready for the next player.
		/// </summary>
		public void HardReset()
		{
			/// Reset the settings

			// Reset the game state
			State = GameState.Attract;
			// Reset the level
			Level = 1;
			// Reset the score
			Score = 0;
			IsNewHighScore = false;

			// remove the extra sharks
			if (SharkCount > initialSharkCount)
			{
				SharkObjects.RemoveRange(0, SharkCount - initialSharkCount);
				SharkCount = initialSharkCount;
			}
			SoftReset();
		}

		/// <summary>
		/// Reset the game ready for the next level. Not as vicious as HardReset().
		/// </summary>
		public void SoftReset()
		{
			// Reset the last rescued CrewMember
			LastRescuedCrewMember = null;

			// Reset the player first
			PlayerObject.Reset(this);
			// Reset the sharks next, they  are intelligent enough not to place themselves too close to the player
			foreach (Shark nextShark in SharkObjects)
			{
				nextShark.Reset(this);
			}

			// Reset the CrewMembers last, they are intelligent enough not to place themselves too close to either the player or the sharks
			foreach (CrewMember nextCrewMember in CrewMemberObjects)
			{
				nextCrewMember.Reset(this);
			}
		}
	}
}
