using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Vanara.Collections;
using Vanara.InteropServices;
using Vanara.PInvoke;
using static Vanara.PInvoke.Ole32;
using static Vanara.PInvoke.PropSys;
using static Vanara.PInvoke.Shell32;

namespace Vanara.Windows.Shell
{
	/// <summary>Queued and static file operations using the Shell.</summary>
	/// <seealso cref="IDisposable"/>
	public partial class ShellFileOperations : IDisposable
	{
		private const OperationFlags defaultOptions = OperationFlags.AllowUndo | OperationFlags.NoConfirmMkDir;
		private ShellFileOperationDialog customProgressDialog;
		private bool disposedValue = false;
		private IFileOperation op;
		private OperationFlags opFlags = defaultOptions;
		private IWin32Window owner;
		private IFileOperationProgressSink sink;
		private uint sinkCookie;

		/// <summary>Initializes a new instance of the <see cref="ShellFileOperations"/> class.</summary>
		/// <param name="owner">The window that owns the modal dialog. This value can be <see langword="null"/>.</param>
		public ShellFileOperations(IWin32Window owner = null)
		{
			op = new IFileOperation();
			if (owner != null) op.SetOwnerWindow(owner.Handle);
			sink = new OpSink(this);
			sinkCookie = op.Advise(sink);
		}

		/// <summary>Finalizes an instance of the <see cref="ShellFileOperations"/> class.</summary>
		~ShellFileOperations()
		{
			Dispose(false);
		}

		/// <summary>Performs caller-implemented actions after the last operation performed by the call to IFileOperation is complete.</summary>
		public event EventHandler<ShellFileOpEventArgs> FinishOperations;

		/// <summary>Performs caller-implemented actions after the copy process for each item is complete.</summary>
		public event EventHandler<ShellFileOpEventArgs> PostCopyItem;

		/// <summary>Performs caller-implemented actions after the delete process for each item is complete.</summary>
		public event EventHandler<ShellFileOpEventArgs> PostDeleteItem;

		/// <summary>Performs caller-implemented actions after the move process for each item is complete.</summary>
		public event EventHandler<ShellFileOpEventArgs> PostMoveItem;

		/// <summary>Performs caller-implemented actions after the new item is created.</summary>
		public event EventHandler<ShellFileNewOpEventArgs> PostNewItem;

		/// <summary>Performs caller-implemented actions after the rename process for each item is complete.</summary>
		public event EventHandler<ShellFileOpEventArgs> PostRenameItem;

		/// <summary>Performs caller-implemented actions before the copy process for each item begins.</summary>
		public event EventHandler<ShellFileOpEventArgs> PreCopyItem;

		/// <summary>Performs caller-implemented actions before the delete process for each item begins.</summary>
		public event EventHandler<ShellFileOpEventArgs> PreDeleteItem;

		/// <summary>Performs caller-implemented actions before the move process for each item begins.</summary>
		public event EventHandler<ShellFileOpEventArgs> PreMoveItem;

		/// <summary>Performs caller-implemented actions before the process to create a new item begins.</summary>
		public event EventHandler<ShellFileOpEventArgs> PreNewItem;

		/// <summary>Performs caller-implemented actions before the rename process for each item begins.</summary>
		public event EventHandler<ShellFileOpEventArgs> PreRenameItem;

		/// <summary>Performs caller-implemented actions before any specific file operations are performed.</summary>
		public event EventHandler StartOperations;

		/// <summary>Occurs when a progress update is received.</summary>
		public event System.ComponentModel.ProgressChangedEventHandler UpdateProgress;

		/// <summary>
		/// Gets a value that states whether any file operations initiated by a call to <see cref="PerformOperations"/> were stopped before
		/// they were complete. The operations could be stopped either by user action or silently by the system.
		/// </summary>
		/// <value><see langword="true"/> if any file operations were aborted before they were complete; otherwise, <see langword="false"/>.</value>
		public bool AnyOperationsAborted => op.GetAnyOperationsAborted();

		/// <summary>Specifies a dialog box used to display the progress of the operation.</summary>
		/// <value>A ShellFileOperationDialog object that represents the dialog box.</value>
		public ShellFileOperationDialog CustomProgressDialog
		{
			get => customProgressDialog;
			set => op.SetProgressDialog((customProgressDialog = value)?.iProgressDialog);
		}

