#region License
/* FNA - XNA4 Reimplementation for Desktop Platforms
 * Copyright 2009-2022 Ethan Lee and the MonoGame Team
 *
 * Released under the Microsoft Public License.
 * See LICENSE for details.
 */
#endregion

#region Using Statements
using System;
using System.IO;
#endregion

namespace Microsoft.Xna.Framework.Storage
{
	/// <summary>
	/// Contains a logical collection of files used for user-data storage.
	/// </summary>
	/// <remarks>
	/// MSDN documentation contains related conceptual article:
	/// http://msdn.microsoft.com/en-us/library/bb200105.aspx#ID4EDB
	/// </remarks>
	public class StorageContainer : IDisposable
	{
		#region Public Properties

		/// <summary>
		/// The title's (i.e. "game's") filename.
		/// </summary>
		public string DisplayName
		{
			get;
			private set;
		}

		/// <summary>
		/// A bool value indicating whether the instance has been disposed.
		/// </summary>
		public bool IsDisposed
		{
			get;
			private set;
		}

		/// <summary>
		/// The <see cref="StorageDevice"/> that holds logical files for the container.
		/// </summary>
		public StorageDevice StorageDevice
		{
			get;
			private set;
		}

		#endregion

		#region Internal Variables

		private readonly string storagePath;

		#endregion

		#region Events

		/// <summary>
		/// Fired when <see cref="Dispose"/> is called or object is finalized or collected
		/// by the garbage collector.
		/// </summary>
		public event EventHandler<EventArgs> Disposing;

		#endregion

		#region Internal Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="StorageContainer"/> class.
		/// </summary>
		/// <param name='device'>The attached storage-device.</param>
		/// <param name='name'>The title's filename.</param>
		/// <param name='rootPath'>The path of the storage root folder</param>
		/// <param name='playerIndex'>
		/// The index of the player whose data is being saved, or null if data is for all
		/// players.
		/// </param>
		internal StorageContainer(
			StorageDevice device,
			string name,
			string rootPath,
			PlayerIndex? playerIndex
		) {
			if (string.IsNullOrEmpty(name))
			{
				throw new ArgumentNullException("A title name has to be provided in parameter name.");
			}

			StorageDevice = device;
			DisplayName = name;

			/* There are two types of subfolders within a StorageContainer.
			 * The first is a PlayerX folder, X being a specified PlayerIndex.
			 * The second is AllPlayers, when PlayerIndex is NOT specified.
			 * Basically, you should NEVER expect to have ANY file in the root
			 * game save folder.
			 * -flibit
			 */
			storagePath = Path.Combine(
				rootPath,		// Title folder (EXE name)...
				name,			// Container folder...
				playerIndex.HasValue ?	// Player folder...
					("Player" + ((int) playerIndex.Value + 1).ToString()) :
					"AllPlayers"
			);

			// Create the folders, if needed.
			if (!Directory.Exists(storagePath))
			{
				Directory.CreateDirectory(storagePath);
			}
		}

		#endregion

		#region Public Dispose Method

		/// <summary>
		/// Disposes un-managed objects referenced by this object.
		/// </summary>
		public void Dispose()
		{
			if (Disposing != null)
			{
				Disposing(this, null);
			}
			IsDisposed = true;
		}

		#endregion

		#region Public Create Methods

		/// <summary>
		/// Creates a new directory in the storage-container.
		/// </summary>
		/// <param name="directory">Relative path of the directory to be created.</param>
		public void CreateDirectory(string directory)
		{
			if (string.IsNullOrEmpty(directory))
			{
				throw new ArgumentNullException("Parameter directory must contain a value.");
			}

			// Directory name is relative, so combine with our path.
			string dirPath = Path.Combine(storagePath, directory);

			// Now let's try to create it.
			if (!Directory.Exists(dirPath))
			{
				Directory.CreateDirectory(dirPath);
			}
		}

		/// <summary>
		/// Creates a file in the storage-container.
		/// </summary>
		/// <param name="file">Relative path of the file to be created.</param>
		/// <returns>Returns <see cref="Stream"/> for the created file.</returns>
		public Stream CreateFile(string file)
		{
			if (string.IsNullOrEmpty(file))
			{
				throw new ArgumentNullException("Parameter file must contain a value.");
			}

			// File name is relative, so combine with our path.
			string filePath = Path.Combine(storagePath, file);

			// Return a new file with read/write access.
			return File.Create(filePath);
		}

		#endregion

		#region Public Delete Methods

		/// <summary>
		/// Deletes specified directory from the storage-container.
		/// </summary>
		/// <param name="directory">The relative path of the directory to be deleted.</param>
		public void DeleteDirectory(string directory)
		{
			if (string.IsNullOrEmpty(directory))
			{
				throw new ArgumentNullException("Parameter directory must contain a value.");
			}

			// Directory name is relative, so combine with our path.
			string dirPath = Path.Combine(storagePath, directory);

			// Now let's try to delete it.
			Directory.Delete(dirPath);
		}

		/// <summary>
		/// Deletes a file from the storage-container.
		/// </summary>
		/// <param name="file">The relative path of the file to be deleted.</param>
		public void DeleteFile(string file)
		{
			if (string.IsNullOrEmpty(file))
			{
				throw new ArgumentNullException("Parameter file must contain a value.");
			}

			// Relative, so combine with our path.
			string filePath = Path.Combine(storagePath, file);

			// Now let's try to delete it.
			File.Delete(filePath);
		}

		#endregion

		#region Public Exists Methods

