using System;
using System.IO;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.Zip.Compression;
namespace ICSharpCode.SharpZipLib.Zip
{
	/// 
	/// FastZipEvents supports all events applicable to FastZip operations.
	/// 
	public class FastZipEvents
	{
		/// 
		/// Delegate to invoke when processing directories.
		/// 
		public event EventHandler ProcessDirectory;
		/// 
		/// Delegate to invoke when processing files.
		/// 
		public ProcessFileHandler ProcessFile;
		/// 
		/// Delegate to invoke during processing of files.
		/// 
		public ProgressHandler Progress;
		/// 
		/// Delegate to invoke when processing for a file has been completed.
		/// 
		public CompletedFileHandler CompletedFile;
		/// 
		/// Delegate to invoke when processing directory failures.
		/// 
		public DirectoryFailureHandler DirectoryFailure;
		/// 
		/// Delegate to invoke when processing file failures.
		/// 
		public FileFailureHandler FileFailure;
		/// 
		/// Raise the directory failure event.
		/// 
		/// The directory causing the failure.
		/// The exception for this event.
		/// A boolean indicating if execution should continue or not.
		public bool OnDirectoryFailure(string directory, Exception e)
		{
			bool result = false;
			DirectoryFailureHandler handler = DirectoryFailure;
			if (handler != null) {
				var args = new ScanFailureEventArgs(directory, e);
				handler(this, args);
				result = args.ContinueRunning;
			}
			return result;
		}
		/// 
		/// Fires the  file failure handler delegate.
		/// 
		/// The file causing the failure.
		/// The exception for this failure.
		/// A boolean indicating if execution should continue or not.
		public bool OnFileFailure(string file, Exception e)
		{
			FileFailureHandler handler = FileFailure;
			bool result = (handler != null);
			if (result) {
				var args = new ScanFailureEventArgs(file, e);
				handler(this, args);
				result = args.ContinueRunning;
			}
			return result;
		}
		/// 
		/// Fires the ProcessFile delegate.
		/// 
		/// The file being processed.
		/// A boolean indicating if execution should continue or not.
		public bool OnProcessFile(string file)
		{
			bool result = true;
			ProcessFileHandler handler = ProcessFile;
			if (handler != null) {
				var args = new ScanEventArgs(file);
				handler(this, args);
				result = args.ContinueRunning;
			}
			return result;
		}
		/// 
		/// Fires the  delegate
		/// 
		/// The file whose processing has been completed.
		/// A boolean indicating if execution should continue or not.
		public bool OnCompletedFile(string file)
		{
			bool result = true;
			CompletedFileHandler handler = CompletedFile;
			if (handler != null) {
				var args = new ScanEventArgs(file);
				handler(this, args);
				result = args.ContinueRunning;
			}
			return result;
		}
		/// 
		/// Fires the process directory delegate.
		/// 
		/// The directory being processed.
		/// Flag indicating if the directory has matching files as determined by the current filter.
		/// A  of true if the operation should continue; false otherwise.
		public bool OnProcessDirectory(string directory, bool hasMatchingFiles)
		{
			bool result = true;
			EventHandler handler = ProcessDirectory;
			if (handler != null) {
				var args = new DirectoryEventArgs(directory, hasMatchingFiles);
				handler(this, args);
				result = args.ContinueRunning;
			}
			return result;
		}
		/// 
		/// The minimum timespan between  events.
		/// 
		/// The minimum period of time between  events.
		/// 
		/// The default interval is three seconds.
		public TimeSpan ProgressInterval {
			get { return progressInterval_; }
			set { progressInterval_ = value; }
		}
		#region Instance Fields
		TimeSpan progressInterval_ = TimeSpan.FromSeconds(3);
		#endregion
	}
	/// 
	/// FastZip provides facilities for creating and extracting zip files.
	/// 
	public class FastZip
	{
		#region Enumerations
		/// 
		/// Defines the desired handling when overwriting files during extraction.
		/// 
		public enum Overwrite
		{
			/// 
			/// Prompt the user to confirm overwriting
			/// 
			Prompt,
			/// 
			/// Never overwrite files.
			/// 
			Never,
			/// 
			/// Always overwrite files.
			/// 
			Always
		}
		#endregion
		#region Constructors
		/// 
		/// Initialise a default instance of .
		/// 
		public FastZip()
		{
		}
		/// 
		/// Initialise a new instance of 
		/// 
		/// The events to use during operations.
		public FastZip(FastZipEvents events)
		{
			events_ = events;
		}
		#endregion
		#region Properties
		/// 
		/// Get/set a value indicating wether empty directories should be created.
		/// 
		public bool CreateEmptyDirectories {
			get { return createEmptyDirectories_; }
			set { createEmptyDirectories_ = value; }
		}
		/// 
		/// Get / set the password value.
		/// 
		public string Password {
			get { return password_; }
			set { password_ = value; }
		}
		/// 
		/// Get or set the  active when creating Zip files.
		/// 
		/// 
		public INameTransform NameTransform {
			get { return entryFactory_.NameTransform; }
			set {
				entryFactory_.NameTransform = value;
			}
		}
		/// 
		/// Get or set the  active when creating Zip files.
		/// 
		public IEntryFactory EntryFactory {
			get { return entryFactory_; }
			set {
				if (value == null) {
					entryFactory_ = new ZipEntryFactory();
				} else {
					entryFactory_ = value;
				}
			}
		}
		/// 
		/// Gets or sets the setting for Zip64 handling when writing.
		/// 
		/// 
		/// The default value is dynamic which is not backwards compatible with old
		/// programs and can cause problems with XP's built in compression which cant
		/// read Zip64 archives. However it does avoid the situation were a large file
		/// is added and cannot be completed correctly.
		/// NOTE: Setting the size for entries before they are added is the best solution!
		/// By default the EntryFactory used by FastZip will set fhe file size.
		/// 
		public UseZip64 UseZip64 {
			get { return useZip64_; }
			set { useZip64_ = value; }
		}
		/// 
		/// Get/set a value indicating wether file dates and times should
		/// be restored when extracting files from an archive.
		/// 
		/// The default value is false.
		public bool RestoreDateTimeOnExtract {
			get {
				return restoreDateTimeOnExtract_;
			}
			set {
				restoreDateTimeOnExtract_ = value;
			}
		}
		/// 
		/// Get/set a value indicating whether file attributes should
		/// be restored during extract operations
		/// 
		public bool RestoreAttributesOnExtract {
			get { return restoreAttributesOnExtract_; }
			set { restoreAttributesOnExtract_ = value; }
		}
        /// 
        /// Get/set the Compression Level that will be used
        /// when creating the zip
        /// 
        public Deflater.CompressionLevel CompressionLevel{
            get { return compressionLevel_; }
            set { compressionLevel_ = value; }
        }
		#endregion
		#region Delegates
		/// 
		/// Delegate called when confirming overwriting of files.
		/// 
		public delegate bool ConfirmOverwriteDelegate(string fileName);
		#endregion
		#region CreateZip
		/// 
		/// Create a zip file.
		/// 
		/// The name of the zip file to create.
		/// The directory to source files from.
		/// True to recurse directories, false for no recursion.
		/// The file filter to apply.
		/// The directory filter to apply.
		public void CreateZip(string zipFileName, string sourceDirectory,
			bool recurse, string fileFilter, string directoryFilter)
		{
			CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, directoryFilter);
		}
		/// 
		/// Create a zip file/archive.
		/// 
		/// The name of the zip file to create.
		/// The directory to obtain files and directories from.
		/// True to recurse directories, false for no recursion.
		/// The file filter to apply.
		public void CreateZip(string zipFileName, string sourceDirectory, bool recurse, string fileFilter)
		{
			CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, null);
		}
		/// 
		/// Create a zip archive sending output to the  passed.
		/// 
		/// The stream to write archive data to.
		/// The directory to source files from.
		/// True to recurse directories, false for no recursion.
		/// The file filter to apply.
		/// The directory filter to apply.
		/// The  is closed after creation.
		public void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, string fileFilter, string directoryFilter)
		{
			NameTransform = new ZipNameTransform(sourceDirectory);
			sourceDirectory_ = sourceDirectory;
			using (outputStream_ = new ZipOutputStream(outputStream)) {
                outputStream_.SetLevel((int)CompressionLevel);
				if (password_ != null) {
					outputStream_.Password = password_;
				}
				outputStream_.UseZip64 = UseZip64;
				var scanner = new FileSystemScanner(fileFilter, directoryFilter);
				scanner.ProcessFile += ProcessFile;
				if (this.CreateEmptyDirectories) {
					scanner.ProcessDirectory += ProcessDirectory;
				}
				if (events_ != null) {
					if (events_.FileFailure != null) {
						scanner.FileFailure += events_.FileFailure;
					}
					if (events_.DirectoryFailure != null) {
						scanner.DirectoryFailure += events_.DirectoryFailure;
					}
				}
				scanner.Scan(sourceDirectory, recurse);
			}
		}
		#endregion
		#region ExtractZip
		/// 
		/// Extract the contents of a zip file.
		/// 
		/// The zip file to extract from.
		/// The directory to save extracted information in.
		/// A filter to apply to files.
		public void ExtractZip(string zipFileName, string targetDirectory, string fileFilter)
		{
			ExtractZip(zipFileName, targetDirectory, Overwrite.Always, null, fileFilter, null, restoreDateTimeOnExtract_);
		}
		/// 
		/// Extract the contents of a zip file.
		/// 
		/// The zip file to extract from.
		/// The directory to save extracted information in.
		/// The style of overwriting to apply.
		/// A delegate to invoke when confirming overwriting.
		/// A filter to apply to files.
		/// A filter to apply to directories.
		/// Flag indicating whether to restore the date and time for extracted files.
		public void ExtractZip(string zipFileName, string targetDirectory,
							   Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate,
							   string fileFilter, string directoryFilter, bool restoreDateTime)
		{
			Stream inputStream = File.Open(zipFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
			ExtractZip(inputStream, targetDirectory, overwrite, confirmDelegate, fileFilter, directoryFilter, restoreDateTime, true);
		}
		/// 
		/// Extract the contents of a zip file held in a stream.
		/// 
		/// The seekable input stream containing the zip to extract from.
		/// The directory to save extracted information in.
		/// The style of overwriting to apply.
		/// A delegate to invoke when confirming overwriting.
		/// A filter to apply to files.
		/// A filter to apply to directories.
		/// Flag indicating whether to restore the date and time for extracted files.
		/// Flag indicating whether the inputStream will be closed by this method.
		public void ExtractZip(Stream inputStream, string targetDirectory,
					   Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate,
					   string fileFilter, string directoryFilter, bool restoreDateTime,
					   bool isStreamOwner)
		{
			if ((overwrite == Overwrite.Prompt) && (confirmDelegate == null)) {
				throw new ArgumentNullException("nameof(confirmDelegate)");
			}
			continueRunning_ = true;
			overwrite_ = overwrite;
			confirmDelegate_ = confirmDelegate;
			extractNameTransform_ = new WindowsNameTransform(targetDirectory);
			fileFilter_ = new NameFilter(fileFilter);
			directoryFilter_ = new NameFilter(directoryFilter);
			restoreDateTimeOnExtract_ = restoreDateTime;
			using (zipFile_ = new ZipFile(inputStream)) {
				if (password_ != null) {
					zipFile_.Password = password_;
				}
				zipFile_.IsStreamOwner = isStreamOwner;
				System.Collections.IEnumerator enumerator = zipFile_.GetEnumerator();
				while (continueRunning_ && enumerator.MoveNext()) {
					var entry = (ZipEntry)enumerator.Current;
					if (entry.IsFile) {
						// TODO Path.GetDirectory can fail here on invalid characters.
						if (directoryFilter_.IsMatch(Path.GetDirectoryName(entry.Name)) && fileFilter_.IsMatch(entry.Name)) {
							ExtractEntry(entry);
						}
					} else if (entry.IsDirectory) {
						if (directoryFilter_.IsMatch(entry.Name) && CreateEmptyDirectories) {
							ExtractEntry(entry);
						}
					} else {
						// Do nothing for volume labels etc...
					}
				}
			}
		}
		#endregion
		#region Internal Processing
		void ProcessDirectory(object sender, DirectoryEventArgs e)
		{
			if (!e.HasMatchingFiles && CreateEmptyDirectories) {
				if (events_ != null) {
					events_.OnProcessDirectory(e.Name, e.HasMatchingFiles);
				}
				if (e.ContinueRunning) {
					if (e.Name != sourceDirectory_) {
						ZipEntry entry = entryFactory_.MakeDirectoryEntry(e.Name);
						outputStream_.PutNextEntry(entry);
					}
				}
			}
		}
		void ProcessFile(object sender, ScanEventArgs e)
		{
			if ((events_ != null) && (events_.ProcessFile != null)) {
				events_.ProcessFile(sender, e);
			}
			if (e.ContinueRunning) {
				try {
					// The open below is equivalent to OpenRead which gaurantees that if opened the
					// file will not be changed by subsequent openers, but precludes opening in some cases
					// were it could succeed. ie the open may fail as its already open for writing and the share mode should reflect that.
					using (FileStream stream = File.Open(e.Name, FileMode.Open, FileAccess.Read, FileShare.Read)) {
						ZipEntry entry = entryFactory_.MakeFileEntry(e.Name);
						outputStream_.PutNextEntry(entry);
						AddFileContents(e.Name, stream);
					}
				} catch (Exception ex) {
					if (events_ != null) {
						continueRunning_ = events_.OnFileFailure(e.Name, ex);
					} else {
						continueRunning_ = false;
						throw;
					}
				}
			}
		}
		void AddFileContents(string name, Stream stream)
		{
			if (stream == null) {
				throw new ArgumentNullException("nameof(stream)");
			}
			if (buffer_ == null) {
				buffer_ = new byte[4096];
			}
			if ((events_ != null) && (events_.Progress != null)) {
				StreamUtils.Copy(stream, outputStream_, buffer_,
					events_.Progress, events_.ProgressInterval, this, name);
			} else {
				StreamUtils.Copy(stream, outputStream_, buffer_);
			}
			if (events_ != null) {
				continueRunning_ = events_.OnCompletedFile(name);
			}
		}
		void ExtractFileEntry(ZipEntry entry, string targetName)
		{
			bool proceed = true;
			if (overwrite_ != Overwrite.Always) {
				if (File.Exists(targetName)) {
					if ((overwrite_ == Overwrite.Prompt) && (confirmDelegate_ != null)) {
						proceed = confirmDelegate_(targetName);
					} else {
						proceed = false;
					}
				}
			}
			if (proceed) {
				if (events_ != null) {
					continueRunning_ = events_.OnProcessFile(entry.Name);
				}
				if (continueRunning_) {
					try {
						using (FileStream outputStream = File.Create(targetName)) {
							if (buffer_ == null) {
								buffer_ = new byte[4096];
							}
							if ((events_ != null) && (events_.Progress != null)) {
								StreamUtils.Copy(zipFile_.GetInputStream(entry), outputStream, buffer_,
									events_.Progress, events_.ProgressInterval, this, entry.Name, entry.Size);
							} else {
								StreamUtils.Copy(zipFile_.GetInputStream(entry), outputStream, buffer_);
							}
							if (events_ != null) {
								continueRunning_ = events_.OnCompletedFile(entry.Name);
							}
						}
						if (restoreDateTimeOnExtract_) {
							File.SetLastWriteTime(targetName, entry.DateTime);
						}
						if (RestoreAttributesOnExtract && entry.IsDOSEntry && (entry.ExternalFileAttributes != -1)) {
							var fileAttributes = (FileAttributes)entry.ExternalFileAttributes;
							// TODO: FastZip - Setting of other file attributes on extraction is a little trickier.
							fileAttributes &= (FileAttributes.Archive | FileAttributes.Normal | FileAttributes.ReadOnly | FileAttributes.Hidden);
							File.SetAttributes(targetName, fileAttributes);
						}
					} catch (Exception ex) {
						if (events_ != null) {
							continueRunning_ = events_.OnFileFailure(targetName, ex);
						} else {
							continueRunning_ = false;
							throw;
						}
					}
				}
			}
		}
		void ExtractEntry(ZipEntry entry)
		{
			bool doExtraction = entry.IsCompressionMethodSupported();
			string targetName = entry.Name;
			if (doExtraction) {
				if (entry.IsFile) {
					targetName = extractNameTransform_.TransformFile(targetName);
				} else if (entry.IsDirectory) {
					targetName = extractNameTransform_.TransformDirectory(targetName);
				}
				doExtraction = !(string.IsNullOrEmpty(targetName));
			}
			// TODO: Fire delegate/throw exception were compression method not supported, or name is invalid?
			string dirName = null;
			if (doExtraction) {
				if (entry.IsDirectory) {
					dirName = targetName;
				} else {
					dirName = Path.GetDirectoryName(Path.GetFullPath(targetName));
				}
			}
			if (doExtraction && !Directory.Exists(dirName)) {
				if (!entry.IsDirectory || CreateEmptyDirectories) {
					try {
						Directory.CreateDirectory(dirName);
					} catch (Exception ex) {
						doExtraction = false;
						if (events_ != null) {
							if (entry.IsDirectory) {
								continueRunning_ = events_.OnDirectoryFailure(targetName, ex);
							} else {
								continueRunning_ = events_.OnFileFailure(targetName, ex);
							}
						} else {
							continueRunning_ = false;
							throw;
						}
					}
				}
			}
			if (doExtraction && entry.IsFile) {
				ExtractFileEntry(entry, targetName);
			}
		}
		static int MakeExternalAttributes(FileInfo info)
		{
			return (int)info.Attributes;
		}
		static bool NameIsValid(string name)
		{
			return !string.IsNullOrEmpty(name) &&
				(name.IndexOfAny(Path.GetInvalidPathChars()) < 0);
		}
		#endregion
		#region Instance Fields
		bool continueRunning_;
		byte[] buffer_;
		ZipOutputStream outputStream_;
		ZipFile zipFile_;
		string sourceDirectory_;
		NameFilter fileFilter_;
		NameFilter directoryFilter_;
		Overwrite overwrite_;
		ConfirmOverwriteDelegate confirmDelegate_;
		bool restoreDateTimeOnExtract_;
		bool restoreAttributesOnExtract_;
		bool createEmptyDirectories_;
		FastZipEvents events_;
		IEntryFactory entryFactory_ = new ZipEntryFactory();
		INameTransform extractNameTransform_;
		UseZip64 useZip64_ = UseZip64.Dynamic;
        Deflater.CompressionLevel compressionLevel_ = Deflater.CompressionLevel.DEFAULT_COMPRESSION;
        string password_;
		#endregion
	}
}