		/// <summary>Gets or sets options that control file operations.</summary>
		public OperationFlags Options
		{
			get => opFlags;
			set { if (value == opFlags) return; op.SetOperationFlags((FILEOP_FLAGS)(opFlags = value)); }
		}

		/// <summary>Gets or sets the parent or owner window for progress and dialog windows.</summary>
		/// <value>The owner window of the operation. This window will receive error messages.</value>
		public IWin32Window OwnerWindow
		{
			get => owner;
			set => op.SetOwnerWindow((owner = value)?.Handle ?? default);
		}

		/// <summary>Gets the number of queued operations.</summary>
		public int QueuedOperations { get; protected set; }

		/// <summary>Copies a single item to a specified destination using the Shell to provide progress and error dialogs.</summary>
		/// <param name="source">A string that specifies the source item's full file path.</param>
		/// <param name="dest">A string that specifies the full path of the destination folder to contain the copy of the item.</param>
		/// <param name="newName">
		/// An optional new name for the item after it has been copied. This can be <see langword="null"/>. If <see langword="null"/>, the
		/// name of the destination item is the same as the source.
		/// </param>
		/// <param name="options">Options that control file operations.</param>
		public static void Copy(string source, string dest, string newName = null, OperationFlags options = defaultOptions)
		{
			using var shfile = new ShellItem(source);
			using var shfld = new ShellFolder(dest);
			Copy(shfile, shfld, newName, options);
		}

		/// <summary>Copies a single item to a specified destination using the Shell to provide progress and error dialogs.</summary>
		/// <param name="source">A <see cref="ShellItem"/> that specifies the source item.</param>
		/// <param name="dest">A <see cref="ShellFolder"/> that specifies the destination folder to contain the copy of the item.</param>
		/// <param name="newName">
		/// An optional new name for the item after it has been copied. This can be <see langword="null"/>. If <see langword="null"/>, the
		/// name of the destination item is the same as the source.
		/// </param>
		/// <param name="options">Options that control file operations.</param>
		public static void Copy(ShellItem source, ShellFolder dest, string newName = null, OperationFlags options = defaultOptions)
		{
			using var sop = new ShellFileOperations();
			sop.Options = options;
			HRESULT hr = HRESULT.S_OK;
			sop.PostCopyItem += OnPost;
			try
			{
				sop.QueueCopyOperation(source, dest, newName);
				sop.PerformOperations();
				hr.ThrowIfFailed();
			}
			finally
			{
				sop.PostCopyItem -= OnPost;
			}

			void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result;
		}

		/// <summary>Copies a set of items to a specified destination using the Shell to provide progress and error dialogs.</summary>
		/// <param name="sourceItems">
		/// An <see cref="IEnumerable{T}"/> of <see cref="ShellItem"/> instances that represent the group of items to be copied.
		/// </param>
		/// <param name="dest">A <see cref="ShellFolder"/> that specifies the destination folder to contain the copy of the items.</param>
		/// <param name="options">Options that control file operations.</param>
		public static void Copy(IEnumerable<ShellItem> sourceItems, ShellFolder dest, OperationFlags options = defaultOptions)
		{
			using var sop = new ShellFileOperations();
			sop.Options = options;
			HRESULT hr = HRESULT.S_OK;
			sop.PostCopyItem += OnPost;
			try
			{
				sop.QueueCopyOperation(sourceItems, dest);
				sop.PerformOperations();
				hr.ThrowIfFailed();
			}
			finally
			{
				sop.PostCopyItem -= OnPost;
			}

			void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result;
		}

		/// <summary>Deletes a single item using the Shell to provide progress and error dialogs.</summary>
		/// <param name="source">A string that specifies the full path of the item to be deleted.</param>
		/// <param name="options">Options that control file operations.</param>
		public static void Delete(string source, OperationFlags options = defaultOptions)
		{
			using var shfile = new ShellItem(source);
			Delete(shfile, options);
		}

