/*  Copyright (C) 2008-2016 Peter Palotas, Jeffrey Jangli, Alexandr Normuradov
 *  
 *  Permission is hereby granted, free of charge, to any person obtaining a copy 
 *  of this software and associated documentation files (the "Software"), to deal 
 *  in the Software without restriction, including without limitation the rights 
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 *  copies of the Software, and to permit persons to whom the Software is 
 *  furnished to do so, subject to the following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included in 
 *  all copies or substantial portions of the Software.
 *  
 *  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. 
 */

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Security;

namespace Alphaleonis.Win32.Filesystem
{
   /// <summary>Provides access to information on a local or remote drive.</summary>
   /// <remarks>
   /// This class models a drive and provides methods and properties to query for drive information.
   /// Use DriveInfo to determine what drives are available, and what type of drives they are.
   /// You can also query to determine the capacity and available free space on the drive.
   /// </remarks>
   [Serializable]
   [SecurityCritical]
   public sealed class DriveInfo 
   {
      #region Private Fields

      [NonSerialized]
      private readonly VolumeInfo _volumeInfo;

      [NonSerialized]
      private readonly DiskSpaceInfo _dsi;

      [NonSerialized]
      private bool _initDsie;

      [NonSerialized]
      private DriveType? _driveType;

      [NonSerialized]
      private string _dosDeviceName;

      [NonSerialized]
      private DirectoryInfo _rootDirectory;

      private readonly string _name;


      #endregion

      #region Constructors