		/// <summary>
		/// Returns true if specified path exists in the storage-container, false otherwise.
		/// </summary>
		/// <param name="directory">The relative path of the directory to query for.</param>
		/// <returns>True if the directory path exists, false otherwise.</returns>
		public bool DirectoryExists(string directory)
		{
			if (string.IsNullOrEmpty(directory))
			{
				throw new ArgumentNullException("Parameter directory must contain a value.");
			}

			// Directory name is relative, so combine with our path.
			string dirPath = Path.Combine(storagePath, directory);

			return Directory.Exists(dirPath);
		}

		/// <summary>
		/// Returns true if the specified file exists in the storage-container, false otherwise.
		/// </summary>
		/// <param name="file">The relative path of the file to query for.</param>
		/// <returns>True if file exists, false otherwise.</returns>
		public bool FileExists(string file)
		{
			if (string.IsNullOrEmpty(file))
			{
				throw new ArgumentNullException("Parameter file must contain a value.");
			}

			// File name is relative, so combine with our path.
			string filePath = Path.Combine(storagePath, file);

			// Return a new file with read/write access.
			return File.Exists(filePath);
		}

		#endregion

		#region Public GetNames Methods

		/// <summary>
		/// Returns an array of the directory names in the storage-container.
		/// </summary>
		/// <returns>Array of directory names.</returns>
		public string[] GetDirectoryNames()
		{
			string[] names = Directory.GetDirectories(storagePath);
			for (int i = 0; i < names.Length; i += 1)
			{
				names[i] = names[i].Substring(storagePath.Length + 1);
			}
			return names;
		}

		/// <summary>
		/// Returns an array of directory names with given search pattern.
		/// </summary>
		/// <param name="searchPattern">
		/// A search pattern that supports single-character ("?") and multicharacter ("*")
		/// wildcards.
		/// </param>
		/// <returns>Array of matched directory names.</returns>
		public string[] GetDirectoryNames(string searchPattern)
		{
			if (string.IsNullOrEmpty(searchPattern))
			{
				throw new ArgumentNullException("Parameter searchPattern must contain a value.");
			}

			string[] names = Directory.GetDirectories(storagePath, searchPattern);
			for (int i = 0; i < names.Length; i += 1)
			{
				names[i] = names[i].Substring(storagePath.Length + 1);
			}
			return names;
		}

		/// <summary>
		/// Returns an array of file names in the storage-container.
		/// </summary>
		/// <returns>Array of file names.</returns>
		public string[] GetFileNames()
		{
			string[] names = Directory.GetFiles(storagePath);
			for (int i = 0; i < names.Length; i += 1)
			{
				names[i] = names[i].Substring(storagePath.Length + 1);
			}
			return names;
		}

		/// <summary>
		/// Returns an array of file names with given search pattern.
		/// </summary>
		/// <param name="searchPattern">
		/// A search pattern that supports single-character ("?") and multicharacter ("*")
		/// wildcards.
		/// </param>
		/// <returns>Array of matched file names.</returns>
		public string[] GetFileNames(string searchPattern)
		{
			if (string.IsNullOrEmpty(searchPattern))
			{
				throw new ArgumentNullException("Parameter searchPattern must contain a value.");
			}

			string[] names = Directory.GetFiles(storagePath, searchPattern);
			for (int i = 0; i < names.Length; i += 1)
			{
				names[i] = names[i].Substring(storagePath.Length + 1);
			}
			return names;
		}

		#endregion

		#region Public OpenFile Methods

		/// <summary>
		/// Opens a file contained in storage-container.
		/// </summary>
		/// <param name="file">Relative path of the file.</param>
		/// <param name="fileMode">
		/// <see cref="FileMode"/> that specifies how the file is to be opened.
		/// </param>
		/// <returns><see cref="Stream"/> object for the opened file.</returns>
		public Stream OpenFile(
			string file,
			FileMode fileMode
		) {
			return OpenFile(
				file,
				fileMode,
				FileAccess.ReadWrite,
				FileShare.ReadWrite
			);
		}

		/// <summary>
		/// Opens a file contained in storage-container.
		/// </summary>
		/// <param name="file">Relative path of the file.</param>
		/// <param name="fileMode">
		/// <see cref="FileMode"/> that specifies how the file is to be opened.
		/// </param>
		/// <param name="fileAccess">
		/// <see cref="FileAccess"/> that specifies access mode.
		/// </param>
		/// <returns><see cref="Stream"/> object for the opened file.</returns>
		public Stream OpenFile(
			string file,
			FileMode fileMode,
			FileAccess fileAccess
		) {
			return OpenFile(
				file,
				fileMode,
				fileAccess,
				FileShare.ReadWrite
			);
		}

		/// <summary>
		/// Opens a file contained in storage-container.
		/// </summary>
		/// <param name="file">Relative path of the file.</param>
		/// <param name="fileMode">
		/// <see cref="FileMode"/> that specifies how the file is to be opened.
		/// </param>
		/// <param name="fileAccess">
		/// <see cref="FileAccess"/> that specifies access mode.
		/// </param>
		/// <param name="fileShare">A bitwise combination of <see cref="FileShare"/>
		/// enumeration values that specifies access modes for other stream objects.</param>
		/// <returns><see cref="Stream"/> object for the opened file.</returns>
		public Stream OpenFile(
			string file,
			FileMode fileMode,
			FileAccess fileAccess,
			FileShare fileShare
		) {
			if (string.IsNullOrEmpty(file))
			{
				throw new ArgumentNullException("Parameter file must contain a value.");
			}

			// Filename is relative, so combine with our path.
			string filePath = Path.Combine(storagePath, file);

			return File.Open(filePath, fileMode, fileAccess, fileShare);
		}

		#endregion
	}
}