		/// <summary>Deletes a single item using the Shell to provide progress and error dialogs.</summary>
		/// <param name="source">A <see cref="ShellItem"/> that specifies the item to be deleted.</param>
		/// <param name="options">Options that control file operations.</param>
		public static void Delete(ShellItem source, OperationFlags options = defaultOptions)
		{
			using var sop = new ShellFileOperations();
			sop.Options = options;
			HRESULT hr = HRESULT.S_OK;
			sop.PostDeleteItem += OnPost;
			try
			{
				sop.QueueDeleteOperation(source);
				sop.PerformOperations();
				hr.ThrowIfFailed();
			}
			finally
			{
				sop.PostDeleteItem -= OnPost;
			}

			void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result;
		}

		/// <summary>Deletes a set of items using the Shell to provide progress and error dialogs.</summary>
		/// <param name="sourceItems">
		/// An <see cref="IEnumerable{T}"/> of <see cref="ShellItem"/> instances which represents the group of items to be deleted.
		/// </param>
		/// <param name="options">Options that control file operations.</param>
		public static void Delete(IEnumerable<ShellItem> sourceItems, OperationFlags options = defaultOptions)
		{
			using var sop = new ShellFileOperations();
			sop.Options = options;
			HRESULT hr = HRESULT.S_OK;
			sop.PostDeleteItem += OnPost;
			try
			{
				sop.QueueDeleteOperation(sourceItems);
				sop.PerformOperations();
				hr.ThrowIfFailed();
			}
			finally
			{
				sop.PostDeleteItem -= OnPost;
			}

			void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result;
		}

		/// <summary>Moves a single item to a specified destination using the Shell to provide progress and error dialogs.</summary>
		/// <param name="source">A string that specifies the source item's full file path.</param>
		/// <param name="dest">A string that specifies the full path of the destination folder to contain the copy of the item.</param>
		/// <param name="newName">
		/// An optional new name for the item in its new location. This can be <see langword="null"/>. If <see langword="null"/>, the name
		/// of the destination item is the same as the source.
		/// </param>
		/// <param name="options">Options that control file operations.</param>
		public static void Move(string source, string dest, string newName = null, OperationFlags options = defaultOptions)
		{
			using var shfile = new ShellItem(source);
			using var shfld = new ShellFolder(dest);
			Move(shfile, shfld, newName, options);
		}

		/// <summary>Moves a single item to a specified destination using the Shell to provide progress and error dialogs.</summary>
		/// <param name="source">A <see cref="ShellItem"/> that specifies the source item.</param>
		/// <param name="dest">A <see cref="ShellFolder"/> that specifies the destination folder to contain the moved item.</param>
		/// <param name="newName">
		/// An optional new name for the item in its new location. This can be <see langword="null"/>. If <see langword="null"/>, the name
		/// of the destination item is the same as the source.
		/// </param>
		/// <param name="options">Options that control file operations.</param>
		public static void Move(ShellItem source, ShellFolder dest, string newName = null, OperationFlags options = defaultOptions)
		{
			using var sop = new ShellFileOperations { Options = options };
			HRESULT hr = HRESULT.S_OK;
			sop.PostMoveItem += OnPost;
			try
			{
				sop.QueueMoveOperation(source, dest, newName);
				sop.PerformOperations();
				hr.ThrowIfFailed();
			}
			finally
			{
				sop.PostMoveItem -= OnPost;
			}

			void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result;
		}

		/// <summary>Moves a set of items to a specified destination using the Shell to provide progress and error dialogs.</summary>
		/// <param name="sourceItems">
		/// An <see cref="IEnumerable{T}"/> of <see cref="ShellItem"/> instances which represents the group of items to be moved.
		/// </param>
		/// <param name="dest">A <see cref="ShellFolder"/> that specifies the destination folder to contain the moved items.</param>
		/// <param name="options">Options that control file operations.</param>
		public static void Move(IEnumerable<ShellItem> sourceItems, ShellFolder dest, OperationFlags options = defaultOptions)
		{
			using var sop = new ShellFileOperations();
			sop.Options = options;
			HRESULT hr = HRESULT.S_OK;
			sop.PostMoveItem += OnPost;
			try
			{
				sop.QueueMoveOperation(sourceItems, dest);
				sop.PerformOperations();
				hr.ThrowIfFailed();
			}
			finally
			{
				sop.PostMoveItem -= OnPost;
			}

			void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result;
		}

