/*  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.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;

namespace Alphaleonis.Win32.Filesystem
{
   /// <summary>Provides the base class for both <see cref="FileInfo"/> and <see cref="DirectoryInfo"/> objects.</summary>
   [SerializableAttribute]
   [ComVisibleAttribute(true)]
   public abstract class FileSystemInfo : MarshalByRefObject
   {
      #region Methods

      #region .NET

      #region Delete

      /// <summary>Deletes a file or directory.</summary>
      [SecurityCritical]
      public abstract void Delete();

      #endregion // Delete

      #region Refresh

      /// <summary>Refreshes the state of the object.</summary>
      /// <remarks>
      ///   <para>FileSystemInfo.Refresh() takes a snapshot of the file from the current file system.</para>
      ///   <para>Refresh cannot correct the underlying file system even if the file system returns incorrect or outdated information.</para>
      ///   <para>This can happen on platforms such as Windows 98.</para>
      ///   <para>Calls must be made to Refresh() before attempting to get the attribute information, or the information will be
      ///   outdated.</para>
      /// </remarks>
      [SecurityCritical]
      protected void Refresh()
      {
         DataInitialised = File.FillAttributeInfoCore(Transaction, LongFullName, ref Win32AttributeData, false, false);
      }
   
      #endregion // Refresh

      #region ToString

      /// <summary>Returns a string that represents the current object.</summary>
      /// <remarks>
      ///   ToString is the major formatting method in the .NET Framework. It converts an object to its string representation so that it is
      ///   suitable for display.
      /// </remarks>
      /// <returns>A string that represents this instance.</returns>
      public override string ToString()
      {
         // "Alphaleonis.Win32.Filesystem.FileSystemInfo"
         return GetType().ToString();
      }

      #endregion // ToString

      #region Equality

      /// <summary>Determines whether the specified Object is equal to the current Object.</summary>
      /// <param name="obj">Another object to compare to.</param>
      /// <returns><see langword="true"/> if the specified Object is equal to the current Object; otherwise, <see langword="false"/>.</returns>
      public override bool Equals(object obj)
      {
         if (obj == null || GetType() != obj.GetType())
            return false;

         FileSystemInfo other = obj as FileSystemInfo;

         return other != null && (other.Name != null &&
                                  (other.FullName.Equals(FullName, StringComparison.OrdinalIgnoreCase) &&
                                   other.Attributes.Equals(Attributes) &&
                                   other.CreationTimeUtc.Equals(CreationTimeUtc) &&
                                   other.LastWriteTimeUtc.Equals(LastWriteTimeUtc)));
      }

      // A random prime number will be picked and added to the HashCode, each time an instance is created.
      [NonSerialized] 
      private readonly int _random = new Random().Next(0, 19);
      
      [NonSerialized] 
      private static readonly int[] Primes = { 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919 };

      /// <summary>Serves as a hash function for a particular type.</summary>
      /// <returns>A hash code for the current Object.</returns>
      public override int GetHashCode()
      {
         string fullName = FullName;
         string name = Name;

         unchecked
         {
            int hash = Primes[_random];

            if (!Utils.IsNullOrWhiteSpace(fullName))
               hash = hash * Primes[1] + fullName.GetHashCode();

            if (!Utils.IsNullOrWhiteSpace(name))
               hash = hash * Primes[1] + name.GetHashCode();

            hash = hash * Primes[1] + Attributes.GetHashCode();
            hash = hash * Primes[1] + CreationTimeUtc.GetHashCode();
            hash = hash * Primes[1] + LastWriteTimeUtc.GetHashCode();

            return hash;
         }
      }

      /// <summary>Implements the operator ==</summary>
      /// <param name="left">A.</param>
      /// <param name="right">B.</param>
      /// <returns>The result of the operator.</returns>
      public static bool operator ==(FileSystemInfo left, FileSystemInfo right)
      {
         return ReferenceEquals(left, null) && ReferenceEquals(right, null) ||
                !ReferenceEquals(left, null) && !ReferenceEquals(right, null) && left.Equals(right);
      }
      
      /// <summary>Implements the operator !=</summary>
      /// <param name="left">A.</param>
      /// <param name="right">B.</param>
      /// <returns>The result of the operator.</returns>
      public static bool operator !=(FileSystemInfo left, FileSystemInfo right)
      {
         return !(left == right);
      }

      #endregion // Equality

      #endregion // .NET

      #region AlphaFS

      #region RefreshEntryInfo

      /// <summary>Refreshes the state of the <see cref="FileSystemEntryInfo"/> EntryInfo instance.</summary>
      /// <remarks>
      ///   <para>FileSystemInfo.RefreshEntryInfo() takes a snapshot of the file from the current file system.</para>
      ///   <para>Refresh cannot correct the underlying file system even if the file system returns incorrect or outdated information.</para>
      ///   <para>This can happen on platforms such as Windows 98.</para>
      ///   <para>Calls must be made to Refresh() before attempting to get the attribute information, or the information will be outdated.</para>
      /// </remarks>
      [SecurityCritical]
      protected void RefreshEntryInfo()
      {
         _entryInfo = File.GetFileSystemEntryInfoCore(IsDirectory, Transaction, LongFullName, true, PathFormat.LongFullPath);
   
         if (_entryInfo == null)
            DataInitialised = -1;
         else
         {
            DataInitialised = 0;
            Win32AttributeData = new NativeMethods.WIN32_FILE_ATTRIBUTE_DATA(_entryInfo.Win32FindData);
         }
      }

      #endregion // RefreshEntryInfo

      #region Reset

      /// <summary>[AlphaFS] Resets the state of the file system object to uninitialized.</summary>
      internal void Reset()
      {
         DataInitialised = -1;
      }

      #endregion // Reset

      #region InitializeCore

      /// <summary>Initializes the specified file name.</summary>
      /// <exception cref="ArgumentException"/>
      /// <exception cref="NotSupportedException"/>
      /// <param name="isFolder">Specifies that <paramref name="path"/> is a file or directory.</param>
      /// <param name="transaction">The transaction.</param>
      /// <param name="path">The full path and name of the file.</param>
      /// <param name="pathFormat">Indicates the format of the path parameter(s).</param>
      internal void InitializeCore(bool isFolder, KernelTransaction transaction, string path, PathFormat pathFormat)
      {
         if (pathFormat == PathFormat.RelativePath)
            Path.CheckSupportedPathFormat(path, true, true);

         LongFullName = Path.GetExtendedLengthPathCore(transaction, path, pathFormat, GetFullPathOptions.TrimEnd | (isFolder ? GetFullPathOptions.RemoveTrailingDirectorySeparator : 0) | GetFullPathOptions.ContinueOnNonExist);

         // (Not on MSDN): .NET 4+ Trailing spaces are removed from the end of the path parameter before creating the FileSystemInfo instance.

         FullPath = Path.GetRegularPathCore(LongFullName, GetFullPathOptions.None, false);

         IsDirectory = isFolder;
         Transaction = transaction;

         OriginalPath = FullPath.Length == 2 && (FullPath[1] == Path.VolumeSeparatorChar)
            ? Path.CurrentDirectoryPrefix
            : path;

         DisplayPath = OriginalPath.Length != 2 || OriginalPath[1] != Path.VolumeSeparatorChar
            ? Path.GetRegularPathCore(OriginalPath, GetFullPathOptions.None, false)
            : Path.CurrentDirectoryPrefix;
      }

      #endregion // InitializeCore

      #endregion // AlphaFS
      
      #endregion // Methods

      #region Properties

      #region .NET

      #region Attributes

      /// <summary>
      ///   Gets or sets the attributes for the current file or directory.
      /// </summary>
      /// <remarks>
      ///   <para>The value of the CreationTime property is pre-cached</para>
      ///   <para>To get the latest value, call the Refresh method.</para>
      /// </remarks>
      /// <value><see cref="FileAttributes"/> of the current <see cref="FileSystemInfo"/>.</value>
      ///
      /// <exception cref="FileNotFoundException"/>
      /// <exception cref="DirectoryNotFoundException"/>
      /// <exception cref="IOException"/>
      public FileAttributes Attributes
      {
         [SecurityCritical]
         get
         {
            if (DataInitialised == -1)
            {
               Win32AttributeData = new NativeMethods.WIN32_FILE_ATTRIBUTE_DATA();
               Refresh();
            }

            // MSDN: .NET 3.5+: IOException: Refresh cannot initialize the data. 
            if (DataInitialised != 0)
               NativeError.ThrowException(DataInitialised, LongFullName);

            return Win32AttributeData.dwFileAttributes;
         }

         [SecurityCritical]
         set
         {
            File.SetAttributesCore(IsDirectory, Transaction, LongFullName, value, false, PathFormat.LongFullPath);
            Reset();
         }
      }

      #endregion // Attributes

      #region CreationTime

      /// <summary>Gets or sets the creation time of the current file or directory.</summary>
      /// <remarks>
      ///   <para>The value of the CreationTime property is pre-cached To get the latest value, call the Refresh method.</para>
      ///   <para>This method may return an inaccurate value, because it uses native functions whose values may not be continuously updated by
      ///   the operating system.</para>
      ///   <para>If the file described in the FileSystemInfo object does not exist, this property will return
      ///   12:00 midnight, January 1, 1601 A.D. (C.E.) Coordinated Universal Time (UTC), adjusted to local time.</para>
      ///   <para>NTFS-formatted drives may cache file meta-info, such as file creation time, for a short period of time.
      ///   This process is known as file tunneling. As a result, it may be necessary to explicitly set the creation time of a file if you are
      ///   overwriting or replacing an existing file.</para>
      /// </remarks>
      /// <value>The creation date and time of the current <see cref="FileSystemInfo"/> object.</value>
      ///
      /// <exception cref="DirectoryNotFoundException"/>
      /// <exception cref="IOException"/>
      public DateTime CreationTime
      {
         [SecurityCritical] get { return CreationTimeUtc.ToLocalTime(); }
         [SecurityCritical] set { CreationTimeUtc = value.ToUniversalTime(); }
      }

      #endregion // CreationTime

      #region CreationTimeUtc

      /// <summary>Gets or sets the creation time, in coordinated universal time (UTC), of the current file or directory.</summary>
      /// <remarks>
      ///   <para>The value of the CreationTimeUtc property is pre-cached
      ///   To get the latest value, call the Refresh method.</para>
      ///   <para>This method may return an inaccurate value, because it uses native functions
      ///   whose values may not be continuously updated by the operating system.</para>
      ///   <para>To get the latest value, call the Refresh method.</para>
      ///   <para>If the file described in the FileSystemInfo object does not exist, this property will return
      ///   12:00 midnight, January 1, 1601 A.D. (C.E.) Coordinated Universal Time (UTC).</para>
      ///   <para>NTFS-formatted drives may cache file meta-info, such as file creation time, for a short period of time.
      ///   This process is known as file tunneling. As a result, it may be necessary to explicitly set the creation time
      ///   of a file if you are overwriting or replacing an existing file.</para>
      /// </remarks>
      /// <value>The creation date and time in UTC format of the current <see cref="FileSystemInfo"/> object.</value>
      ///
      /// <exception cref="DirectoryNotFoundException"/>
      /// <exception cref="IOException"/>
      [ComVisible(false)]
      public DateTime CreationTimeUtc
      {
         [SecurityCritical]
         get
         {
            if (DataInitialised == -1)
            {
               Win32AttributeData = new NativeMethods.WIN32_FILE_ATTRIBUTE_DATA();
               Refresh();
            }

            // MSDN: .NET 3.5+: IOException: Refresh cannot initialize the data. 
            if (DataInitialised != 0)
               NativeError.ThrowException(DataInitialised, LongFullName);

            return DateTime.FromFileTimeUtc(Win32AttributeData.ftCreationTime);
         }

         [SecurityCritical]
         set
         {
            File.SetFsoDateTimeCore(IsDirectory, Transaction, LongFullName, value, null, null, false, PathFormat.LongFullPath);
            Reset();
         }
      }

      #endregion // CreationTimeUtc

      #region Exists

      /// <summary>
      ///   Gets a value indicating whether the file or directory exists.
      /// </summary>
      /// <remarks>
      ///   <para>The <see cref="Exists"/> property returns <see langword="false"/> if any error occurs while trying to determine if the
      ///   specified file or directory exists.</para>
      ///   <para>This can occur in situations that raise exceptions such as passing a directory- or file name with invalid characters or too
      ///   many characters,</para>
      ///   <para>a failing or missing disk, or if the caller does not have permission to read the file or directory.</para>
      /// </remarks>
      /// <value><see langword="true"/> if the file or directory exists; otherwise, <see langword="false"/>.</value>
      public abstract bool Exists { get; }

      #endregion // Exists

      #region Extension

      /// <summary>
      ///   Gets the string representing the extension part of the file.
      /// </summary>
      /// <remarks>
      ///   <para>The Extension property returns the <see cref="FileSystemInfo"/> extension, including the period (.).</para>
      ///   <para>For example, for a file c:\NewFile.txt, this property returns ".txt".</para>
      /// </remarks>
      /// <value>A string containing the <see cref="FileSystemInfo"/> extension.</value>
      public string Extension
      {
         get { return Path.GetExtension(FullPath, false); }
      }

      #endregion // Extension

      #region FullName

      /// <summary>
      ///   Gets the full path of the directory or file.
      /// </summary>
      /// <value>A string containing the full path.</value>
      public virtual string FullName
      {
         [SecurityCritical]
         get { return FullPath; }
      }

      #endregion // FullName

      #region LastAccessTime

      /// <summary>Gets or sets the time the current file or directory was last accessed.</summary>
      /// <remarks>
      ///   <para>The value of the LastAccessTime property is pre-cached
      ///   To get the latest value, call the Refresh method.</para>
      ///   <para>This method may return an inaccurate value, because it uses native functions
      ///   whose values may not be continuously updated by the operating system.</para>
      ///   <para>If the file described in the FileSystemInfo object does not exist, this property will return
      ///   12:00 midnight, January 1, 1601 A.D. (C.E.) Coordinated Universal Time (UTC), adjusted to local time.</para>
      /// </remarks>
      /// <value>The time that the current file or directory was last accessed.</value>
      ///
      /// <exception cref="IOException"/>
      public DateTime LastAccessTime
      {
         [SecurityCritical] get { return LastAccessTimeUtc.ToLocalTime(); }
         [SecurityCritical] set { LastAccessTimeUtc = value.ToUniversalTime(); }
      }

      #endregion // LastAccessTime

      #region LastAccessTimeUtc

      /// <summary>Gets or sets the time, in coordinated universal time (UTC), that the current file or directory was last accessed.</summary>
      /// <remarks>
      ///   <para>The value of the LastAccessTimeUtc property is pre-cached.
      ///   To get the latest value, call the Refresh method.</para>
      ///   <para>This method may return an inaccurate value, because it uses native functions
      ///   whose values may not be continuously updated by the operating system.</para>
      ///   <para>If the file described in the FileSystemInfo object does not exist, this property will return
      ///   12:00 midnight, January 1, 1601 A.D. (C.E.) Coordinated Universal Time (UTC), adjusted to local time.</para>
      /// </remarks>
      /// <value>The UTC time that the current file or directory was last accessed.</value>
      ///
      /// <exception cref="IOException"/>
      [ComVisible(false)]
      public DateTime LastAccessTimeUtc
      {
         [SecurityCritical]
         get
         {
            if (DataInitialised == -1)
            {
               Win32AttributeData = new NativeMethods.WIN32_FILE_ATTRIBUTE_DATA();
               Refresh();
            }

            // MSDN: .NET 3.5+: IOException: Refresh cannot initialize the data. 
            if (DataInitialised != 0)
               NativeError.ThrowException(DataInitialised, LongFullName);

            return DateTime.FromFileTimeUtc(Win32AttributeData.ftLastAccessTime);
         }

         [SecurityCritical]
         set
         {
            File.SetFsoDateTimeCore(IsDirectory, Transaction, LongFullName, null, value, null, false, PathFormat.LongFullPath);
            Reset();
         }
      }

      #endregion // LastAccessTimeUtc

      #region LastWriteTime

      /// <summary>Gets or sets the time when the current file or directory was last written to.</summary>
      /// <remarks>
      ///   <para>The value of the LastWriteTime property is pre-cached.
      ///   To get the latest value, call the Refresh method.</para>
      ///   <para>This method may return an inaccurate value, because it uses native functions
      ///   whose values may not be continuously updated by the operating system.</para>
      ///   <para>If the file described in the FileSystemInfo object does not exist, this property will return
      ///   12:00 midnight, January 1, 1601 A.D. (C.E.) Coordinated Universal Time (UTC), adjusted to local time.</para>
      /// </remarks>
      /// <value>The time the current file was last written.</value>
      ///
      /// <exception cref="IOException"/>
      public DateTime LastWriteTime
      {
         get { return LastWriteTimeUtc.ToLocalTime(); }
         set { LastWriteTimeUtc = value.ToUniversalTime(); }
      }

      #endregion // LastWriteTime

      #region LastWriteTimeUtc

      /// <summary>Gets or sets the time, in coordinated universal time (UTC), when the current file or directory was last written to.</summary>
      /// <remarks>
      ///   <para>The value of the LastWriteTimeUtc property is pre-cached. To get the latest value, call the Refresh method.</para>
      ///   <para>This method may return an inaccurate value, because it uses native functions whose values may not be continuously updated by
      ///   the operating system.</para>
      ///   <para>If the file described in the FileSystemInfo object does not exist, this property will return 12:00 midnight, January 1, 1601
      ///   A.D. (C.E.) Coordinated Universal Time (UTC), adjusted to local time.</para>
      /// </remarks>
      /// <value>The UTC time when the current file was last written to.</value>
      [ComVisible(false)]
      public DateTime LastWriteTimeUtc
      {
         [SecurityCritical]
         get
         {
            if (DataInitialised == -1)
            {
               Win32AttributeData = new NativeMethods.WIN32_FILE_ATTRIBUTE_DATA();
               Refresh();
            }

            // MSDN: .NET 3.5+: IOException: Refresh cannot initialize the data. 
            if (DataInitialised != 0)
               NativeError.ThrowException(DataInitialised, LongFullName);

            return DateTime.FromFileTimeUtc(Win32AttributeData.ftLastWriteTime);
         }

         [SecurityCritical]
         set
         {
            File.SetFsoDateTimeCore(IsDirectory, Transaction, LongFullName, null, null, value, false, PathFormat.LongFullPath);
            Reset();
         }
      }

      #endregion // LastWriteTimeUtc

      #region Name

      /// <summary>
      ///   For files, gets the name of the file. For directories, gets the name of the last directory in the hierarchy if a hierarchy exists.
      ///   <para>Otherwise, the Name property gets the name of the directory.</para>
      /// </summary>
      /// <remarks>
      ///   <para>For a directory, Name returns only the name of the parent directory, such as Dir, not c:\Dir.</para>
      ///   <para>For a subdirectory, Name returns only the name of the subdirectory, such as Sub1, not c:\Dir\Sub1.</para>
      ///   <para>For a file, Name returns only the file name and file name extension, such as MyFile.txt, not c:\Dir\Myfile.txt.</para>
      /// </remarks>
      /// <value>
      ///   <para>A string that is the name of the parent directory, the name of the last directory in the hierarchy,</para>
      ///   <para>or the name of a file, including the file name extension.</para>
      /// </value>
      public abstract string Name { get; }

      #endregion // Name

      #endregion // .NET

      #region AlphaFS

      #region DisplayPath

      /// <summary>Returns the path as a string.</summary>
      protected internal string DisplayPath { get; set; }

      #endregion // DisplayPath

      #region EntryInfo

      private FileSystemEntryInfo _entryInfo;

      /// <summary>[AlphaFS] Gets the instance of the <see cref="FileSystemEntryInfo"/> class.</summary>
      public FileSystemEntryInfo EntryInfo
      {
         [SecurityCritical]
         get
         {
            if (_entryInfo == null)
            {
               Win32AttributeData = new NativeMethods.WIN32_FILE_ATTRIBUTE_DATA();
               RefreshEntryInfo();
            }

            // MSDN: .NET 3.5+: IOException: Refresh cannot initialize the data. 
            if (DataInitialised > 0)
               NativeError.ThrowException(DataInitialised, LongFullName);

            return _entryInfo;
         }

         internal set
         {
            _entryInfo = value;

            DataInitialised = value == null ? -1 : 0;

            if (DataInitialised == 0)
               Win32AttributeData = new NativeMethods.WIN32_FILE_ATTRIBUTE_DATA(_entryInfo.Win32FindData);
         }
      }

      #endregion // EntryInfo

      #region IsDirectory

      /// <summary>[AlphaFS] The initial "IsDirectory" indicator that was passed to the constructor.</summary>
      protected internal bool IsDirectory { get; set; }

      #endregion // IsDirectory

      #region LongFullName

      /// <summary>The full path of the file system object in Unicode (LongPath) format.</summary>
      protected string LongFullName { get; set; }

      #endregion // LongFullName

      #region Transaction

      /// <summary>[AlphaFS] Represents the KernelTransaction that was passed to the constructor.</summary>
      public KernelTransaction Transaction { get; set; }

      #endregion // Transaction

      #endregion // AlphaFS

      #endregion // Properties

      #region Fields

      // We use this field in conjunction with the Refresh methods, if we succeed
      // we store a zero, on failure we store the HResult in it so that we can
      // give back a generic error back.
      [NonSerialized] internal int DataInitialised = -1;

      // The pre-cached FileSystemInfo information.
      [NonSerialized] internal NativeMethods.WIN32_FILE_ATTRIBUTE_DATA Win32AttributeData;

      #region .NET

      /// <summary>Represents the fully qualified path of the file or directory.</summary>
      /// <remarks>
      ///   <para>Classes derived from <see cref="FileSystemInfo"/> can use the FullPath field</para>
      ///   <para>to determine the full path of the object being manipulated.</para>
      /// </remarks>
      [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")]
      protected string FullPath;

      /// <summary>The path originally specified by the user, whether relative or absolute.</summary>
      [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")]
      protected string OriginalPath;

      #endregion // .NET
       
      #endregion // Fields
   }
}