      /// <summary>Provides access to information on the specified drive.</summary>
      /// <exception cref="ArgumentNullException"/>
      /// <exception cref="ArgumentException"/>
      /// <param name="driveName">
      ///   A valid drive path or drive letter.
      ///   <para>This can be either uppercase or lowercase,</para>
      ///   <para>'a' to 'z' or a network share in the format: \\server\share</para>
      /// </param>
      [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Utils.IsNullOrWhiteSpace validates arguments.")]
      [SecurityCritical]
      public DriveInfo(string driveName)
      {
         if (Utils.IsNullOrWhiteSpace(driveName))
            throw new ArgumentNullException("driveName");

         if (driveName.Length == 1)
            _name += Path.VolumeSeparatorChar;
         else
            _name = Path.GetPathRoot(driveName, false);

         if (Utils.IsNullOrWhiteSpace(_name))
            throw new ArgumentException("Argument must be a drive letter (\"C\"), RootDir (\"C:\\\") or UNC path (\"\\\\server\\share\")");

         // If an exception is thrown, the original drivePath is used.
         _name = Path.AddTrailingDirectorySeparator(_name, false);


         // Initiate VolumeInfo() lazyload instance.
         _volumeInfo = new VolumeInfo(_name, false, true);

         // Initiate DiskSpaceInfo() lazyload instance.
         _dsi = new DiskSpaceInfo(_name, null, false, true);
      } 

      #endregion // Constructors

      #region Properties

      /// <summary>Indicates the amount of available free space on a drive.</summary>
      /// <returns>The amount of free space available on the drive, in bytes.</returns>
      /// <remarks>This property indicates the amount of free space available on the drive. Note that this number may be different from the <see cref="TotalFreeSpace"/> number because this property takes into account disk quotas.</remarks>
      public long AvailableFreeSpace
      {
         get
         {
            GetDeviceInfo(3, 0);
            return _dsi == null ? 0 : _dsi.FreeBytesAvailable;
         }
      }

      /// <summary>Gets the name of the file system, such as NTFS or FAT32.</summary>
      /// <remarks>Use DriveFormat to determine what formatting a drive uses.</remarks>
      public string DriveFormat
      {
         get { return (string)GetDeviceInfo(0, 1); }
      }

      /// <summary>Gets the drive type.</summary>
      /// <returns>One of the <see cref="System.IO.DriveType"/> values.</returns>
      /// <remarks>
      /// The DriveType property indicates whether a drive is any of: CDRom, Fixed, Unknown, Network, NoRootDirectory,
      /// Ram, Removable, or Unknown. Values are listed in the <see cref="System.IO.DriveType"/> enumeration.
      /// </remarks>
      public DriveType DriveType
      {
         get { return (DriveType)GetDeviceInfo(2, 0); }
      }

      /// <summary>Gets a value indicating whether a drive is ready.</summary>
      /// <returns><see langword="true"/> if the drive is ready; otherwise, <see langword="false"/>.</returns>
      /// <remarks>
      /// IsReady indicates whether a drive is ready. For example, it indicates whether a CD is in a CD drive or whether
      /// a removable storage device is ready for read/write operations. If you do not test whether a drive is ready, and
      /// it is not ready, querying the drive using DriveInfo will raise an IOException.
      /// 
      /// Do not rely on IsReady() to avoid catching exceptions from other members such as TotalSize, TotalFreeSpace, and DriveFormat.
      /// Between the time that your code checks IsReady and then accesses one of the other properties
      /// (even if the access occurs immediately after the check), a drive may have been disconnected or a disk may have been removed.
      /// </remarks>
      public bool IsReady
      {
         get { return File.ExistsCore(true, null, Name, PathFormat.LongFullPath); }
      }


      /// <summary>Gets the name of the drive.</summary>
      /// <returns>The name of the drive.</returns>
      /// <remarks>This property is the name assigned to the drive, such as C:\ or E:\</remarks>
      public string Name
      {
         get { return _name; }
      }

      /// <summary>Gets the root directory of a drive.</summary>
      /// <returns>A DirectoryInfo object that contains the root directory of the drive.</returns>
      public DirectoryInfo RootDirectory
      {
         get { return (DirectoryInfo)GetDeviceInfo(2, 1); }
      }

      /// <summary>Gets the total amount of free space available on a drive.</summary>
      /// <returns>The total free space available on a drive, in bytes.</returns>
      /// <remarks>This property indicates the total amount of free space available on the drive, not just what is available to the current user.</remarks>
      public long TotalFreeSpace
      {
         get
         {
            GetDeviceInfo(3, 0);
            return _dsi == null ? 0 : _dsi.TotalNumberOfFreeBytes;
         }
      }

      /// <summary>Gets the total size of storage space on a drive.</summary>
      /// <returns>The total size of the drive, in bytes.</returns>
      /// <remarks>This property indicates the total size of the drive in bytes, not just what is available to the current user.</remarks>
      public long TotalSize
      {
         get
         {
            GetDeviceInfo(3, 0);
            return _dsi == null ? 0 : _dsi.TotalNumberOfBytes;
         }
      }

      /// <summary>Gets or sets the volume label of a drive.</summary>
      /// <returns>The volume label.</returns>
      /// <remarks>
      /// The label length is determined by the operating system. For example, NTFS allows a volume label
      /// to be up to 32 characters long. Note that <see langword="null"/> is a valid VolumeLabel.
      /// </remarks>

      public string VolumeLabel
      {
         get { return (string)GetDeviceInfo(0, 2); }
         set { Volume.SetVolumeLabel(Name, value); }
      }

      /// <summary>[AlphaFS] Returns the <see ref="Alphaleonis.Win32.Filesystem.DiskSpaceInfo"/> instance.</summary>
      public DiskSpaceInfo DiskSpaceInfo
      {
         get
         {
            GetDeviceInfo(3, 0);
            return _dsi;
         }
      }

      /// <summary>[AlphaFS] The MS-DOS device name.</summary>
      public string DosDeviceName
      {
         get { return (string)GetDeviceInfo(1, 0); }
      }

      /// <summary>[AlphaFS] Indicates if this drive is a SUBST.EXE / DefineDosDevice drive mapping.</summary>
      public bool IsDosDeviceSubstitute
      {
         get { return !Utils.IsNullOrWhiteSpace(DosDeviceName) && DosDeviceName.StartsWith(Path.SubstitutePrefix, StringComparison.OrdinalIgnoreCase); }
      }

      /// <summary>[AlphaFS] Indicates if this drive is a UNC path.</summary>
      /// <remarks>Only retrieve this information if we're dealing with a real network share mapping: http://alphafs.codeplex.com/discussions/316583</remarks>
      public bool IsUnc
      {
         get { return !IsDosDeviceSubstitute && DriveType == DriveType.Network; }
      }

      /// <summary>[AlphaFS] Determines whether the specified volume name is a defined volume on the current computer.</summary>
      public bool IsVolume
      {
         get { return GetDeviceInfo(0, 0) != null; }
      }

      /// <summary>[AlphaFS] Contains information about a file-system volume.</summary>
      /// <returns>A VolumeInfo object that contains file-system volume information of the drive.</returns>
      public VolumeInfo VolumeInfo
      {
         get { return (VolumeInfo)GetDeviceInfo(0, 0); }
      }


      #endregion // Properties

      #region Methods

      /// <summary>Retrieves the drive names of all logical drives on a computer.</summary>
      /// <returns>An array of type <see cref="Alphaleonis.Win32.Filesystem.DriveInfo"/> that represents the logical drives on a computer.</returns>
      
      [SecurityCritical]
      public static DriveInfo[] GetDrives()
      {
         return Directory.EnumerateLogicalDrivesCore(false, false).ToArray();
      }

      /// <summary>Returns a drive name as a string.</summary>
      /// <returns>The name of the drive.</returns>
      /// <remarks>This method returns the Name property.</remarks>
      public override string ToString()
      {
         return _name;
      }


      /// <summary>[AlphaFS] Enumerates the drive names of all logical drives on a computer.</summary>
      /// <param name="fromEnvironment">Retrieve logical drives as known by the Environment.</param>
      /// <param name="isReady">Retrieve only when accessible (IsReady) logical drives.</param>
      /// <returns>
      ///   An IEnumerable of type <see cref="Alphaleonis.Win32.Filesystem.DriveInfo"/> that represents
      ///   the logical drives on a computer.
      /// </returns>      
      [SecurityCritical]
      public static IEnumerable<DriveInfo> EnumerateDrives(bool fromEnvironment, bool isReady)
      {
         return Directory.EnumerateLogicalDrivesCore(fromEnvironment, isReady);
      }


      /// <summary>[AlphaFS] Gets the first available drive letter on the local system.</summary>
      /// <returns>A drive letter as <see cref="char"/>. When no drive letters are available, an exception is thrown.</returns>
      /// <remarks>The letters "A" and "B" are reserved for floppy drives and will never be returned by this function.</remarks>
      
      public static char GetFreeDriveLetter()
      {
         return GetFreeDriveLetter(false);
      }

      /// <summary>Gets an available drive letter on the local system.</summary>
      /// <param name="getLastAvailable">When <see langword="true"/> get the last available drive letter. When <see langword="false"/> gets the first available drive letter.</param>
      /// <returns>A drive letter as <see cref="char"/>. When no drive letters are available, an exception is thrown.</returns>
      /// <remarks>The letters "A" and "B" are reserved for floppy drives and will never be returned by this function.</remarks>
      
      [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
      [SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes")]
      public static char GetFreeDriveLetter(bool getLastAvailable)
      {
         IEnumerable<char> freeDriveLetters = "CDEFGHIJKLMNOPQRSTUVWXYZ".Except(Directory.EnumerateLogicalDrivesCore(false, false).Select(d => d.Name[0]));

         try
         {
            return getLastAvailable ? freeDriveLetters.Last() : freeDriveLetters.First();
         }
         catch
         {
            throw new Exception("There are no drive letters available.");
         }
      }

      #endregion // Methods

      #region Private Methods

      /// <summary>Retrieves information about the file system and volume associated with the specified root file or directorystream.</summary>
      [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
      [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
      [SecurityCritical]
      private object GetDeviceInfo(int type, int mode)
      {
         try
         {
            switch (type)
            {
               #region Volume

               // VolumeInfo properties.
               case 0:
                  if (Utils.IsNullOrWhiteSpace(_volumeInfo.FullPath))
                     _volumeInfo.Refresh();

                  switch (mode)
                  {
                     case 0:
                        // IsVolume, VolumeInfo
                        return _volumeInfo;

                     case 1:
                        // DriveFormat
                        return _volumeInfo == null ? DriveType.Unknown.ToString() : _volumeInfo.FileSystemName ?? DriveType.Unknown.ToString();

                     case 2:
                        // VolumeLabel
                        return _volumeInfo == null ? string.Empty : _volumeInfo.Name ?? string.Empty;
                  }
                  break;

               // Volume related.
               case 1:
                  switch (mode)
                  {
                     case 0:
                        // DosDeviceName

                        // Do not use ?? expression here.
                        if (_dosDeviceName == null)
                           _dosDeviceName = Volume.QueryDosDevice(Name).FirstOrDefault();

                        return _dosDeviceName;
                  }
                  break;

               #endregion // Volume

               #region Drive

               // Drive related.
               case 2:
                  switch (mode)
                  {
                     case 0:
                        // DriveType
                        // Do not use ?? expression here.
                        if (_driveType == null)
                           _driveType = Volume.GetDriveType(Name);

                        return _driveType;

                     case 1:
                        // RootDirectory

                        // Do not use ?? expression here.
                        if (_rootDirectory == null)
                           _rootDirectory = new DirectoryInfo(null, Name, PathFormat.RelativePath);

                        return _rootDirectory;
                  }
                  break;

               // DiskSpaceInfo related.
               case 3:
                  switch (mode)
                  {
                     case 0:
                        // AvailableFreeSpace, TotalFreeSpace, TotalSize, DiskSpaceInfo
                        if (!_initDsie)
                        {
                           _dsi.Refresh();
                           _initDsie = true;
                        }
                        break;
                  }
                  break;

               #endregion // Drive
            }
         }
         catch
         {
         }

         return type == 0 && mode > 0 ? string.Empty : null;
      }


      #endregion // Private

   }
}