		/// <summary>Creates a new item in a specified location using the Shell to provide progress and error dialogs.</summary>
		/// <param name="dest">A <see cref="ShellItem"/> that specifies the destination folder that will contain the new item.</param>
		/// <param name="name">The file name of the new item, for instance Newfile.txt.</param>
		/// <param name="attr">A value that specifies the file system attributes for the file or folder.</param>
		/// <param name="template">
		/// The name of the template file (for example Excel9.xls) that the new item is based on, stored in one of the following locations:
		/// <list type="bullet">
		/// <item>
		/// <description>CSIDL_COMMON_TEMPLATES. The default path for this folder is %ALLUSERSPROFILE%\Templates.</description>
		/// </item>
		/// <item>
		/// <description>CSIDL_TEMPLATES. The default path for this folder is %USERPROFILE%\Templates.</description>
		/// </item>
		/// <item>
		/// <description>%SystemRoot%\shellnew</description>
		/// </item>
		/// </list>
		/// <para>
		/// This is a string used to specify an existing file of the same type as the new file, containing the minimal content that an
		/// application wants to include in any new file.
		/// </para>
		/// <para>This parameter is normally <see langword="null"/> to specify a new, blank file.</para>
		/// </param>
		/// <param name="options">Options that control file operations.</param>
		public static void NewItem(ShellFolder dest, string name, System.IO.FileAttributes attr = System.IO.FileAttributes.Normal, string template = null, OperationFlags options = defaultOptions)
		{
			using var sop = new ShellFileOperations();
			sop.Options = options;
			HRESULT hr = HRESULT.S_OK;
			sop.PostNewItem += OnPost;
			try
			{
				sop.QueueNewItemOperation(dest, name, attr, template);
				sop.PerformOperations();
				hr.ThrowIfFailed();
			}
			finally
			{
				sop.PostRenameItem -= OnPost;
			}

			void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result;
		}

		/// <summary>Renames a single item to a new display name using the Shell to provide progress and error dialogs.</summary>
		/// <param name="source">A <see cref="ShellItem"/> that specifies the source item.</param>
		/// <param name="newName">The new display name of the item.</param>
		/// <param name="options">Options that control file operations.</param>
		public static void Rename(ShellItem source, string newName = null, OperationFlags options = defaultOptions)
		{
			using var sop = new ShellFileOperations();
			sop.Options = options;
			HRESULT hr = HRESULT.S_OK;
			sop.PostRenameItem += OnPost;
			try
			{
				sop.QueueRenameOperation(source, newName);
				sop.PerformOperations();
				hr.ThrowIfFailed();
			}
			finally
			{
				sop.PostRenameItem -= OnPost;
			}

			void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result;
		}

		/// <summary>
		/// Renames a set of items that are to be given a new display name using the Shell to provide progress and error dialogs. All items
		/// are given the same name.
		/// </summary>
		/// <param name="sourceItems">
		/// An <see cref="IEnumerable{T}"/> of <see cref="ShellItem"/> instances which represents the group of items to be renamed.
		/// </param>
		/// <param name="newName">The new display name of the items.</param>
		/// <param name="options">Options that control file operations.</param>
		public static void Rename(IEnumerable<ShellItem> sourceItems, string newName, OperationFlags options = defaultOptions)
		{
			using var sop = new ShellFileOperations();
			sop.Options = options;
			HRESULT hr = HRESULT.S_OK;
			sop.PostRenameItem += OnPost;
			try
			{
				sop.QueueRenameOperation(sourceItems, newName);
				sop.PerformOperations();
				hr.ThrowIfFailed();
			}
			finally
			{
				sop.PostRenameItem -= OnPost;
			}

			void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result;
		}

		/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>Executes all selected operations.</summary>
		/// <remarks>
		/// This method is called last to execute those actions that have been specified earlier by calling their individual methods. For
		/// instance, <see cref="QueueRenameOperation(ShellItem, string)"/> does not rename the item, it simply sets the parameters. The
		/// actual renaming is done when you call PerformOperations.
		/// </remarks>
		public void PerformOperations()
		{
			op.PerformOperations();
			QueuedOperations = 0;
		}

