/*
 * +================================================================================+
 *      __  ___  ______________  __  __  ______________   ____
 *     / / / / |/ /  _/_  __/\ \/ / / / / /_  __/  _/ /  / __/
 *    / /_/ /    // /  / /    \  / / /_/ / / / _/ // /___\ \
 *    \____/_/|_/___/ /_/     /_/  \____/ /_/ /___/____/___/
 *
 * +================================================================================+
 *
 *    "THE BEER-WARE LICENSE"
 *    -----------------------
 *
 *    As long as you retain this notice you can do whatever you want with this
 *    stuff. However it is expressly forbidden to sell or commercially distribute
 *    Unity Utils or parts of it apart from your games.
 *    If you meet any of the authors some day, and you think this stuff is
 *    worth it, you can buy them a beer in return.
 *
 *    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *    THE SOFTWARE.
 *
 * +================================================================================+
 *
 *    Authors:
 *    David Krummenacher (dave@timbookedtwo.com)
 *
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text.RegularExpressions;
using UnityEngine;
using Utils.Helpers;

namespace Utils.SaveGame {

	/// <summary>
	/// Save game class to handle saving and loading of savegame data.
	/// The class uses headers to allow you to display save games without the need to load the whole file.
	/// Use GetAllHeaders to get an array containing all headers of the available save games in the save game folder.
	/// When you want to save or load data you'll either pass or receive the data as an object. It's best practise to
	/// create your own data class that represents your games data and pass it when saving and parse to it when loading.
	/// </summary>
	public abstract class SaveGame {

		/// <summary>
		/// The save game folder.
		/// </summary>
		private static string saveGameFolder;
		/// <summary>
		/// The save game file prefix.
		/// </summary>
		private static string saveGamePrefix;
		/// <summary>
		/// The save game file extention.
		/// </summary>
		private static string saveGameExtention;

		/// <summary>
		/// Static constructor.
		/// </summary>
		static SaveGame() {
			saveGameFolder = Application.persistentDataPath + "/Saves";

			// Illegal characters handling for product name
			string regexSearch = new string(Path.GetInvalidFileNameChars());
			Regex regex = new Regex(string.Format("[{0}]", Regex.Escape(regexSearch)));
			saveGamePrefix = regex.Replace(Application.productName, "");

			saveGameExtention = ".sav";
		}

		/// <summary>
		/// Creates a new header. Use this when you need to create a new savegame.
		/// <c>SaveGame.Save(SaveGame.CreateHeader(), mySuperDuperdata);</c>
		/// 
		/// You're free to create headers yourself but this method automatically takes care of
		/// path handling and applies the application, version and time.
		/// </summary>
		/// <param name="name">The name (not used for file handling).</param>
		/// <param name="description">The description.</param>
		/// <param name="additionalData">
		/// The additional data. This can be used for storing a screenshot or whatever you'd like.
		/// Do not fill this with lots of data because it is loaded within the header and can slow
		/// down the loading process.
		/// </param>
		public static SaveGameHeader CreateHeader(string name, string description, byte[] additionalData) {
			SaveGameHeader header = new SaveGameHeader();
			header.FilePath = GetNextFilePath();
			header.ApplicationProductName = Application.productName;
			header.ApplicationVersion = Application.version;
			header.Name = name;
			header.Description = description;
			header.Time = DateTime.Now.ToFileTime();
			header.AdditionalData = additionalData;

			return header;
		}

		/// <summary>
		/// Creates a new header. Use this when you need to create a new savegame.
		/// <c>SaveGame.Save(SaveGame.CreateHeader(), mySuperDuperdata);</c>
		/// No additional data is added.
		/// 
		/// You're free to create headers yourself but this method automatically takes care of
		/// path handling and applies the application, version and time.
		/// </summary>
		/// <param name="name">The name (not used for file handling).</param>
		/// <param name="description">The description.</param>
		public static SaveGameHeader CreateHeader(string name, string description) {
			return CreateHeader(name, description, new byte[0]);
		}

		/// <summary>
		/// Creates a new header. Use this when you need to create a new savegame.
		/// <c>SaveGame.Save(SaveGame.CreateHeader(), mySuperDuperdata);</c>
		/// The description will be empty. No additional data is added.
		/// 
		/// You're free to create headers yourself but this method automatically takes care of
		/// path handling and applies the application, version and time.
		/// </summary>
		/// <param name="name">The name (not used for file handling).</param>
		public static SaveGameHeader CreateHeader(string name) {
			return CreateHeader(name, "", new byte[0]);
		}

		/// <summary>
		/// Creates a new header. Use this when you need to create a new savegame.
		/// <c>SaveGame.Save(SaveGame.CreateHeader(), mySuperDuperdata);</c>
		/// The name and the description will be empty. No additional data is added.
		/// 
		/// You're free to create headers yourself but this method automatically takes care of
		/// path handling and applies the application, version and time.
		/// </summary>
		public static SaveGameHeader CreateHeader() {
			return CreateHeader("", "", new byte[0]);
		}

		/// <summary>
		/// Gets the next available path.
		/// </summary>
		/// <returns>The next available path.</returns>
		private static string GetNextFilePath() {
			// Numbering
			int number = 0;

			// Check all files for higher numbers
			string[] files = Directory.GetFiles(saveGameFolder, saveGamePrefix + "*" + saveGameExtention);
			for (int i = 0; i < files.Length; i++) {
				// Isolate number value from filename
				string idString = files[i].TrimStart((saveGameFolder + "/" + saveGamePrefix).ToCharArray()).TrimEnd(saveGameExtention.ToCharArray());

				// If it really is a number check against current number
				int id;
				if (int.TryParse(idString, out id)) {
					if (id > number) number = id + 1;
				}
			}

			// Generate new filename
			return saveGameFolder + "/" + saveGamePrefix + number.ToString() + saveGameExtention;
		}

		/// <summary>
		/// Gets the headers of all available savegames in the save games folder.
		/// </summary>
		/// <returns>All headers.</returns>
		public static SaveGameHeader[] GetAllHeaders() {
			string[] files = Directory.GetFiles(saveGameFolder, "*" + saveGameExtention);

			SaveGameHeader[] result = new SaveGameHeader[files.Length];

			for (int i = 0; i < files.Length; i++) {
				result[i] = GetHeader(files[i]);
			}

			return result;
		}

		/// <summary>
		/// Gets a header of a savegame.
		/// </summary>
		/// <returns>The header of the savegame.</returns>
		/// <param name="filePath">The file path.</param>
		private static SaveGameHeader GetHeader(string filePath) {
			// Try to read file or return empty header
			if (File.Exists(filePath)) {
				SaveGameHeader header = new SaveGameHeader();
				header.FilePath = filePath;

				// Try to read header
				using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
				using (BinaryReader reader = new BinaryReader(stream)) {
					try {
						// Read header
						header.ApplicationProductName = reader.ReadString();
						header.ApplicationVersion = reader.ReadString();
						header.Name = reader.ReadString();
						header.Description = reader.ReadString();
						header.Time = reader.ReadInt64();
					} catch (Exception e) {
						Debug.LogException(e);
						header = new SaveGameHeader(); // Make empty header
					} finally {
						if (reader != null) reader.Close();
						if (stream != null) stream.Close();
					}
				}

				return header;
			} else {
				// File not found warning
				Debug.LogWarning("File \"" + filePath + "\" not found! Returning empty header.");
				return new SaveGameHeader();
			}
		}

		/// <summary>
		/// Loads savegame data from the specified the savegame with the specified header.
		/// 
		/// If you feel brave enough to do things your way and not use the extra convenient GetAllHeaders,
		/// you only need to specify the file path inside the savegame header to load a file.
		/// </summary>
		/// <returns>The savegame data</returns>
		/// <param name="header">The header of the savegame.</param>
		public static object LoadSaveGameData(SaveGameHeader header) {
			// Try to read file or return null
			if (File.Exists(header.FilePath)) {
				object saveGameData = null;
				
				// Try to read file
				using (FileStream stream = new FileStream(header.FilePath, FileMode.Open, FileAccess.Read))
				using (BinaryReader reader = new BinaryReader(stream)) {
					try {
						// Read header
						header.ApplicationProductName = reader.ReadString();
						header.ApplicationVersion = reader.ReadString();
						header.Name = reader.ReadString();
						header.Description = reader.ReadString();
						header.Time = reader.ReadInt64();
						int length = reader.ReadInt32();
						if (length > 0) header.AdditionalData = reader.ReadBytes(length);
						else header.AdditionalData = new byte[0];
						
						// Read data
						BinaryFormatter deserializer = new BinaryFormatter();
						saveGameData = deserializer.Deserialize(stream);
					} catch (Exception e) {
						Debug.LogException(e);
					} finally {
						if (reader != null) reader.Close();
						if (stream != null) stream.Close();
					}
				}
				
				return saveGameData;
			} else {
				// File not found warning
				Debug.LogWarning("File \"" + header.FilePath + "\" not found! Returning null.");
				return null;
			}
		}

		/// <summary>
		/// Saves a savegame. Existing files will be overwritten.
		/// If you want to create a new savegame use <c>SaveGame.Save(SaveGame.CreateHeader(), mySuperDuperdata);</c>.
		/// If you want to overwrite an existing save you need to specify the header.
		/// </summary>
		/// <param name="header">The savegame header.</param>
		/// <param name="data">The savegame data.</param>
		public static void Save(SaveGameHeader header, object data) {
			// Try to write file
			using (FileStream stream = new FileStream(header.FilePath, FileMode.Create, FileAccess.Write))
			using (BinaryWriter writer = new BinaryWriter(stream)) {
				try {
					// Write header
					writer.Write(header.ApplicationProductName);
					writer.Write(header.ApplicationVersion);
					writer.Write(header.Name);
					writer.Write(header.Description);
					writer.Write(header.Time);
					if (header.AdditionalData != null) {
						writer.Write(header.AdditionalData.Length);
						writer.Write(header.AdditionalData);
					} else { writer.Write(0); }

					// Write data
					BinaryFormatter serializer = new BinaryFormatter();
					serializer.Serialize(stream, data);
				} catch (Exception e) {
					Debug.LogException(e);
				} finally {
					if (writer != null) writer.Close();
					if (stream != null) stream.Close();
				}
			}
		}

		/// <summary>
		/// Deletes a savegame based on its header.
		/// </summary>
		/// <param name="header">The savegame header.</param>
		public static void Delete(SaveGameHeader header) {
			File.Delete(header.FilePath);
		}
	}

	/// <summary
	/// The header data for savegames.
	/// </summary>
	public struct SaveGameHeader {
		/// <summary>
		/// The file path of the savegame.
		/// </summary>
		public string FilePath;
		/// <summary>
		/// The name of the application.
		/// </summary>
		public string ApplicationProductName;
		/// <summary>
		/// The application version.
		/// </summary>
		public string ApplicationVersion;
		/// <summary>
		/// The name of the savegame (not the filename).
		/// </summary>
		public string Name;
		/// <summary>
		/// The description of the savegame.
		/// </summary>
		public string Description;
		/// <summary>
		/// The time of the savegame.
		/// </summary>
		public long Time;
		/// <summary>
		/// The additional data. This can be used for storing a screenshot or whatever you'd like.
		/// Do not fill this with lots of data because it is loaded within the header and can slow
		/// down the loading process.
		/// </summary>
		public byte[] AdditionalData;
	}
}