		/// <summary>Declares a set of properties and values to be set on an item.</summary>
		/// <param name="item">The item to receive the new property values.</param>
		/// <param name="props">
		/// An <see cref="ShellItemPropertyUpdates"/>, which contains a dictionary of objects that specify the properties to be set and
		/// their new values.
		/// </param>
		public void QueueApplyPropertiesOperation(ShellItem item, ShellItemPropertyUpdates props)
		{
			op.SetProperties(props.IPropertyChangeArray);
			op.ApplyPropertiesToItem(item.IShellItem);
			QueuedOperations++;
		}

		/// <summary>Declares a set of properties and values to be set on a set of items.</summary>
		/// <param name="items">
		/// An <see cref="IEnumerable{T}"/> of <see cref="ShellItem"/> instances that represent the group of items to which to apply the properties.
		/// </param>
		/// <param name="props">
		/// An <see cref="ShellItemPropertyUpdates"/>, which contains a dictionary of objects that specify the properties to be set and
		/// their new values.
		/// </param>
		public void QueueApplyPropertiesOperation(IEnumerable<ShellItem> items, ShellItemPropertyUpdates props)
		{
			op.SetProperties(props.IPropertyChangeArray);
			op.ApplyPropertiesToItems(GetSHArray(items).IShellItemArray);
			QueuedOperations++;
		}

		/// <summary>Declares a single item that is to be copied to a specified destination.</summary>
		/// <param name="source">A <see cref="ShellItem"/> that specifies the source item.</param>
		/// <param name="dest">A <see cref="ShellFolder"/> that specifies the destination folder to contain the copy of the item.</param>
		/// <param name="newName">
		/// An optional new name for the item after it has been copied. This can be <see langword="null"/>. If <see langword="null"/>, the
		/// name of the destination item is the same as the source.
		/// </param>
		public void QueueCopyOperation(ShellItem source, ShellFolder dest, string newName = null)
		{
			op.CopyItem(source.IShellItem, dest.IShellItem, newName, null);
			QueuedOperations++;
		}

		/// <summary>Declares a set of items that are to be copied to a specified destination.</summary>
		/// <param name="sourceItems">
		/// An <see cref="IEnumerable{T}"/> of <see cref="ShellItem"/> instances that represent the group of items to be copied.
		/// </param>
		/// <param name="dest">A <see cref="ShellFolder"/> that specifies the destination folder to contain the copy of the items.</param>
		public void QueueCopyOperation(IEnumerable<ShellItem> sourceItems, ShellFolder dest)
		{
			op.CopyItems(GetSHArray(sourceItems).IShellItemArray, dest.IShellItem);
			QueuedOperations++;
		}

		/// <summary>Declares a single item that is to be deleted.</summary>
		/// <param name="item">&gt;A <see cref="ShellItem"/> that specifies the item to be deleted.</param>
		public void QueueDeleteOperation(ShellItem item)
		{
			op.DeleteItem(item.IShellItem, null);
			QueuedOperations++;
		}

		/// <summary>Declares a set of items that are to be deleted.</summary>
		/// <param name="items">
		/// An <see cref="IEnumerable{T}"/> of <see cref="ShellItem"/> instances which represents the group of items to be deleted.
		/// </param>
		public void QueueDeleteOperation(IEnumerable<ShellItem> items)
		{
			op.DeleteItems(GetSHArray(items).IShellItemArray);
			QueuedOperations++;
		}

		/// <summary>Declares a single item that is to be moved to a specified destination.</summary>
		/// <param name="source">A <see cref="ShellItem"/> that specifies the source item.</param>
		/// <param name="dest">A <see cref="ShellFolder"/> that specifies the destination folder to contain the moved item.</param>
		/// <param name="newName">
		/// An optional new name for the item in its new location. This can be <see langword="null"/>. If <see langword="null"/>, the name
		/// of the destination item is the same as the source.
		/// </param>
		public void QueueMoveOperation(ShellItem source, ShellFolder dest, string newName = null)
		{
			op.MoveItem(source.IShellItem, dest.IShellItem, newName, null);
			QueuedOperations++;
		}

		/// <summary>Declares a set of items that are to be moved to a specified destination.</summary>
		/// <param name="sourceItems">
		/// An <see cref="IEnumerable{T}"/> of <see cref="ShellItem"/> instances which represents the group of items to be moved.
		/// </param>
		/// <param name="dest">A <see cref="ShellFolder"/> that specifies the destination folder to contain the moved items.</param>
		public void QueueMoveOperation(IEnumerable<ShellItem> sourceItems, ShellFolder dest)
		{
			op.MoveItems(GetSHArray(sourceItems).IShellItemArray, dest.IShellItem);
			QueuedOperations++;
		}

		/// <summary>Declares a new item that is to be created in a specified location.</summary>
		/// <param name="dest">A <see cref="ShellItem"/> that specifies the destination folder that will contain the new item.</param>
		/// <param name="name">The file name of the new item, for instance Newfile.txt.</param>
		/// <param name="attr">A value that specifies the file system attributes for the file or folder.</param>
		/// <param name="template">
		/// The name of the template file (for example Excel9.xls) that the new item is based on, stored in one of the following locations:
		/// <list type="bullet">
		/// <item>
		/// <description>CSIDL_COMMON_TEMPLATES. The default path for this folder is %ALLUSERSPROFILE%\Templates.</description>
		/// </item>
		/// <item>
		/// <description>CSIDL_TEMPLATES. The default path for this folder is %USERPROFILE%\Templates.</description>
		/// </item>
		/// <item>
		/// <description>%SystemRoot%\shellnew</description>
		/// </item>
		/// </list>
		/// <para>
		/// This is a string used to specify an existing file of the same type as the new file, containing the minimal content that an
		/// application wants to include in any new file.
		/// </para>
		/// <para>This parameter is normally <see langword="null"/> to specify a new, blank file.</para>
		/// </param>
		public void QueueNewItemOperation(ShellFolder dest, string name, System.IO.FileAttributes attr = System.IO.FileAttributes.Normal, string template = null)
		{
			op.NewItem(dest.IShellItem, attr, name, template, null);
			QueuedOperations++;
		}

		/// <summary>Declares a single item that is to be given a new display name.</summary>
		/// <param name="source">A <see cref="ShellItem"/> that specifies the source item.</param>
		/// <param name="newName">The new display name of the item.</param>
		public void QueueRenameOperation(ShellItem source, string newName)
		{
			op.RenameItem(source.IShellItem, newName, null);
			QueuedOperations++;
		}

		/// <summary>Declares a set of items that are to be given a new display name. All items are given the same name.</summary>
		/// <param name="sourceItems">
		/// An <see cref="IEnumerable{T}"/> of <see cref="ShellItem"/> instances which represents the group of items to be renamed.
		/// </param>
		/// <param name="newName">The new display name of the items.</param>
		public void QueueRenameOperation(IEnumerable<ShellItem> sourceItems, string newName)
		{
			op.RenameItems(GetSHArray(sourceItems).IShellItemArray, newName);
			QueuedOperations++;
		}

		/// <summary>Releases unmanaged and - optionally - managed resources.</summary>
		/// <param name="disposing">
		/// <c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.
		/// </param>
		protected virtual void Dispose(bool disposing)
		{
			if (!disposedValue)
			{
				if (disposing)
				{
					// Dispose managed state (managed objects).
				}

				if (sink != null) op.Unadvise(sinkCookie);
				if (op != null)
				{
					Marshal.FinalReleaseComObject(op);
					op = null;
				}
				disposedValue = true;
			}
		}

		private ShellItemArray GetSHArray(IEnumerable<ShellItem> items) => items is ShellItemArray a ? a : new ShellItemArray(items);

		private class OpSink : IFileOperationProgressSink
		{
			private readonly ShellFileOperations parent;

			public OpSink(ShellFileOperations ops)
			{
				parent = ops;
			}

			public HRESULT FinishOperations(HRESULT hrResult) => CallChkErr(() => parent.FinishOperations?.Invoke(parent, new ShellFileOpEventArgs(0, null, null, null, null, hrResult)));

			public HRESULT PauseTimer() => HRESULT.E_NOTIMPL;

			public HRESULT PostCopyItem(TRANSFER_SOURCE_FLAGS dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, HRESULT hrCopy, IShellItem psiNewlyCreated) =>
				CallChkErr(() => parent.PostCopyItem?.Invoke(parent, new ShellFileOpEventArgs(dwFlags, psiItem, psiDestinationFolder, psiNewlyCreated, pszNewName, hrCopy)));

			public HRESULT PostDeleteItem(TRANSFER_SOURCE_FLAGS dwFlags, IShellItem psiItem, HRESULT hrDelete, IShellItem psiNewlyCreated) =>
				CallChkErr(() => parent.PostDeleteItem?.Invoke(parent, new ShellFileOpEventArgs(dwFlags, psiItem, null, psiNewlyCreated, null, hrDelete)));

			public HRESULT PostMoveItem(TRANSFER_SOURCE_FLAGS dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, HRESULT hrMove, IShellItem psiNewlyCreated) =>
				CallChkErr(() => parent.PostMoveItem?.Invoke(parent, new ShellFileOpEventArgs(dwFlags, psiItem, psiDestinationFolder, psiNewlyCreated, pszNewName, hrMove)));

			public HRESULT PostNewItem(TRANSFER_SOURCE_FLAGS dwFlags, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, [MarshalAs(UnmanagedType.LPWStr)] string pszTemplateName, uint dwFileAttributes, HRESULT hrNew, IShellItem psiNewItem) =>
				CallChkErr(() => parent.PostNewItem?.Invoke(parent, new ShellFileNewOpEventArgs(dwFlags, null, psiDestinationFolder, psiNewItem, pszNewName, hrNew, pszTemplateName, dwFileAttributes)));

			public HRESULT PostRenameItem(TRANSFER_SOURCE_FLAGS dwFlags, IShellItem psiItem, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, HRESULT hrRename, IShellItem psiNewlyCreated) =>
				CallChkErr(() => parent.PostRenameItem?.Invoke(parent, new ShellFileOpEventArgs(dwFlags, psiItem, null, psiNewlyCreated, pszNewName, hrRename)));

			public HRESULT PreCopyItem(TRANSFER_SOURCE_FLAGS dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName) =>
				CallChkErr(() => parent.PreCopyItem?.Invoke(parent, new ShellFileOpEventArgs(dwFlags, psiItem, psiDestinationFolder, null, pszNewName)));

			public HRESULT PreDeleteItem(TRANSFER_SOURCE_FLAGS dwFlags, IShellItem psiItem) =>
				CallChkErr(() => parent.PreDeleteItem?.Invoke(parent, new ShellFileOpEventArgs(dwFlags, psiItem)));

			public HRESULT PreMoveItem(TRANSFER_SOURCE_FLAGS dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName) =>
				CallChkErr(() => parent.PreMoveItem?.Invoke(parent, new ShellFileOpEventArgs(dwFlags, psiItem, psiDestinationFolder, null, pszNewName)));

			public HRESULT PreNewItem(TRANSFER_SOURCE_FLAGS dwFlags, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName) =>
				CallChkErr(() => parent.PreNewItem?.Invoke(parent, new ShellFileOpEventArgs(dwFlags, null, psiDestinationFolder, null, pszNewName)));

			public HRESULT PreRenameItem(TRANSFER_SOURCE_FLAGS dwFlags, IShellItem psiItem, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName) => CallChkErr(() => parent.PreRenameItem?.Invoke(parent, new ShellFileOpEventArgs(dwFlags, psiItem, null, null, pszNewName)));

			public HRESULT ResetTimer() => HRESULT.E_NOTIMPL;

			public HRESULT ResumeTimer() => HRESULT.E_NOTIMPL;

			public HRESULT StartOperations() => CallChkErr(() => parent.StartOperations?.Invoke(parent, EventArgs.Empty));

			public HRESULT UpdateProgress(uint iWorkTotal, uint iWorkSoFar) => CallChkErr(() => parent.UpdateProgress?.Invoke(parent, new System.ComponentModel.ProgressChangedEventArgs(iWorkTotal == 0 ? 0 : (int)(iWorkSoFar * 100 / iWorkTotal), null)));

			private HRESULT CallChkErr(Action action)
			{
				try { action(); }
				catch (COMException comex) { return comex.ErrorCode; }
				catch (Win32Exception w32ex) { return new Win32Error(unchecked((uint)w32ex.NativeErrorCode)).ToHRESULT(); }
				catch (Exception e)
				{
#if !(NET20 || NET35 || NET40)
					return e.HResult;
#else
					throw e;
#endif
				}
				return HRESULT.S_OK;
			}
		}
	}
}